add button color, corner radius & hover/active states + fix input sizing

This commit is contained in:
Face
2025-07-29 17:10:38 +03:00
parent 179d21f09a
commit 4362991412
7 changed files with 298 additions and 43 deletions

View File

@@ -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

View File

@@ -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())

View File

@@ -31,6 +31,7 @@ var HTML_CONTENT2 = "<head>
<style>
h1 { text-[#ff0000] font-italic hover:text-[#00ff00] }
p { text-[#333333] text-2xl }
button { hover:bg-[#FF6B35] hover:text-[#FFFFFF] active:bg-[#CC5429] active:text-[#F0F0F0] }
</style>
<style src=\"styles.css\">
<script src=\"script.lua\" />
@@ -116,10 +117,6 @@ So
<h2>File Upload</h2>
<input type=\"file\" accept=\".txt,.pdf,image/*\" />
<input type=\"password\" placeholder=\"your password...\" />
<button type=\"submit\">Submit</button>
</form>
<separator direction=\"horizontal\" />
# Ordered list
<ol>
@@ -241,7 +238,6 @@ var HTML_CONTENT = """<head>
<style>
h1 { text-[#4ade80] text-3xl font-bold }
p { text-[#94a3b8] text-lg }
button { bg-[#4ade80] text-[#ffffff] hover:bg-[#22c55e] }
input { border border-[#cbd5e1] px-2 py-1 rounded }
</style>
@@ -256,15 +252,15 @@ var HTML_CONTENT = """<head>
<div style="flex flex-col gap-2 w-80 mx-auto bg-[#f8fafc] p-4 rounded">
<span style="flex justify-between items-center bg-[#e2e8f0] px-2 py-1 rounded">
<span>✅ Finish homework</span>
<button>Delete</button>
<button style="bg-[#4ade80] text-[#ffffff] hover:bg-[#22c55e]">Delete</button>
</span>
<span style="flex justify-between items-center bg-[#e2e8f0] px-2 py-1 rounded">
<span>✍️ Write blog post</span>
<button>Delete</button>
<button style="bg-[#4ade80] text-[#ffffff] hover:bg-[#22c55e]">Delete</button>
</span>
<span style="flex justify-between items-center bg-[#e2e8f0] px-2 py-1 rounded">
<span>💪 Gym workout</span>
<button>Delete</button>
<button style="bg-[#4ade80] text-[#ffffff] hover:bg-[#22c55e]">Delete</button>
</span>
</div>
@@ -275,7 +271,7 @@ var HTML_CONTENT = """<head>
<form action="/add-task" method="POST" style="flex flex-col gap-2 w-80 mx-auto">
<input type="text" placeholder="Enter task..." minlength="3" required="true" />
<input type="date" />
<button type="submit">Add Task</button>
<button type="submit" style="bg-[#4ade80] text-[#ffffff] hover:bg-[#22c55e]">Add Task</button>
</form>
<separator direction="horizontal" />
@@ -286,5 +282,70 @@ var HTML_CONTENT = """<head>
<span style="bg-[#d1fae5] px-4 py-2 rounded">💼 Work</span>
<span style="bg-[#e0e7ff] px-4 py-2 rounded">🏋️ Health</span>
</div>
<form>
<input type=\"password\" placeholder=\"your password...\" />
<button type=\"submit\" style=\"bg-[#4CAF50] rounded-lg text-[#FFFFFF]\">Submit</button>
<button style=\"bg-[#2196F3] rounded-xl text-[#FFFFFF]\">Blue Button</button>
<button style=\"bg-[#FF5722] rounded-full text-[#FFFFFF]\">Orange Pill</button>
<button style=\"bg-[#9C27B0] rounded-[20px] text-[#FFFFFF]\">Purple Custom</button>
<button style=\"bg-[#FFD700] rounded text-[#000000] hover:bg-[#FFA500] hover:text-[#FFFFFF]\">Hover Test</button>
</form>
<h2>Button Style Tests</h2>
<button>Normal, no-styling button.</button>
<h3>Corner Radius Variants</h3>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-none\">No Radius</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-sm\">Small (2px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded\">Default (4px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-md\">Medium (6px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-lg\">Large (8px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-xl\">Extra Large (12px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-2xl\">2XL (16px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-3xl\">3XL (24px)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-full\">Full (Pill)</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-[30px]\">Custom 30px</button>
<h3>Color Combinations</h3>
<button style=\"bg-[#FF6B6B] text-[#FFFFFF] rounded-lg\">Red Background</button>
<button style=\"bg-[#4ECDC4] text-[#2C3E50] rounded-lg\">Teal & Dark Text</button>
<button style=\"bg-[#45B7D1] text-[#FFFFFF] rounded-lg\">Sky Blue</button>
<button style=\"bg-[#96CEB4] text-[#2C3E50] rounded-lg\">Mint Green</button>
<button style=\"bg-[#FFEAA7] text-[#2D3436] rounded-lg\">Yellow Cream</button>
<button style=\"bg-[#DDA0DD] text-[#FFFFFF] rounded-lg\">Plum Purple</button>
<button style=\"bg-[#98D8C8] text-[#2C3E50] rounded-lg\">Seafoam</button>
<h3>Hover Effects</h3>
<button style=\"bg-[#3498DB] text-[#FFFFFF] rounded-lg hover:bg-[#2980B9] hover:text-[#F8F9FA]\">Blue Hover</button>
<button style=\"bg-[#E67E22] text-[#FFFFFF] rounded-xl hover:bg-[#D35400] hover:text-[#ECF0F1]\">Orange Hover</button>
<button style=\"bg-[#9B59B6] text-[#FFFFFF] rounded-full hover:bg-[#8E44AD] hover:text-[#F4F4F4]\">Purple Pill Hover</button>
<button style=\"bg-[#1ABC9C] text-[#FFFFFF] rounded-2xl hover:bg-[#16A085]\">Turquoise Hover</button>
<h3>Advanced Hover Combinations</h3>
<button style=\"bg-[#34495E] text-[#ECF0F1] rounded hover:bg-[#E74C3C] hover:text-[#FFFFFF]\">Dark to Red</button>
<button style=\"bg-[#F39C12] text-[#2C3E50] rounded-lg hover:bg-[#27AE60] hover:text-[#FFFFFF]\">Gold to Green</button>
<button style=\"bg-[#FFFFFF] text-[#2C3E50] rounded-xl hover:bg-[#2C3E50] hover:text-[#FFFFFF]\">Light to Dark</button>
<h3>Text Color Focus</h3>
<button style=\"text-[#E74C3C] rounded-lg\">Red Text Only</button>
<button style=\"text-[#27AE60] rounded-lg\">Green Text Only</button>
<button style=\"text-[#3498DB] rounded-lg\">Blue Text Only</button>
<button style=\"text-[#9B59B6] rounded-full\">Purple Text Pill</button>
<h3>Mixed Styles</h3>
<button style=\"bg-[#FF7675] text-[#FFFFFF] rounded-[15px] hover:bg-[#FD79A8] hover:text-[#2D3436]\">Custom Mix 1</button>
<button style=\"bg-[#6C5CE7] text-[#DDD] rounded-3xl hover:bg-[#A29BFE] hover:text-[#2D3436]\">Custom Mix 2</button>
<button style=\"bg-[#00B894] text-[#FFFFFF] rounded-[25px] hover:bg-[#00CEC9] hover:text-[#2D3436]\">Custom Mix 3</button>
<button style=\"bg-[#0000ff] text-[#FFFFFF] rounded-[25px] hover:bg-[#ff0000] hover:text-[#2D3436]\">Blue normal, red hover</button>
<h3>Active State Tests</h3>
<button style=\"bg-[#3498DB] text-[#FFFFFF] rounded-lg hover:bg-[#2980B9] active:bg-[#1F618D] active:text-[#F8F9FA]\">Blue with Active</button>
<button style=\"bg-[#E74C3C] text-[#FFFFFF] rounded-xl hover:bg-[#C0392B] active:bg-[#A93226] active:text-[#ECF0F1]\">Red with Active</button>
<button style=\"bg-[#27AE60] text-[#FFFFFF] rounded-full hover:bg-[#229954] active:bg-[#1E8449] active:text-[#D5DBDB]\">Green Pill Active</button>
<button style=\"bg-[#F39C12] text-[#2C3E50] rounded hover:bg-[#E67E22] hover:text-[#FFFFFF] active:bg-[#D35400] active:text-[#F7F9FC]\">Gold Multi-State</button>
<button style=\"bg-[#9B59B6] text-[#FFFFFF] rounded-2xl active:bg-[#7D3C98] active:text-[#E8DAEF]\">Purple Active Only</button>
</body>
""".to_utf8_buffer()

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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)