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)