add create, append, remove, set_timeout, clear_timeout, get_children, refactor ul/ol and dynamic recounting

This commit is contained in:
Face
2025-08-04 16:31:16 +03:00
parent e22ad21fd0
commit 6c0a08d501
17 changed files with 786 additions and 242 deletions

View File

@@ -1,9 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://bli1234568aa"]
[ext_resource type="Script" uid="uid://ps8duq0aw3tu" path="res://Scripts/Tags/li.gd" id="1_li"]
[node name="li" type="VBoxContainer"]
anchors_preset = 10
anchor_right = 1.0
grow_horizontal = 2
script = ExtResource("1_li")

View File

@@ -1,6 +1,5 @@
[gd_scene load_steps=3 format=3 uid="uid://bopt1234568aa"]
[gd_scene load_steps=2 format=3 uid="uid://bopt1234568aa"]
[ext_resource type="Script" uid="uid://ps8duq0aw3tu" path="res://Scripts/Tags/li.gd" id="1_option"]
[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_theme"]
[node name="option" type="Control"]
@@ -8,7 +7,6 @@ layout_mode = 3
anchors_preset = 10
anchor_right = 1.0
grow_horizontal = 2
script = ExtResource("1_option")
[node name="RichTextLabel" type="RichTextLabel" parent="."]
layout_mode = 2

View File

@@ -18,8 +18,8 @@ class HTMLElement:
func has_attribute(name_: String) -> bool:
return attributes.has(name_)
func get_class_name() -> String:
return get_attribute("class")
func set_attribute(name_: String, value: String) -> void:
attributes.set(name_, value)
func get_id() -> String:
return get_attribute("id")

View File

@@ -16,6 +16,30 @@ 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()
func get_or_assign_element_id(element: HTMLParser.HTMLElement) -> String:
var existing_id = element.get_attribute("id")
if not existing_id.is_empty():
element_id_registry[element] = existing_id
return existing_id
if element_id_registry.has(element):
return element_id_registry[element]
var new_id = "auto_" + str(element_id_counter)
element_id_counter += 1
element.set_attribute("id", new_id)
element_id_registry[element] = new_id
return new_id
func _gurt_select_handler(vm: LuauVM) -> int:
var selector: String = vm.luaL_checkstring(1)
@@ -38,6 +62,87 @@ func _gurt_select_handler(vm: LuauVM) -> int:
add_element_methods(vm)
return 1
# selectAll() function to find multiple elements
func _gurt_select_all_handler(vm: LuauVM) -> int:
var selector: String = vm.luaL_checkstring(1)
var elements: Array[HTMLParser.HTMLElement] = []
# Handle different selector types
if selector.begins_with("#"):
# ID selector - find single element
var element_id = selector.substr(1)
var element = dom_parser.find_by_id(element_id)
if element:
elements.append(element)
LuaPrintUtils.lua_print_direct("WARNING: Using ID selector in select_all is not recommended, use select instead.")
elif selector.begins_with("."):
# Class selector - find all elements with class
var cls = selector.substr(1)
for element in dom_parser.parse_result.all_elements:
var element_classes = CSSParser.smart_split_utility_classes(element.get_attribute("style"))
if cls in element_classes:
elements.append(element)
else:
# Tag selector - find all elements with tag name
elements = dom_parser.find_all(selector)
vm.lua_newtable()
var index = 1
for element in elements:
var element_id = get_or_assign_element_id(element)
# Create element wrapper
vm.lua_newtable()
vm.lua_pushstring(element_id)
vm.lua_setfield(-2, "_element_id")
vm.lua_pushstring(element.tag_name)
vm.lua_setfield(-2, "_tag_name")
add_element_methods(vm)
# Add to array at index
vm.lua_rawseti(-2, index)
index += 1
return 1
# create() function to create HTML element
func _gurt_create_handler(vm: LuauVM) -> int:
var tag_name: String = vm.luaL_checkstring(1)
var options: Dictionary = {}
if vm.lua_gettop() >= 2 and vm.lua_istable(2):
options = vm.lua_todictionary(2)
var element = HTMLParser.HTMLElement.new(tag_name)
# Apply options as attributes and content
for key in options:
if key == "text":
element.text_content = str(options[key])
else:
element.attributes[str(key)] = str(options[key])
# Add to parser's element collection first
dom_parser.parse_result.all_elements.append(element)
# Get or assign stable ID
var unique_id = get_or_assign_element_id(element)
# Create Lua element wrapper with methods
vm.lua_newtable()
vm.lua_pushstring(unique_id)
vm.lua_setfield(-2, "_element_id")
vm.lua_pushstring(tag_name)
vm.lua_setfield(-2, "_tag_name")
vm.lua_pushboolean(true)
vm.lua_setfield(-2, "_is_dynamic")
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")
@@ -47,6 +152,45 @@ func add_element_methods(vm: LuauVM) -> void:
vm.lua_pushcallable(_element_on_event_handler, "element.on")
vm.lua_setfield(-2, "on")
vm.lua_pushcallable(_element_append_handler, "element.append")
vm.lua_setfield(-2, "append")
vm.lua_pushcallable(_element_remove_handler, "element.remove")
vm.lua_setfield(-2, "remove")
vm.lua_pushcallable(_element_get_children_handler, "element.get_children")
vm.lua_setfield(-2, "get_children")
func _element_get_children_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
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, "tag_name")
vm.lua_pushstring(child.get_text_content())
vm.lua_setfield(-2, "text")
vm.lua_rawseti(-2, index)
index += 1
return 1
# Element manipulation handlers
func _element_set_text_handler(vm: LuauVM) -> int:
@@ -83,6 +227,129 @@ func _element_get_text_handler(vm: LuauVM) -> int:
vm.lua_pushstring(text)
return 1
# 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
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)
# Timeout management handlers
func _gurt_set_timeout_handler(vm: LuauVM) -> int:
return timeout_manager.set_timeout_handler(vm, self)
func _gurt_clear_timeout_handler(vm: LuauVM) -> int:
return timeout_manager.clear_timeout_handler(vm)
# Event system handlers
func _element_on_event_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)

