lua: select, log, body, on, get_text, set_text, subscribtions, events (keyup, keydown, keypress, mouseover, mouseup, mousedown, mouseenter, mouseexit, click, focusin, focusout)

This commit is contained in:
Face
2025-08-04 14:07:56 +03:00
parent cf43bac2cf
commit e22ad21fd0
31 changed files with 879 additions and 11 deletions

View File

@@ -43,12 +43,19 @@ class HTMLElement:
func is_inline_element() -> bool:
return tag_name in ["b", "i", "u", "small", "mark", "code", "span", "a", "input"]
class HTMLBody extends HTMLElement:
var body_node: Node = null
func _init():
super._init("body")
class ParseResult:
var root: HTMLElement
var all_elements: Array[HTMLElement] = []
var errors: Array[String] = []
var css_parser: CSSParser = null
var inline_styles: Dictionary = {}
var dom_nodes: Dictionary = {}
func _init():
root = HTMLElement.new("document")
@@ -271,6 +278,11 @@ func find_by_id(element_id: String) -> HTMLElement:
return null
func register_dom_node(element: HTMLElement, node: Control) -> void:
var element_id = element.get_id()
if element_id.length() > 0:
parse_result.dom_nodes[element_id] = node
func find_first(tag: String, attribute: String = "") -> HTMLElement:
var results = find_all(tag, attribute)
return results[0] if results.size() > 0 else null
@@ -334,6 +346,24 @@ func get_all_images() -> Array[String]:
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")
return
lua_api.dom_parser = self
for script_element in find_all("script"):
var src = script_element.get_attribute("src")
var inline_code = script_element.text_content.strip_edges()
if not src.is_empty():
# TODO: add support for external Lua script
print("External script found: ", src)
elif not inline_code.is_empty():
print("Executing inline Lua script")
lua_api.execute_lua_script(inline_code, lua_vm)
func get_all_stylesheets() -> Array[String]:
return get_attribute_values("style", "src")

371
flumi/Scripts/B9/Lua.gd Normal file
View File

@@ -0,0 +1,371 @@
class_name LuaAPI
extends Node
class EventSubscription:
var id: int
var element_id: String
var event_name: String
var callback_ref: int
var vm: LuauVM
var lua_api: LuaAPI
var connected_signal: String = ""
var connected_node: Node = null
var dom_parser: HTMLParser
var event_subscriptions: Dictionary = {}
var next_subscription_id: int = 1
var next_callback_ref: int = 1
func _gurt_select_handler(vm: LuauVM) -> int:
var selector: String = vm.luaL_checkstring(1)
var element_id = ""
if selector.begins_with("#"):
element_id = selector.substr(1)
else:
vm.lua_pushnil()
return 1
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
if not dom_node:
vm.lua_pushnil()
return 1
vm.lua_newtable()
vm.lua_pushstring(element_id)
vm.lua_setfield(-2, "_element_id")
add_element_methods(vm)
return 1
func add_element_methods(vm: LuauVM) -> void:
vm.lua_pushcallable(_element_set_text_handler, "element.set_text")
vm.lua_setfield(-2, "set_text")
vm.lua_pushcallable(_element_get_text_handler, "element.get_text")
vm.lua_setfield(-2, "get_text")
vm.lua_pushcallable(_element_on_event_handler, "element.on")
vm.lua_setfield(-2, "on")
# Element manipulation handlers
func _element_set_text_handler(vm: LuauVM) -> int:
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 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
return 0
func _element_get_text_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)
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
# Event system handlers
func _element_on_event_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
var event_name: String = vm.luaL_checkstring(2)
vm.luaL_checktype(3, vm.LUA_TFUNCTION)
vm.lua_getfield(1, "_element_id")
var element_id: String = vm.lua_tostring(-1)
vm.lua_pop(1)
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
if not dom_node:
vm.lua_pushnil()
return 1
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)
return _handle_subscription_result(vm, subscription, success)
func _body_on_event_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
var event_name: String = vm.luaL_checkstring(2)
vm.luaL_checktype(3, vm.LUA_TFUNCTION)
var subscription = _create_subscription(vm, "body", event_name)
event_subscriptions[subscription.id] = subscription
var success = LuaEventUtils.connect_body_event(event_name, subscription, self)
return _handle_subscription_result(vm, subscription, success)
func _subscription_unsubscribe_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
vm.lua_getfield(1, "_subscription_id")
var subscription_id: int = vm.lua_tointeger(-1)
vm.lua_pop(1)
var subscription = event_subscriptions.get(subscription_id, null)
if subscription:
LuaEventUtils.disconnect_subscription(subscription, self)
event_subscriptions.erase(subscription_id)
vm.lua_pushnil()
vm.lua_rawseti(vm.LUA_REGISTRYINDEX, subscription.callback_ref)
return 0
# Subscription management
func _create_subscription(vm: LuauVM, element_id: String, event_name: String) -> EventSubscription:
var subscription_id = next_subscription_id
next_subscription_id += 1
var callback_ref = next_callback_ref
next_callback_ref += 1
vm.lua_pushvalue(3)
vm.lua_rawseti(vm.LUA_REGISTRYINDEX, callback_ref)
var subscription = EventSubscription.new()
subscription.id = subscription_id
subscription.element_id = element_id
subscription.event_name = event_name
subscription.callback_ref = callback_ref
subscription.vm = vm
subscription.lua_api = self
return subscription
func _handle_subscription_result(vm: LuauVM, subscription: EventSubscription, success: bool) -> int:
if success:
vm.lua_newtable()
vm.lua_pushinteger(subscription.id)
vm.lua_setfield(-2, "_subscription_id")
vm.lua_pushcallable(_subscription_unsubscribe_handler, "subscription.unsubscribe")
vm.lua_setfield(-2, "unsubscribe")
return 1
else:
vm.lua_pushnil()
vm.lua_rawseti(vm.LUA_REGISTRYINDEX, subscription.callback_ref)
event_subscriptions.erase(subscription.id)
vm.lua_pushnil()
return 1
# Event callbacks
func _on_event_triggered(subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
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)
func _on_gui_input_click(event: InputEvent, subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
if event is InputEventMouseButton:
var mouse_event = event as InputEventMouseButton
if mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed:
_execute_lua_callback(subscription)
func _on_gui_input_mouse_universal(event: InputEvent, signal_node: Node) -> void:
if event is InputEventMouseButton:
var mouse_event = event as InputEventMouseButton
if mouse_event.button_index == MOUSE_BUTTON_LEFT:
# Find all subscriptions for this node with mouse events
for subscription_id in event_subscriptions:
var subscription = event_subscriptions[subscription_id]
if subscription.connected_node == signal_node and subscription.connected_signal == "gui_input_mouse":
var should_trigger = false
if subscription.event_name == "mousedown" and mouse_event.pressed:
should_trigger = true
elif subscription.event_name == "mouseup" and not mouse_event.pressed:
should_trigger = true
if should_trigger:
_execute_lua_callback(subscription)
# Event callback handlers
func _on_gui_input_mousemove(event: InputEvent, subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
if event is InputEventMouseMotion:
var mouse_event = event as InputEventMouseMotion
_handle_mousemove_event(mouse_event, subscription)
func _on_focus_gui_input(event: InputEvent, subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
if event is InputEventMouseButton:
var mouse_event = event as InputEventMouseButton
if mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed:
if subscription.event_name == "focusin":
_execute_lua_callback(subscription)
func _on_body_mouse_enter(subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
if subscription.event_name == "mouseenter":
_execute_lua_callback(subscription)
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)
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)
# Global input processing
func _input(event: InputEvent) -> void:
if event is InputEventKey:
var key_event = event as InputEventKey
for subscription_id in event_subscriptions:
var subscription = event_subscriptions[subscription_id]
if subscription.element_id == "body" and subscription.connected_signal == "input":
var should_trigger = false
match subscription.event_name:
"keydown":
should_trigger = key_event.pressed
"keyup":
should_trigger = not key_event.pressed
"keypress":
should_trigger = key_event.pressed
if should_trigger:
var key_info = {
"key": OS.get_keycode_string(key_event.keycode),
"keycode": key_event.keycode,
"ctrl": key_event.ctrl_pressed,
"shift": key_event.shift_pressed,
"alt": key_event.alt_pressed
}
_execute_lua_callback(subscription, [key_info])
elif event is InputEventMouseMotion:
var mouse_event = event as InputEventMouseMotion
for subscription_id in event_subscriptions:
var subscription = event_subscriptions[subscription_id]
if subscription.element_id == "body" and subscription.connected_signal == "input_mousemove":
if subscription.event_name == "mousemove":
_handle_mousemove_event(mouse_event, subscription)
func _handle_mousemove_event(mouse_event: InputEventMouseMotion, subscription: EventSubscription) -> void:
# TODO: pass reference instead of hardcoded path
var body_container = get_node("/root/Main").website_container
if body_container.get_parent() is MarginContainer:
body_container = body_container.get_parent()
if not body_container:
return
var container_rect = body_container.get_global_rect()
var local_x = mouse_event.global_position.x - container_rect.position.x
var local_y = mouse_event.global_position.y - container_rect.position.y
# Only provide coordinates if mouse is within the container bounds
if local_x >= 0 and local_y >= 0 and local_x <= container_rect.size.x and local_y <= container_rect.size.y:
var mouse_info = {
"x": local_x,
"y": local_y,
"deltaX": mouse_event.relative.x,
"deltaY": mouse_event.relative.y
}
_execute_lua_callback(subscription, [mouse_info])
# DOM node utilities
func get_dom_node(node: Node, purpose: String = "general") -> Node:
if not node:
return null
if node is MarginContainer:
node = node.get_child(0)
match purpose:
"signal":
if node is HTMLButton:
return node.get_node_or_null("ButtonNode")
elif node is RichTextLabel:
return node
elif node.has_method("get") and node.get("rich_text_label"):
return node.get("rich_text_label")
elif node.get_node_or_null("RichTextLabel"):
return node.get_node_or_null("RichTextLabel")
else:
return node
"text":
if node.has_method("set_text") and node.has_method("get_text"):
return node
elif node is RichTextLabel:
return node
elif node.has_method("get") and node.get("rich_text_label"):
return node.get("rich_text_label")
elif node.get_node_or_null("RichTextLabel"):
return node.get_node_or_null("RichTextLabel")
else:
if "text" in node:
return node
return null
"general":
if node is HTMLButton:
return node.get_node_or_null("ButtonNode")
elif node is RichTextLabel:
return node
elif node.get_node_or_null("RichTextLabel"):
return node.get_node_or_null("RichTextLabel")
else:
return node
return node
# Main execution function
func execute_lua_script(code: String, vm: LuauVM):
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])
LuaFunctionUtils.setup_gurt_api(vm, self, dom_parser)
if vm.lua_dostring(code) != vm.LUA_OK:
print("LUA ERROR: ", vm.lua_tostring(-1))
vm.lua_pop(1)

