From 5759ee62214dde1ac2adc5c5b6b48871ece88501 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:51:21 +0300 Subject: [PATCH] threaded Lua VM --- flumi/Scripts/B9/HTMLParser.gd | 4 +- flumi/Scripts/B9/Lua.gd | 612 ++++++--------- flumi/Scripts/Tags/button.gd | 2 +- flumi/Scripts/Utils/Lua/Class.gd | 155 +++- flumi/Scripts/Utils/Lua/DOM.gd | 859 +++++++++++++++++----- flumi/Scripts/Utils/Lua/Event.gd | 2 - flumi/Scripts/Utils/Lua/Function.gd | 73 -- flumi/Scripts/Utils/Lua/Signal.gd | 1 - flumi/Scripts/Utils/Lua/ThreadedVM.gd | 501 +++++++++++++ flumi/Scripts/Utils/Lua/ThreadedVM.gd.uid | 1 + flumi/Scripts/Utils/Lua/Time.gd | 11 +- flumi/Scripts/Utils/Lua/Timeout.gd | 52 +- flumi/Scripts/main.gd | 9 +- 13 files changed, 1552 insertions(+), 730 deletions(-) delete mode 100644 flumi/Scripts/Utils/Lua/Function.gd create mode 100644 flumi/Scripts/Utils/Lua/ThreadedVM.gd create mode 100644 flumi/Scripts/Utils/Lua/ThreadedVM.gd.uid diff --git a/flumi/Scripts/B9/HTMLParser.gd b/flumi/Scripts/B9/HTMLParser.gd index ab2486e..3d21cba 100644 --- a/flumi/Scripts/B9/HTMLParser.gd +++ b/flumi/Scripts/B9/HTMLParser.gd @@ -354,8 +354,8 @@ func get_all_scripts() -> Array[String]: return get_attribute_values("script", "src") func process_scripts(lua_api: LuaAPI, lua_vm) -> void: - if not lua_api or not lua_vm: - print("Warning: Lua API or VM not available for script processing") + if not lua_api: + print("Warning: Lua API not available for script processing") return lua_api.dom_parser = self diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd index cc48d49..fd32576 100644 --- a/flumi/Scripts/B9/Lua.gd +++ b/flumi/Scripts/B9/Lua.gd @@ -1,6 +1,9 @@ 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 @@ -17,12 +20,15 @@ var event_subscriptions: Dictionary = {} var next_subscription_id: int = 1 var next_callback_ref: int = 1 -var timeout_manager: LuaTimeoutManager var element_id_counter: int = 1 var element_id_registry: Dictionary = {} func _init(): timeout_manager = LuaTimeoutManager.new() + threaded_vm = ThreadedLuaVM.new() + threaded_vm.script_completed.connect(_on_threaded_script_completed) + 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") @@ -57,7 +63,7 @@ func _gurt_select_handler(vm: LuauVM) -> int: vm.lua_pushstring(element.tag_name) vm.lua_setfield(-2, "_tag_name") - add_element_methods(vm) + LuaDOMUtils.add_element_methods(vm, self) return 1 # selectAll() function to find multiple elements @@ -79,7 +85,7 @@ func _gurt_select_all_handler(vm: LuauVM) -> int: vm.lua_pushstring(element.tag_name) vm.lua_setfield(-2, "_tag_name") - add_element_methods(vm) + LuaDOMUtils.add_element_methods(vm, self) # Add to array at index vm.lua_rawseti(-2, index) @@ -119,355 +125,22 @@ func _gurt_create_handler(vm: LuauVM) -> int: vm.lua_pushboolean(true) vm.lua_setfield(-2, "_is_dynamic") - add_element_methods(vm) + LuaDOMUtils.add_element_methods(vm, self) return 1 -func add_element_methods(vm: LuauVM, index: String = "element") -> void: - # Add methods directly to element table first - vm.lua_pushcallable(_element_on_event_handler, index + ".on") - vm.lua_setfield(-2, "on") - - vm.lua_pushcallable(_element_append_handler, index + ".append") - vm.lua_setfield(-2, "append") - - vm.lua_pushcallable(_element_remove_handler, index + ".remove") - vm.lua_setfield(-2, "remove") - - vm.lua_pushcallable(_element_get_attribute_handler, index + ".getAttribute") - vm.lua_setfield(-2, "getAttribute") - - vm.lua_pushcallable(_element_set_attribute_handler, index + ".setAttribute") - vm.lua_setfield(-2, "setAttribute") - - LuaDOMUtils.add_enhanced_element_methods(vm, self, index) - - vm.lua_newtable() - - vm.lua_pushcallable(_index_handler, index + ".__index") - vm.lua_setfield(-2, "__index") - - vm.lua_pushcallable(_element_newindex_handler, index + ".__newindex") - vm.lua_setfield(-2, "__newindex") - - vm.lua_setmetatable(-2) +var timeout_manager: LuaTimeoutManager -func _index_handler(vm: LuauVM) -> int: - return LuaDOMUtils._index_handler(vm, self) - -func _element_index_handler(vm: LuauVM) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) - var key: String = vm.luaL_checkstring(2) - - vm.lua_getfield(1, "_element_id") - var element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - match key: - "text": - var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) - var text = "" - - var text_node = get_dom_node(dom_node, "text") - if text_node: - if text_node.has_method("get_text"): - text = text_node.get_text() - else: - text = text_node.text - - vm.lua_pushstring(text) - return 1 - "children": - # Find the element - var element: HTMLParser.HTMLElement = null - if element_id == "body": - element = dom_parser.find_first("body") - else: - element = dom_parser.find_by_id(element_id) - - vm.lua_newtable() - var index = 1 - - if element: - for child in element.children: - vm.lua_newtable() - vm.lua_pushstring(child.tag_name) - vm.lua_setfield(-2, "tagName") - vm.lua_pushstring(child.get_text_content()) - vm.lua_setfield(-2, "text") - - vm.lua_rawseti(-2, index) - index += 1 - - return 1 - "classList": - # Create classList object with add, remove, toggle methods - vm.lua_newtable() - - # Add methods to classList using the utility class - vm.lua_pushcallable(_element_classlist_add_wrapper, "classList.add") - vm.lua_setfield(-2, "add") - - vm.lua_pushcallable(_element_classlist_remove_wrapper, "classList.remove") - vm.lua_setfield(-2, "remove") - - vm.lua_pushcallable(_element_classlist_toggle_wrapper, "classList.toggle") - vm.lua_setfield(-2, "toggle") - - # Store element reference for the classList methods - vm.lua_getfield(1, "_element_id") - vm.lua_setfield(-2, "_element_id") - - return 1 - _: - # Fall back to checking the original table for methods - vm.lua_pushvalue(1) # Push the original table - vm.lua_pushstring(key) # Push the key - vm.lua_rawget(-2) # Get table[key] without triggering metamethods - vm.lua_remove(-2) # Remove the table, leaving just the result - return 1 - -func _element_newindex_handler(vm: LuauVM) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) - var key: String = vm.luaL_checkstring(2) - var value = vm.lua_tovariant(3) - - vm.lua_getfield(1, "_element_id") - var element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - match key: - "text": - var text: String = str(value) - var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) - var text_node = get_dom_node(dom_node, "text") - if text_node: - text_node.text = text - _: - # Ignore unknown properties - pass - - return 0 - -# append() function to add a child element -func _element_append_handler(vm: LuauVM) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) - vm.luaL_checktype(2, vm.LUA_TTABLE) - - # Get parent element info - vm.lua_getfield(1, "_element_id") - var parent_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Get child element info - vm.lua_getfield(2, "_element_id") - var child_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - vm.lua_getfield(2, "_is_dynamic") - vm.lua_pop(1) - - # Find parent element - var parent_element: HTMLParser.HTMLElement = null - if parent_element_id == "body": - parent_element = dom_parser.find_first("body") - else: - parent_element = dom_parser.find_by_id(parent_element_id) - - if not parent_element: - return 0 - - # Find child element - var child_element = dom_parser.find_by_id(child_element_id) - if not child_element: - return 0 - - # Add child to parent in DOM tree - child_element.parent = parent_element - parent_element.children.append(child_element) - - # If the parent is already rendered, we need to create and add the visual node - var parent_dom_node: Node = null - if parent_element_id == "body": - var main_scene = get_node("/root/Main") - if main_scene: - parent_dom_node = main_scene.website_container - else: - parent_dom_node = dom_parser.parse_result.dom_nodes.get(parent_element_id, null) - - if parent_dom_node: - _render_new_element.call_deferred(child_element, parent_dom_node) - - return 0 - -# remove() function to remove an element -func _element_remove_handler(vm: LuauVM) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) - - vm.lua_getfield(1, "_element_id") - var element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Find the element in DOM - var element = dom_parser.find_by_id(element_id) - if not element: - return 0 - - # Remove from parent's children array - if element.parent: - var parent_children = element.parent.children - var idx = parent_children.find(element) - if idx >= 0: - parent_children.remove_at(idx) - - # Remove the visual node - var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) - if dom_node: - dom_node.queue_free() - dom_parser.parse_result.dom_nodes.erase(element_id) - - # Remove from all_elements array - var all_elements = dom_parser.parse_result.all_elements - var index = all_elements.find(element) - if index >= 0: - all_elements.remove_at(index) - - # Remove from element_id_registry to avoid memory leaks - if element_id_registry.has(element): - element_id_registry.erase(element) - - return 0 - -# getAttribute() function to get element attribute -func _element_get_attribute_handler(vm: LuauVM) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) - var attribute_name: String = vm.luaL_checkstring(2) - - vm.lua_getfield(1, "_element_id") - var element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Find the element - var element: HTMLParser.HTMLElement = null - if element_id == "body": - element = dom_parser.find_first("body") - else: - element = dom_parser.find_by_id(element_id) - - if not element: - vm.lua_pushnil() - return 1 - - # Get the attribute value - var attribute_value = element.get_attribute(attribute_name) - if attribute_value.is_empty(): - vm.lua_pushnil() - else: - vm.lua_pushstring(attribute_value) - - return 1 - -# setAttribute() function to set element attribute -func _element_set_attribute_handler(vm: LuauVM) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) - var attribute_name: String = vm.luaL_checkstring(2) - var attribute_value: String = vm.luaL_checkstring(3) - - vm.lua_getfield(1, "_element_id") - var element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Find the element - var element: HTMLParser.HTMLElement = null - if element_id == "body": - element = dom_parser.find_first("body") - else: - element = dom_parser.find_by_id(element_id) - - if not element: - return 0 - - if attribute_value == "": - element.attributes.erase(attribute_name) - else: - element.set_attribute(attribute_name, attribute_value) - - # Trigger visual update by calling init() again - var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) - if dom_node and dom_node.has_method("init"): - dom_node.init(element, dom_parser) - - return 0 - -func _element_classlist_add_wrapper(vm: LuauVM) -> int: - return LuaClassListUtils.element_classlist_add_handler(vm, dom_parser) - -func _element_classlist_remove_wrapper(vm: LuauVM) -> int: - return LuaClassListUtils.element_classlist_remove_handler(vm, dom_parser) - -func _element_classlist_toggle_wrapper(vm: LuauVM) -> int: - return LuaClassListUtils.element_classlist_toggle_handler(vm, dom_parser) - -# DOM manipulation wrapper functions -func _element_insert_before_wrapper(vm: LuauVM) -> int: - return LuaDOMUtils.insert_before_handler(vm, dom_parser, self) - -func _element_insert_after_wrapper(vm: LuauVM) -> int: - return LuaDOMUtils.insert_after_handler(vm, dom_parser, self) - -func _element_replace_wrapper(vm: LuauVM) -> int: - return LuaDOMUtils.replace_handler(vm, dom_parser, self) - -func _element_clone_wrapper(vm: LuauVM) -> int: - return LuaDOMUtils.clone_handler(vm, dom_parser, self) - -# DOM traversal property wrapper functions -func _get_element_parent_wrapper(vm: LuauVM, lua_api: LuaAPI) -> int: - return LuaDOMUtils.get_element_parent_handler(vm, dom_parser, lua_api) - -func _get_element_next_sibling_wrapper(vm: LuauVM, lua_api: LuaAPI) -> int: - return LuaDOMUtils.get_element_next_sibling_handler(vm, dom_parser, lua_api) - -func _get_element_previous_sibling_wrapper(vm: LuauVM, lua_api: LuaAPI) -> int: - return LuaDOMUtils.get_element_previous_sibling_handler(vm, dom_parser, lua_api) - -func _get_element_first_child_wrapper(vm: LuauVM, lua_api: LuaAPI) -> int: - return LuaDOMUtils.get_element_first_child_handler(vm, dom_parser, lua_api) - -func _get_element_last_child_wrapper(vm: LuauVM, lua_api: LuaAPI) -> int: - return LuaDOMUtils.get_element_last_child_handler(vm, dom_parser, lua_api) - -func _render_new_element(element: HTMLParser.HTMLElement, parent_node: Node) -> void: - # Get reference to main scene for rendering - var main_scene = get_node("/root/Main") - if not main_scene: - return - - # Create the visual node for the element - var element_node = await main_scene.create_element_node(element, dom_parser) - if not element_node: - LuaPrintUtils.lua_print_direct("Failed to create visual node for element: " + str(element)) - return - - # Set metadata so ul/ol can detect dynamically added li elements - element_node.set_meta("html_element", element) - - # Register the DOM node - dom_parser.register_dom_node(element, element_node) - - # Add to parent - handle body special case - var container_node = parent_node - if parent_node is MarginContainer and parent_node.get_child_count() > 0: - container_node = parent_node.get_child(0) - elif parent_node == main_scene.website_container: - container_node = parent_node - - main_scene.safe_add_child(container_node, element_node) +func _ensure_timeout_manager(): + if not timeout_manager: + timeout_manager = LuaTimeoutManager.new() # Timeout management handlers func _gurt_set_timeout_handler(vm: LuauVM) -> int: - return timeout_manager.set_timeout_handler(vm, self) + _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) # Event system handlers @@ -480,20 +153,22 @@ func _element_on_event_handler(vm: LuauVM) -> int: var element_id: String = vm.lua_tostring(-1) vm.lua_pop(1) - var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) - if not dom_node: - vm.lua_pushnil() - return 1 - + # Create a proper subscription with real ID var subscription = _create_subscription(vm, element_id, event_name) event_subscriptions[subscription.id] = subscription - var signal_node = get_dom_node(dom_node, "signal") - var success = LuaEventUtils.connect_element_event(signal_node, event_name, subscription) - if not success: - print("ERROR: Failed to connect ", event_name, " event for ", element_id) + # Register the event on main thread + call_deferred("_register_event_on_main_thread", element_id, event_name, subscription.callback_ref, subscription.id) - return _handle_subscription_result(vm, subscription, success) + # 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) @@ -565,13 +240,7 @@ func _on_event_triggered(subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): return - subscription.vm.lua_rawgeti(subscription.vm.LUA_REGISTRYINDEX, subscription.callback_ref) - if subscription.vm.lua_isfunction(-1): - if subscription.vm.lua_pcall(0, 0, 0) != subscription.vm.LUA_OK: - print("GURT ERROR in event callback: ", subscription.vm.lua_tostring(-1)) - subscription.vm.lua_pop(1) - else: - subscription.vm.lua_pop(1) + _execute_lua_callback(subscription) func _on_gui_input_click(event: InputEvent, subscription: EventSubscription) -> void: if not event_subscriptions.has(subscription.id): @@ -618,31 +287,18 @@ func _on_focus_gui_input(event: InputEvent, subscription: EventSubscription) -> 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: - if not event_subscriptions.has(subscription.id): - return - - if subscription.event_name == "mouseenter": - _execute_lua_callback(subscription) + _handle_body_event(subscription, "mouseenter", {}) func _on_body_mouse_exit(subscription: EventSubscription) -> void: - if not event_subscriptions.has(subscription.id): - return - - if subscription.event_name == "mouseexit": - _execute_lua_callback(subscription) + _handle_body_event(subscription, "mouseexit", {}) func _execute_lua_callback(subscription: EventSubscription, args: Array = []) -> void: - subscription.vm.lua_rawgeti(subscription.vm.LUA_REGISTRYINDEX, subscription.callback_ref) - if subscription.vm.lua_isfunction(-1): - for arg in args: - subscription.vm.lua_pushvariant(arg) - - if subscription.vm.lua_pcall(args.size(), 0, 0) != subscription.vm.LUA_OK: - print("GURT ERROR in callback: ", subscription.vm.lua_tostring(-1)) - subscription.vm.lua_pop(1) - else: - subscription.vm.lua_pop(1) + 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): @@ -814,13 +470,18 @@ func get_dom_node(node: Node, purpose: String = "general") -> Node: if not node: return null - if node is MarginContainer: + 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"): @@ -860,12 +521,185 @@ func get_dom_node(node: Node, purpose: String = "general") -> Node: # Main execution function func execute_lua_script(code: String, vm: LuauVM): - vm.open_libraries([vm.LUA_BASE_LIB, vm.LUA_BIT32_LIB, - vm.LUA_COROUTINE_LIB, vm.LUA_MATH_LIB, vm.LUA_UTF8_LIB, - vm.LUA_TABLE_LIB, vm.LUA_STRING_LIB, vm.LUA_VECTOR_LIB]) + 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) - LuaFunctionUtils.setup_gurt_api(vm, self, dom_parser) + 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 - if vm.lua_dostring(code) != vm.LUA_OK: - print("LUA ERROR: ", vm.lua_tostring(-1)) - vm.lua_pop(1) + 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: + # Always update the HTML element's text content first + element.text_content = text + + # If the element has a DOM node, update it too + 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: + var text_node = get_dom_node(dom_node, "text") + if text_node: + if text_node.has_method("set_text"): + text_node.set_text(text) + elif "text" in text_node: + text_node.text = text + +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 the element's cached text content from the HTML element + # This avoids the need for a callback system since we have the text cached + 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: + 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 _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() diff --git a/flumi/Scripts/Tags/button.gd b/flumi/Scripts/Tags/button.gd index 5bde3a9..4bd6fb2 100644 --- a/flumi/Scripts/Tags/button.gd +++ b/flumi/Scripts/Tags/button.gd @@ -27,7 +27,7 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void: apply_button_styles(element, parser) - parser.register_dom_node(element, button_node) + parser.register_dom_node(element, self) func apply_button_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void: if not element or not parser: diff --git a/flumi/Scripts/Utils/Lua/Class.gd b/flumi/Scripts/Utils/Lua/Class.gd index b718c9f..9e1bf1a 100644 --- a/flumi/Scripts/Utils/Lua/Class.gd +++ b/flumi/Scripts/Utils/Lua/Class.gd @@ -16,17 +16,22 @@ static func element_classlist_add_handler(vm: LuauVM, dom_parser: HTMLParser) -> return 0 # Get classes + var classes_to_add = [css_class] + var current_style = element.get_attribute("style", "") var style_classes = CSSParser.smart_split_utility_classes(current_style) if current_style.length() > 0 else [] # Add new css_class if not already present - if css_class not in style_classes: - style_classes.append(css_class) + var changed = false + for class_to_add in classes_to_add: + if class_to_add not in style_classes: + style_classes.append(class_to_add) + changed = true + + if changed: var new_style_attr = " ".join(style_classes) element.set_attribute("style", new_style_attr) trigger_element_restyle(element, dom_parser) - else: - print("DEBUG: classList.add - Class already exists") return 0 @@ -43,25 +48,26 @@ static func element_classlist_remove_handler(vm: LuauVM, dom_parser: HTMLParser) if not element: return 0 - # Get style attribute + var classes_to_remove = [css_class] + var current_style = element.get_attribute("style", "") - if current_style.length() == 0: - return 0 + var style_classes = CSSParser.smart_split_utility_classes(current_style) if current_style.length() > 0 else [] - var style_classes = CSSParser.smart_split_utility_classes(current_style) - var clean_classes = [] - for style_cls in style_classes: - if style_cls != css_class: - clean_classes.append(style_cls) + # Remove classes if present + var changed = false + for class_to_remove in classes_to_remove: + if class_to_remove in style_classes: + style_classes.erase(class_to_remove) + changed = true - # Update style attribute - if clean_classes.size() > 0: - var new_style_attr = " ".join(clean_classes) - element.set_attribute("style", new_style_attr) - else: - element.attributes.erase("style") + if changed: + var new_style_attr = " ".join(style_classes) if style_classes.size() > 0 else "" + if new_style_attr.length() > 0: + element.set_attribute("style", new_style_attr) + else: + element.attributes.erase("style") + trigger_element_restyle(element, dom_parser) - trigger_element_restyle(element, dom_parser) return 0 static func element_classlist_toggle_handler(vm: LuauVM, dom_parser: HTMLParser) -> int: @@ -78,34 +84,111 @@ static func element_classlist_toggle_handler(vm: LuauVM, dom_parser: HTMLParser) vm.lua_pushboolean(false) return 1 - # Get style attribute + var utility_classes = [css_class] + var current_style = element.get_attribute("style", "") var style_classes = CSSParser.smart_split_utility_classes(current_style) if current_style.length() > 0 else [] - var has_css_class = css_class in style_classes + var has_all_classes = true + for utility_class in utility_classes: + if utility_class not in style_classes: + has_all_classes = false + break - if has_css_class: - # Remove css_class - var new_classes = [] - for style_cls in style_classes: - if style_cls != css_class: - new_classes.append(style_cls) - - if new_classes.size() > 0: - element.set_attribute("style", " ".join(new_classes)) - else: - element.attributes.erase("style") - + if has_all_classes: + # Remove all utility classes + for utility_class in utility_classes: + style_classes.erase(utility_class) vm.lua_pushboolean(false) else: - # Add css_class - style_classes.append(css_class) - element.set_attribute("style", " ".join(style_classes)) + for utility_class in utility_classes: + if utility_class not in style_classes: + style_classes.append(utility_class) vm.lua_pushboolean(true) + if style_classes.size() > 0: + var new_style_attr = " ".join(style_classes) + element.set_attribute("style", new_style_attr) + else: + element.attributes.erase("style") + trigger_element_restyle(element, dom_parser) return 1 +# DOM operation handlers for class management +static func handle_add_class(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var cls: String = operation.class_name + + var element = dom_parser.find_by_id(element_id) if element_id != "body" else dom_parser.find_first("body") + if element: + # Use the original working logic from LuaClassListUtils + var current_style = element.get_attribute("style", "") + var style_classes = CSSParser.smart_split_utility_classes(current_style) if current_style.length() > 0 else [] + + # Add new class if not already present + if cls not in style_classes: + style_classes.append(cls) + var new_style_attr = " ".join(style_classes) + element.set_attribute("style", new_style_attr) + trigger_element_restyle(element, dom_parser) + +static func handle_remove_class(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var cls: String = operation.class_name + + var element = dom_parser.find_by_id(element_id) if element_id != "body" else dom_parser.find_first("body") + if element: + # Use the original working logic from LuaClassListUtils + var current_style = element.get_attribute("style", "") + if current_style.length() == 0: + return + + var style_classes = CSSParser.smart_split_utility_classes(current_style) + var clean_classes = [] + for style_cls in style_classes: + if style_cls != cls: + clean_classes.append(style_cls) + + # Update style attribute + if clean_classes.size() > 0: + var new_style_attr = " ".join(clean_classes) + element.set_attribute("style", new_style_attr) + else: + element.attributes.erase("style") + + trigger_element_restyle(element, dom_parser) + +static func handle_toggle_class(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var cls: String = operation.class_name + + var element = dom_parser.find_by_id(element_id) if element_id != "body" else dom_parser.find_first("body") + if element: + # Use the original working logic from LuaClassListUtils + var current_style = element.get_attribute("style", "") + var style_classes = CSSParser.smart_split_utility_classes(current_style) if current_style.length() > 0 else [] + + var has_class = cls in style_classes + + if has_class: + # Remove class + var new_classes = [] + for style_cls in style_classes: + if style_cls != cls: + new_classes.append(style_cls) + + if new_classes.size() > 0: + element.set_attribute("style", " ".join(new_classes)) + else: + element.attributes.erase("style") + else: + # Add class + style_classes.append(cls) + element.set_attribute("style", " ".join(style_classes)) + + trigger_element_restyle(element, dom_parser) + static func trigger_element_restyle(element: HTMLParser.HTMLElement, dom_parser: HTMLParser) -> void: # Find DOM node for element var element_id = element.get_attribute("id") diff --git a/flumi/Scripts/Utils/Lua/DOM.gd b/flumi/Scripts/Utils/Lua/DOM.gd index 7cc79ec..f3880f9 100644 --- a/flumi/Scripts/Utils/Lua/DOM.gd +++ b/flumi/Scripts/Utils/Lua/DOM.gd @@ -94,40 +94,78 @@ static func get_element_last_child_handler(vm: LuauVM, dom_parser: HTMLParser, l return 1 # DOM Manipulation Methods -static func insert_before_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) # parent - vm.luaL_checktype(2, vm.LUA_TTABLE) # new_child - vm.luaL_checktype(3, vm.LUA_TTABLE) # reference_child + +static func handle_element_append(operation: Dictionary, dom_parser: HTMLParser, lua_api) -> void: + var parent_id: String = operation.parent_id + var child_id: String = operation.child_id - # Get parent element info - vm.lua_getfield(1, "_element_id") - var parent_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) + # Find the parent and child elements + var parent_element = dom_parser.find_by_id(parent_id) if parent_id != "body" else dom_parser.find_first("body") + var child_element = dom_parser.find_by_id(child_id) if child_id != "body" else dom_parser.find_first("body") - # Get new child element info - vm.lua_getfield(2, "_element_id") - var new_child_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) + if not parent_element or not child_element: + return - # Get reference child element info - vm.lua_getfield(3, "_element_id") - var reference_child_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) + # Remove child from its current parent if it has one + if child_element.parent: + var current_parent = child_element.parent + var current_index = current_parent.children.find(child_element) + if current_index >= 0: + current_parent.children.remove_at(current_index) - # Find elements - var parent_element = find_element_by_id(parent_element_id, dom_parser) - var new_child_element = find_element_by_id(new_child_element_id, dom_parser) - var reference_child_element = find_element_by_id(reference_child_element_id, dom_parser) + # Append child to new parent + child_element.parent = parent_element + parent_element.children.append(child_element) + + # Handle visual rendering if parent is already rendered + var parent_dom_node: Node = null + if parent_id == "body": + var main_scene = lua_api.get_node("/root/Main") + if main_scene: + parent_dom_node = main_scene.website_container + else: + parent_dom_node = dom_parser.parse_result.dom_nodes.get(parent_id, null) + + if parent_dom_node: + # Render the appended element + render_new_element.call_deferred(child_element, parent_dom_node, dom_parser, lua_api) + +static func handle_element_remove(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + + var element = dom_parser.find_by_id(element_id) if element_id != "body" else dom_parser.find_first("body") + if element and element.parent: + # Remove element from HTML parser tree + var parent_element = element.parent + var element_index = parent_element.children.find(element) + if element_index >= 0: + parent_element.children.remove_at(element_index) + element.parent = null + + # Remove element from DOM tree + var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) + if dom_node and dom_node.get_parent(): + dom_node.get_parent().remove_child(dom_node) + dom_node.queue_free() + dom_parser.parse_result.dom_nodes.erase(element_id) + + # Remove from parser's all_elements list + var all_elements_index = dom_parser.parse_result.all_elements.find(element) + if all_elements_index >= 0: + dom_parser.parse_result.all_elements.remove_at(all_elements_index) + +static func handle_insert_before(operation: Dictionary, dom_parser: HTMLParser, lua_api) -> void: + var parent_id: String = operation.parent_id + var new_child_id: String = operation.new_child_id + var reference_child_id: String = operation.reference_child_id + + # Find the elements + var parent_element = dom_parser.find_by_id(parent_id) if parent_id != "body" else dom_parser.find_first("body") + var new_child_element = dom_parser.find_by_id(new_child_id) if new_child_id != "body" else dom_parser.find_first("body") + var reference_child_element = dom_parser.find_by_id(reference_child_id) if reference_child_id != "body" else dom_parser.find_first("body") if not parent_element or not new_child_element or not reference_child_element: - vm.lua_pushnil() - return 1 - - # Find reference child index in parent's children - var reference_index = parent_element.children.find(reference_child_element) - if reference_index < 0: - vm.lua_pushnil() - return 1 + return # Remove new child from its current parent if it has one if new_child_element.parent: @@ -136,51 +174,37 @@ static func insert_before_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) - if current_index >= 0: current_parent.children.remove_at(current_index) - # Insert new child before reference child - new_child_element.parent = parent_element - parent_element.children.insert(reference_index, new_child_element) - - # Handle visual rendering if parent is already rendered - handle_visual_insertion_by_reference(parent_element_id, new_child_element, reference_child_element_id, true, dom_parser, lua_api) - - # Return the new child - vm.lua_pushvalue(2) - return 1 + # Find reference child position in parent + var reference_index = parent_element.children.find(reference_child_element) + if reference_index >= 0: + # Insert new child before reference child + new_child_element.parent = parent_element + parent_element.children.insert(reference_index, new_child_element) + + # Handle visual rendering + var parent_dom_node: Node = null + if parent_id == "body": + var main_scene = lua_api.get_node("/root/Main") + if main_scene: + parent_dom_node = main_scene.website_container + else: + parent_dom_node = dom_parser.parse_result.dom_nodes.get(parent_id, null) + + if parent_dom_node: + handle_visual_insertion_by_reference(parent_id, new_child_element, reference_child_id, true, dom_parser, lua_api) -static func insert_after_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) # parent - vm.luaL_checktype(2, vm.LUA_TTABLE) # new_child - vm.luaL_checktype(3, vm.LUA_TTABLE) # reference_child +static func handle_insert_after(operation: Dictionary, dom_parser: HTMLParser, lua_api) -> void: + var parent_id: String = operation.parent_id + var new_child_id: String = operation.new_child_id + var reference_child_id: String = operation.reference_child_id - # Get parent element info - vm.lua_getfield(1, "_element_id") - var parent_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Get new child element info - vm.lua_getfield(2, "_element_id") - var new_child_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Get reference child element info - vm.lua_getfield(3, "_element_id") - var reference_child_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Find elements - var parent_element = find_element_by_id(parent_element_id, dom_parser) - var new_child_element = find_element_by_id(new_child_element_id, dom_parser) - var reference_child_element = find_element_by_id(reference_child_element_id, dom_parser) + # Find the elements + var parent_element = dom_parser.find_by_id(parent_id) if parent_id != "body" else dom_parser.find_first("body") + var new_child_element = dom_parser.find_by_id(new_child_id) if new_child_id != "body" else dom_parser.find_first("body") + var reference_child_element = dom_parser.find_by_id(reference_child_id) if reference_child_id != "body" else dom_parser.find_first("body") if not parent_element or not new_child_element or not reference_child_element: - vm.lua_pushnil() - return 1 - - # Find reference child index in parent's children - var reference_index = parent_element.children.find(reference_child_element) - if reference_index < 0: - vm.lua_pushnil() - return 1 + return # Remove new child from its current parent if it has one if new_child_element.parent: @@ -189,51 +213,37 @@ static func insert_after_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) -> if current_index >= 0: current_parent.children.remove_at(current_index) - # Insert new child after reference child - new_child_element.parent = parent_element - parent_element.children.insert(reference_index + 1, new_child_element) - - # Handle visual rendering if parent is already rendered - handle_visual_insertion_by_reference(parent_element_id, new_child_element, reference_child_element_id, false, dom_parser, lua_api) - - # Return the new child - vm.lua_pushvalue(2) - return 1 + # Find reference child position in parent + var reference_index = parent_element.children.find(reference_child_element) + if reference_index >= 0: + # Insert new child after reference child + new_child_element.parent = parent_element + parent_element.children.insert(reference_index + 1, new_child_element) + + # Handle visual rendering + var parent_dom_node: Node = null + if parent_id == "body": + var main_scene = lua_api.get_node("/root/Main") + if main_scene: + parent_dom_node = main_scene.website_container + else: + parent_dom_node = dom_parser.parse_result.dom_nodes.get(parent_id, null) + + if parent_dom_node: + handle_visual_insertion_by_reference(parent_id, new_child_element, reference_child_id, false, dom_parser, lua_api) -static func replace_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) # parent - vm.luaL_checktype(2, vm.LUA_TTABLE) # new_child - vm.luaL_checktype(3, vm.LUA_TTABLE) # old_child +static func handle_replace_child(operation: Dictionary, dom_parser: HTMLParser, lua_api) -> void: + var parent_id: String = operation.parent_id + var new_child_id: String = operation.new_child_id + var old_child_id: String = operation.old_child_id - # Get parent element info - vm.lua_getfield(1, "_element_id") - var parent_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Get new child element info - vm.lua_getfield(2, "_element_id") - var new_child_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Get old child element info - vm.lua_getfield(3, "_element_id") - var old_child_element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Find elements - var parent_element = find_element_by_id(parent_element_id, dom_parser) - var new_child_element = find_element_by_id(new_child_element_id, dom_parser) - var old_child_element = find_element_by_id(old_child_element_id, dom_parser) + # Find the elements + var parent_element = dom_parser.find_by_id(parent_id) if parent_id != "body" else dom_parser.find_first("body") + var new_child_element = dom_parser.find_by_id(new_child_id) if new_child_id != "body" else dom_parser.find_first("body") + var old_child_element = dom_parser.find_by_id(old_child_id) if old_child_id != "body" else dom_parser.find_first("body") if not parent_element or not new_child_element or not old_child_element: - vm.lua_pushnil() - return 1 - - # Find old child index in parent's children - var old_index = parent_element.children.find(old_child_element) - if old_index < 0: - vm.lua_pushnil() - return 1 + return # Remove new child from its current parent if it has one if new_child_element.parent: @@ -242,46 +252,43 @@ static func replace_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) -> int: if current_index >= 0: current_parent.children.remove_at(current_index) - # Replace old child with new child - old_child_element.parent = null - new_child_element.parent = parent_element - parent_element.children[old_index] = new_child_element - - # Handle visual updates - handle_visual_replacement(old_child_element_id, new_child_element, parent_element_id, dom_parser, lua_api) - - # Return the old child - vm.lua_pushvalue(3) - return 1 + # Find old child position in parent + var old_index = parent_element.children.find(old_child_element) + if old_index >= 0: + # Replace old child with new child + new_child_element.parent = parent_element + parent_element.children[old_index] = new_child_element + old_child_element.parent = null + + # Handle visual rendering + handle_visual_replacement(old_child_id, new_child_element, parent_id, dom_parser, lua_api) -static func clone_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) # element to clone - var deep: bool = false +static func render_new_element(element: HTMLParser.HTMLElement, parent_node: Node, dom_parser: HTMLParser, lua_api) -> void: + # Get reference to main scene for rendering + var main_scene = lua_api.get_node("/root/Main") + if not main_scene: + return - if vm.lua_gettop() >= 2: - deep = vm.lua_toboolean(2) - - # Get element info - vm.lua_getfield(1, "_element_id") - var element_id: String = vm.lua_tostring(-1) - vm.lua_pop(1) - - # Find the element - var element = find_element_by_id(element_id, dom_parser) - if not element: - vm.lua_pushnil() - return 1 - - # Clone the element - var cloned_element = clone_element(element, deep) - - # Add cloned element to parser's element collection - dom_parser.parse_result.all_elements.append(cloned_element) - - # Create Lua element wrapper with full functionality - create_element_wrapper(vm, cloned_element, lua_api) - - return 1 + # Create the visual node for the element + var element_node = await main_scene.create_element_node(element, dom_parser) + if not element_node: + LuaPrintUtils.lua_print_direct("Failed to create visual node for element: " + str(element)) + return + + # Set metadata so ul/ol can detect dynamically added li elements + element_node.set_meta("html_element", element) + + # Register the DOM node + dom_parser.register_dom_node(element, element_node) + + # Add to parent - handle body special case + var container_node = parent_node + if parent_node is MarginContainer and parent_node.get_child_count() > 0: + container_node = parent_node.get_child(0) + elif parent_node == main_scene.website_container: + container_node = parent_node + + main_scene.safe_add_child(container_node, element_node) # Helper functions static func find_element_by_id(element_id: String, dom_parser: HTMLParser) -> HTMLParser.HTMLElement: @@ -290,26 +297,6 @@ static func find_element_by_id(element_id: String, dom_parser: HTMLParser) -> HT else: return dom_parser.find_by_id(element_id) -static func create_element_wrapper(vm: LuauVM, element: HTMLParser.HTMLElement, lua_api: LuaAPI) -> void: - vm.lua_newtable() - - var element_id: String - if element.tag_name == "body": - element_id = "body" - else: - element_id = element.get_attribute("id") - if element_id.is_empty(): - element_id = lua_api.get_or_assign_element_id(element) - element.set_attribute("id", element_id) - - vm.lua_pushstring(element_id) - vm.lua_setfield(-2, "_element_id") - vm.lua_pushstring(element.tag_name) - vm.lua_setfield(-2, "_tag_name") - - if lua_api: - lua_api.add_element_methods(vm) - static func clone_element(element: HTMLParser.HTMLElement, deep: bool) -> HTMLParser.HTMLElement: var cloned = HTMLParser.HTMLElement.new(element.tag_name) @@ -370,18 +357,7 @@ static func handle_visual_replacement(old_child_element_id: String, new_child_el if parent_dom_node: render_new_element_at_position.call_deferred(new_child_element, parent_dom_node, old_position, dom_parser) -static func add_enhanced_element_methods(vm: LuauVM, lua_api, index: String = "element") -> void: - vm.lua_pushcallable(lua_api._element_insert_before_wrapper, index + ".insertBefore") - vm.lua_setfield(-2, "insertBefore") - - vm.lua_pushcallable(lua_api._element_insert_after_wrapper, index + ".insertAfter") - vm.lua_setfield(-2, "insertAfter") - - vm.lua_pushcallable(lua_api._element_replace_wrapper, index + ".replace") - vm.lua_setfield(-2, "replace") - - vm.lua_pushcallable(lua_api._element_clone_wrapper, index + ".clone") - vm.lua_setfield(-2, "clone") + static func is_same_element_visual_node(node1: Node, node2: Node) -> bool: if node1 == node2: @@ -475,20 +451,531 @@ static func render_new_element_by_reference(element: HTMLParser.HTMLElement, par if insert_position >= 0 and insert_position < container_node.get_child_count() - 1: container_node.move_child(element_node, insert_position) +# Threaded-safe wrapper functions +static func emit_dom_operation(lua_api: LuaAPI, operation: Dictionary) -> void: + lua_api.threaded_vm.call_deferred("_emit_dom_operation_request", operation) + +static func create_element_wrapper(vm: LuauVM, element: HTMLParser.HTMLElement, lua_api: LuaAPI) -> void: + vm.lua_newtable() + + var element_id: String + if element.tag_name == "body": + element_id = "body" + else: + element_id = element.get_attribute("id") + if element_id.is_empty(): + element_id = lua_api.get_or_assign_element_id(element) + element.set_attribute("id", element_id) + + vm.lua_pushstring(element_id) + vm.lua_setfield(-2, "_element_id") + vm.lua_pushstring(element.tag_name) + vm.lua_setfield(-2, "_tag_name") + + add_element_methods(vm, lua_api) + +static func add_element_methods(vm: LuauVM, lua_api: LuaAPI) -> void: + vm.set_meta("lua_api", lua_api) + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_element_on_wrapper"), "element.on") + vm.lua_setfield(-2, "on") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_element_append_wrapper"), "element.append") + vm.lua_setfield(-2, "append") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_element_set_text_wrapper"), "element.setText") + vm.lua_setfield(-2, "setText") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_element_remove_wrapper"), "element.remove") + vm.lua_setfield(-2, "remove") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_element_insert_before_wrapper"), "element.insertBefore") + vm.lua_setfield(-2, "insertBefore") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_element_insert_after_wrapper"), "element.insertAfter") + vm.lua_setfield(-2, "insertAfter") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_element_replace_wrapper"), "element.replace") + vm.lua_setfield(-2, "replace") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_element_clone_wrapper"), "element.clone") + vm.lua_setfield(-2, "clone") + + _add_classlist_support(vm, lua_api) + + vm.lua_newtable() + vm.lua_pushcallable(Callable(LuaDOMUtils, "_element_index_wrapper"), "element.__index") + vm.lua_setfield(-2, "__index") + vm.lua_pushcallable(Callable(LuaDOMUtils, "_element_newindex_wrapper"), "element.__newindex") + vm.lua_setfield(-2, "__newindex") + vm.lua_setmetatable(-2) + +static func _element_on_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + 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) + + var callback_ref = lua_api.next_callback_ref + lua_api.next_callback_ref += 1 + + var subscription_id = lua_api.next_subscription_id + lua_api.next_subscription_id += 1 + + vm.lua_pushvalue(3) + vm.lua_rawseti(vm.LUA_REGISTRYINDEX, callback_ref) + + lua_api.call_deferred("_register_event_on_main_thread", element_id, event_name, callback_ref, subscription_id) + + vm.lua_newtable() + vm.lua_pushinteger(subscription_id) + vm.lua_setfield(-2, "_subscription_id") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_unsubscribe_wrapper"), "subscription.unsubscribe") + vm.lua_setfield(-2, "unsubscribe") + return 1 + +static func _element_append_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + # Queue append operation for main thread + vm.luaL_checktype(1, vm.LUA_TTABLE) # parent + vm.luaL_checktype(2, vm.LUA_TTABLE) # child + + vm.lua_getfield(1, "_element_id") + var parent_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + vm.lua_getfield(2, "_element_id") + var child_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "append_element", + "parent_id": parent_id, + "child_id": child_id + } + + emit_dom_operation(lua_api, operation) + return 0 + +static func _element_set_text_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var text: String = vm.luaL_checkstring(2) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var element = lua_api.dom_parser.find_by_id(element_id) if element_id != "body" else lua_api.dom_parser.find_first("body") + if element: + element.text_content = text + + var operation = { + "type": "set_text", + "selector": "#" + element_id, + "text": text + } + + emit_dom_operation(lua_api, operation) + return 0 + +static func _element_remove_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + # Get element ID from self table + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "remove_element", + "element_id": element_id + } + + # Queue operation for main thread + emit_dom_operation(lua_api, operation) + return 0 + +static func _element_insert_before_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + # Get parent element ID from self table + vm.luaL_checktype(1, vm.LUA_TTABLE) # parent + vm.luaL_checktype(2, vm.LUA_TTABLE) # new_child + vm.luaL_checktype(3, vm.LUA_TTABLE) # reference_child + + vm.lua_getfield(1, "_element_id") + var parent_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + vm.lua_getfield(2, "_element_id") + var new_child_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + vm.lua_getfield(3, "_element_id") + var reference_child_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "insert_before", + "parent_id": parent_id, + "new_child_id": new_child_id, + "reference_child_id": reference_child_id + } + + emit_dom_operation(lua_api, operation) + return 0 + +static func _element_insert_after_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + # Get parent element ID from self table + vm.luaL_checktype(1, vm.LUA_TTABLE) # parent + vm.luaL_checktype(2, vm.LUA_TTABLE) # new_child + vm.luaL_checktype(3, vm.LUA_TTABLE) # reference_child + + vm.lua_getfield(1, "_element_id") + var parent_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + vm.lua_getfield(2, "_element_id") + var new_child_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + vm.lua_getfield(3, "_element_id") + var reference_child_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "insert_after", + "parent_id": parent_id, + "new_child_id": new_child_id, + "reference_child_id": reference_child_id + } + + emit_dom_operation(lua_api, operation) + return 0 + +static func _element_replace_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + # Get parent element ID from self table + vm.luaL_checktype(1, vm.LUA_TTABLE) # parent + vm.luaL_checktype(2, vm.LUA_TTABLE) # new_child + vm.luaL_checktype(3, vm.LUA_TTABLE) # old_child + + vm.lua_getfield(1, "_element_id") + var parent_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + vm.lua_getfield(2, "_element_id") + var new_child_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + vm.lua_getfield(3, "_element_id") + var old_child_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "replace_child", + "parent_id": parent_id, + "new_child_id": new_child_id, + "old_child_id": old_child_id + } + + emit_dom_operation(lua_api, operation) + return 0 + +static func _element_clone_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + # Get element ID from self table + vm.luaL_checktype(1, vm.LUA_TTABLE) # element + var deep: bool = true # Default to deep clone + + if vm.lua_gettop() >= 2: + deep = vm.lua_toboolean(2) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Find the element to clone + var element = lua_api.dom_parser.find_by_id(element_id) if element_id != "body" else lua_api.dom_parser.find_first("body") + if element: + var cloned_element = clone_element(element, deep) + + # Assign new ID to cloned element + var new_id = lua_api.get_or_assign_element_id(cloned_element) + + # Add to parser's element collection + lua_api.dom_parser.parse_result.all_elements.append(cloned_element) + + # Create element wrapper for the cloned element + create_element_wrapper(vm, cloned_element, lua_api) + return 1 + + vm.lua_pushnil() + return 1 + +static func _element_index_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + var key: String = vm.luaL_checkstring(2) + + match key: + "text": + # Get lua_api from VM metadata + var lua_api = vm.get_meta("lua_api") as LuaAPI + if lua_api: + # Get element ID and find the element + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var element = lua_api.dom_parser.find_by_id(element_id) if element_id != "body" else lua_api.dom_parser.find_first("body") + if element: + vm.lua_pushstring(element.text_content) + return 1 + + # Fallback to empty string + vm.lua_pushstring("") + return 1 + "children": + # Get lua_api from VM metadata + var lua_api = vm.get_meta("lua_api") as LuaAPI + if lua_api: + # Get element ID and find the element + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var element = lua_api.dom_parser.find_by_id(element_id) if element_id != "body" else lua_api.dom_parser.find_first("body") + if element: + # Create array of child elements + vm.lua_newtable() + var index = 1 + for child in element.children: + create_element_wrapper(vm, child, lua_api) + vm.lua_rawseti(-2, index) + index += 1 + return 1 + + # Fallback to empty array + vm.lua_newtable() + return 1 + _: + # Check for DOM traversal properties first + var lua_api = vm.get_meta("lua_api") as LuaAPI + if lua_api: + match key: + "parent": + return get_element_parent_handler(vm, lua_api.dom_parser, lua_api) + "nextSibling": + return get_element_next_sibling_handler(vm, lua_api.dom_parser, lua_api) + "previousSibling": + return get_element_previous_sibling_handler(vm, lua_api.dom_parser, lua_api) + "firstChild": + return get_element_first_child_handler(vm, lua_api.dom_parser, lua_api) + "lastChild": + return get_element_last_child_handler(vm, lua_api.dom_parser, lua_api) + + # Check if it's a method in the original table + vm.lua_pushvalue(1) + vm.lua_pushstring(key) + vm.lua_rawget(-2) + vm.lua_remove(-2) + return 1 + +static func _add_classlist_support(vm: LuauVM, lua_api: LuaAPI) -> void: + # Create classList table with threaded methods + vm.lua_newtable() + + # Store the element_id in the classList table + vm.lua_getfield(-2, "_element_id") # Get element_id from parent element + vm.lua_setfield(-2, "_element_id") # Store it in classList table + + # Add classList methods + vm.lua_pushcallable(Callable(LuaDOMUtils, "_classlist_add_wrapper"), "classList.add") + vm.lua_setfield(-2, "add") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_classlist_remove_wrapper"), "classList.remove") + vm.lua_setfield(-2, "remove") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_classlist_toggle_wrapper"), "classList.toggle") + vm.lua_setfield(-2, "toggle") + + # Set classList on the element + vm.lua_setfield(-2, "classList") + +static func _classlist_add_wrapper(vm: LuauVM) -> int: + # Get lua_api from VM metadata + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) # classList table + var cls: String = vm.luaL_checkstring(2) + + # Get element_id from classList table + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "add_class", + "element_id": element_id, + "class_name": cls + } + + emit_dom_operation(lua_api, operation) + return 0 + +static func _classlist_remove_wrapper(vm: LuauVM) -> int: + # Get lua_api from VM metadata + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) # classList table + var cls: String = vm.luaL_checkstring(2) + + # Get element_id from classList table + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "remove_class", + "element_id": element_id, + "class_name": cls + } + + emit_dom_operation(lua_api, operation) + return 0 + +static func _classlist_toggle_wrapper(vm: LuauVM) -> int: + # Get lua_api from VM metadata + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) # classList table + var cls: String = vm.luaL_checkstring(2) + + # Get element_id from classList table + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "toggle_class", + "element_id": element_id, + "class_name": cls + } + + emit_dom_operation(lua_api, operation) + return 0 + +static func _element_newindex_wrapper(vm: LuauVM) -> int: + # Get lua_api from VM metadata + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var key: String = vm.luaL_checkstring(2) + var value = vm.lua_tovariant(3) + + match key: + "text": + var text: String = str(value) # Convert value to string + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var element = lua_api.dom_parser.find_by_id(element_id) if element_id != "body" else lua_api.dom_parser.find_first("body") + if element: + element.text_content = text + + # Also queue the DOM operation for visual updates if the element is already rendered + var operation = { + "type": "set_text", + "selector": "#" + element_id, + "text": text + } + + emit_dom_operation(lua_api, operation) + return 0 + _: + # Store in table normally + vm.lua_pushvalue(2) + vm.lua_pushvalue(3) + vm.lua_rawset(1) + return 0 + +static func _unsubscribe_wrapper(vm: LuauVM) -> int: + # Get subscription ID from the subscription table + 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) + + # Get lua_api from VM metadata + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + vm.lua_pushboolean(false) + return 1 + + # Handle unsubscribe on main thread + if subscription_id > 0: + lua_api.call_deferred("_unsubscribe_event_on_main_thread", subscription_id) + vm.lua_pushboolean(true) + else: + vm.lua_pushboolean(false) + + return 1 + static func _index_handler(vm: LuauVM, lua_api: LuaAPI) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE) var key: String = vm.luaL_checkstring(2) match key: "parent": - return lua_api._get_element_parent_wrapper(vm, lua_api) + return get_element_parent_handler(vm, lua_api.dom_parser, lua_api) "nextSibling": - return lua_api._get_element_next_sibling_wrapper(vm, lua_api) + return get_element_next_sibling_handler(vm, lua_api.dom_parser, lua_api) "previousSibling": - return lua_api._get_element_previous_sibling_wrapper(vm, lua_api) + return get_element_previous_sibling_handler(vm, lua_api.dom_parser, lua_api) "firstChild": - return lua_api._get_element_first_child_wrapper(vm, lua_api) + return get_element_first_child_handler(vm, lua_api.dom_parser, lua_api) "lastChild": - return lua_api._get_element_last_child_wrapper(vm, lua_api) + return get_element_last_child_handler(vm, lua_api.dom_parser, lua_api) _: - return lua_api._element_index_handler(vm) + return _element_index_wrapper(vm) diff --git a/flumi/Scripts/Utils/Lua/Event.gd b/flumi/Scripts/Utils/Lua/Event.gd index 586bad3..fa00c98 100644 --- a/flumi/Scripts/Utils/Lua/Event.gd +++ b/flumi/Scripts/Utils/Lua/Event.gd @@ -9,7 +9,6 @@ static func is_date_button(node: Node) -> bool: static func connect_element_event(signal_node: Node, event_name: String, subscription) -> bool: if not signal_node: - print("ERROR: Signal node is null for event: ", event_name) return false match event_name: @@ -151,7 +150,6 @@ static func connect_element_event(signal_node: Node, event_name: String, subscri subscription.callback_func = callback # Store for later disconnect return true else: - print("ERROR: No text_changed signal found for input event on ", signal_node.get_class()) return false "submit": # For form elements - look for a submit button or form container diff --git a/flumi/Scripts/Utils/Lua/Function.gd b/flumi/Scripts/Utils/Lua/Function.gd deleted file mode 100644 index 3535437..0000000 --- a/flumi/Scripts/Utils/Lua/Function.gd +++ /dev/null @@ -1,73 +0,0 @@ -class_name LuaFunctionUtils -extends RefCounted - -# Core Lua handler functions that extend Lua functionality - -static func table_tostring_handler(vm: LuauVM) -> int: - vm.luaL_checktype(1, vm.LUA_TTABLE) - var table_string = LuaPrintUtils.table_to_string(vm, 1) - vm.lua_pushstring(table_string) - return 1 - -static func setup_gurt_api(vm: LuauVM, lua_api, dom_parser: HTMLParser) -> void: - # override global print - # This makes print() behave like gurt.log() - vm.lua_pushcallable(LuaPrintUtils.lua_print, "print") - vm.lua_setglobal("print") - - # Add table.tostring utility - vm.lua_getglobal("table") - if vm.lua_isnil(-1): - vm.lua_pop(1) - vm.lua_newtable() - vm.lua_setglobal("table") - vm.lua_getglobal("table") - - vm.lua_pushcallable(LuaFunctionUtils.table_tostring_handler, "table.tostring") - vm.lua_setfield(-2, "tostring") - vm.lua_pop(1) # Pop table from stack - - # Setup Signal API - LuaSignalUtils.setup_signal_api(vm) - - # Setup Time API - LuaTimeUtils.setup_time_api(vm) - - # Setup Clipboard API - LuaClipboardUtils.setup_clipboard_api(vm) - - vm.lua_newtable() - - vm.lua_pushcallable(LuaPrintUtils.lua_print, "gurt.log") - vm.lua_setfield(-2, "log") - - vm.lua_pushcallable(lua_api._gurt_select_handler, "gurt.select") - vm.lua_setfield(-2, "select") - - vm.lua_pushcallable(lua_api._gurt_select_all_handler, "gurt.selectAll") - vm.lua_setfield(-2, "selectAll") - - vm.lua_pushcallable(lua_api._gurt_create_handler, "gurt.create") - vm.lua_setfield(-2, "create") - - vm.lua_pushcallable(lua_api._gurt_set_timeout_handler, "gurt.setTimeout") - vm.lua_setfield(-2, "setTimeout") - - vm.lua_pushcallable(lua_api._gurt_clear_timeout_handler, "gurt.clearTimeout") - vm.lua_setfield(-2, "clearTimeout") - - # Add body element access - var body_element = dom_parser.find_first("body") - if body_element: - vm.lua_newtable() - vm.lua_pushstring("body") - vm.lua_setfield(-2, "_element_id") - - lua_api.add_element_methods(vm) - - vm.lua_pushcallable(lua_api._body_on_event_handler, "body.on") - vm.lua_setfield(-2, "on") - - vm.lua_setfield(-2, "body") - - vm.lua_setglobal("gurt") diff --git a/flumi/Scripts/Utils/Lua/Signal.gd b/flumi/Scripts/Utils/Lua/Signal.gd index 7bab829..c12077c 100644 --- a/flumi/Scripts/Utils/Lua/Signal.gd +++ b/flumi/Scripts/Utils/Lua/Signal.gd @@ -61,7 +61,6 @@ class LuaSignal: # Call the function if vm.lua_pcall(args.size(), 0, 0) != vm.LUA_OK: - print("GURT ERROR in Signal callback: ", vm.lua_tostring(-1)) vm.lua_pop(1) # Pop the callbacks table vm.lua_pop(1) diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd b/flumi/Scripts/Utils/Lua/ThreadedVM.gd new file mode 100644 index 0000000..b99a58c --- /dev/null +++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd @@ -0,0 +1,501 @@ +class_name ThreadedLuaVM +extends RefCounted + +signal script_completed(result: Dictionary) +signal script_error(error: String) +signal print_output(message: String) +signal dom_operation_request(operation: Dictionary) + +var lua_thread: Thread +var lua_vm: LuauVM +var lua_api: LuaAPI +var dom_parser: HTMLParser +var command_queue: Array = [] +var queue_mutex: Mutex +var should_exit: bool = false +var thread_semaphore: Semaphore + +# Sleep system +var sleep_mutex: Mutex +var sleep_condition: bool = false +var sleep_end_time: float = 0.0 + +func _init(): + queue_mutex = Mutex.new() + sleep_mutex = Mutex.new() + thread_semaphore = Semaphore.new() + +func start_lua_thread(dom_parser_ref: HTMLParser, lua_api_ref: LuaAPI) -> bool: + if lua_thread and lua_thread.is_alive(): + return false + + dom_parser = dom_parser_ref + lua_api = lua_api_ref + should_exit = false + + lua_thread = Thread.new() + var error = lua_thread.start(_lua_thread_worker) + + if error != OK: + return false + + return true + +func stop_lua_thread(): + if not lua_thread or not lua_thread.is_alive(): + return + + should_exit = true + thread_semaphore.post() # Wake up BRO + + # short time to exit gracefully + var timeout_start = Time.get_ticks_msec() + while lua_thread.is_alive() and (Time.get_ticks_msec() - timeout_start) < 500: + OS.delay_msec(10) + + lua_thread = null + +func execute_script_async(script_code: String): + queue_mutex.lock() + command_queue.append({ + "type": "execute_script", + "code": script_code + }) + queue_mutex.unlock() + thread_semaphore.post() + +func execute_callback_async(callback_ref: int, args: Array = []): + if not lua_thread or not lua_thread.is_alive(): + return + + queue_mutex.lock() + command_queue.append({ + "type": "execute_callback", + "callback_ref": callback_ref, + "args": args + }) + queue_mutex.unlock() + thread_semaphore.post() + +func execute_timeout_callback_async(timeout_id: int): + if not lua_thread or not lua_thread.is_alive(): + return + + queue_mutex.lock() + command_queue.append({ + "type": "execute_timeout", + "timeout_id": timeout_id + }) + queue_mutex.unlock() + thread_semaphore.post() + +func sleep_lua(duration_seconds: float): + sleep_mutex.lock() + sleep_end_time = Time.get_ticks_msec() / 1000.0 + duration_seconds + sleep_condition = true + sleep_mutex.unlock() + + while true: + sleep_mutex.lock() + var current_time = Time.get_ticks_msec() / 1000.0 + var should_continue = sleep_condition and current_time < sleep_end_time + sleep_mutex.unlock() + + if not should_continue: + break + # Yield to allow other threads to run + OS.delay_msec(1) + +func _lua_thread_worker(): + lua_vm = LuauVM.new() + + lua_vm.open_libraries([lua_vm.LUA_BASE_LIB, lua_vm.LUA_BIT32_LIB, + lua_vm.LUA_COROUTINE_LIB, lua_vm.LUA_MATH_LIB, lua_vm.LUA_UTF8_LIB, + lua_vm.LUA_TABLE_LIB, lua_vm.LUA_STRING_LIB, lua_vm.LUA_VECTOR_LIB]) + + lua_vm.lua_pushcallable(_threaded_print_handler, "print") + lua_vm.lua_setglobal("print") + + # Setup threaded Time.sleep function + lua_vm.lua_newtable() + lua_vm.lua_pushcallable(_threaded_time_sleep_handler, "Time.sleep") + lua_vm.lua_setfield(-2, "sleep") + lua_vm.lua_setglobal("Time") + + # Setup GURT API with thread-safe versions + _setup_threaded_gurt_api() + + # Setup additional API functions that are needed in callbacks + _setup_additional_lua_apis() + + while not should_exit: + if thread_semaphore.try_wait(): + _process_command_queue() + else: + OS.delay_msec(10) + + lua_vm = null + +func _process_command_queue(): + queue_mutex.lock() + var commands_to_process = command_queue.duplicate() + command_queue.clear() + queue_mutex.unlock() + + for command in commands_to_process: + match command.type: + "execute_script": + _execute_script_in_thread(command.code) + "execute_callback": + _execute_callback_in_thread(command.callback_ref, command.args) + "execute_timeout": + _execute_timeout_in_thread(command.timeout_id) + +func _execute_script_in_thread(script_code: String): + if not lua_vm: + call_deferred("_emit_script_error", "Lua VM not initialized") + return + + var result = lua_vm.lua_dostring(script_code) + + if result == lua_vm.LUA_OK: + call_deferred("_emit_script_completed", {"success": true}) + else: + var error_msg = lua_vm.lua_tostring(-1) + lua_vm.lua_pop(1) + call_deferred("_emit_script_error", error_msg) + +func _call_lua_function_with_args(args: Array) -> bool: + # Push arguments + for arg in args: + lua_vm.lua_pushvariant(arg) + + # Execute the callback with proper error handling + if lua_vm.lua_pcall(args.size(), 0, 0) != lua_vm.LUA_OK: + var error_msg = lua_vm.lua_tostring(-1) + lua_vm.lua_pop(1) + call_deferred("_emit_script_error", "Callback error: " + error_msg) + return false + return true + +func _execute_callback_in_thread(callback_ref: int, args: Array): + if not lua_vm: + return + + lua_vm.lua_pushstring("THREADED_CALLBACKS") + lua_vm.lua_rawget(lua_vm.LUA_REGISTRYINDEX) + if not lua_vm.lua_isnil(-1): + lua_vm.lua_pushinteger(callback_ref) + lua_vm.lua_rawget(-2) + if lua_vm.lua_isfunction(-1): + lua_vm.lua_remove(-2) # Remove the table, keep the function + if _call_lua_function_with_args(args): + return + else: + lua_vm.lua_pop(1) # Pop non-function value + + lua_vm.lua_pop(1) # Pop the table + + # Fallback to regular registry lookup + lua_vm.lua_rawgeti(lua_vm.LUA_REGISTRYINDEX, callback_ref) + if lua_vm.lua_isfunction(-1): + _call_lua_function_with_args(args) + else: + lua_vm.lua_pop(1) + +func _execute_timeout_in_thread(timeout_id: int): + if not lua_vm: + return + + # Retrieve timeout callback from the special timeout registry + lua_vm.lua_pushstring("GURT_THREADED_TIMEOUTS") + lua_vm.lua_rawget(lua_vm.LUA_REGISTRYINDEX) + if not lua_vm.lua_isnil(-1): + lua_vm.lua_pushinteger(timeout_id) + lua_vm.lua_rawget(-2) + if lua_vm.lua_isfunction(-1): + lua_vm.lua_remove(-2) # Remove the table, keep the function + if _call_lua_function_with_args([]): + # Clean up the callback from registry after execution + lua_vm.lua_pushstring("GURT_THREADED_TIMEOUTS") + lua_vm.lua_rawget(lua_vm.LUA_REGISTRYINDEX) + if not lua_vm.lua_isnil(-1): + lua_vm.lua_pushinteger(timeout_id) + lua_vm.lua_pushnil() + lua_vm.lua_rawset(-3) + lua_vm.lua_pop(1) + return + else: + lua_vm.lua_pop(1) # Pop non-function value + + lua_vm.lua_pop(1) # Pop the table + +func _threaded_print_handler(vm: LuauVM) -> int: + var message_parts: Array = [] + var num_args = vm.lua_gettop() + + for i in range(1, num_args + 1): + var arg_str = "" + if vm.lua_isstring(i): + arg_str = vm.lua_tostring(i) + elif vm.lua_isnumber(i): + arg_str = str(vm.lua_tonumber(i)) + elif vm.lua_isboolean(i): + arg_str = "true" if vm.lua_toboolean(i) else "false" + elif vm.lua_isnil(i): + arg_str = "nil" + else: + arg_str = vm.lua_typename(vm.lua_type(i)) + + message_parts.append(arg_str) + + var final_message = "\t".join(message_parts) + var current_time = Time.get_ticks_msec() / 1000.0 + + call_deferred("_emit_print_output", final_message) + + return 0 + +func _threaded_time_sleep_handler(vm: LuauVM) -> int: + vm.luaL_checknumber(1) + var seconds = vm.lua_tonumber(1) + + if seconds > 0: + sleep_lua(seconds) + + return 0 + +func _setup_threaded_gurt_api(): + lua_vm.lua_pushcallable(_threaded_print_handler, "print") + lua_vm.lua_setglobal("print") + + LuaTimeUtils.setup_time_api(lua_vm) + + lua_vm.lua_getglobal("Time") + if not lua_vm.lua_isnil(-1): + lua_vm.lua_pushcallable(_threaded_time_sleep_handler, "Time.sleep") + lua_vm.lua_setfield(-2, "sleep") + lua_vm.lua_pop(1) + + lua_vm.lua_newtable() + + lua_vm.lua_pushcallable(_threaded_print_handler, "gurt.log") + lua_vm.lua_setfield(-2, "log") + + lua_vm.lua_pushcallable(_threaded_gurt_select_handler, "gurt.select") + lua_vm.lua_setfield(-2, "select") + + lua_vm.lua_pushcallable(_threaded_gurt_select_all_handler, "gurt.selectAll") + lua_vm.lua_setfield(-2, "selectAll") + + lua_vm.lua_pushcallable(_threaded_gurt_create_handler, "gurt.create") + lua_vm.lua_setfield(-2, "create") + + lua_vm.lua_pushcallable(_threaded_set_timeout_handler, "gurt.setTimeout") + lua_vm.lua_setfield(-2, "setTimeout") + + lua_vm.lua_pushcallable(_threaded_clear_timeout_handler, "gurt.clearTimeout") + lua_vm.lua_setfield(-2, "clearTimeout") + + # Add body element access + var body_element = dom_parser.find_first("body") + if body_element: + LuaDOMUtils.create_element_wrapper(lua_vm, body_element, lua_api) + lua_vm.lua_pushcallable(_threaded_body_on_handler, "body.on") + lua_vm.lua_setfield(-2, "on") + lua_vm.lua_setfield(-2, "body") + + lua_vm.lua_setglobal("gurt") + +func _setup_additional_lua_apis(): + # Add table.tostring utility that's needed in callbacks + lua_vm.lua_getglobal("table") + if lua_vm.lua_isnil(-1): + lua_vm.lua_pop(1) + lua_vm.lua_newtable() + lua_vm.lua_setglobal("table") + lua_vm.lua_getglobal("table") + + lua_vm.lua_pushcallable(_threaded_table_tostring_handler, "table.tostring") + lua_vm.lua_setfield(-2, "tostring") + lua_vm.lua_pop(1) # Pop table from stack + + # Setup Signal API for threaded execution + LuaSignalUtils.setup_signal_api(lua_vm) + + # Setup Clipboard API for threaded execution + LuaClipboardUtils.setup_clipboard_api(lua_vm) + +func _threaded_table_tostring_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + var table_string = LuaPrintUtils.table_to_string(vm, 1) + vm.lua_pushstring(table_string) + return 1 + +func _emit_script_completed(result: Dictionary): + script_completed.emit(result) + +func _emit_script_error(error: String): + script_error.emit(error) + +func _emit_print_output(message: String): + print_output.emit(message) + +func _threaded_gurt_select_all_handler(vm: LuauVM) -> int: + # For threaded mode, selectAll is complex as it requires DOM access + # Return empty array for now, or implement via main thread operation + vm.lua_newtable() + return 1 + +func _threaded_gurt_create_handler(vm: LuauVM) -> int: + # Create new HTML element using existing system + var tag_name: String = vm.luaL_checkstring(1) + var attributes = {} + + if vm.lua_gettop() >= 2 and not vm.lua_isnil(2): + vm.luaL_checktype(2, vm.LUA_TTABLE) + attributes = vm.lua_todictionary(2) + + # Create HTML element using existing HTMLParser + var new_element = HTMLParser.HTMLElement.new(tag_name) + + # Apply attributes and content + for attr_name in attributes: + if attr_name == "text": + # Set text content directly on the HTML element + new_element.text_content = str(attributes[attr_name]) + else: + new_element.set_attribute(attr_name, str(attributes[attr_name])) + + # Assign a unique ID + var element_id = lua_api.get_or_assign_element_id(new_element) + new_element.set_attribute("id", element_id) + + # Add to parser's element collection + dom_parser.parse_result.all_elements.append(new_element) + + LuaDOMUtils.create_element_wrapper(vm, new_element, lua_api) + return 1 + +func _threaded_set_timeout_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TFUNCTION) + var delay_ms: int = vm.luaL_checkint(2) + + # Generate a unique timeout ID + var timeout_id = lua_api.timeout_manager.next_timeout_id + lua_api.timeout_manager.next_timeout_id += 1 + + # Store the callback in THIS threaded VM's registry + vm.lua_pushstring("GURT_THREADED_TIMEOUTS") + vm.lua_rawget(vm.LUA_REGISTRYINDEX) + if vm.lua_isnil(-1): + vm.lua_pop(1) + vm.lua_newtable() + vm.lua_pushstring("GURT_THREADED_TIMEOUTS") + vm.lua_pushvalue(-2) + vm.lua_rawset(vm.LUA_REGISTRYINDEX) + + vm.lua_pushinteger(timeout_id) + vm.lua_pushvalue(1) # Copy the callback function + vm.lua_rawset(-3) + vm.lua_pop(1) + + # Create timeout info and send timer creation command to main thread + call_deferred("_create_threaded_timeout", timeout_id, delay_ms) + + vm.lua_pushinteger(timeout_id) + return 1 + +func _threaded_clear_timeout_handler(vm: LuauVM) -> int: + # Delegate to Lua API timeout system + return lua_api._gurt_clear_timeout_handler(vm) + +func _threaded_gurt_select_handler(vm: LuauVM) -> int: + var selector: String = vm.luaL_checkstring(1) + + if not dom_parser or not dom_parser.parse_result: + vm.lua_pushnil() + return 1 + + # Find the element using the existing SelectorUtils + var element = SelectorUtils.find_first_matching(selector, dom_parser.parse_result.all_elements) + if element: + # Use DOM.gd element wrapper + LuaDOMUtils.create_element_wrapper(vm, element, lua_api) + return 1 + else: + # Return nil if element not found + vm.lua_pushnil() + return 1 + +# All element handlers now use DOM.gd wrappers + +func _threaded_body_on_handler(vm: LuauVM) -> int: + # Handle body event registration in threaded mode + # Arguments: (self, event_name, callback) due to colon syntax + vm.luaL_checktype(1, vm.LUA_TTABLE) # self (body table) + var event_name: String = vm.luaL_checkstring(2) # event name + vm.luaL_checktype(3, vm.LUA_TFUNCTION) # callback function + + # Store callback in registry + vm.lua_pushstring("THREADED_CALLBACKS") + vm.lua_rawget(vm.LUA_REGISTRYINDEX) + if vm.lua_isnil(-1): + vm.lua_pop(1) + vm.lua_newtable() + vm.lua_pushstring("THREADED_CALLBACKS") + vm.lua_pushvalue(-2) + vm.lua_rawset(vm.LUA_REGISTRYINDEX) + + var callback_ref = lua_api.next_callback_ref + lua_api.next_callback_ref += 1 + + # Get a proper subscription ID + var subscription_id = lua_api.next_subscription_id + lua_api.next_subscription_id += 1 + + vm.lua_pushinteger(callback_ref) + vm.lua_pushvalue(3) # Copy the callback function (3rd argument) + vm.lua_rawset(-3) + vm.lua_pop(1) + + # Queue DOM operation for main thread (body events) + var operation = { + "type": "register_body_event", + "event_name": event_name, + "callback_ref": callback_ref, + "subscription_id": subscription_id + } + + call_deferred("_emit_dom_operation_request", operation) + + # Return subscription with unsubscribe method + vm.lua_newtable() + vm.lua_pushinteger(subscription_id) + vm.lua_setfield(-2, "_subscription_id") + + vm.lua_pushcallable(Callable(LuaDOMUtils, "_unsubscribe_wrapper"), "subscription.unsubscribe") + vm.lua_setfield(-2, "unsubscribe") + return 1 + +func _emit_dom_operation_request(operation: Dictionary): + dom_operation_request.emit(operation) + +func _create_threaded_timeout(timeout_id: int, delay_ms: int): + # Ensure timeout manager exists + lua_api._ensure_timeout_manager() + + # Create timeout info for threaded execution + var timeout_info = lua_api.timeout_manager.TimeoutInfo.new(timeout_id, timeout_id, lua_vm, lua_api.timeout_manager) + lua_api.timeout_manager.active_timeouts[timeout_id] = timeout_info + lua_api.timeout_manager.threaded_vm = self + + # Create and start timer on main thread + var timer = Timer.new() + timer.wait_time = delay_ms / 1000.0 + timer.one_shot = true + timer.timeout.connect(lua_api.timeout_manager._on_timeout_triggered.bind(timeout_info)) + + timeout_info.timer = timer + lua_api.add_child(timer) + timer.start() diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd.uid b/flumi/Scripts/Utils/Lua/ThreadedVM.gd.uid new file mode 100644 index 0000000..335ba2c --- /dev/null +++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd.uid @@ -0,0 +1 @@ +uid://c5t0so2od1scw diff --git a/flumi/Scripts/Utils/Lua/Time.gd b/flumi/Scripts/Utils/Lua/Time.gd index 21a6a93..f699933 100644 --- a/flumi/Scripts/Utils/Lua/Time.gd +++ b/flumi/Scripts/Utils/Lua/Time.gd @@ -64,12 +64,13 @@ static func time_date_handler(vm: LuauVM) -> int: static func time_sleep_handler(vm: LuauVM) -> int: vm.luaL_checknumber(1) var seconds = vm.lua_tonumber(1) - var _milliseconds = int(seconds * 1000) - # TODO: implement a proper sleep function - - vm.lua_pushnumber(seconds) - return 1 + if seconds > 0: + var target_time = Time.get_ticks_msec() + (seconds * 1000.0) + while Time.get_ticks_msec() < target_time: + OS.delay_msec(1) + + return 0 static func time_benchmark_handler(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TFUNCTION) diff --git a/flumi/Scripts/Utils/Lua/Timeout.gd b/flumi/Scripts/Utils/Lua/Timeout.gd index 7c413ad..0ddd914 100644 --- a/flumi/Scripts/Utils/Lua/Timeout.gd +++ b/flumi/Scripts/Utils/Lua/Timeout.gd @@ -3,6 +3,7 @@ extends RefCounted var active_timeouts: Dictionary = {} var next_timeout_id: int = 1 +var threaded_vm: ThreadedLuaVM = null class TimeoutInfo: var id: int @@ -58,6 +59,20 @@ func set_timeout_handler(vm: LuauVM, parent_node: Node) -> int: vm.lua_pushinteger(timeout_id) return 1 +func set_threaded_timeout_handler(vm: LuauVM, parent_node: Node, threaded_vm_ref: ThreadedLuaVM) -> int: + threaded_vm = threaded_vm_ref + return set_timeout_handler(vm, parent_node) + +func _create_timer_on_main_thread(timeout_info: TimeoutInfo, delay_ms: int, parent_node: Node): + var timer = Timer.new() + timer.wait_time = delay_ms / 1000.0 + timer.one_shot = true + timer.timeout.connect(_on_timeout_triggered.bind(timeout_info)) + + timeout_info.timer = timer + parent_node.add_child(timer) + timer.start() + func clear_timeout_handler(vm: LuauVM) -> int: var timeout_id: int = vm.luaL_checkint(1) @@ -68,14 +83,6 @@ func clear_timeout_handler(vm: LuauVM) -> int: timeout_info.timer.stop() timeout_info.timer.queue_free() - # Clean up callback reference - vm.lua_pushstring("GURT_TIMEOUTS") - vm.lua_rawget(vm.LUA_REGISTRYINDEX) - if not vm.lua_isnil(-1): - vm.lua_pushinteger(timeout_info.callback_ref) - vm.lua_pushnil() - vm.lua_rawset(-3) - vm.lua_pop(1) # Remove from active timeouts active_timeouts.erase(timeout_id) @@ -86,29 +93,10 @@ func _on_timeout_triggered(timeout_info: TimeoutInfo) -> void: if not active_timeouts.has(timeout_info.id): return - # Execute the callback - timeout_info.vm.lua_pushstring("GURT_TIMEOUTS") - timeout_info.vm.lua_rawget(timeout_info.vm.LUA_REGISTRYINDEX) - timeout_info.vm.lua_pushinteger(timeout_info.callback_ref) - timeout_info.vm.lua_rawget(-2) - timeout_info.vm.lua_remove(-2) + if threaded_vm: + _execute_threaded_timeout_callback(timeout_info.id) - if timeout_info.vm.lua_isfunction(-1): - if timeout_info.vm.lua_pcall(0, 0, 0) != timeout_info.vm.LUA_OK: - print("GURT ERROR in timeout callback: ", timeout_info.vm.lua_tostring(-1)) - timeout_info.vm.lua_pop(1) - else: - timeout_info.vm.lua_pop(1) - - # Clean up timeout timeout_info.timer.queue_free() - timeout_info.vm.lua_pushstring("GURT_TIMEOUTS") - timeout_info.vm.lua_rawget(timeout_info.vm.LUA_REGISTRYINDEX) - if not timeout_info.vm.lua_isnil(-1): - timeout_info.vm.lua_pushinteger(timeout_info.callback_ref) - timeout_info.vm.lua_pushnil() - timeout_info.vm.lua_rawset(-3) - timeout_info.vm.lua_pop(1) active_timeouts.erase(timeout_info.id) func cleanup_all_timeouts(): @@ -129,3 +117,9 @@ func cleanup_all_timeouts(): timeout_info.vm.lua_rawset(-3) timeout_info.vm.lua_pop(1) active_timeouts.clear() + +func _execute_threaded_timeout_callback(timeout_id: int) -> void: + if threaded_vm and threaded_vm.lua_thread and threaded_vm.lua_thread.is_alive(): + threaded_vm.execute_timeout_callback_async(timeout_id) + else: + active_timeouts.erase(timeout_id) diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index 0043f23..2c9efe9 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -102,12 +102,9 @@ func render() -> void: parser.register_dom_node(body, website_container) var scripts = parser.find_all("script") - var lua_vm = null var lua_api = null if scripts.size() > 0: - lua_vm = LuauVM.new() lua_api = LuaAPI.new() - add_child(lua_vm) add_child(lua_api) var i = 0 @@ -163,8 +160,8 @@ func render() -> void: i += 1 - if scripts.size() > 0 and lua_vm and lua_api: - parser.process_scripts(lua_api, lua_vm) + if scripts.size() > 0 and lua_api: + parser.process_scripts(lua_api, null) static func safe_add_child(parent: Node, child: Node) -> void: if child.get_parent(): @@ -353,7 +350,7 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP return node "li": node = P.instantiate() - node.init(element) + node.init(element, parser) "select": node = SELECT.instantiate() node.init(element, parser)