View File

@@ -590,7 +590,8 @@ var HTML_CONTENT3 = """<head>
</body>
""".to_utf8_buffer()
var HTML_CONTENT = """<head>
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">
@@ -663,6 +664,38 @@ var HTML_CONTENT = """<head>
else
gurt.log('Could not find button or event log element')
end
-- DOM Manipulation Demo
gurt.log('Testing DOM manipulation...')
-- Create a new div with styling
local new_div = gurt.create('div', { style = 'bg-red-500 p-4 rounded-lg mb-4' })
-- Create a paragraph with text
local new_p = gurt.create('p', {
style = 'text-white font-bold text-lg',
text = 'This element was created dynamically with Lua!'
})
-- Append paragraph to div
new_div:append(new_p)
-- Append div to body
gurt.body:append(new_div)
-- Create another element to test removal
local temp_element = gurt.create('div', {
style = 'bg-yellow-400 p-2 rounded text-black',
text = 'This will be removed in 3 seconds...'
})
gurt.body:append(temp_element)
local test = gurt.set_timeout(function()
print('removed')
temp_element:remove()
end, 3000)
-- gurt.clear_timeout(test)
</script>
</head>
@@ -681,5 +714,61 @@ var HTML_CONTENT = """<head>
<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()
var HTML_CONTENT_ADD_REMOVE = """<head>
<title>Lua List Manipulation 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="Adding and popping list items with GURT Lua API">
<style>
body { bg-[#f8f9fa] p-6 }
h1 { text-[#2563eb] text-4xl font-bold }
.container { flex flex-row bg-[#ffffff] p-4 rounded-lg shadow-lg }
.demo-button { bg-[#3b82f6] text-white px-4 py-2 rounded hover:bg-[#2563eb] cursor-pointer }
ul { list-disc pl-6 }
li { text-[#111827] py-1 }
</style>
<script>
local add_button = gurt.select('#add-button')
local pop_button = gurt.select('#pop-button')
local list = gurt.select('#item-list')
local counter = 1
gurt.log('List manipulation script started.')
add_button:on('click', function()
local new_item = gurt.create('li', {
text = 'Item #' .. counter
})
list:append(new_item)
counter = counter + 1
end)
pop_button:on('click', function()
local items = list:get_children()
local last = items[#items]
if last then
last:remove()
counter = math.max(1, counter - 1)
end
end)
</script>
</head>
<body>
<h1 id="main-heading">List Manipulation with Lua</h1>
<div style="container">
<p>Use the buttons below to add or remove items from the list:</p>
<button id="add-button" style="demo-button inline-block mr-2">Add Item</button>
<button id="pop-button" style="demo-button inline-block">Pop Item</button>
</div>
<ul id="item-list" style="mt-4 bg-[#f3f4f6] p-4 rounded min-h-24">
<!-- List items will appear here -->
</ul>
</body>
""".to_utf8_buffer()

View File

@@ -204,7 +204,16 @@ static func apply_margin_wrapper(node: Control, styles: Dictionary) -> Control:
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
margin_container.add_child(node)
# Handle reparenting properly
var original_parent = node.get_parent()
if original_parent:
var node_index = node.get_index()
original_parent.remove_child(node)
margin_container.add_child(node)
original_parent.add_child(margin_container)
original_parent.move_child(margin_container, node_index)
else:
margin_container.add_child(node)
return margin_container

View File

@@ -0,0 +1,282 @@
class_name BaseListContainer
extends VBoxContainer
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
var list_type: String
var marker_width: float
var parser_ref: HTMLParser = null
var is_ordered: bool = false
func _ready():
child_entered_tree.connect(_on_child_added)
child_exiting_tree.connect(_on_child_removed)
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
list_type = element.get_attribute("type").to_lower()
if list_type == "":
list_type = "disc" if not is_ordered else "decimal"
parser_ref = parser
marker_width = await calculate_marker_width(element)
var index = 1
for child_element in element.children:
if child_element.tag_name == "li":
var li_node = create_li_node(child_element, index, parser)
if li_node:
add_child(li_node)
index += 1
func calculate_marker_width(element: HTMLParser.HTMLElement) -> float:
var temp_label = RichTextLabel.new()
temp_label.bbcode_enabled = true
temp_label.fit_content = true
temp_label.scroll_active = false
temp_label.theme = BROWSER_TEXT
add_child(temp_label)
var sample_text = ""
if is_ordered:
var item_count = 0
for child_element in element.children:
if child_element.tag_name == "li":
item_count += 1
sample_text = str(item_count) + "."
else:
match list_type:
"circle":
sample_text = ""
"disc":
sample_text = ""
"square":
sample_text = ""
"none":
sample_text = " "
_:
sample_text = ""
StyleManager.apply_styles_to_label(temp_label, {}, null, null, sample_text)
await get_tree().process_frame
var width = temp_label.get_content_width() + 5
remove_child(temp_label)
temp_label.queue_free()
return max(width, 20.0 if not is_ordered else 30.0)
func create_li_node(element: HTMLParser.HTMLElement, index: int, parser: HTMLParser = null) -> Control:
var li_container = HBoxContainer.new()
# Create marker
var marker_label = RichTextLabel.new()
marker_label.custom_minimum_size = Vector2(marker_width, 0)
marker_label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
marker_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
marker_label.bbcode_enabled = true
marker_label.fit_content = true
marker_label.scroll_active = false
marker_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
marker_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
marker_label.theme = BROWSER_TEXT
var marker_text = get_marker_text(index)
var marker_styles = parser.get_element_styles_with_inheritance(element, "", []) if parser else {}
StyleManager.apply_styles_to_label(marker_label, marker_styles, element, parser, marker_text)
# Create content
var content_label = RichTextLabel.new()
content_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content_label.bbcode_enabled = true
content_label.fit_content = true
content_label.scroll_active = false
content_label.theme = BROWSER_TEXT
var content_text = element.get_bbcode_formatted_text(parser)
var content_styles = parser.get_element_styles_with_inheritance(element, "", []) if parser else {}
StyleManager.apply_styles_to_label(content_label, content_styles, element, parser, content_text)
li_container.add_theme_constant_override("separation", 0)
li_container.add_child(marker_label)
li_container.add_child(content_label)
# Store element metadata on the container for renumbering
li_container.set_meta("html_element", element)
var styles = parser.get_element_styles_with_inheritance(element, "", [])
if BackgroundUtils.needs_background_wrapper(styles):
var panel_container = BackgroundUtils.create_panel_container_with_background(styles)
panel_container.name = "Li"
# Store element metadata on the panel container too
panel_container.set_meta("html_element", element)
# Get the VBoxContainer inside PanelContainer and replace it with our HBoxContainer
var vbox = panel_container.get_child(0)
panel_container.remove_child(vbox)
vbox.queue_free()
panel_container.add_child(li_container)
return panel_container
else:
return li_container
func _on_child_added(child: Node):
if child.has_meta("html_element"):
var element = child.get_meta("html_element")
if element is HTMLParser.HTMLElement and element.tag_name == "li":
call_deferred("_process_dynamic_li", child, element)
func _process_dynamic_li(child: Node, element: HTMLParser.HTMLElement):
child_entered_tree.disconnect(_on_child_added)
# Get the correct index for this new item
var current_li_count = 0
for existing_child in get_children():
if existing_child != child:
current_li_count += 1
# Remove the basic li node and replace with properly formatted one
if child.get_parent() == self:
remove_child(child)
var li_node = create_li_node(element, current_li_count + 1, parser_ref)
if li_node:
var element_id = element.get_attribute("id")
if parser_ref and element_id:
parser_ref.parse_result.dom_nodes[element_id] = li_node
add_child(li_node)
child.queue_free()
# Reconnect signal
child_entered_tree.connect(_on_child_added)
func _on_child_removed(_child: Node):
if is_ordered: # Only OL needs renumbering
call_deferred("_renumber_list")
func _renumber_list():
# Temporarily disconnect signals to avoid recursion
child_entered_tree.disconnect(_on_child_added)
child_exiting_tree.disconnect(_on_child_removed)
# Get all current li children
var li_children = []
for child in get_children():
var is_li = false
if child is HBoxContainer:
is_li = true
elif child is PanelContainer and child.get_child_count() > 0:
var inner_child = child.get_child(0)
if inner_child is HBoxContainer:
is_li = true
if is_li:
li_children.append(child)
# Renumber all existing items
for i in range(li_children.size()):
var child = li_children[i]
var marker_label = null
# Find the marker label within the child structure
if child is HBoxContainer and child.get_child_count() > 0:
marker_label = child.get_child(0)
elif child is PanelContainer and child.get_child_count() > 0:
var hbox = child.get_child(0)
if hbox is HBoxContainer and hbox.get_child_count() > 0:
marker_label = hbox.get_child(0)
# Update the marker text - recreate it completely to avoid BBCode corruption
if marker_label and marker_label is RichTextLabel:
var index = i + 1
var new_marker_text = get_marker_text(index)
# Get the HTMLElement from the li container to reapply styles properly
var element = null
if child.has_meta("html_element"):
element = child.get_meta("html_element")
elif child is PanelContainer and child.get_child_count() > 0:
var hbox = child.get_child(0)
if hbox.has_meta("html_element"):
element = hbox.get_meta("html_element")
if element and parser_ref:
var marker_styles = parser_ref.get_element_styles_with_inheritance(element, "", [])
StyleManager.apply_styles_to_label(marker_label, marker_styles, element, parser_ref, new_marker_text)
else:
# Fallback - just set the text
marker_label.text = new_marker_text
# Reconnect signals
child_entered_tree.connect(_on_child_added)
child_exiting_tree.connect(_on_child_removed)
func refresh_list():
# Force refresh of all li children for dynamically added content
var children_to_process = []
for child in get_children():
if child.has_meta("html_element"):
var element = child.get_meta("html_element")
if element is HTMLParser.HTMLElement and element.tag_name == "li":
children_to_process.append([child, element])
# Clear all children first
for child_data in children_to_process:
var child = child_data[0]
remove_child(child)
child.queue_free()
# Recalculate marker width if needed
var new_count = children_to_process.size()
if new_count > 0 and is_ordered:
marker_width = await calculate_marker_width(children_to_process[0][1])
# Re-add with correct indices
for i in range(children_to_process.size()):
var element = children_to_process[i][1]
var li_node = create_li_node(element, i + 1, parser_ref)
if li_node:
add_child(li_node)
func int_to_roman(num: int) -> String:
var values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
var symbols = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
var result = ""
for i in range(values.size()):
while num >= values[i]:
result += symbols[i]
num -= values[i]
return result
func get_marker_text(index: int) -> String:
if is_ordered:
match list_type:
"decimal":
return str(index) + "."
"zero-lead":
return "%02d." % index
"lower-alpha":
return char(96 + index) + "."
"lower-roman":
return int_to_roman(index).to_lower() + "."
"upper-alpha":
return char(64 + index) + "."
"upper-roman":
return int_to_roman(index) + "."
"none":
return ""
_:
return str(index) + "."
else:
match list_type:
"circle":
return ""
"disc":
return ""
"square":
return ""
"none":
return " "
_:
return ""

View File

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

View File

@@ -1,8 +0,0 @@
extends Control
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
# This is mainly for cases where <li> appears outside of <ul>/<ol>
var label: RichTextLabel = $RichTextLabel
var styles = parser.get_element_styles_with_inheritance(element, "", [])
StyleManager.apply_styles_to_label(label, styles, element, parser)

View File

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

View File

@@ -1,117 +1,5 @@
extends VBoxContainer
extends BaseListContainer
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
var list_type = element.get_attribute("type").to_lower()
if list_type == "": list_type = "decimal" # Default
var item_count = 0
for child_element in element.children:
if child_element.tag_name == "li":
item_count += 1
var marker_min_width = await calculate_marker_width(list_type, item_count)
var index = 1
for child_element in element.children:
if child_element.tag_name == "li":
var li_node = create_li_node(child_element, list_type, index, marker_min_width, parser)
if li_node:
add_child(li_node)
index += 1
func calculate_marker_width(list_type: String, max_index: int) -> float:
var temp_label = RichTextLabel.new()
temp_label.bbcode_enabled = true
temp_label.fit_content = true
temp_label.scroll_active = false
temp_label.theme = BROWSER_TEXT
add_child(temp_label)
var marker_text = get_marker_for_type(list_type, max_index)
StyleManager.apply_styles_to_label(temp_label, {}, null, null, marker_text)
await get_tree().process_frame
var width = temp_label.get_content_width() + 5
remove_child(temp_label)
temp_label.queue_free()
return max(width, 30) # Minimum pixels
func create_li_node(element: HTMLParser.HTMLElement, list_type: String, index: int, marker_width: float = 30, parser: HTMLParser = null) -> Control:
var li_container = HBoxContainer.new()
# Create number/letter marker
var marker_label = RichTextLabel.new()
marker_label.custom_minimum_size = Vector2(marker_width, 0)
marker_label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
marker_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
marker_label.bbcode_enabled = true
marker_label.fit_content = true
marker_label.scroll_active = false
marker_label.theme = BROWSER_TEXT
var marker_text = get_marker_for_type(list_type, index)
var marker_styles = parser.get_element_styles_with_inheritance(element, "", []) if parser else {}
StyleManager.apply_styles_to_label(marker_label, marker_styles, element, parser, marker_text)
# Create content
var content_label = RichTextLabel.new()
content_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content_label.bbcode_enabled = true
content_label.fit_content = true
content_label.scroll_active = false
content_label.theme = BROWSER_TEXT
var content_text = element.get_bbcode_formatted_text(parser)
var content_styles = parser.get_element_styles_with_inheritance(element, "", []) if parser else {}
StyleManager.apply_styles_to_label(content_label, content_styles, element, parser, content_text)
li_container.add_theme_constant_override("separation", 0)
li_container.add_child(marker_label)
li_container.add_child(content_label)
var styles = parser.get_element_styles_with_inheritance(element, "", [])
if BackgroundUtils.needs_background_wrapper(styles):
var panel_container = BackgroundUtils.create_panel_container_with_background(styles)
panel_container.name = "Li"
# Get the VBoxContainer inside PanelContainer and replace it with our HBoxContainer
var vbox = panel_container.get_child(0)
panel_container.remove_child(vbox)
vbox.queue_free()
panel_container.add_child(li_container)
return panel_container
else:
return li_container
func get_marker_for_type(list_type: String, index: int) -> String:
match list_type:
"decimal":
return str(index) + "."
"zero-lead":
return "%02d." % index
"lower-alpha", "lower-roman":
return char(96 + index) + "." if list_type == "lower-alpha" else int_to_roman(index).to_lower() + "."
"upper-alpha", "upper-roman":
return char(64 + index) + "." if list_type == "upper-alpha" else int_to_roman(index) + "."
"none":
return ""
_:
return str(index) + "." # Default to decimal
func int_to_roman(num: int) -> String:
var values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
var symbols = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
var result = ""
for i in range(values.size()):
while num >= values[i]:
result += symbols[i]
num -= values[i]
return result
func _ready():
is_ordered = true
super._ready()

View File

@@ -1,97 +1,5 @@
extends VBoxContainer
extends BaseListContainer
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
var list_type = element.get_attribute("type").to_lower()
if list_type == "": list_type = "disc" # Default
var marker_min_width = await calculate_marker_width(list_type)
for child_element in element.children:
if child_element.tag_name == "li":
var li_node = create_li_node(child_element, list_type, marker_min_width, parser)
if li_node:
add_child(li_node)
func calculate_marker_width(list_type: String) -> float:
var temp_label = RichTextLabel.new()
temp_label.bbcode_enabled = true
temp_label.fit_content = true
temp_label.scroll_active = false
temp_label.theme = BROWSER_TEXT
add_child(temp_label)
var bullet_text = get_bullet_for_type(list_type)
StyleManager.apply_styles_to_label(temp_label, {}, null, null, bullet_text)
await get_tree().process_frame
var width = temp_label.get_content_width() + 5 # padding
remove_child(temp_label)
temp_label.queue_free()
return max(width, 20) # Minimum pixels
func create_li_node(element: HTMLParser.HTMLElement, list_type: String, marker_width: float = 20, parser: HTMLParser = null) -> Control:
var li_container = HBoxContainer.new()
# Create bullet point
var bullet_label = RichTextLabel.new()
bullet_label.custom_minimum_size = Vector2(marker_width, 0)
bullet_label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
bullet_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
bullet_label.bbcode_enabled = true
bullet_label.fit_content = true
bullet_label.scroll_active = false
bullet_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
bullet_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
bullet_label.theme = BROWSER_TEXT
var bullet_text = get_bullet_for_type(list_type)
var bullet_styles = parser.get_element_styles_with_inheritance(element, "", []) if parser else {}
StyleManager.apply_styles_to_label(bullet_label, bullet_styles, element, parser, bullet_text)
# Create content
var content_label = RichTextLabel.new()
content_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content_label.bbcode_enabled = true
content_label.fit_content = true
content_label.theme = BROWSER_TEXT
content_label.scroll_active = false
var content_text = element.get_bbcode_formatted_text(parser)
var content_styles = parser.get_element_styles_with_inheritance(element, "", []) if parser else {}
StyleManager.apply_styles_to_label(content_label, content_styles, element, parser, content_text)
li_container.add_theme_constant_override("separation", 0)
li_container.add_child(bullet_label)
li_container.add_child(content_label)
var styles = parser.get_element_styles_with_inheritance(element, "", [])
if BackgroundUtils.needs_background_wrapper(styles):
var panel_container = BackgroundUtils.create_panel_container_with_background(styles)
panel_container.name = "Li"
# Get the VBoxContainer inside PanelContainer and replace it with our HBoxContainer
var vbox = panel_container.get_child(0)
panel_container.remove_child(vbox)
vbox.queue_free()
panel_container.add_child(li_container)
return panel_container
else:
return li_container
func get_bullet_for_type(list_type: String) -> String:
match list_type:
"circle":
return ""
"disc":
return ""
"square":
return ""
"none":
return " "
_:
return "" # Default to disc
func _ready():
is_ordered = false
super._ready()

View File

@@ -35,6 +35,18 @@ static func setup_gurt_api(vm: LuauVM, lua_api, dom_parser: HTMLParser) -> void:
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.select_all")
vm.lua_setfield(-2, "select_all")
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.set_timeout")
vm.lua_setfield(-2, "set_timeout")
vm.lua_pushcallable(lua_api._gurt_clear_timeout_handler, "gurt.clear_timeout")
vm.lua_setfield(-2, "clear_timeout")
# Add body element access
var body_element = dom_parser.find_first("body")
if body_element:
@@ -52,6 +64,9 @@ static func setup_gurt_api(vm: LuauVM, lua_api, dom_parser: HTMLParser) -> void:
vm.lua_pushcallable(lua_api._body_on_event_handler, "body.on")
vm.lua_setfield(-2, "on")
vm.lua_pushcallable(lua_api._element_append_handler, "body.append")
vm.lua_setfield(-2, "append")
vm.lua_setfield(-2, "body")
vm.lua_setglobal("gurt")

View File

@@ -10,9 +10,12 @@ static func lua_print(vm: LuauVM) -> int:
message_parts.append(value_str)
var final_message = "\t".join(message_parts)
print("GURT LOG: ", final_message)
lua_print_direct(final_message)
return 0
static func lua_print_direct(msg) -> void:
print("GURT LOG: ", msg)
static func lua_value_to_string(vm: LuauVM, index: int) -> String:
var lua_type = vm.lua_type(index)
@@ -84,4 +87,4 @@ static func table_to_string(vm: LuauVM, index: int, max_depth: int = 3, current_
result += ", ..."
result += "}"
return result
return result

View File

@@ -0,0 +1,102 @@
class_name LuaTimeoutManager
extends RefCounted
var active_timeouts: Dictionary = {}
var next_timeout_id: int = 1
var next_callback_ref: int = 1
class TimeoutInfo:
var id: int
var callback_ref: int
var vm: LuauVM
var timer: Timer
var timeout_manager: LuaTimeoutManager
func _init(timeout_id: int, cb_ref: int, lua_vm: LuauVM, manager: LuaTimeoutManager):
id = timeout_id
callback_ref = cb_ref
vm = lua_vm
timeout_manager = manager
func set_timeout_handler(vm: LuauVM, parent_node: Node) -> int:
vm.luaL_checktype(1, vm.LUA_TFUNCTION)
var delay_ms: int = vm.luaL_checkint(2)
var timeout_id = next_timeout_id
next_timeout_id += 1
# Store the callback function in the registry
vm.lua_pushvalue(1)
var callback_ref = vm.luaL_ref(vm.LUA_REGISTRYINDEX)
# Create timeout info
var timeout_info = TimeoutInfo.new(timeout_id, callback_ref, vm, self)
# Create and configure timer
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
active_timeouts[timeout_id] = timeout_info
# Add timer to scene tree
parent_node.add_child(timer)
timer.start()
# Return timeout ID
vm.lua_pushinteger(timeout_id)
return 1
func clear_timeout_handler(vm: LuauVM) -> int:
var timeout_id: int = vm.luaL_checkint(1)
var timeout_info = active_timeouts.get(timeout_id, null)
if timeout_info:
# Stop and remove timer
if timeout_info.timer:
timeout_info.timer.stop()
timeout_info.timer.queue_free()
# Clean up callback reference
vm.lua_pushnil()
vm.lua_rawseti(vm.LUA_REGISTRYINDEX, timeout_info.callback_ref)
# Remove from active timeouts
active_timeouts.erase(timeout_id)
return 0
func _on_timeout_triggered(timeout_info: TimeoutInfo) -> void:
if not active_timeouts.has(timeout_info.id):
return
# Execute the callback
timeout_info.vm.lua_rawgeti(timeout_info.vm.LUA_REGISTRYINDEX, timeout_info.callback_ref)
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_pushnil()
timeout_info.vm.lua_rawseti(timeout_info.vm.LUA_REGISTRYINDEX, timeout_info.callback_ref)
active_timeouts.erase(timeout_info.id)
func cleanup_all_timeouts():
# Clean up all active timeouts
for timeout_id in active_timeouts:
var timeout_info = active_timeouts[timeout_id]
if timeout_info.timer:
timeout_info.timer.stop()
timeout_info.timer.queue_free()
# Release Lua callback reference
if timeout_info.vm and timeout_info.callback_ref:
timeout_info.vm.lua_pushnil()
timeout_info.vm.lua_rawseti(timeout_info.vm.LUA_REGISTRYINDEX, timeout_info.callback_ref)
active_timeouts.clear()

View File

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

View File

@@ -24,7 +24,6 @@ const INPUT = preload("res://Scenes/Tags/input.tscn")
const BUTTON = preload("res://Scenes/Tags/button.tscn")
const UL = preload("res://Scenes/Tags/ul.tscn")
const OL = preload("res://Scenes/Tags/ol.tscn")
const LI = preload("res://Scenes/Tags/li.tscn")
const SELECT = preload("res://Scenes/Tags/select.tscn")
const OPTION = preload("res://Scenes/Tags/option.tscn")
const TEXTAREA = preload("res://Scenes/Tags/textarea.tscn")
@@ -347,8 +346,8 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
await node.init(element, parser)
return node
"li":
node = LI.instantiate()
node.init(element, parser)
node = P.instantiate()
node.init(element)
"select":
node = SELECT.instantiate()
node.init(element)