class_name LuaAPI extends Node var threaded_vm: ThreadedLuaVM var script_start_time: float = 0.0 class EventSubscription: var id: int var element_id: String var event_name: String var callback_ref: int var vm: LuauVM var lua_api: LuaAPI var connected_signal: String = "" var connected_node: Node = null var callback_func: Callable var wrapper_func: Callable var dom_parser: HTMLParser var event_subscriptions: Dictionary = {} var next_subscription_id: int = 1 var next_callback_ref: int = 1 var element_id_counter: int = 1 var element_id_registry: Dictionary = {} var pending_event_registrations: Array = [] func _init(): timeout_manager = LuaTimeoutManager.new() threaded_vm = ThreadedLuaVM.new() threaded_vm.script_completed.connect(_on_threaded_script_completed) threaded_vm.script_error.connect(func(e): print(e)) threaded_vm.dom_operation_request.connect(_handle_dom_operation) threaded_vm.print_output.connect(_on_print_output) func get_or_assign_element_id(element: HTMLParser.HTMLElement) -> String: var existing_id = element.get_attribute("id") if not existing_id.is_empty(): element_id_registry[element] = existing_id return existing_id if element_id_registry.has(element): return element_id_registry[element] var new_id = "auto_" + str(element_id_counter) element_id_counter += 1 element.set_attribute("id", new_id) element_id_registry[element] = new_id return new_id func _gurt_select_handler(vm: LuauVM) -> int: var selector: String = vm.luaL_checkstring(1) var element = SelectorUtils.find_first_matching(selector, dom_parser.parse_result.all_elements) if not element: vm.lua_pushnil() return 1 var element_id = get_or_assign_element_id(element) vm.lua_newtable() vm.lua_pushstring(element_id) vm.lua_setfield(-2, "_element_id") vm.lua_pushstring(element.tag_name) vm.lua_setfield(-2, "_tag_name") LuaDOMUtils.add_element_methods(vm, self) return 1 # selectAll() function to find multiple elements func _gurt_select_all_handler(vm: LuauVM) -> int: var selector: String = vm.luaL_checkstring(1) var elements = SelectorUtils.find_all_matching(selector, dom_parser.parse_result.all_elements) vm.lua_newtable() var index = 1 for element in elements: var element_id = get_or_assign_element_id(element) # Create element wrapper vm.lua_newtable() vm.lua_pushstring(element_id) vm.lua_setfield(-2, "_element_id") vm.lua_pushstring(element.tag_name) vm.lua_setfield(-2, "_tag_name") LuaDOMUtils.add_element_methods(vm, self) # Add to array at index vm.lua_rawseti(-2, index) index += 1 return 1 # create() function to create HTML element func _gurt_create_handler(vm: LuauVM) -> int: var tag_name: String = vm.luaL_checkstring(1) var options: Dictionary = {} if vm.lua_gettop() >= 2 and vm.lua_istable(2): options = vm.lua_todictionary(2) var element = HTMLParser.HTMLElement.new(tag_name) # Apply options as attributes and content for key in options: if key == "text": element.text_content = str(options[key]) else: element.attributes[str(key)] = str(options[key]) # Add to parser's element collection first dom_parser.parse_result.all_elements.append(element) # Get or assign stable ID var unique_id = get_or_assign_element_id(element) # Create Lua element wrapper with methods vm.lua_newtable() vm.lua_pushstring(unique_id) vm.lua_setfield(-2, "_element_id") vm.lua_pushstring(tag_name) vm.lua_setfield(-2, "_tag_name") vm.lua_pushboolean(true) vm.lua_setfield(-2, "_is_dynamic") LuaDOMUtils.add_element_methods(vm, self) return 1 var timeout_manager: LuaTimeoutManager func _ensure_timeout_manager(): if not timeout_manager: timeout_manager = LuaTimeoutManager.new() # Timeout management handlers func _gurt_set_timeout_handler(vm: LuauVM) -> int: _ensure_timeout_manager() return timeout_manager.set_threaded_timeout_handler(vm, self, threaded_vm) func _gurt_clear_timeout_handler(vm: LuauVM) -> int: _ensure_timeout_manager() return timeout_manager.clear_timeout_handler(vm) func _gurt_set_interval_handler(vm: LuauVM) -> int: _ensure_timeout_manager() return timeout_manager.set_threaded_interval_handler(vm, self, threaded_vm) func _gurt_clear_interval_handler(vm: LuauVM) -> int: _ensure_timeout_manager() return timeout_manager.clear_interval_handler(vm) # Location API handlers func _gurt_location_reload_handler(vm: LuauVM) -> int: call_deferred("_reload_current_page") return 0 func _gurt_location_goto_handler(vm: LuauVM) -> int: var url: String = vm.luaL_checkstring(1) call_deferred("_navigate_to_url", url) return 0 func _gurt_location_get_href_handler(vm: LuauVM) -> int: var main_node = Engine.get_main_loop().current_scene if main_node and main_node.has_method("get_current_url"): var current_url = main_node.get_current_url() vm.lua_pushstring(current_url) else: vm.lua_pushstring("") return 1 func _gurt_location_query_get_handler(vm: LuauVM) -> int: var key: String = vm.luaL_checkstring(1) var query_params = get_current_query_params() if query_params.has(key): vm.lua_pushstring(query_params[key]) else: vm.lua_pushnil() return 1 func _gurt_location_query_has_handler(vm: LuauVM) -> int: var key: String = vm.luaL_checkstring(1) var query_params = get_current_query_params() vm.lua_pushboolean(query_params.has(key)) return 1 func _gurt_location_query_getAll_handler(vm: LuauVM) -> int: var key: String = vm.luaL_checkstring(1) var query_params = get_current_query_params() vm.lua_newtable() if query_params.has(key): var value = query_params[key] if value is Array: for i in range(value.size()): vm.lua_pushstring(str(value[i])) vm.lua_rawseti(-2, i + 1) else: vm.lua_pushstring(str(value)) vm.lua_rawseti(-2, 1) return 1 func get_current_query_params() -> Dictionary: var main_node = Engine.get_main_loop().current_scene var current_url = "" if main_node and main_node.has_method("get_current_url"): current_url = main_node.get_current_url() elif main_node and main_node.has_property("current_domain"): current_url = main_node.current_domain var query_params = {} if "?" in current_url: var query_string = current_url.split("?")[1] if "#" in query_string: query_string = query_string.split("#")[0] for param in query_string.split("&"): if "=" in param: var key_value = param.split("=", false, 1) var key = key_value[0].uri_decode() var value = key_value[1].uri_decode() if key_value.size() > 1 else "" if query_params.has(key): if query_params[key] is Array: query_params[key].append(value) else: query_params[key] = [query_params[key], value] else: query_params[key] = value else: var key = param.uri_decode() query_params[key] = "" return query_params func _reload_current_page(): var main_node = Engine.get_main_loop().current_scene if main_node and main_node.has_method("reload_current_page"): main_node.reload_current_page() func _navigate_to_url(url: String): var main_node = Engine.get_main_loop().current_scene if main_node and main_node.has_method("navigate_to_url"): main_node.navigate_to_url(url) # Event system handlers func _element_on_event_handler(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE) var event_name: String = vm.luaL_checkstring(2) vm.luaL_checktype(3, vm.LUA_TFUNCTION) vm.lua_getfield(1, "_element_id") var element_id: String = vm.lua_tostring(-1) vm.lua_pop(1) # Create a proper subscription with real ID var subscription = _create_subscription(vm, element_id, event_name) event_subscriptions[subscription.id] = subscription # Register the event on main thread call_deferred("_register_event_on_main_thread", element_id, event_name, subscription.callback_ref, subscription.id) # Return subscription with proper unsubscribe method vm.lua_newtable() vm.lua_pushinteger(subscription.id) vm.lua_setfield(-2, "_subscription_id") vm.lua_pushcallable(_subscription_unsubscribe_handler, "subscription.unsubscribe") vm.lua_setfield(-2, "unsubscribe") return 1 func _body_on_event_handler(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE) var event_name: String = vm.luaL_checkstring(2) vm.luaL_checktype(3, vm.LUA_TFUNCTION) var subscription = _create_subscription(vm, "body", event_name) event_subscriptions[subscription.id] = subscription var success = LuaEventUtils.connect_body_event(event_name, subscription, self) return _handle_subscription_result(vm, subscription, success) func _subscription_unsubscribe_handler(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE) vm.lua_getfield(1, "_subscription_id") var subscription_id: int = vm.lua_tointeger(-1) vm.lua_pop(1) var subscription = event_subscriptions.get(subscription_id, null) if subscription: LuaEventUtils.disconnect_subscription(subscription, self) event_subscriptions.erase(subscription_id) vm.lua_pushnil() vm.lua_rawseti(vm.LUA_REGISTRYINDEX, subscription.callback_ref) return 0 # Subscription management func _create_subscription(vm: LuauVM, element_id: String, event_name: String) -> EventSubscription: var subscription_id = next_subscription_id next_subscription_id += 1 var callback_ref = next_callback_ref next_callback_ref += 1 vm.lua_pushvalue(3) vm.lua_rawseti(vm.LUA_REGISTRYINDEX, callback_ref) var subscription = EventSubscription.new() subscription.id = subscription_id subscription.element_id = element_id subscription.event_name = event_name subscription.callback_ref = callback_ref subscription.vm = vm subscription.lua_api = self return subscription func _handle_subscription_result(vm: LuauVM, subscription: EventSubscription, success: bool) -> int: if success: vm.lua_newtable() vm.lua_pushinteger(subscription.id) vm.lua_setfield(-2, "_subscription_id") vm.lua_pushcallable(_subscription_unsubscribe_handler, "subscription.unsubscribe") vm.lua_setfield(-2, "unsubscribe") return 1 else: vm.lua_pushnil() vm.lua_rawseti(vm.LUA_REGISTRYINDEX, subscription.callback_ref) event_subscriptions.erase(subscription.id) vm.lua_pushnil() return 1 # Event callbacks func _on_event_triggered(subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return _execute_lua_callback(subscription) func _on_gui_input_click(event: InputEvent, subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return if event is InputEventMouseButton: var mouse_event = event as InputEventMouseButton if mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed: _execute_lua_callback(subscription) func _on_gui_input_mouse_universal(event: InputEvent, signal_node: Node) -> void: if event is InputEventMouseButton: var mouse_event = event as InputEventMouseButton if mouse_event.button_index == MOUSE_BUTTON_LEFT: # Find all subscriptions for this node with mouse events for subscription_id in event_subscriptions: var subscription = event_subscriptions[subscription_id] if subscription.connected_node == signal_node and subscription.connected_signal == "gui_input_mouse": var should_trigger = false if subscription.event_name == "mousedown" and mouse_event.pressed: should_trigger = true elif subscription.event_name == "mouseup" and not mouse_event.pressed: should_trigger = true if should_trigger: _execute_lua_callback(subscription) # Event callback handlers func _on_gui_input_mousemove(event: InputEvent, subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return if event is InputEventMouseMotion: var mouse_event = event as InputEventMouseMotion _handle_mousemove_event(mouse_event, subscription) func _on_focus_gui_input(event: InputEvent, subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return if event is InputEventMouseButton: var mouse_event = event as InputEventMouseButton if mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed: if subscription.event_name == "focusin": _execute_lua_callback(subscription) func _handle_body_event(subscription: EventSubscription, event_name: String, event_data: Dictionary = {}) -> void: if event_subscriptions.has(subscription.id) and subscription.event_name == event_name: _execute_lua_callback(subscription, [event_data]) func _on_body_mouse_enter(subscription: EventSubscription) -> void: _handle_body_event(subscription, "mouseenter", {}) func _on_body_mouse_exit(subscription: EventSubscription) -> void: _handle_body_event(subscription, "mouseexit", {}) func _execute_lua_callback(subscription: EventSubscription, args: Array = []) -> void: threaded_vm.execute_callback_async(subscription.callback_ref, args) func _execute_input_event_callback(subscription: EventSubscription, event_data: Dictionary) -> void: if not event_subscriptions.has(subscription.id): return _execute_lua_callback(subscription, [event_data]) # Global input processing func _input(event: InputEvent) -> void: if event is InputEventKey: var key_event = event as InputEventKey for subscription_id in event_subscriptions: var subscription = event_subscriptions[subscription_id] if subscription.element_id == "body" and subscription.connected_signal == "input": var should_trigger = false match subscription.event_name: "keydown": should_trigger = key_event.pressed "keyup": should_trigger = not key_event.pressed "keypress": should_trigger = key_event.pressed if should_trigger: var key_info = { "key": OS.get_keycode_string(key_event.keycode), "keycode": key_event.keycode, "ctrl": key_event.ctrl_pressed, "shift": key_event.shift_pressed, "alt": key_event.alt_pressed } _execute_lua_callback(subscription, [key_info]) elif event is InputEventMouseMotion: var mouse_event = event as InputEventMouseMotion for subscription_id in event_subscriptions: var subscription = event_subscriptions[subscription_id] if subscription.element_id == "body" and subscription.connected_signal == "input_mousemove": if subscription.event_name == "mousemove": _handle_mousemove_event(mouse_event, subscription) func _handle_mousemove_event(mouse_event: InputEventMouseMotion, subscription: EventSubscription) -> void: # TODO: pass reference instead of hardcoded path var body_container = get_node("/root/Main").website_container if body_container.get_parent() is MarginContainer: body_container = body_container.get_parent() if not body_container: return var container_rect = body_container.get_global_rect() var local_x = mouse_event.global_position.x - container_rect.position.x var local_y = mouse_event.global_position.y - container_rect.position.y # Only provide coordinates if mouse is within the container bounds if local_x >= 0 and local_y >= 0 and local_x <= container_rect.size.x and local_y <= container_rect.size.y: var mouse_info = { "x": local_x, "y": local_y, "deltaX": mouse_event.relative.x, "deltaY": mouse_event.relative.y } _execute_lua_callback(subscription, [mouse_info]) # Input event handlers func _on_input_text_changed(new_text: String, subscription: EventSubscription) -> void: _execute_input_event_callback(subscription, {"value": new_text}) func _on_input_focus_lost(subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return # Get the current text value from the input node var dom_node = dom_parser.parse_result.dom_nodes.get(subscription.element_id, null) if dom_node: var current_text = "" if dom_node.has_method("get_text"): current_text = dom_node.get_text() elif "text" in dom_node: current_text = dom_node.text else: var element = dom_parser.find_by_id(subscription.element_id) if element: current_text = element.text_content var event_info = {"value": current_text} _execute_lua_callback(subscription, [event_info]) func _on_input_value_changed(new_value, subscription: EventSubscription) -> void: _execute_input_event_callback(subscription, {"value": new_value}) func _on_input_color_changed(new_color: Color, subscription: EventSubscription) -> void: _execute_input_event_callback(subscription, {"value": "#" + new_color.to_html(false)}) func _on_input_toggled(pressed: bool, subscription: EventSubscription) -> void: _execute_input_event_callback(subscription, {"value": pressed}) func _on_input_item_selected(index: int, subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return # Get value from OptionButton var dom_node = dom_parser.parse_result.dom_nodes.get(subscription.element_id, null) var value = "" var text = "" if dom_node and dom_node is OptionButton: var option_button = dom_node as OptionButton text = option_button.get_item_text(index) # Get actual value attribute (stored as metadata) var metadata = option_button.get_item_metadata(index) value = str(metadata) if metadata != null else text var event_info = {"index": index, "value": value, "text": text} _execute_lua_callback(subscription, [event_info]) func _on_file_selected(file_path: String, subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return var dom_node = dom_parser.parse_result.dom_nodes.get(subscription.element_id, null) if dom_node: var file_container = dom_node.get_parent() # FileContainer (HBoxContainer) if file_container: var input_element = file_container.get_parent() # Input Control if input_element and input_element.has_method("get_file_info"): var file_info = input_element.get_file_info() if not file_info.is_empty(): _execute_lua_callback(subscription, [file_info]) return # Fallback var file_name = file_path.get_file() _execute_lua_callback(subscription, [{"fileName": file_name}]) func _on_date_selected_text(date_text: String, subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return var event_info = {"value": date_text} _execute_lua_callback(subscription, [event_info]) func _on_form_submit(subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return # Find parent form var form_data = {} var element = dom_parser.find_by_id(subscription.element_id) if element: var form_element = element.parent while form_element and form_element.tag_name != "form": form_element = form_element.parent if form_element: var form_dom_node = dom_parser.parse_result.dom_nodes.get(form_element.get_attribute("id"), null) if form_dom_node and form_dom_node.has_method("submit_form"): form_data = form_dom_node.submit_form() var event_info = {"data": form_data} _execute_lua_callback(subscription, [event_info]) func _on_text_submit(text: String, subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return var event_info = {"value": text} _execute_lua_callback(subscription, [event_info]) # DOM node utilities func get_dom_node(node: Node, purpose: String = "general") -> Node: if not node: return null if node is MarginContainer and node.get_child_count() > 0: node = node.get_child(0) if not node: return null match purpose: "signal": if node is HTMLButton: return node.get_node_or_null("ButtonNode") elif node is HBoxContainer and node.get_node_or_null("ButtonNode"): return node.get_node_or_null("ButtonNode") elif node is RichTextLabel: return node elif node.has_method("get") and node.get("rich_text_label"): return node.get("rich_text_label") elif node.get_node_or_null("RichTextLabel"): return node.get_node_or_null("RichTextLabel") elif node is LineEdit or node is TextEdit or node is SpinBox or node is HSlider: return node elif node is CheckBox or node is ColorPickerButton or node is OptionButton: return node else: return node "text": if node.has_method("set_text") and node.has_method("get_text"): return node elif node is RichTextLabel: return node elif node.has_method("get") and node.get("rich_text_label"): return node.get("rich_text_label") elif node.get_node_or_null("RichTextLabel"): return node.get_node_or_null("RichTextLabel") else: if "text" in node: return node return null "general": if node is HTMLButton: return node.get_node_or_null("ButtonNode") elif node is RichTextLabel: return node elif node.get_node_or_null("RichTextLabel"): return node.get_node_or_null("RichTextLabel") else: return node return node # Main execution function func execute_lua_script(code: String, vm: LuauVM): if not threaded_vm.lua_thread or not threaded_vm.lua_thread.is_alive(): # Start the thread if it's not running threaded_vm.start_lua_thread(dom_parser, self) script_start_time = Time.get_ticks_msec() / 1000.0 threaded_vm.execute_script_async(code) func _on_threaded_script_completed(result: Dictionary): var execution_time = (Time.get_ticks_msec() / 1000.0) - script_start_time func _on_print_output(message: String): LuaPrintUtils.lua_print_direct(message) func kill_script_execution(): threaded_vm.stop_lua_thread() # Restart a fresh thread for future scripts threaded_vm.start_lua_thread(dom_parser, self) func is_script_hanging() -> bool: return threaded_vm.lua_thread != null and threaded_vm.lua_thread.is_alive() func get_script_runtime() -> float: if script_start_time > 0 and is_script_hanging(): return (Time.get_ticks_msec() / 1000.0) - script_start_time return 0.0 func _handle_dom_operation(operation: Dictionary): match operation.type: "register_event": _handle_event_registration(operation) "register_body_event": _handle_body_event_registration(operation) "set_text": _handle_text_setting(operation) "get_text": _handle_text_getting(operation) "append_element": LuaDOMUtils.handle_element_append(operation, dom_parser, self) "add_class": LuaClassListUtils.handle_add_class(operation, dom_parser) "remove_class": LuaClassListUtils.handle_remove_class(operation, dom_parser) "toggle_class": LuaClassListUtils.handle_toggle_class(operation, dom_parser) "remove_element": LuaDOMUtils.handle_element_remove(operation, dom_parser) "insert_before": LuaDOMUtils.handle_insert_before(operation, dom_parser, self) "insert_after": LuaDOMUtils.handle_insert_after(operation, dom_parser, self) "replace_child": LuaDOMUtils.handle_replace_child(operation, dom_parser, self) _: pass # Unknown operation type, ignore func _handle_event_registration(operation: Dictionary): var selector: String = operation.selector var event_name: String = operation.event_name var callback_ref: int = operation.callback_ref var element = SelectorUtils.find_first_matching(selector, dom_parser.parse_result.all_elements) if not element: return var element_id = get_or_assign_element_id(element) # Create subscription for threaded callback var subscription = EventSubscription.new() subscription.id = next_subscription_id next_subscription_id += 1 subscription.element_id = element_id subscription.event_name = event_name subscription.callback_ref = callback_ref subscription.vm = threaded_vm.lua_vm if threaded_vm else null subscription.lua_api = self event_subscriptions[subscription.id] = subscription # Connect to DOM element var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) if dom_node: var signal_node = get_dom_node(dom_node, "signal") LuaEventUtils.connect_element_event(signal_node, event_name, subscription) func _handle_text_setting(operation: Dictionary): var selector: String = operation.selector var text: String = operation.text var element = SelectorUtils.find_first_matching(selector, dom_parser.parse_result.all_elements) if element: # If the element has a DOM node, update it directly without updating text_content var element_id = get_or_assign_element_id(element) var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) if dom_node: if element.tag_name == "button": var button_node = dom_node.get_node_or_null("ButtonNode") if button_node and button_node is Button: button_node.text = text element.text_content = text return if element.tag_name == "p" and dom_node.has_method("set_text"): dom_node.set_text(text) element.text_content = text return element.text_content = text if dom_node: var text_node = get_dom_node(dom_node, "text") if text_node: if text_node is RichTextLabel: StyleManager.apply_styles_to_label(text_node, dom_parser.get_element_styles_with_inheritance(element, "", []), element, dom_parser, text) try_apply_auto_resize(text_node) elif text_node.has_method("set_text"): text_node.set_text(text) elif "text" in text_node: text_node.text = text try_apply_auto_resize(text_node) else: var rich_text_label = _find_rich_text_label_recursive(dom_node) if rich_text_label: StyleManager.apply_styles_to_label(rich_text_label, dom_parser.get_element_styles_with_inheritance(element, "", []), element, dom_parser, text) try_apply_auto_resize(rich_text_label) func try_apply_auto_resize(text_node: Node) -> void: var parent = text_node.get_parent() if parent and parent.has_method("_apply_auto_resize_to_label"): parent.call_deferred("_apply_auto_resize_to_label", text_node) func _find_rich_text_label_recursive(node: Node) -> RichTextLabel: if node is RichTextLabel: return node for child in node.get_children(): var result = _find_rich_text_label_recursive(child) if result: return result return null func _handle_text_getting(operation: Dictionary): var selector: String = operation.selector var element = SelectorUtils.find_first_matching(selector, dom_parser.parse_result.all_elements) if element: return element.text_content return "" func _handle_body_event_registration(operation: Dictionary): var event_name: String = operation.event_name var callback_ref: int = operation.callback_ref var subscription_id: int = operation.get("subscription_id", -1) # Use provided subscription_id or generate a new one if subscription_id == -1: subscription_id = next_subscription_id next_subscription_id += 1 # Create subscription for threaded callback var subscription = EventSubscription.new() subscription.id = subscription_id subscription.element_id = "body" subscription.event_name = event_name subscription.callback_ref = callback_ref subscription.vm = threaded_vm.lua_vm if threaded_vm else null subscription.lua_api = self event_subscriptions[subscription.id] = subscription # Connect to body events LuaEventUtils.connect_body_event(event_name, subscription, self) func _register_event_on_main_thread(element_id: String, event_name: String, callback_ref: int, subscription_id: int = -1): # This runs on the main thread - safe to access DOM nodes var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) if not dom_node: var pending_registration = { "element_id": element_id, "event_name": event_name, "callback_ref": callback_ref, "subscription_id": subscription_id if subscription_id != -1 else next_subscription_id } if subscription_id == -1: next_subscription_id += 1 pending_event_registrations.append(pending_registration) call_deferred("_process_pending_event_registrations") return # Use provided subscription_id or generate a new one if subscription_id == -1: subscription_id = next_subscription_id next_subscription_id += 1 # Create subscription using the threaded VM's callback reference var subscription = EventSubscription.new() subscription.id = subscription_id subscription.element_id = element_id subscription.event_name = event_name subscription.callback_ref = callback_ref subscription.vm = threaded_vm.lua_vm if threaded_vm else null subscription.lua_api = self event_subscriptions[subscription.id] = subscription var signal_node = get_dom_node(dom_node, "signal") LuaEventUtils.connect_element_event(signal_node, event_name, subscription) func _process_pending_event_registrations(): var i = 0 while i < pending_event_registrations.size(): var registration = pending_event_registrations[i] var element_id = registration.element_id var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) if dom_node: pending_event_registrations.remove_at(i) _register_event_on_main_thread( registration.element_id, registration.event_name, registration.callback_ref, registration.subscription_id ) else: i += 1 if pending_event_registrations.size() > 0: call_deferred("_process_pending_event_registrations") func _unsubscribe_event_on_main_thread(subscription_id: int): # This runs on the main thread - safe to cleanup event subscriptions var subscription = event_subscriptions.get(subscription_id, null) if subscription: LuaEventUtils.disconnect_subscription(subscription, self) event_subscriptions.erase(subscription_id) # Clean up Lua callback reference if subscription.callback_ref and subscription.vm: subscription.vm.lua_pushnil() subscription.vm.lua_rawseti(subscription.vm.LUA_REGISTRYINDEX, subscription.callback_ref) func _notification(what: int): if what == NOTIFICATION_PREDELETE: if timeout_manager: timeout_manager.cleanup_all_timeouts() threaded_vm.stop_lua_thread()