diff --git a/flumi/Scripts/B9/HTMLParser.gd b/flumi/Scripts/B9/HTMLParser.gd
index 991e965..ab2486e 100644
--- a/flumi/Scripts/B9/HTMLParser.gd
+++ b/flumi/Scripts/B9/HTMLParser.gd
@@ -34,7 +34,7 @@ class HTMLElement:
func get_preserved_text() -> String:
return text_content
- func get_bbcode_formatted_text(parser: HTMLParser = null) -> String:
+ func get_bbcode_formatted_text(parser: HTMLParser) -> String:
var styles = {}
if parser != null:
styles = parser.get_element_styles_with_inheritance(self, "", [])
@@ -142,8 +142,7 @@ func get_element_styles_with_inheritance(element: HTMLElement, event: String = "
var styles = {}
- var class_names = extract_class_names(element)
- styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event, class_names, element))
+ styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(event, element))
# Apply inline styles (higher priority) - force override CSS rules
var inline_style = element.get_attribute("style")
if inline_style.length() > 0:
@@ -169,8 +168,7 @@ func get_element_styles_internal(element: HTMLElement, event: String = "") -> Di
# Apply CSS rules
if parse_result.css_parser:
- var class_names = extract_class_names(element)
- styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event, class_names, element))
+ styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(event, element))
# Apply inline styles (higher priority) - force override CSS rules
var inline_style = element.get_attribute("style")
@@ -203,7 +201,7 @@ func parse_inline_style_with_event(style_string: String, event: String = "") ->
else:
# Check if this is a CSS class that might have pseudo-class rules
if parse_result.css_parser and parse_result.css_parser.stylesheet:
- var pseudo_styles = parse_result.css_parser.stylesheet.get_styles_for_element("", event, [utility_name], null)
+ var pseudo_styles = parse_result.css_parser.stylesheet.get_styles_for_element(event, null)
if not pseudo_styles.is_empty():
for property in pseudo_styles:
properties[property] = pseudo_styles[property]
@@ -287,7 +285,7 @@ func find_by_id(element_id: String) -> HTMLElement:
return null
-func register_dom_node(element: HTMLElement, node: Control) -> void:
+func register_dom_node(element: HTMLElement, node) -> void:
var element_id = element.get_id()
if element_id.length() > 0:
parse_result.dom_nodes[element_id] = node
diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd
index b524032..cc48d49 100644
--- a/flumi/Scripts/B9/Lua.gd
+++ b/flumi/Scripts/B9/Lua.gd
@@ -10,6 +10,7 @@ class EventSubscription:
var lua_api: LuaAPI
var connected_signal: String = ""
var connected_node: Node = null
+ var callback_func: Callable
var dom_parser: HTMLParser
var event_subscriptions: Dictionary = {}
@@ -489,6 +490,8 @@ func _element_on_event_handler(vm: LuauVM) -> int:
var signal_node = get_dom_node(dom_node, "signal")
var success = LuaEventUtils.connect_element_event(signal_node, event_name, subscription)
+ if not success:
+ print("ERROR: Failed to connect ", event_name, " event for ", element_id)
return _handle_subscription_result(vm, subscription, success)
@@ -641,6 +644,11 @@ func _execute_lua_callback(subscription: EventSubscription, args: Array = []) ->
else:
subscription.vm.lua_pop(1)
+func _execute_input_event_callback(subscription: EventSubscription, event_data: Dictionary) -> void:
+ if not event_subscriptions.has(subscription.id):
+ return
+ _execute_lua_callback(subscription, [event_data])
+
# Global input processing
func _input(event: InputEvent) -> void:
if event is InputEventKey:
@@ -699,6 +707,108 @@ func _handle_mousemove_event(mouse_event: InputEventMouseMotion, subscription: E
}
_execute_lua_callback(subscription, [mouse_info])
+# Input event handlers
+func _on_input_text_changed(new_text: String, subscription: EventSubscription) -> void:
+ _execute_input_event_callback(subscription, {"value": new_text})
+
+func _on_input_focus_lost(subscription: EventSubscription) -> void:
+ if not event_subscriptions.has(subscription.id):
+ return
+
+ # Get the current text value from the input node
+ var dom_node = dom_parser.parse_result.dom_nodes.get(subscription.element_id, null)
+ if dom_node:
+ var current_text = ""
+ if dom_node.has_method("get_text"):
+ current_text = dom_node.get_text()
+ elif "text" in dom_node:
+ current_text = dom_node.text
+
+ var event_info = {"value": current_text}
+ _execute_lua_callback(subscription, [event_info])
+
+func _on_input_value_changed(new_value, subscription: EventSubscription) -> void:
+ _execute_input_event_callback(subscription, {"value": new_value})
+
+func _on_input_color_changed(new_color: Color, subscription: EventSubscription) -> void:
+ _execute_input_event_callback(subscription, {"value": "#" + new_color.to_html(false)})
+
+func _on_input_toggled(pressed: bool, subscription: EventSubscription) -> void:
+ _execute_input_event_callback(subscription, {"value": pressed})
+
+func _on_input_item_selected(index: int, subscription: EventSubscription) -> void:
+ if not event_subscriptions.has(subscription.id):
+ return
+
+ # Get value from OptionButton
+ var dom_node = dom_parser.parse_result.dom_nodes.get(subscription.element_id, null)
+ var value = ""
+ var text = ""
+
+ if dom_node and dom_node is OptionButton:
+ var option_button = dom_node as OptionButton
+ text = option_button.get_item_text(index)
+ # Get actual value attribute (stored as metadata)
+ var metadata = option_button.get_item_metadata(index)
+ value = str(metadata) if metadata != null else text
+
+ var event_info = {"index": index, "value": value, "text": text}
+ _execute_lua_callback(subscription, [event_info])
+
+func _on_file_selected(file_path: String, subscription: EventSubscription) -> void:
+ if not event_subscriptions.has(subscription.id):
+ return
+
+ var dom_node = dom_parser.parse_result.dom_nodes.get(subscription.element_id, null)
+
+ if dom_node:
+ var file_container = dom_node.get_parent() # FileContainer (HBoxContainer)
+ if file_container:
+ var input_element = file_container.get_parent() # Input Control
+ if input_element and input_element.has_method("get_file_info"):
+ var file_info = input_element.get_file_info()
+ if not file_info.is_empty():
+ _execute_lua_callback(subscription, [file_info])
+ return
+
+ # Fallback
+ var file_name = file_path.get_file()
+ _execute_lua_callback(subscription, [{"fileName": file_name}])
+
+func _on_date_selected_text(date_text: String, subscription: EventSubscription) -> void:
+ if not event_subscriptions.has(subscription.id):
+ return
+
+ var event_info = {"value": date_text}
+ _execute_lua_callback(subscription, [event_info])
+
+func _on_form_submit(subscription: EventSubscription) -> void:
+ if not event_subscriptions.has(subscription.id):
+ return
+
+ # Find parent form
+ var form_data = {}
+ var element = dom_parser.find_by_id(subscription.element_id)
+ if element:
+ var form_element = element.parent
+ while form_element and form_element.tag_name != "form":
+ form_element = form_element.parent
+
+ if form_element:
+ var form_dom_node = dom_parser.parse_result.dom_nodes.get(form_element.get_attribute("id"), null)
+ if form_dom_node and form_dom_node.has_method("submit_form"):
+ form_data = form_dom_node.submit_form()
+
+ var event_info = {"data": form_data}
+ _execute_lua_callback(subscription, [event_info])
+
+func _on_text_submit(text: String, subscription: EventSubscription) -> void:
+ if not event_subscriptions.has(subscription.id):
+ return
+
+ var event_info = {"value": text}
+ _execute_lua_callback(subscription, [event_info])
+
# DOM node utilities
func get_dom_node(node: Node, purpose: String = "general") -> Node:
if not node:
@@ -717,6 +827,10 @@ func get_dom_node(node: Node, purpose: String = "general") -> Node:
return node.get("rich_text_label")
elif node.get_node_or_null("RichTextLabel"):
return node.get_node_or_null("RichTextLabel")
+ elif node is LineEdit or node is TextEdit or node is SpinBox or node is HSlider:
+ return node
+ elif node is CheckBox or node is ColorPickerButton or node is OptionButton:
+ return node
else:
return node
"text":
diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd
index 793a708..e6ba3c9 100644
--- a/flumi/Scripts/Constants.gd
+++ b/flumi/Scripts/Constants.gd
@@ -871,6 +871,281 @@ var HTML_CONTENT_DOM_MANIPULATION = """
""".to_utf8_buffer()
var HTML_CONTENT = """
+ Input Events API Demo
+
+
+
+
+
+
+
+
+
+
+ 🎛️ Input Events API Demo
+
+
+
+
+
+
+
+
+
Event Log
+
+
Waiting for events...
+
+
+
+
+
Available Events:
+
+ - input: Fires as you type (real-time)
+ - change: Fires when value changes and element loses focus
+ - focusin: Fires when element gains focus
+ - focusout: Fires when element loses focus
+ - click: Fires when button is clicked
+ - submit: Fires when form is submitted (includes form data)
+
+
+
+
+
+
+""".to_utf8_buffer()
+
+var HTML_CONTENT_CLIPBOARD = """
Network & Clipboard API Demo
diff --git a/flumi/Scripts/FontManager.gd b/flumi/Scripts/FontManager.gd
index b1d4bac..6c8cc8e 100644
--- a/flumi/Scripts/FontManager.gd
+++ b/flumi/Scripts/FontManager.gd
@@ -39,7 +39,7 @@ static func load_web_font(font_info: Dictionary) -> void:
http_request.timeout = 30.0
- http_request.request_completed.connect(func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
+ http_request.request_completed.connect(func(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray):
if response_code == 200:
if body.size() > 0:
diff --git a/flumi/Scripts/Tags/BaseListContainer.gd b/flumi/Scripts/Tags/BaseListContainer.gd
index 59421c4..dafa1b3 100644
--- a/flumi/Scripts/Tags/BaseListContainer.gd
+++ b/flumi/Scripts/Tags/BaseListContainer.gd
@@ -12,7 +12,7 @@ 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:
+func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
list_type = element.get_attribute("type").to_lower()
if list_type == "":
list_type = "disc" if not is_ordered else "decimal"
@@ -67,7 +67,7 @@ func calculate_marker_width(element: HTMLParser.HTMLElement) -> float:
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:
+func create_li_node(element: HTMLParser.HTMLElement, index: int, parser: HTMLParser) -> Control:
var li_container = HBoxContainer.new()
# Create marker
diff --git a/flumi/Scripts/Tags/br.gd b/flumi/Scripts/Tags/br.gd
index 80b5b27..07af994 100644
--- a/flumi/Scripts/Tags/br.gd
+++ b/flumi/Scripts/Tags/br.gd
@@ -1,4 +1,4 @@
extends Control
-func init(_element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
+func init(_element: HTMLParser.HTMLElement, _parser: HTMLParser) -> void:
pass
diff --git a/flumi/Scripts/Tags/button.gd b/flumi/Scripts/Tags/button.gd
index 3245d27..5bde3a9 100644
--- a/flumi/Scripts/Tags/button.gd
+++ b/flumi/Scripts/Tags/button.gd
@@ -4,7 +4,7 @@ extends HBoxContainer
var current_element: HTMLParser.HTMLElement
var current_parser: HTMLParser
-func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
+func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
current_element = element
current_parser = parser
var button_node: Button = $ButtonNode
@@ -26,6 +26,8 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
button_node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
apply_button_styles(element, parser)
+
+ parser.register_dom_node(element, button_node)
func apply_button_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
if not element or not parser:
@@ -150,12 +152,6 @@ func apply_button_color_with_states(button: Button, normal_color: Color, hover_c
button.add_theme_stylebox_override("normal", style_normal)
button.add_theme_stylebox_override("hover", style_hover)
button.add_theme_stylebox_override("pressed", style_pressed)
-
-func apply_button_radius(button: Button, radius: int) -> void:
- # Radius is now handled in create_button_stylebox
- # This method is kept for backward compatibility but is deprecated
- pass
-
func apply_padding_to_stylebox(style_box: StyleBoxFlat, styles: Dictionary) -> void:
# Apply general padding first
diff --git a/flumi/Scripts/Tags/div.gd b/flumi/Scripts/Tags/div.gd
index 1455830..ff8187d 100644
--- a/flumi/Scripts/Tags/div.gd
+++ b/flumi/Scripts/Tags/div.gd
@@ -1,5 +1,5 @@
class_name HTMLDiv
extends VBoxContainer
-func init(_element: HTMLParser.HTMLElement, _parser: HTMLParser = null):
+func init(_element: HTMLParser.HTMLElement, _parser: HTMLParser):
pass
diff --git a/flumi/Scripts/Tags/form.gd b/flumi/Scripts/Tags/form.gd
index 4c888c9..1fb3efc 100644
--- a/flumi/Scripts/Tags/form.gd
+++ b/flumi/Scripts/Tags/form.gd
@@ -1,4 +1,75 @@
extends VBoxContainer
-func init(_element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
- pass
+var form_element: HTMLParser.HTMLElement
+var form_parser: HTMLParser
+
+func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
+ form_element = element
+ form_parser = parser
+
+ parser.register_dom_node(element, self)
+
+func submit_form() -> Dictionary:
+ var form_data = {}
+
+ if not form_element:
+ return form_data
+
+ var form_inputs = collect_form_elements(form_element)
+
+ for input_element in form_inputs:
+ # Use 'key' attribute as primary identifier for form field mapping
+ var key_attr = input_element.get_attribute("key")
+ var name_attr = input_element.get_attribute("name")
+ var id_attr = input_element.get_attribute("id")
+
+ # Priority: key > name > id > tag_name
+ var key = key_attr if not key_attr.is_empty() else name_attr if not name_attr.is_empty() else id_attr if not id_attr.is_empty() else input_element.tag_name
+
+ # Get the DOM node for this element
+ if form_parser:
+ var element_id = input_element.get_attribute("id")
+ if element_id.is_empty():
+ element_id = input_element.tag_name
+ var dom_node = form_parser.parse_result.dom_nodes.get(element_id, null)
+ if dom_node:
+ var value = get_input_value(input_element.tag_name, dom_node)
+ if value != null:
+ form_data[key] = value
+
+ return form_data
+
+func collect_form_elements(element: HTMLParser.HTMLElement) -> Array:
+ var form_inputs = []
+
+ # Check if current element is an input element
+ if element.tag_name in ["input", "textarea", "select"]:
+ form_inputs.append(element)
+
+ # Recursively check children
+ for child in element.children:
+ form_inputs.append_array(collect_form_elements(child))
+
+ return form_inputs
+
+func get_input_value(tag_name: String, dom_node: Node):
+ match tag_name:
+ "input":
+ if dom_node.has_method("get_text"):
+ return dom_node.get_text()
+ elif dom_node.has_method("is_pressed"):
+ return dom_node.is_pressed()
+ elif dom_node is ColorPickerButton:
+ return "#" + dom_node.color.to_html()
+ elif dom_node is SpinBox:
+ return dom_node.value
+ elif dom_node is HSlider:
+ return dom_node.value
+ "textarea":
+ if dom_node is TextEdit:
+ return dom_node.text
+ "select":
+ if dom_node is OptionButton:
+ return dom_node.get_item_metadata(dom_node.selected)
+
+ return null
diff --git a/flumi/Scripts/Tags/img.gd b/flumi/Scripts/Tags/img.gd
index 4375b22..0e82b45 100644
--- a/flumi/Scripts/Tags/img.gd
+++ b/flumi/Scripts/Tags/img.gd
@@ -1,6 +1,6 @@
extends TextureRect
-func init(element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
+func init(element: HTMLParser.HTMLElement, _parser: HTMLParser) -> void:
var src = element.get_attribute("src")
if !src: return print("Ignoring
tag without \"src\" attribute.")
diff --git a/flumi/Scripts/Tags/input.gd b/flumi/Scripts/Tags/input.gd
index 8ba7a18..0ddeca0 100644
--- a/flumi/Scripts/Tags/input.gd
+++ b/flumi/Scripts/Tags/input.gd
@@ -6,8 +6,9 @@ const BROWSER_TEXT: Theme = preload("res://Scenes/Styles/BrowserText.tres")
var custom_hex_input: LineEdit
var _file_text_content: String = ""
var _file_binary_content: PackedByteArray = PackedByteArray()
+var _file_info: Dictionary = {}
-func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
+func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
var color_picker_button: ColorPickerButton = $ColorPickerButton
var picker: ColorPicker = color_picker_button.get_picker()
@@ -60,7 +61,7 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
# Define which child should be active for each input type
var active_child_map = {
"checkbox": "CheckBox",
- "radio": "RadioButton",
+ "radio": "CheckBox",
"color": "ColorPickerButton",
"password": "LineEdit",
"date": "DateButton",
@@ -72,6 +73,9 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
var active_child_name = active_child_map.get(input_type, "LineEdit")
remove_unused_children(active_child_name)
+ if not has_node(active_child_name):
+ return
+
var active_child = get_node(active_child_name)
active_child.visible = true
@@ -134,6 +138,18 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
active_child.set("disabled", true)
if element.has_attribute("readonly") and active_child.has_method("set_editable"):
active_child.set_editable(false)
+
+ # Enable focus mode for text inputs to support change events on focus lost
+ if active_child is LineEdit:
+ active_child.focus_mode = Control.FOCUS_ALL
+
+ if input_type == "file":
+ var file_dialog = active_child.get_node("FileDialog")
+ parser.register_dom_node(element, file_dialog)
+ elif input_type == "date":
+ parser.register_dom_node(element, active_child)
+ else:
+ parser.register_dom_node(element, active_child)
func remove_unused_children(keep_child_name: String) -> void:
for child in get_children():
@@ -289,16 +305,72 @@ func _on_file_selected(path: String) -> void:
var file_name = path.get_file()
file_label.text = file_name
- var file = FileAccess.open(path, FileAccess.READ)
+ _process_file_data(path)
+
+func _process_file_data(file_path: String) -> void:
+ var file_name = file_path.get_file()
+ var file_extension = file_path.get_extension().to_lower()
+ var file_size = 0
+ var mime_type = _get_mime_type(file_extension)
+ var is_image = _is_image_file(file_extension)
+ var is_text = _is_text_file(file_extension)
+
+ # Read file contents
+ var file = FileAccess.open(file_path, FileAccess.READ)
if file:
- _file_text_content = file.get_as_text()
- file.close()
-
- file = FileAccess.open(path, FileAccess.READ)
+ file_size = file.get_length()
_file_binary_content = file.get_buffer(file.get_length())
file.close()
-
- # TODO: when adding Lua, make these actually usable
+
+ _file_info = {
+ "fileName": file_name,
+ "size": file_size,
+ "type": mime_type,
+ "binary": _file_binary_content,
+ "isImage": is_image,
+ "isText": is_text
+ }
+
+ # Add text content only for text files
+ if is_text:
+ _file_text_content = _file_binary_content.get_string_from_utf8()
+ _file_info["text"] = _file_text_content
+
+ # Add base64 data URL for images
+ if is_image:
+ var base64_data = Marshalls.raw_to_base64(_file_binary_content)
+ _file_info["dataURL"] = "data:" + mime_type + ";base64," + base64_data
+
+func get_file_info() -> Dictionary:
+ return _file_info
+
+func _get_mime_type(extension: String) -> String:
+ match extension:
+ "png": return "image/png"
+ "jpg", "jpeg": return "image/jpeg"
+ "gif": return "image/gif"
+ "webp": return "image/webp"
+ "svg": return "image/svg+xml"
+ "bmp": return "image/bmp"
+ "txt": return "text/plain"
+ "html", "htm": return "text/html"
+ "css": return "text/css"
+ "js": return "application/javascript"
+ "json": return "application/json"
+ "pdf": return "application/pdf"
+ "mp3": return "audio/mpeg"
+ "wav": return "audio/wav"
+ "ogg": return "audio/ogg"
+ "mp4": return "video/mp4"
+ "avi": return "video/x-msvideo"
+ "mov": return "video/quicktime"
+ _: return "application/octet-stream"
+
+func _is_image_file(extension: String) -> bool:
+ return extension in ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"]
+
+func _is_text_file(extension: String) -> bool:
+ return extension in ["txt", "html", "htm", "css", "js", "json", "xml", "csv", "md", "gd"]
func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
if not element or not parser:
diff --git a/flumi/Scripts/Tags/option.gd b/flumi/Scripts/Tags/option.gd
index 83ac6ff..bfb28ab 100644
--- a/flumi/Scripts/Tags/option.gd
+++ b/flumi/Scripts/Tags/option.gd
@@ -1,6 +1,6 @@
extends Control
-func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
+func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
# This is mainly for cases where