diff --git a/flumi/Scripts/B9/CSSParser.gd b/flumi/Scripts/B9/CSSParser.gd index 683459a..865dad9 100644 --- a/flumi/Scripts/B9/CSSParser.gd +++ b/flumi/Scripts/B9/CSSParser.gd @@ -613,7 +613,12 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) -> rule.properties["border-radius"] = "4px" # Default rounded return - # Handle padding classes like p-8, px-4, py-2, etc. + # Handle padding classes like p-8, px-4, py-2, p-[20px], etc. + if utility_name.begins_with("p-[") and utility_name.ends_with("]"): + var val = SizeUtils.extract_bracket_content(utility_name, 2) # after 'p-' + var padding_value = SizeUtils.parse_size(val) + rule.properties["padding"] = padding_value + return if utility_name.begins_with("p-"): var val = utility_name.substr(2) var padding_value = SizeUtils.parse_size(val) @@ -694,16 +699,58 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) -> rule.properties["border-radius"] = str(int(val)) + "px" return - # Handle margin auto classes for centering - if utility_name == "mx-auto": - rule.properties["mx-auto"] = true + # Handle margin classes like m-8, mx-4, my-2, m-[10px], etc. + if utility_name.begins_with("m-[") and utility_name.ends_with("]"): + var val = SizeUtils.extract_bracket_content(utility_name, 2) # after 'm-' + var margin_value = SizeUtils.parse_size(val) + rule.properties["margin"] = margin_value return - if utility_name == "my-auto": - rule.properties["my-auto"] = true + if utility_name.begins_with("m-"): + var val = utility_name.substr(2) + if val == "auto": + rule.properties["mx-auto"] = true + rule.properties["my-auto"] = true + return + var margin_value = SizeUtils.parse_size(val) + rule.properties["margin"] = margin_value return - if utility_name == "m-auto": - rule.properties["mx-auto"] = true - rule.properties["my-auto"] = true + if utility_name.begins_with("mx-"): + var val = utility_name.substr(3) + if val == "auto": + rule.properties["mx-auto"] = true + return + var margin_value = SizeUtils.parse_size(val) + rule.properties["margin-left"] = margin_value + rule.properties["margin-right"] = margin_value + return + if utility_name.begins_with("my-"): + var val = utility_name.substr(3) + if val == "auto": + rule.properties["my-auto"] = true + return + var margin_value = SizeUtils.parse_size(val) + rule.properties["margin-top"] = margin_value + rule.properties["margin-bottom"] = margin_value + return + if utility_name.begins_with("mt-"): + var val = utility_name.substr(3) + var margin_value = SizeUtils.parse_size(val) + rule.properties["margin-top"] = margin_value + return + if utility_name.begins_with("mr-"): + var val = utility_name.substr(3) + var margin_value = SizeUtils.parse_size(val) + rule.properties["margin-right"] = margin_value + return + if utility_name.begins_with("mb-"): + var val = utility_name.substr(3) + var margin_value = SizeUtils.parse_size(val) + rule.properties["margin-bottom"] = margin_value + return + if utility_name.begins_with("ml-"): + var val = utility_name.substr(3) + var margin_value = SizeUtils.parse_size(val) + rule.properties["margin-left"] = margin_value return # Apply border properties diff --git a/flumi/Scripts/B9/HTMLParser.gd b/flumi/Scripts/B9/HTMLParser.gd index fd20e9a..8911e73 100644 --- a/flumi/Scripts/B9/HTMLParser.gd +++ b/flumi/Scripts/B9/HTMLParser.gd @@ -140,12 +140,12 @@ func get_element_styles_with_inheritance(element: HTMLElement, event: String = " # Apply inline styles (higher priority) - force override CSS rules var inline_style = element.get_attribute("style") if inline_style.length() > 0: - var inline_parsed = CSSParser.parse_inline_style(inline_style) + var inline_parsed = parse_inline_style_with_event(inline_style, event) for property in inline_parsed: styles[property] = inline_parsed[property] # Inherit certain properties from parent elements - var inheritable_properties = ["width", "height", "font-size", "color", "font-family"] + var inheritable_properties = ["width", "height", "font-size", "color", "font-family", "cursor"] var parent_element = element.parent while parent_element: var parent_styles = get_element_styles_internal(parent_element, event) diff --git a/flumi/Scripts/StyleManager.gd b/flumi/Scripts/StyleManager.gd index 6402aa6..3cb493a 100644 --- a/flumi/Scripts/StyleManager.gd +++ b/flumi/Scripts/StyleManager.gd @@ -54,38 +54,46 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement, if (width != null or height != null) and not skip_sizing: # FlexContainers handle percentage sizing differently than regular controls if node is FlexContainer: - if width != null and typeof(width) != TYPE_STRING: - node.custom_minimum_size.x = width - var should_center_h = styles.has("mx-auto") or styles.has("justify-self-center") or (styles.has("text-align") and styles["text-align"] == "center") - - node.size_flags_horizontal = Control.SIZE_SHRINK_CENTER if should_center_h else Control.SIZE_SHRINK_BEGIN - node.set_meta("size_flags_set_by_style_manager", true) - if height != null and typeof(height) != TYPE_STRING: - node.custom_minimum_size.y = height - var should_center_v = styles.has("my-auto") or styles.has("align-self-center") - node.size_flags_vertical = Control.SIZE_SHRINK_CENTER if should_center_v else Control.SIZE_SHRINK_BEGIN - if not node.has_meta("size_flags_set_by_style_manager"): - node.set_meta("size_flags_set_by_style_manager", true) + if width != null: + if SizingUtils.is_percentage(width): + # For FlexContainers with percentage width, use proportion sizing + var percentage_value = float(width.replace("%", "")) / 100.0 + node.size_flags_horizontal = Control.SIZE_EXPAND_FILL + node.size_flags_stretch_ratio = percentage_value + else: + node.custom_minimum_size.x = width + var should_center_h = styles.has("mx-auto") or styles.has("justify-self-center") or (styles.has("text-align") and styles["text-align"] == "center") + node.size_flags_horizontal = Control.SIZE_SHRINK_CENTER if should_center_h else Control.SIZE_SHRINK_BEGIN + + if height != null: + if SizingUtils.is_percentage(height): + node.size_flags_vertical = Control.SIZE_EXPAND_FILL + else: + node.custom_minimum_size.y = height + var should_center_v = styles.has("my-auto") or styles.has("align-self-center") + node.size_flags_vertical = Control.SIZE_SHRINK_CENTER if should_center_v else Control.SIZE_SHRINK_BEGIN + + node.set_meta("size_flags_set_by_style_manager", true) elif node is VBoxContainer or node is HBoxContainer or node is Container: # Hcontainer nodes (like ul, ol) - SizingUtils.apply_container_dimension_sizing(node, width, height) + SizingUtils.apply_container_dimension_sizing(node, width, height, styles) elif node is HTMLP: # Only apply sizing if element has explicit size, otherwise preserve natural sizing var element_styles = parser.get_element_styles_internal(element, "") if element_styles.has("width") or element_styles.has("height"): var orig_h_flag = node.size_flags_horizontal var orig_v_flag = node.size_flags_vertical - SizingUtils.apply_regular_control_sizing(node, width, height) + SizingUtils.apply_regular_control_sizing(node, width, height, styles) if not element_styles.has("width"): node.size_flags_horizontal = orig_h_flag if not element_styles.has("height"): node.size_flags_vertical = orig_v_flag else: # regular controls - SizingUtils.apply_regular_control_sizing(node, width, height) + SizingUtils.apply_regular_control_sizing(node, width, height, styles) - if label and label != node: - label.anchors_preset = Control.PRESET_FULL_RECT + if label and label != node: + label.anchors_preset = Control.PRESET_FULL_RECT # Apply z-index if styles.has("z-index"): @@ -99,20 +107,30 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement, if styles.has("cursor"): var cursor_shape = get_cursor_shape_from_type(styles["cursor"]) node.mouse_default_cursor_shape = cursor_shape - - # Handle cursor inheritance for text elements (RichTextLabel) - # If current element has no cursor, traverse up parent chain to find one - if not styles.has("cursor") and label: - var current_parent = element.parent - while current_parent: - var parent_styles = parser.get_element_styles_with_inheritance(current_parent, "", []) - if parent_styles.has("cursor"): - var parent_cursor_shape = get_cursor_shape_from_type(parent_styles["cursor"]) - label.mouse_default_cursor_shape = parent_cursor_shape - break # Found a cursor, stop traversing - current_parent = current_parent.parent + + # For text elements, apply cursor and handle mouse events appropriately + if label: + label.mouse_default_cursor_shape = cursor_shape + + # For non-pointer cursors on RichTextLabel, disable text interaction and let parent handle cursor + if label is RichTextLabel and cursor_shape != Control.CURSOR_POINTING_HAND: + label.selection_enabled = false + label.context_menu_enabled = false + label.shortcut_keys_enabled = false + # Let parent container handle the cursor by ignoring mouse on text element + label.mouse_filter = Control.MOUSE_FILTER_IGNORE + else: + # For pointer cursors or non-RichTextLabel, ensure they can receive mouse events + if label.mouse_filter == Control.MOUSE_FILTER_PASS: + label.mouse_filter = Control.MOUSE_FILTER_STOP - # Apply background color, border radius, borders + # Check for margins first and wrap in MarginContainer if needed + var has_margin = styles.has("margin") or styles.has("margin-top") or styles.has("margin-right") or styles.has("margin-bottom") or styles.has("margin-left") + + if has_margin: + node = apply_margin_wrapper(node, styles) + + # Apply background color, border radius, borders var needs_styling = styles.has("background-color") or styles.has("border-radius") or styles.has("border-width") or styles.has("border-top-width") or styles.has("border-right-width") or styles.has("border-bottom-width") or styles.has("border-left-width") or styles.has("border-color") if needs_styling: @@ -144,6 +162,52 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement, return node +static func apply_margin_wrapper(node: Control, styles: Dictionary) -> Control: + var margin_container = MarginContainer.new() + margin_container.name = "MarginWrapper_" + node.name + + # Copy size flags from the original node + margin_container.size_flags_horizontal = node.size_flags_horizontal + margin_container.size_flags_vertical = node.size_flags_vertical + + # Set margin values using theme overrides + var general_margin_str = null + if styles.has("margin"): + general_margin_str = styles["margin"] + + if general_margin_str != null: + var general_margin = parse_size(general_margin_str) + if general_margin != null: + margin_container.add_theme_constant_override("margin_top", general_margin) + margin_container.add_theme_constant_override("margin_right", general_margin) + margin_container.add_theme_constant_override("margin_bottom", general_margin) + margin_container.add_theme_constant_override("margin_left", general_margin) + + # Individual margin overrides + var margin_sides = [ + ["margin-top", "margin_top"], + ["margin-right", "margin_right"], + ["margin-bottom", "margin_bottom"], + ["margin-left", "margin_left"] + ] + + for side_pair in margin_sides: + var style_key = side_pair[0] + var theme_key = side_pair[1] + if styles.has(style_key): + var margin_val_str = styles[style_key] + var margin_val = parse_size(margin_val_str) + if margin_val != null: + margin_container.add_theme_constant_override(theme_key, margin_val) + + # Reset the original node's size flags since they're now handled by the wrapper + node.size_flags_horizontal = Control.SIZE_EXPAND_FILL + node.size_flags_vertical = Control.SIZE_EXPAND_FILL + + margin_container.add_child(node) + + return margin_container + static func apply_styles_to_label(label: Control, styles: Dictionary, element: HTMLParser.HTMLElement, parser, text_override: String = "") -> void: if label is Button: apply_font_to_button(label, styles) diff --git a/flumi/Scripts/Tags/p.gd b/flumi/Scripts/Tags/p.gd index 0ea1fac..c5186eb 100644 --- a/flumi/Scripts/Tags/p.gd +++ b/flumi/Scripts/Tags/p.gd @@ -4,9 +4,12 @@ extends RichTextLabel func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void: text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser) + # Allow mouse events to pass through to parent containers for hover effects while keeping text selection + mouse_filter = Control.MOUSE_FILTER_PASS + # NOTE: estimate width/height because FlexContainer removes our anchor preset (sets 0 width) var plain_text = element.get_collapsed_text() var estimated_height = 30 - var estimated_width = min(400, max(100, plain_text.length() * 12)) + var estimated_width = min(200, max(100, plain_text.length() * 12)) custom_minimum_size = Vector2(estimated_width, estimated_height) diff --git a/flumi/Scripts/Utils/BackgroundUtils.gd b/flumi/Scripts/Utils/BackgroundUtils.gd index 9e70da0..d9f8268 100644 --- a/flumi/Scripts/Utils/BackgroundUtils.gd +++ b/flumi/Scripts/Utils/BackgroundUtils.gd @@ -155,17 +155,51 @@ static func is_background_panel(node: Node) -> bool: return node.name == "BackgroundPanel" and node is Panel # for any other tag -static func create_panel_container_with_background(styles: Dictionary) -> PanelContainer: +static func create_panel_container_with_background(styles: Dictionary, hover_styles: Dictionary = {}) -> PanelContainer: var panel_container = PanelContainer.new() panel_container.name = "Div" var vbox = VBoxContainer.new() vbox.name = "VBoxContainer" + # Allow mouse events to pass through to the parent PanelContainer + vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE panel_container.add_child(vbox) var style_box = create_stylebox_from_styles(styles) panel_container.add_theme_stylebox_override("panel", style_box) + + # Add hover support if hover styles exist + if hover_styles.size() > 0: + setup_panel_hover_support(panel_container, styles, hover_styles) + return panel_container +static func setup_panel_hover_support(panel: PanelContainer, normal_styles: Dictionary, hover_styles: Dictionary): + var normal_stylebox = create_stylebox_from_styles(normal_styles) + + # Merge normal styles with hover styles for the hover state + var merged_hover_styles = normal_styles.duplicate() + for key in hover_styles: + merged_hover_styles[key] = hover_styles[key] + var hover_stylebox = create_stylebox_from_styles(merged_hover_styles) + + # Store references for the hover handlers + panel.set_meta("normal_stylebox", normal_stylebox) + panel.set_meta("hover_stylebox", hover_stylebox) + + # Connect mouse events + panel.mouse_entered.connect(_on_panel_mouse_entered.bind(panel)) + panel.mouse_exited.connect(_on_panel_mouse_exited.bind(panel)) + +static func _on_panel_mouse_entered(panel: PanelContainer): + if panel.has_meta("hover_stylebox"): + var hover_stylebox = panel.get_meta("hover_stylebox") + panel.add_theme_stylebox_override("panel", hover_stylebox) + +static func _on_panel_mouse_exited(panel: PanelContainer): + if panel.has_meta("normal_stylebox"): + var normal_stylebox = panel.get_meta("normal_stylebox") + panel.add_theme_stylebox_override("panel", normal_stylebox) + static func needs_background_wrapper(styles: Dictionary) -> bool: return styles.has("background-color") or styles.has("border-radius") or styles.has("padding") or styles.has("padding-top") or styles.has("padding-right") or styles.has("padding-bottom") or styles.has("padding-left") or styles.has("border-width") or styles.has("border-top-width") or styles.has("border-right-width") or styles.has("border-bottom-width") or styles.has("border-left-width") or styles.has("border-color") or styles.has("border-style") or styles.has("border-top-color") or styles.has("border-right-color") or styles.has("border-bottom-color") or styles.has("border-left-color") diff --git a/flumi/Scripts/Utils/SizingUtils.gd b/flumi/Scripts/Utils/SizingUtils.gd index 31ddd89..f2442d5 100644 --- a/flumi/Scripts/Utils/SizingUtils.gd +++ b/flumi/Scripts/Utils/SizingUtils.gd @@ -48,12 +48,12 @@ static func should_skip_sizing(node: Control, element, parser) -> bool: return false -static func apply_container_dimension_sizing(node: Control, width, height) -> void: +static func apply_container_dimension_sizing(node: Control, width, height, styles: Dictionary = {}) -> void: if width != null: if is_percentage(width): node.set_meta("container_percentage_width", width) node.size_flags_horizontal = Control.SIZE_EXPAND_FILL - apply_container_percentage_sizing(node) + # Don't call immediately - will be called later when parent is available else: node.custom_minimum_size.x = width node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN @@ -66,8 +66,10 @@ static func apply_container_dimension_sizing(node: Control, width, height) -> vo else: node.custom_minimum_size.y = height node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + + apply_size_constraints_and_flags(node, styles) -static func apply_regular_control_sizing(node: Control, width, height) -> void: +static func apply_regular_control_sizing(node: Control, width, height, styles: Dictionary = {}) -> void: if width != null: if is_percentage(width): var estimated_width = calculate_percentage_size(width, DEFAULT_VIEWPORT_WIDTH) @@ -85,6 +87,8 @@ static func apply_regular_control_sizing(node: Control, width, height) -> void: else: node.custom_minimum_size.y = height node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + + apply_size_constraints_and_flags(node, styles) static func is_percentage(value) -> bool: return typeof(value) == TYPE_STRING and value.ends_with("%") @@ -103,16 +107,64 @@ static func apply_container_percentage_sizing(node: Control) -> void: if node.has_meta("container_percentage_width"): var percentage_str = node.get_meta("container_percentage_width") - var parent_width = get_parent_dimension(parent, true, DEFAULT_VIEWPORT_WIDTH) - new_min_size.x = calculate_percentage_size(percentage_str, parent_width) + var parent_width = get_parent_dimension(parent, true, 0) + if parent_width > 0: + new_min_size.x = calculate_percentage_size(percentage_str, parent_width) + node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + else: + # Parent size not available yet, defer sizing by using SIZE_EXPAND_FILL + node.size_flags_horizontal = Control.SIZE_EXPAND_FILL + var percentage_value = float(percentage_str.replace("%", "")) / 100.0 + node.size_flags_stretch_ratio = percentage_value if node.has_meta("container_percentage_height"): var percentage_str = node.get_meta("container_percentage_height") - var parent_height = get_parent_dimension(parent, false, DEFAULT_VIEWPORT_HEIGHT) - new_min_size.y = calculate_percentage_size(percentage_str, parent_height) + var parent_height = get_parent_dimension(parent, false, 0) + if parent_height > 0: + new_min_size.y = calculate_percentage_size(percentage_str, parent_height) + else: + # Parent size not available yet, defer sizing by using SIZE_EXPAND_FILL + node.size_flags_vertical = Control.SIZE_EXPAND_FILL node.custom_minimum_size = new_min_size +static func apply_size_constraints_and_flags(node: Control, styles: Dictionary) -> void: + # Apply min/max width constraints + if styles.has("min-width"): + var min_width = parse_size_value(styles["min-width"]) + if min_width != null and typeof(min_width) != TYPE_STRING: + node.custom_minimum_size.x = max(node.custom_minimum_size.x, min_width) + + if styles.has("max-width"): + var max_width = parse_size_value(styles["max-width"]) + if max_width != null and typeof(max_width) != TYPE_STRING: + if node.custom_minimum_size.x > max_width: + node.custom_minimum_size.x = max_width + # Prevent expansion beyond max width + if node.size_flags_horizontal == Control.SIZE_EXPAND_FILL: + node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + + # Apply min/max height constraints + if styles.has("min-height"): + var min_height = parse_size_value(styles["min-height"]) + if min_height != null and typeof(min_height) != TYPE_STRING: + node.custom_minimum_size.y = max(node.custom_minimum_size.y, min_height) + + if styles.has("max-height"): + var max_height = parse_size_value(styles["max-height"]) + if max_height != null and typeof(max_height) != TYPE_STRING: + if node.custom_minimum_size.y > max_height: + node.custom_minimum_size.y = max_height + # Prevent expansion beyond max height + if node.size_flags_vertical == Control.SIZE_EXPAND_FILL: + node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + + # Handle intrinsic sizing for elements with constraints but no explicit size + if not styles.has("width") and (styles.has("min-width") or styles.has("max-width")): + node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + if not styles.has("height") and (styles.has("min-height") or styles.has("max-height")): + node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + static func get_parent_dimension(parent: Control, is_width: bool, fallback: float) -> float: var size_value = parent.size.x if is_width else parent.size.y if size_value > 0: diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index 18c5554..c58182f 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -51,6 +51,18 @@ func _ready(): ProjectSettings.set_setting("display/window/size/min_width", MIN_SIZE.x) ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y) DisplayServer.window_set_min_size(MIN_SIZE) + + get_viewport().size_changed.connect(_on_viewport_size_changed) + +func _on_viewport_size_changed(): + recalculate_percentage_elements(website_container) + +func recalculate_percentage_elements(node: Node): + if node is Control and node.has_meta("needs_percentage_recalc"): + SizingUtils.apply_container_percentage_sizing(node) + + for child in node.get_children(): + recalculate_percentage_elements(child) func render() -> void: # Clear existing content @@ -137,6 +149,10 @@ static func safe_add_child(parent: Node, child: Node) -> void: if child.get_parent(): child.get_parent().remove_child(child) parent.add_child(child) + + if child.has_meta("container_percentage_width") or child.has_meta("container_percentage_height"): + SizingUtils.apply_container_percentage_sizing(child) + child.set_meta("needs_percentage_recalc", true) func contains_hyperlink(element: HTMLParser.HTMLElement) -> bool: if element.tag_name == "a": @@ -317,13 +333,14 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP node.init(element) "div": var styles = parser.get_element_styles_with_inheritance(element, "", []) + var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", []) # Create div container - if BackgroundUtils.needs_background_wrapper(styles): - node = BackgroundUtils.create_panel_container_with_background(styles) + if BackgroundUtils.needs_background_wrapper(styles) or hover_styles.size() > 0: + node = BackgroundUtils.create_panel_container_with_background(styles, hover_styles) else: node = DIV.instantiate() - node.init(element) + node.init(element, parser) var has_only_text = is_text_only_element(element)