From 4362991412d7d3b84b79ce2736d49dde703d3944 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:10:38 +0300 Subject: [PATCH] add button color, corner radius & hover/active states + fix input sizing --- Scripts/B9/CSSParser.gd | 45 ++++++++++++++++++-- Scripts/B9/HTMLParser.gd | 30 +++++++++++++- Scripts/Constants.gd | 79 +++++++++++++++++++++++++++++++---- Scripts/StyleManager.gd | 10 +++++ Scripts/Tags/button.gd | 89 ++++++++++++++++++++++++++++++++++++++-- Scripts/Tags/input.gd | 57 +++++++++++++++++-------- Scripts/main.gd | 31 ++++++++++---- 7 files changed, 298 insertions(+), 43 deletions(-) diff --git a/Scripts/B9/CSSParser.gd b/Scripts/B9/CSSParser.gd index 0d5515c..e53b185 100644 --- a/Scripts/B9/CSSParser.gd +++ b/Scripts/B9/CSSParser.gd @@ -148,7 +148,9 @@ func parse_rule(rule_data: Dictionary) -> CSSRule: return rule -func parse_utility_class(rule: CSSRule, utility_name: String) -> void: +# Parses a utility class (e.g. "text-red-500") and adds properties to the rule (e.g. "color: red") +# Used as a translation layer for Tailwind-like utility classes, as it becomes easier to manage these programmatically +static func parse_utility_class(rule: CSSRule, utility_name: String) -> void: # Handle color classes like text-[#ff0000] if utility_name.begins_with("text-[") and utility_name.ends_with("]"): var color_value = extract_bracket_content(utility_name, 5) # after 'text-' @@ -348,10 +350,45 @@ func parse_utility_class(rule: CSSRule, utility_name: String) -> void: rule.properties["order"] = val.to_int() return + # Handle border radius classes like rounded, rounded-lg, rounded-[12px] + if utility_name == "rounded": + rule.properties["border-radius"] = "4px" + return + if utility_name == "rounded-none": + rule.properties["border-radius"] = "0px" + return + if utility_name == "rounded-sm": + rule.properties["border-radius"] = "2px" + return + if utility_name == "rounded-md": + rule.properties["border-radius"] = "6px" + return + if utility_name == "rounded-lg": + rule.properties["border-radius"] = "8px" + return + if utility_name == "rounded-xl": + rule.properties["border-radius"] = "12px" + return + if utility_name == "rounded-2xl": + rule.properties["border-radius"] = "16px" + return + if utility_name == "rounded-3xl": + rule.properties["border-radius"] = "24px" + return + if utility_name == "rounded-full": + rule.properties["border-radius"] = "9999px" + return + + # Handle custom border radius like rounded-[12px] + if utility_name.begins_with("rounded-[") and utility_name.ends_with("]"): + var radius_value = extract_bracket_content(utility_name, 8) # after 'rounded-' + rule.properties["border-radius"] = radius_value + return + # Handle more utility classes as needed # Add more cases here for other utilities -func parse_size(val: String) -> String: +static func parse_size(val: String) -> String: var named = { "0": "0px", "1": "4px", "2": "8px", "3": "12px", "4": "16px", "5": "20px", "6": "24px", "8": "32px", "10": "40px", "12": "48px", "16": "64px", "20": "80px", "24": "96px", "28": "112px", "32": "128px", "36": "144px", "40": "160px", @@ -372,7 +409,7 @@ func parse_size(val: String) -> String: return val # Helper to extract content inside first matching brackets after a given index -func extract_bracket_content(string: String, start_idx: int) -> String: +static func extract_bracket_content(string: String, start_idx: int) -> String: var open_idx = string.find("[", start_idx) if open_idx == -1: return "" @@ -381,7 +418,7 @@ func extract_bracket_content(string: String, start_idx: int) -> String: return "" return string.substr(open_idx + 1, close_idx - open_idx - 1) -func parse_color(color_string: String) -> Color: +static func parse_color(color_string: String) -> Color: color_string = color_string.strip_edges() # Handle hex colors diff --git a/Scripts/B9/HTMLParser.gd b/Scripts/B9/HTMLParser.gd index b744ecb..bf67708 100644 --- a/Scripts/B9/HTMLParser.gd +++ b/Scripts/B9/HTMLParser.gd @@ -164,12 +164,40 @@ func get_element_styles_internal(element: HTMLElement, event: String = "") -> Di # 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] # Force override return styles +func parse_inline_style_with_event(style_string: String, event: String = "") -> Dictionary: + var properties = {} + + # Split style string into individual utility classes + var utility_classes = style_string.split(" ") # e.g. ["bg-red-500, "text-lg", "hover:bg-blue-500"] + + for utility_name in utility_classes: + utility_name = utility_name.strip_edges() # e.g. "bg-red-500" + if utility_name.is_empty(): + continue + + # Check if this utility is for the requested event + if event.length() > 0: + if utility_name.begins_with(event + ":"): # e.g. "hover:bg-blue-500" + var actual_utility = utility_name.substr(event.length() + 1) # bg-blue-500 + var rule = CSSParser.CSSRule.new() + CSSParser.parse_utility_class(rule, actual_utility) + for property in rule.properties: + properties[property] = rule.properties[property] + else: + if not utility_name.contains(":"): + var rule = CSSParser.CSSRule.new() + CSSParser.parse_utility_class(rule, utility_name) + for property in rule.properties: + properties[property] = rule.properties[property] + + return properties + # Creates element from CURRENT xml parser node func create_element() -> HTMLElement: var element = HTMLElement.new(xml_parser.get_node_name()) diff --git a/Scripts/Constants.gd b/Scripts/Constants.gd index 50bff1f..a73452f 100644 --- a/Scripts/Constants.gd +++ b/Scripts/Constants.gd @@ -31,6 +31,7 @@ var HTML_CONTENT2 = " @@ -256,15 +252,15 @@ var HTML_CONTENT = """
✅ Finish homework - + ✍️ Write blog post - + 💪 Gym workout - +
@@ -275,7 +271,7 @@ var HTML_CONTENT = """
- +
@@ -286,5 +282,70 @@ var HTML_CONTENT = """ 💼 Work 🏋️ Health + +
+ + + + + + +
+ +

Button Style Tests

+ + + +

Corner Radius Variants

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

Color Combinations

+ + + + + + + + +

Hover Effects

+ + + + + +

Advanced Hover Combinations

+ + + + +

Text Color Focus

+ + + + + +

Mixed Styles

+ + + + + +

Active State Tests

+ + + + + + """.to_utf8_buffer() diff --git a/Scripts/StyleManager.gd b/Scripts/StyleManager.gd index 153268e..696b8ab 100644 --- a/Scripts/StyleManager.gd +++ b/Scripts/StyleManager.gd @@ -272,3 +272,13 @@ static func should_skip_sizing(node: Control, element: HTMLParser.HTMLElement, p return true return false + +static func parse_radius(radius_str: String) -> int: + if radius_str.ends_with("px"): + return int(radius_str.replace("px", "")) + elif radius_str.ends_with("rem"): + return int(radius_str.replace("rem", "")) * 16 + elif radius_str.is_valid_float(): + return int(radius_str) + else: + return 0 diff --git a/Scripts/Tags/button.gd b/Scripts/Tags/button.gd index 3500f1b..b402fe0 100644 --- a/Scripts/Tags/button.gd +++ b/Scripts/Tags/button.gd @@ -1,6 +1,11 @@ extends Control +var current_element: HTMLParser.HTMLElement +var current_parser: HTMLParser + func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void: + current_element = element + current_parser = parser var button_node: Button = $ButtonNode var button_text = element.text_content.strip_edges() @@ -34,9 +39,30 @@ func apply_button_styles(element: HTMLParser.HTMLElement, parser: HTMLParser, na return var styles = parser.get_element_styles_internal(element, "") + var hover_styles = parser.get_element_styles_internal(element, "hover") + var active_styles = parser.get_element_styles_internal(element, "active") + var button_node = $ButtonNode + # Apply text color with state-dependent colors + apply_button_text_color(button_node, styles, hover_styles, active_styles) + + # Apply background color (hover: + active:) if styles.has("background-color"): - set_meta("custom_css_background_color", styles["background-color"]) + var normal_color = styles["background-color"] as Color + var hover_color: Color = Color() + var active_color: Color = Color() + + if hover_styles.has("background-color"): + hover_color = hover_styles["background-color"] as Color + if active_styles.has("background-color"): + active_color = active_styles["background-color"] as Color + + apply_button_color_with_states(button_node, normal_color, hover_color, active_color) + + # Apply corner radius + if styles.has("border-radius"): + var radius = StyleManager.parse_radius(styles["border-radius"]) + apply_button_radius(button_node, radius) var width = null var height = null @@ -46,8 +72,6 @@ func apply_button_styles(element: HTMLParser.HTMLElement, parser: HTMLParser, na if styles.has("height"): height = StyleManager.parse_size(styles["height"]) - var button_node = $ButtonNode - # Only apply size flags if there's explicit sizing if width != null or height != null: apply_size_and_flags(self, width, height) @@ -59,6 +83,65 @@ func apply_button_styles(element: HTMLParser.HTMLElement, parser: HTMLParser, na button_node.custom_minimum_size = Vector2.ZERO button_node.anchors_preset = Control.PRESET_FULL_RECT +func apply_button_text_color(button: Button, normal_styles: Dictionary, hover_styles: Dictionary, active_styles: Dictionary) -> void: + var normal_color = normal_styles.get("color", Color.WHITE) + var hover_color = hover_styles.get("color", normal_color) + var active_color = active_styles.get("color", hover_color) + + button.add_theme_color_override("font_color", normal_color) + button.add_theme_color_override("font_hover_color", hover_color) + button.add_theme_color_override("font_pressed_color", active_color) + button.add_theme_color_override("font_focus_color", normal_color) + +func apply_button_color_with_states(button: Button, normal_color: Color, hover_color: Color, active_color: Color) -> void: + var existing_normal: StyleBoxFlat = button.get_theme_stylebox("normal") if button.has_theme_stylebox_override("normal") else null + + var style_normal = StyleBoxFlat.new() + var style_hover = StyleBoxFlat.new() + var style_pressed = StyleBoxFlat.new() + + var radius: int = existing_normal.corner_radius_top_left if existing_normal else 0 + + style_normal.set_corner_radius_all(radius) + style_hover.set_corner_radius_all(radius) + style_pressed.set_corner_radius_all(radius) + + # Set normal color + style_normal.bg_color = normal_color + + # Set hover: color + # If hover isn't default, use it + if hover_color != Color(): + style_hover.bg_color = hover_color + else: + # If no hover, fallback to normal color + style_hover.bg_color = normal_color + + # Set active: color + if active_color != Color(): + style_pressed.bg_color = active_color + elif hover_color != Color(): + style_pressed.bg_color = hover_color # Fallback to hover if defined + else: + style_pressed.bg_color = normal_color # Final fallback to normal + + 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: + var style_normal = button.get_theme_stylebox("normal") + var style_hover = button.get_theme_stylebox("hover") + var style_pressed = button.get_theme_stylebox("pressed") + + style_normal.set_corner_radius_all(radius) + style_hover.set_corner_radius_all(radius) + style_pressed.set_corner_radius_all(radius) + + 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_size_and_flags(ctrl: Control, width: Variant, height: Variant, reset_layout := false) -> void: if width != null or height != null: ctrl.custom_minimum_size = Vector2( diff --git a/Scripts/Tags/input.gd b/Scripts/Tags/input.gd index fbc30ae..eeb06e0 100644 --- a/Scripts/Tags/input.gd +++ b/Scripts/Tags/input.gd @@ -310,7 +310,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> var parent_styles = parser.get_element_styles_with_inheritance(element.parent, "", []) if element.parent else {} if parent_styles.has("width"): var parent_width = StyleManager.parse_size(parent_styles["width"]) - if parent_width != null: + if parent_width: width = parent_width else: width = StyleManager.parse_size(styles["width"]) @@ -323,23 +323,44 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> active_child = child break - if active_child and (width != null or height != null): - var new_child_size = Vector2( - width if width != null else active_child.custom_minimum_size.x, - height if height != null else max(active_child.custom_minimum_size.y, active_child.size.y) - ) - - active_child.custom_minimum_size = new_child_size - - if width != null: - active_child.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN - if height != null: - active_child.size_flags_vertical = Control.SIZE_SHRINK_BEGIN - - if active_child.size.x < new_child_size.x or (new_child_size.y > 0 and active_child.size.y < new_child_size.y): - active_child.size = new_child_size - - custom_minimum_size = new_child_size + if active_child: + if width or height: + # Explicit sizing from CSS + var new_child_size = Vector2( + width if width else active_child.custom_minimum_size.x, + height if height else max(active_child.custom_minimum_size.y, active_child.size.y) + ) + + active_child.custom_minimum_size = new_child_size + + if width: + active_child.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + if height: + active_child.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + + if active_child.size.x < new_child_size.x or (new_child_size.y > 0 and active_child.size.y < new_child_size.y): + active_child.size = new_child_size + + custom_minimum_size = new_child_size + + # Root Control adjusts size flags to match child + if width: + size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + else: + size_flags_horizontal = Control.SIZE_EXPAND_FILL + if height: + size_flags_vertical = Control.SIZE_SHRINK_BEGIN + else: + size_flags_vertical = Control.SIZE_SHRINK_CENTER + else: + # No explicit CSS sizing - sync root Control with child's natural size + var child_natural_size = active_child.get_combined_minimum_size() + if child_natural_size == Vector2.ZERO: + child_natural_size = active_child.size + + custom_minimum_size = child_natural_size + size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + size_flags_vertical = Control.SIZE_SHRINK_CENTER if active_child.name == "DateButton": active_child.anchors_preset = Control.PRESET_TOP_LEFT diff --git a/Scripts/main.gd b/Scripts/main.gd index d80179c..b0c3dce 100644 --- a/Scripts/main.gd +++ b/Scripts/main.gd @@ -164,8 +164,15 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) -> # Apply flex ITEM properties StyleManager.apply_flex_item_properties(final_node, styles) - # Add child elements (but NOT for ul/ol which handle their own children) - if element.tag_name != "ul" and element.tag_name != "ol": + # Skip ul/ol and non-flex forms, they handle their own children + var skip_general_processing = false + + if element.tag_name == "ul" or element.tag_name == "ol": + skip_general_processing = true + elif element.tag_name == "form": + skip_general_processing = not is_flex_container + + if not skip_general_processing: for child_element in element.children: # Only add child nodes if the child is NOT an inline element # UNLESS the parent is a flex container (inline elements become flex items) @@ -205,13 +212,21 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP node = SEPARATOR.instantiate() node.init(element) "form": - node = FORM.instantiate() - node.init(element) + var form_styles = parser.get_element_styles_with_inheritance(element, "", []) + var is_flex_form = form_styles.has("display") and ("flex" in form_styles["display"]) - # Forms need to manually process their children - for child_element in element.children: - var child_node = await create_element_node(child_element, parser) - safe_add_child(node, child_node) + if is_flex_form: + # Don't create a form node here - return null so general processing takes over + return null + else: + node = FORM.instantiate() + node.init(element) + + # Manually process children for non-flex forms + for child_element in element.children: + var child_node = await create_element_node(child_element, parser) + if child_node: + safe_add_child(node, child_node) "input": node = INPUT.instantiate() node.init(element, parser)