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)

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Tigran Mamedov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

View File

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,13 @@
[configuration]
entry_symbol = "gdluau_library_init"
compatibility_minimum = "4.2"
[libraries]
windows.debug.x86_64 = "res://addons/gdluau/bin/windows/gdluau.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "res://addons/gdluau/bin/windows/gdluau.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "res://addons/gdluau/bin/linux/libgdluau.linux.template_debug.x86_64.so"
linux.release.x86_64 = "res://addons/gdluau/bin/linux/libgdluau.linux.template_release.x86_64.so"
macos.debug = "res://addons/gdluau/bin/macos/macos.framework/libgdluau.macos.template_debug"
macos.release = "res://addons/gdluau/bin/macos/macos.framework/libgdluau.macos.template_release"

View File

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