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

+ +
+
+ +
+

Input Elements

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +

Checkbox Option

+
+ +
+ Option 1 + Option 2 + Option 3 +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+

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