diff --git a/README.md b/README.md
index 6314c63..a09d98a 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,8 @@ TODO:
Issues:
1. **< br />** counts as 1 element in **WebsiteContainer**, therefore despite being (0,0) in size, it counts as double in spacing
2. **Tween** API doesn't modify CSS, it operates independently at Godot level.
+3. Certain properties like `scale` and `rotate` don't apply to the `active` pseudo-class because they rely on mouse_enter and mouse_exit events
+4. `
📝 Transform Utility Reference
diff --git a/flumi/Scripts/StyleManager.gd b/flumi/Scripts/StyleManager.gd
index d222a55..bd21331 100644
--- a/flumi/Scripts/StyleManager.gd
+++ b/flumi/Scripts/StyleManager.gd
@@ -680,67 +680,58 @@ static func apply_transform_properties(node: Control, styles: Dictionary) -> voi
apply_transform_properties_direct(node, styles)
static func apply_transform_properties_direct(node: Control, styles: Dictionary) -> void:
- var has_transform = false
- var scale_x = 1.0
- var scale_y = 1.0
- var rotation_z = 0.0
+ var scale_x = styles.get("scale-x", 1.0)
+ var scale_y = styles.get("scale-y", 1.0)
+ var rotation = styles.get("rotate", 0.0)
- # Check for scale properties
- if styles.has("scale-x"):
- scale_x = styles["scale-x"]
- has_transform = true
- if styles.has("scale-y"):
- scale_y = styles["scale-y"]
- has_transform = true
-
- # Check for rotation properties (prioritize Z-axis for 2D)
- if styles.has("rotate-z"):
- rotation_z = styles["rotate-z"]
- has_transform = true
- elif styles.has("rotate-x"):
- rotation_z = styles["rotate-x"]
- has_transform = true
- elif styles.has("rotate-y"):
- rotation_z = styles["rotate-y"]
- has_transform = true
+ var has_transform = scale_x != 1.0 or scale_y != 1.0 or rotation != 0.0
+ var duration = get_transition_duration(styles)
if has_transform:
- # Set metadata flag to tell FlexContainer to preserve transforms
node.set_meta("css_transform_applied", true)
- node.set_meta("pending_scale", Vector2(scale_x, scale_y))
- node.set_meta("pending_rotation", rotation_z)
- # Apply immediately
- node.scale = Vector2(scale_x, scale_y)
- node.rotation = rotation_z
+ # Set pivot point to center
+ node.pivot_offset = node.size / 2
- # Reapply after FlexContainer layout using timer
- var timer = Timer.new()
- timer.wait_time = 0.1
- timer.one_shot = true
- timer.autostart = true
- var main_scene = Engine.get_main_loop().current_scene
- if main_scene:
- timer.timeout.connect(func(): _reapply_transforms(node))
- main_scene.add_child(timer)
+ if duration > 0:
+ animate_transform(node, Vector2(scale_x, scale_y), rotation, duration)
+ else:
+ node.scale = Vector2(scale_x, scale_y)
+ node.rotation = rotation
+ await_and_restore_transform(node, Vector2(scale_x, scale_y), rotation)
else:
- # Reset transforms to default when no transforms are specified
- node.scale = Vector2.ONE
- node.rotation = 0.0
+ if duration > 0:
+ animate_transform(node, Vector2.ONE, 0.0, duration)
+ else:
+ node.scale = Vector2.ONE
+ node.rotation = 0.0
if node.has_meta("css_transform_applied"):
node.remove_meta("css_transform_applied")
-static func _reapply_transforms(node: Control) -> void:
- if not is_instance_valid(node):
- return
+static func get_transition_duration(styles: Dictionary) -> float:
+ if styles.has("transition-transform"):
+ return parse_transition_duration(styles["transition-transform"])
+ elif styles.has("transition"):
+ return parse_transition_duration(styles["transition"])
+ return 0.0
+
+static func parse_transition_duration(value: String) -> float:
+ if value.ends_with("ms"):
+ return float(value.replace("ms", "")) / 1000.0
+ elif value.ends_with("s"):
+ return float(value.replace("s", ""))
+ return float(value) if value.is_valid_float() else 0.0
+
+static func animate_transform(node: Control, target_scale: Vector2, target_rotation: float, duration: float) -> void:
+ var tween = node.create_tween()
+ tween.set_parallel(true)
+ tween.tween_property(node, "scale", target_scale, duration).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT)
+ tween.tween_property(node, "rotation", target_rotation, duration).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT)
+
+static func await_and_restore_transform(node: Control, target_scale: Vector2, target_rotation: float) -> void:
+ var tree = Engine.get_main_loop()
- if node.has_meta("pending_scale"):
- node.scale = node.get_meta("pending_scale")
-
- if node.has_meta("pending_rotation"):
- node.rotation = node.get_meta("pending_rotation")
-
- if node.has_meta("pending_scale"):
- node.remove_meta("pending_scale")
- if node.has_meta("pending_rotation"):
- node.remove_meta("pending_rotation")
+ await tree.process_frame
+ node.scale = target_scale
+ node.rotation = target_rotation
+ node.pivot_offset = node.size / 2
diff --git a/flumi/Scripts/Tags/button.gd b/flumi/Scripts/Tags/button.gd
index 4bd6fb2..8f768fb 100644
--- a/flumi/Scripts/Tags/button.gd
+++ b/flumi/Scripts/Tags/button.gd
@@ -51,6 +51,9 @@ func apply_button_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
# Apply text color with state-dependent colors
apply_button_text_color(button_node, styles, hover_styles, active_styles)
+ # Apply transform properties with hover support
+ apply_button_transforms(button_node, styles, hover_styles)
+
# Apply background color (hover: + active:)
if styles.has("background-color"):
var normal_color: Color = styles["background-color"]
@@ -194,3 +197,52 @@ func apply_size_and_flags(ctrl: Control, width: Variant, height: Variant) -> voi
ctrl.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
if height != null:
ctrl.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
+
+func apply_button_transforms(button: Button, normal_styles: Dictionary, hover_styles: Dictionary) -> void:
+ # Apply normal transforms to the parent HBoxContainer (self)
+ StyleManager.apply_transform_properties_direct(self, normal_styles)
+
+ # Set pivot point to center of the container
+ self.pivot_offset = self.size / 2
+
+ # Set up hover transforms if present
+ var has_hover_transforms = hover_styles.has("scale-x") or hover_styles.has("scale-y") or hover_styles.has("rotate")
+ if has_hover_transforms:
+ # Store original and hover values
+ var original_scale = Vector2(
+ normal_styles.get("scale-x", 1.0),
+ normal_styles.get("scale-y", 1.0)
+ )
+ var original_rotation = normal_styles.get("rotate", 0.0)
+
+ var hover_scale = Vector2(
+ hover_styles.get("scale-x", original_scale.x),
+ hover_styles.get("scale-y", original_scale.y)
+ )
+ var hover_rotation = hover_styles.get("rotate", original_rotation)
+
+ # Get transition duration
+ var duration = StyleManager.get_transition_duration(normal_styles)
+ if duration == 0:
+ duration = StyleManager.get_transition_duration(hover_styles)
+
+ # Connect hover events to the button but apply transforms to self
+ button.mouse_entered.connect(func():
+ # Update pivot point in case size changed
+ self.pivot_offset = self.size / 2
+ if duration > 0:
+ StyleManager.animate_transform(self, hover_scale, hover_rotation, duration)
+ else:
+ self.scale = hover_scale
+ self.rotation = hover_rotation
+ )
+
+ button.mouse_exited.connect(func():
+ # Update pivot point in case size changed
+ self.pivot_offset = self.size / 2
+ if duration > 0:
+ StyleManager.animate_transform(self, original_scale, original_rotation, duration)
+ else:
+ self.scale = original_scale
+ self.rotation = original_rotation
+ )
diff --git a/flumi/Scripts/Utils/TransformUtils.gd b/flumi/Scripts/Utils/TransformUtils.gd
index 3c4436f..6620507 100644
--- a/flumi/Scripts/Utils/TransformUtils.gd
+++ b/flumi/Scripts/Utils/TransformUtils.gd
@@ -132,26 +132,12 @@ static func parse_scale_utility(utility_name: String) -> Dictionary:
static func parse_rotation_utility(utility_name: String) -> Dictionary:
var result = {}
- if utility_name.begins_with("rotate-x-"):
- var val = utility_name.substr(9) # after "rotate-x-"
- if val.begins_with("[") and val.ends_with("]"):
- val = val.substr(1, val.length() - 2)
- var rotation = parse_rotation(val)
- result["rotate-x"] = rotation
- return result
- elif utility_name.begins_with("rotate-y-"):
- var val = utility_name.substr(9) # after "rotate-y-"
- if val.begins_with("[") and val.ends_with("]"):
- val = val.substr(1, val.length() - 2)
- var rotation = parse_rotation(val)
- result["rotate-y"] = rotation
- return result
- elif utility_name.begins_with("rotate-"):
+ if utility_name.begins_with("rotate-"):
var val = utility_name.substr(7) # after "rotate-"
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
var rotation = parse_rotation(val)
- result["rotate-z"] = rotation # Default rotation is around Z-axis
+ result["rotate"] = rotation
return result
return result
diff --git a/flumi/Scripts/Utils/UtilityClassValidator.gd b/flumi/Scripts/Utils/UtilityClassValidator.gd
index 7817cad..03c05aa 100644
--- a/flumi/Scripts/Utils/UtilityClassValidator.gd
+++ b/flumi/Scripts/Utils/UtilityClassValidator.gd
@@ -43,8 +43,9 @@ static func init_patterns():
"^cursor-[a-zA-Z-]+$", # cursor types
"^scale-(x-|y-)?\\d+$", # scale utilities like scale-100, scale-x-75, scale-y-150
"^scale-(x-|y-)?\\[.*\\]$", # custom scale values like scale-[1.5], scale-x-[2.0]
- "^rotate-(x-|y-)?\\d+$", # rotation utilities like rotate-45, rotate-x-90, rotate-y-180
- "^rotate-(x-|y-)?\\[.*\\]$", # custom rotation values like rotate-[45deg], rotate-x-[90deg], rotate-y-[3.5rad]
+ "^rotate-\\d+$", # rotation utilities like rotate-45, rotate-90
+ "^rotate-\\[.*\\]$", # custom rotation values like rotate-[45deg], rotate-[3.5rad]
+ "^transition(-colors|-opacity|-transform)?$", # transition utilities
"^(hover|active):", # pseudo classes
]
for pattern in utility_patterns: