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)