View File

@@ -0,0 +1 @@
uid://biv2ch1mi3lnn

View File

@@ -100,7 +100,7 @@ var HTML_CONTENT2 = """<head>
</body>
""".to_utf8_buffer()
var HTML_CONTENT = """<head>
var HTML_CONTENTbv = """<head>
<title>My cool web</title>
<icon src=\"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/768px-Google_%22G%22_logo.svg.png\">
@@ -111,7 +111,6 @@ var HTML_CONTENT = """<head>
<style>
h1 { text-[#ff0000] font-italic hover:text-[#00ff00] }
p { text-[#333333] text-2xl }
button { hover:bg-[#FF6B35] hover:text-[#FFFFFF] active:bg-[#CC5429] active:text-[#F0F0F0] }
</style>
<style src=\"styles.css\">
@@ -416,17 +415,17 @@ var HTML_CONTENT_S = """<head>
a[href^="https"] { text-[#008000] font-bold }
button[disabled] { bg-[#888888] text-[#cccccc] }
input[placeholder*="email"] { border-2 border-[#0066cc] bg-[#ffffff] }
div[class$="special"] { bg-[#ffffaa] }
div[style$="special"] { bg-[#ffffaa] }
</style>
</head>
<body>
<h1>CSS Selector Test Page</h1>
<p>This paragraph should be red and bold (h1 + p)</p>
<p class="second-p">This paragraph should be blue (h1 ~ p)</p>
<p style="second-p">This paragraph should be blue (h1 ~ p)</p>
<h2>Descendant vs Child Selectors</h2>
<div class="outer-div">
<div style="outer-div">
<p>This paragraph should be purple and bold (div p and .outer-div > p)</p>
<div>
<p>This paragraph should be purple but not bold (div p only)</p>
@@ -452,20 +451,20 @@ var HTML_CONTENT_S = """<head>
<span>This span should have light red bg (h3 ~ span)</span>
<span>This span should also have light red bg (h3 ~ span)</span>
<div class="container">
<div style="container">
<span>This span should have yellow bg (.container span)</span>
<p>Regular paragraph in container</p>
</div>
<div class="parent">
<div style="parent">
<button>This button should be green (.parent > button)</button>
<div>
<button>This button should be normal (not direct child)</button>
</div>
</div>
<div class="item-special">This div should have yellow bg (class ends with 'special')</div>
<div class="special-item">This div should be normal</div>
<div style="item-special">This div should have yellow bg (class ends with 'special')</div>
<div style="special-item">This div should be normal</div>
</body>
""".to_utf8_buffer()
@@ -590,3 +589,97 @@ var HTML_CONTENT3 = """<head>
</body>
""".to_utf8_buffer()
var HTML_CONTENT = """<head>
<title>Lua API Demo</title>
<icon src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Lua-Logo.svg/256px-Lua-Logo.svg.png">
<meta name="theme-color" content="#000080">
<meta name="description" content="Demonstrating the GURT Lua API">
<style>
body { bg-[#f8f9fa] p-6 }
h1 { text-[#2563eb] text-4xl font-bold }
.container { bg-[#ffffff] p-4 rounded-lg shadow-lg }
.demo-button { bg-[#3b82f6] text-white px-4 py-2 rounded hover:bg-[#2563eb] }
</style>
<script>
local typing = gurt.select('#type')
local mouse = gurt.select('#mouse')
local btnmouse = gurt.select('#btnmouse')
gurt.log('Starting Lua script execution...')
gurt.body:on('keypress', function(el)
typing:set_text(table.tostring(el))
end)
gurt.body:on('mousemove', function(el)
mouse:set_text(table.tostring(el))
end)
-- Test element selection and manipulation
local heading = gurt.select('#main-heading')
heading:set_text('Welcome to the New Web!')
local button = gurt.select('#demo-button')
local event_log = gurt.select('#event-log')
button:on('mousedown', function()
print('Mouse down')
end)
button:on('mouseup', function()
print('Mouse up')
end)
button:on('mouseenter', function()
print('Mouse enter')
end)
button:on('mouseexit', function()
print('Mouse exit')
end)
button:on('mousemove', function(el)
btnmouse:set_text(table.tostring(el))
end)
if button and event_log then
local click_count = 0
local subscription = button:on('click', function()
click_count = click_count + 1
local new_text = 'Button clicked ' .. click_count .. ' time(s)!'
event_log:set_text(new_text)
end)
heading:on('focusin', function()
print('oh u flck')
subscription:unsubscribe()
end)
gurt.log('Event listener attached to button with subscription ID')
else
gurt.log('Could not find button or event log element')
end
</script>
</head>
<body>
<h1 id="main-heading">Welcome to GURT Lua API Demo</h1>
<div style="container">
<p>This page demonstrates the GURT Lua API in action.</p>
<div id="demo-button" style="w-40 h-40 bg-red-500 p-4 rounded-lg">Click me to see Lua in action!</div>
</div>
<p id="event-log" style="mt-4 p-4 bg-[#f3f4f6] rounded min-h-24">Click the button</p>
<p id="mouse" style="mt-4 p-4 bg-[#f3f4f6] rounded min-h-24">Move your mouse</p>
<p id="btnmouse" style="mt-4 p-4 bg-[#f3f4f6] rounded min-h-24">Move mouse over Button</p>
<p id="type" style="mt-4 p-4 bg-[#f3f4f6] rounded min-h-24">Type something</p>
</body>
""".to_utf8_buffer()

View File

@@ -0,0 +1,161 @@
class_name LuaEventUtils
extends RefCounted
static func connect_element_event(signal_node: Node, event_name: String, subscription) -> bool:
if not signal_node:
return false
match event_name:
"click":
if signal_node.has_signal("pressed"):
signal_node.pressed.connect(subscription.lua_api._on_event_triggered.bind(subscription))
subscription.connected_signal = "pressed"
subscription.connected_node = signal_node if signal_node != subscription.lua_api.get_dom_node(signal_node.get_parent(), "signal") else null
return true
elif signal_node is Control:
signal_node.gui_input.connect(subscription.lua_api._on_gui_input_click.bind(subscription))
subscription.connected_signal = "gui_input"
subscription.connected_node = signal_node
return true
"mousedown", "mouseup":
if signal_node is Control:
# Check if we already have a mouse handler connected to this node
var already_connected = false
for existing_id in subscription.lua_api.event_subscriptions:
var existing_sub = subscription.lua_api.event_subscriptions[existing_id]
if existing_sub.connected_node == signal_node and existing_sub.connected_signal == "gui_input_mouse":
already_connected = true
break
if not already_connected:
signal_node.gui_input.connect(subscription.lua_api._on_gui_input_mouse_universal.bind(signal_node))
subscription.connected_signal = "gui_input_mouse"
subscription.connected_node = signal_node
return true
"mousemove":
if signal_node is Control:
signal_node.gui_input.connect(subscription.lua_api._on_gui_input_mousemove.bind(subscription))
subscription.connected_signal = "gui_input_mousemove"
subscription.connected_node = signal_node
return true
"mouseenter":
if signal_node is Control and signal_node.has_signal("mouse_entered"):
signal_node.mouse_entered.connect(subscription.lua_api._on_event_triggered.bind(subscription))
subscription.connected_signal = "mouse_entered"
subscription.connected_node = signal_node
return true
"mouseexit":
if signal_node is Control and signal_node.has_signal("mouse_exited"):
signal_node.mouse_exited.connect(subscription.lua_api._on_event_triggered.bind(subscription))
subscription.connected_signal = "mouse_exited"
subscription.connected_node = signal_node
return true
"focusin":
if signal_node is Control:
signal_node.focus_mode = Control.FOCUS_ALL
if signal_node.has_signal("focus_entered"):
signal_node.focus_entered.connect(subscription.lua_api._on_event_triggered.bind(subscription))
subscription.connected_signal = "focus_entered"
subscription.connected_node = signal_node
return true
else:
signal_node.gui_input.connect(subscription.lua_api._on_focus_gui_input.bind(subscription))
subscription.connected_signal = "gui_input_focus"
subscription.connected_node = signal_node
return true
"focusout":
if signal_node is Control and signal_node.has_signal("focus_exited"):
signal_node.focus_exited.connect(subscription.lua_api._on_event_triggered.bind(subscription))
subscription.connected_signal = "focus_exited"
subscription.connected_node = signal_node
return true
return false
static func connect_body_event(event_name: String, subscription, lua_api) -> bool:
match event_name:
"keydown", "keypress", "keyup":
lua_api.set_process_input(true)
subscription.connected_signal = "input"
subscription.connected_node = lua_api
return true
"mousemove":
lua_api.set_process_input(true)
subscription.connected_signal = "input_mousemove"
subscription.connected_node = lua_api
return true
"mouseenter", "mouseexit":
var main_container = lua_api.dom_parser.parse_result.dom_nodes.get("body", null)
if main_container:
if event_name == "mouseenter":
main_container.mouse_entered.connect(lua_api._on_body_mouse_enter.bind(subscription))
subscription.connected_signal = "mouse_entered"
elif event_name == "mouseexit":
main_container.mouse_exited.connect(lua_api._on_body_mouse_exit.bind(subscription))
subscription.connected_signal = "mouse_exited"
subscription.connected_node = main_container
return true
"focusin", "focusout":
subscription.connected_signal = "focus_events"
subscription.connected_node = lua_api
return true
return false
static func _count_active_input_subscriptions(lua_api) -> int:
var count = 0
for sub_id in lua_api.event_subscriptions:
var sub = lua_api.event_subscriptions[sub_id]
if sub.connected_signal in ["input", "input_mousemove"]:
count += 1
return count
static func disconnect_subscription(subscription, lua_api) -> void:
var target_node = subscription.connected_node if subscription.connected_node else lua_api.dom_parser.parse_result.dom_nodes.get(subscription.element_id, null)
if target_node and subscription.connected_signal:
match subscription.connected_signal:
"pressed":
if target_node.has_signal("pressed"):
target_node.pressed.disconnect(lua_api._on_event_triggered.bind(subscription))
"gui_input":
if target_node.has_signal("gui_input"):
target_node.gui_input.disconnect(lua_api._on_gui_input_click.bind(subscription))
"gui_input_mouse":
if target_node.has_signal("gui_input"):
target_node.gui_input.disconnect(lua_api._on_gui_input_mouse_universal.bind(target_node))
"gui_input_mousemove":
if target_node.has_signal("gui_input"):
target_node.gui_input.disconnect(lua_api._on_gui_input_mousemove.bind(subscription))
"gui_input_focus":
if target_node.has_signal("gui_input"):
target_node.gui_input.disconnect(lua_api._on_focus_gui_input.bind(subscription))
"mouse_entered":
if target_node.has_signal("mouse_entered"):
# Check if this is a body event or element event
if subscription.element_id == "body":
target_node.mouse_entered.disconnect(lua_api._on_body_mouse_enter.bind(subscription))
else:
target_node.mouse_entered.disconnect(lua_api._on_event_triggered.bind(subscription))
"mouse_exited":
if target_node.has_signal("mouse_exited"):
# Check if this is a body event or element event
if subscription.element_id == "body":
target_node.mouse_exited.disconnect(lua_api._on_body_mouse_exit.bind(subscription))
else:
target_node.mouse_exited.disconnect(lua_api._on_event_triggered.bind(subscription))
"focus_entered":
if target_node.has_signal("focus_entered"):
target_node.focus_entered.disconnect(lua_api._on_event_triggered.bind(subscription))
"focus_exited":
if target_node.has_signal("focus_exited"):
target_node.focus_exited.disconnect(lua_api._on_event_triggered.bind(subscription))
"input":
# Only disable input processing if no other input subscriptions remain
if _count_active_input_subscriptions(lua_api) <= 1:
lua_api.set_process_input(false)
"input_mousemove":
# Only disable input processing if no other input subscriptions remain
if _count_active_input_subscriptions(lua_api) <= 1:
lua_api.set_process_input(false)

View File

@@ -0,0 +1 @@
uid://b7bck02xxt0u6

View File

@@ -0,0 +1,57 @@
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
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")
# 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")
# NOTE: same code as add_element_methods, but lazy to handle body.xxxx prop
vm.lua_pushcallable(lua_api._element_set_text_handler, "body.set_text")
vm.lua_setfield(-2, "set_text")
vm.lua_pushcallable(lua_api._element_get_text_handler, "body.get_text")
vm.lua_setfield(-2, "get_text")
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")

View File

@@ -0,0 +1 @@
uid://dbsrdfnhv4h16

View File

@@ -0,0 +1,87 @@
class_name LuaPrintUtils
extends RefCounted
static func lua_print(vm: LuauVM) -> int:
var message_parts: Array[String] = []
var num_args: int = vm.lua_gettop()
for i in range(1, num_args + 1):
var value_str = lua_value_to_string(vm, i)
message_parts.append(value_str)
var final_message = "\t".join(message_parts)
print("GURT LOG: ", final_message)
return 0
static func lua_value_to_string(vm: LuauVM, index: int) -> String:
var lua_type = vm.lua_type(index)
match lua_type:
vm.LUA_TNIL:
return "nil"
vm.LUA_TBOOLEAN:
return "true" if vm.lua_toboolean(index) else "false"
vm.LUA_TNUMBER:
return str(vm.lua_tonumber(index))
vm.LUA_TSTRING:
return vm.lua_tostring(index)
vm.LUA_TTABLE:
return table_to_string(vm, index)
vm.LUA_TFUNCTION:
return "[function]"
vm.LUA_TUSERDATA:
return "[userdata]"
vm.LUA_TVECTOR:
var vec = vm.lua_tovector(index)
return "vector(" + str(vec.x) + ", " + str(vec.y) + ", " + str(vec.z) + ", " + str(vec.w) + ")"
_:
return "[" + vm.lua_typename(lua_type) + "]"
static func table_to_string(vm: LuauVM, index: int, max_depth: int = 3, current_depth: int = 0) -> String:
if current_depth >= max_depth:
return "{...}"
var result = "{"
var first = true
var count = 0
var max_items = 10
# Convert negative index to positive
if index < 0:
index = vm.lua_gettop() + index + 1
# Iterate through table
vm.lua_pushnil() # First key
while vm.lua_next(index):
if count >= max_items:
# We need to pop the value and key before breaking
vm.lua_pop(2) # Remove value and key
break
if not first:
result += ", "
first = false
# Get key
var key_str = lua_value_to_string(vm, -2)
# Get value
var value_str = ""
if vm.lua_type(-1) == vm.LUA_TTABLE:
value_str = table_to_string(vm, -1, max_depth, current_depth + 1)
else:
value_str = lua_value_to_string(vm, -1)
# Check if key is a valid identifier (for shorthand)
if key_str.is_valid_identifier():
result += key_str + ": " + value_str
else:
result += "[" + key_str + "]: " + value_str
vm.lua_pop(1) # Remove value, keep key for next iteration
count += 1
if count >= max_items:
result += ", ..."
result += "}"
return result

View File

@@ -0,0 +1 @@
uid://dvdnveday3ev1

View File

@@ -0,0 +1 @@
uid://bt0njoa0olpb7

View File

@@ -0,0 +1 @@
uid://dxti5hc236dcp

View File

@@ -0,0 +1 @@
uid://bwbbe8r7u10ov

View File

@@ -99,7 +99,18 @@ func render() -> void:
if body:
StyleManager.apply_body_styles(body, parser, website_container, website_background)
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
while i < body.children.size():
var element: HTMLParser.HTMLElement = body.children[i]
@@ -118,6 +129,8 @@ func render() -> void:
for inline_element in inline_elements:
var inline_node = await create_element_node(inline_element, parser)
if inline_node:
parser.register_dom_node(inline_element, inline_node)
safe_add_child(hbox, inline_node)
# Handle hyperlinks for all inline elements
if contains_hyperlink(inline_element) and inline_node is RichTextLabel:
@@ -130,6 +143,8 @@ func render() -> void:
var element_node = await create_element_node(element, parser)
if element_node:
parser.register_dom_node(element, element_node)
# ul/ol handle their own adding
if element.tag_name != "ul" and element.tag_name != "ol":
safe_add_child(website_container, element_node)
@@ -144,6 +159,9 @@ func render() -> void:
print("Couldn't parse unsupported HTML tag \"%s\"" % element.tag_name)
i += 1
if scripts.size() > 0 and lua_vm and lua_api:
parser.process_scripts(lua_api, lua_vm)
static func safe_add_child(parent: Node, child: Node) -> void:
if child.get_parent():
@@ -232,7 +250,15 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
# Apply flex CONTAINER properties if it's a flex container
if is_flex_container:
StyleManager.apply_flex_container_properties(final_node, styles)
var flex_container_node = final_node
# If the node was wrapped in a MarginContainer, get the inner FlexContainer
if final_node is MarginContainer and final_node.get_child_count() > 0:
var first_child = final_node.get_child(0)
if first_child is FlexContainer:
flex_container_node = first_child
if flex_container_node is FlexContainer:
StyleManager.apply_flex_container_properties(flex_container_node, styles)
# Apply flex ITEM properties
StyleManager.apply_flex_item_properties(final_node, styles)
@@ -252,6 +278,7 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
if not child_element.is_inline_element() or is_flex_container:
var child_node = await create_element_node(child_element, parser)
if child_node and is_instance_valid(container_for_children):
parser.register_dom_node(child_element, child_node)
safe_add_child(container_for_children, child_node)
return final_node
@@ -340,7 +367,7 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
node = BackgroundUtils.create_panel_container_with_background(styles, hover_styles)
else:
node = DIV.instantiate()
node.init(element, parser)
node.init(element)
var has_only_text = is_text_only_element(element)