2025-07-28 15:22:34 +03:00
|
|
|
@tool
|
|
|
|
|
class_name AutoSizingFlexContainer
|
|
|
|
|
extends FlexContainer
|
|
|
|
|
|
|
|
|
|
signal flex_resized
|
|
|
|
|
|
|
|
|
|
var bgcolor: Color = Color(0,0,0,0)
|
2025-07-28 20:52:13 +03:00
|
|
|
var content_size: Vector2 = Vector2.ZERO
|
2025-07-28 15:22:34 +03:00
|
|
|
|
|
|
|
|
func set_background_color(color: Color) -> void:
|
|
|
|
|
bgcolor = color
|
|
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
func _notification(what: int) -> void:
|
|
|
|
|
super._notification(what)
|
|
|
|
|
|
|
|
|
|
if what == NOTIFICATION_DRAW and bgcolor.a > 0:
|
2025-07-28 20:52:13 +03:00
|
|
|
draw_rect(Rect2(Vector2.ZERO, content_size), bgcolor)
|
2025-07-28 15:22:34 +03:00
|
|
|
|
|
|
|
|
# This is the overridden layout logic for the auto-sizing container
|
|
|
|
|
func _resort() -> void:
|
2025-07-28 20:52:13 +03:00
|
|
|
# Check if we should fill horizontally (for w-full)
|
|
|
|
|
if has_meta("should_fill_horizontal"):
|
|
|
|
|
size_flags_horizontal = Control.SIZE_FILL
|
|
|
|
|
else:
|
|
|
|
|
size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
|
|
|
|
|
|
|
|
|
# Check if we should fill vertically (for h-full)
|
|
|
|
|
if has_meta("should_fill_vertical"):
|
|
|
|
|
size_flags_vertical = Control.SIZE_FILL
|
|
|
|
|
else:
|
|
|
|
|
size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
2025-07-28 15:22:34 +03:00
|
|
|
|
|
|
|
|
if debug_draw:
|
|
|
|
|
_draw_rects.clear()
|
|
|
|
|
|
|
|
|
|
var child_count = get_child_count()
|
|
|
|
|
var valid_child_index = 0
|
|
|
|
|
for i in range(child_count):
|
|
|
|
|
var c = get_child(i)
|
|
|
|
|
if not c is Control or c.is_set_as_top_level():
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
var cid = c.get_instance_id()
|
|
|
|
|
var target_index = _find_index_from_flex_list(_flex_list, cid)
|
|
|
|
|
var flexbox: Flexbox
|
|
|
|
|
|
|
|
|
|
# If the child is not visible, remove its corresponding flexbox node
|
|
|
|
|
if not c.is_visible_in_tree():
|
|
|
|
|
if target_index != -1:
|
|
|
|
|
_root.remove_child_at(target_index)
|
|
|
|
|
_flex_list.remove_at(target_index)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Find, swap, or create a new flexbox node for the child
|
|
|
|
|
if target_index != -1:
|
|
|
|
|
var old_flex_data = _flex_list[valid_child_index]
|
|
|
|
|
var new_flex_data = _flex_list[target_index]
|
|
|
|
|
flexbox = new_flex_data[FlexDataType.FLEXBOX]
|
|
|
|
|
|
|
|
|
|
if old_flex_data[FlexDataType.CID] != cid:
|
|
|
|
|
_root.swap_child(valid_child_index, target_index)
|
|
|
|
|
_flex_list[target_index] = old_flex_data
|
|
|
|
|
_flex_list[valid_child_index] = new_flex_data
|
|
|
|
|
else:
|
|
|
|
|
flexbox = Flexbox.new()
|
|
|
|
|
_root.insert_child(flexbox, valid_child_index)
|
|
|
|
|
_flex_list.insert(valid_child_index, [cid, flexbox, c])
|
|
|
|
|
|
|
|
|
|
# Set the minimum size and apply flex properties for the child
|
|
|
|
|
_set_control_min_size(c, flexbox)
|
|
|
|
|
var flex_metas = c.get_meta("flex_metas", {})
|
|
|
|
|
if flex_metas.size():
|
|
|
|
|
apply_flex_meta(flexbox, flex_metas)
|
|
|
|
|
if flex_metas.has("padding"):
|
|
|
|
|
padding_wrapper(c, flex_metas.get("padding"))
|
|
|
|
|
valid_child_index += 1
|
|
|
|
|
|
|
|
|
|
# Clean up any flexbox nodes for children that were removed
|
|
|
|
|
child_count = valid_child_index
|
|
|
|
|
if child_count != _flex_list.size():
|
|
|
|
|
for i in range(_flex_list.size() - 1, child_count - 1, -1):
|
|
|
|
|
_root.remove_child_at(i)
|
|
|
|
|
_flex_list.resize(child_count)
|
|
|
|
|
_root.mark_dirty_and_propogate()
|
|
|
|
|
|
|
|
|
|
|
2025-07-30 14:39:33 +03:00
|
|
|
var auto_size_width = not has_meta("custom_css_width") and not has_meta("should_fill_horizontal") and not has_meta("custom_css_width_percentage")
|
|
|
|
|
var auto_size_height = not has_meta("custom_css_height") and not has_meta("should_fill_vertical") and not has_meta("custom_css_height_percentage")
|
2025-07-28 15:22:34 +03:00
|
|
|
|
2025-07-28 20:52:13 +03:00
|
|
|
var available_width = NAN
|
|
|
|
|
var available_height = NAN
|
|
|
|
|
|
|
|
|
|
if not auto_size_width:
|
2025-07-30 14:39:33 +03:00
|
|
|
available_width = calculate_available_dimension(true)
|
2025-07-28 20:52:13 +03:00
|
|
|
|
|
|
|
|
if not auto_size_height:
|
2025-07-30 14:39:33 +03:00
|
|
|
available_height = calculate_available_dimension(false)
|
2025-07-28 15:22:34 +03:00
|
|
|
|
|
|
|
|
_root.calculate_layout(available_width, available_height, 1) # 1 = LTR direction
|
|
|
|
|
|
|
|
|
|
# Get the size computed by Yoga
|
|
|
|
|
var computed_size = Vector2(
|
|
|
|
|
_root.get_computed_width(),
|
|
|
|
|
_root.get_computed_height()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Respect any explicit width/height set via metadata
|
2025-07-30 14:39:33 +03:00
|
|
|
var custom_w = calculate_custom_dimension(true)
|
|
|
|
|
var custom_h = calculate_custom_dimension(false)
|
2025-07-28 15:22:34 +03:00
|
|
|
|
|
|
|
|
var needed_size = Vector2(
|
|
|
|
|
max(custom_w, computed_size.x),
|
|
|
|
|
max(custom_h, computed_size.y)
|
|
|
|
|
)
|
2025-07-28 20:52:13 +03:00
|
|
|
|
|
|
|
|
# Store the actual content size for background drawing
|
|
|
|
|
content_size = needed_size
|
2025-07-28 15:22:34 +03:00
|
|
|
|
|
|
|
|
# Construct the new minimum size for this container
|
|
|
|
|
var new_min_size = custom_minimum_size
|
|
|
|
|
if auto_size_width:
|
|
|
|
|
new_min_size.x = needed_size.x
|
2025-07-28 20:52:13 +03:00
|
|
|
else:
|
|
|
|
|
# For w-full, ensure minimum size matches the needed size
|
|
|
|
|
new_min_size.x = needed_size.x
|
2025-07-28 15:22:34 +03:00
|
|
|
if auto_size_height:
|
|
|
|
|
new_min_size.y = needed_size.y
|
2025-07-28 20:52:13 +03:00
|
|
|
else:
|
|
|
|
|
# For h-full, ensure minimum size matches the needed size
|
|
|
|
|
new_min_size.y = needed_size.y
|
2025-07-28 15:22:34 +03:00
|
|
|
|
|
|
|
|
if not custom_minimum_size.is_equal_approx(new_min_size):
|
|
|
|
|
custom_minimum_size = new_min_size
|
2025-07-28 20:52:13 +03:00
|
|
|
|
|
|
|
|
# For w-full/h-full, also force the actual size if SIZE_FILL isn't working
|
|
|
|
|
if has_meta("should_fill_horizontal") and size.x < new_min_size.x:
|
|
|
|
|
size.x = new_min_size.x
|
|
|
|
|
if has_meta("should_fill_vertical") and size.y < new_min_size.y:
|
|
|
|
|
size.y = new_min_size.y
|
2025-07-28 15:22:34 +03:00
|
|
|
|
|
|
|
|
# Apply the calculated layout to each child control
|
|
|
|
|
for flex_data in _flex_list:
|
|
|
|
|
var flexbox = flex_data[FlexDataType.FLEXBOX]
|
|
|
|
|
var c = flex_data[FlexDataType.CONTROL]
|
|
|
|
|
var offset = Vector2(flexbox.get_computed_left(), flexbox.get_computed_top())
|
|
|
|
|
var rect_size = Vector2(flexbox.get_computed_width(), flexbox.get_computed_height())
|
|
|
|
|
_fit_child_in_rect(c, Rect2(offset, rect_size))
|
|
|
|
|
|
|
|
|
|
if debug_draw:
|
|
|
|
|
_draw_debug_rect(Rect2(offset, rect_size), Color(1, 0, 0, 0.8))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if has_meta("custom_css_background_color"):
|
|
|
|
|
set_background_color(get_meta("custom_css_background_color"))
|
|
|
|
|
|
|
|
|
|
queue_redraw()
|
|
|
|
|
emit_signal("flex_resized")
|
2025-07-30 14:39:33 +03:00
|
|
|
|
|
|
|
|
func calculate_available_dimension(is_width: bool) -> float:
|
|
|
|
|
var percentage_key = "custom_css_width_percentage" if is_width else "custom_css_height_percentage"
|
|
|
|
|
var fill_key = "should_fill_horizontal" if is_width else "should_fill_vertical"
|
|
|
|
|
|
|
|
|
|
if has_meta(fill_key):
|
|
|
|
|
return get_parent_or_fallback_size(is_width)
|
|
|
|
|
elif has_meta(percentage_key):
|
|
|
|
|
var percentage_str = get_meta(percentage_key)
|
|
|
|
|
var percentage = float(percentage_str.replace("%", "")) / 100.0
|
|
|
|
|
var parent_size = get_parent_size(is_width)
|
|
|
|
|
return parent_size * percentage if parent_size > 0 else (custom_minimum_size.x if is_width else custom_minimum_size.y)
|
|
|
|
|
else:
|
|
|
|
|
return size.x if is_width else size.y
|
|
|
|
|
|
|
|
|
|
func calculate_custom_dimension(is_width: bool) -> float:
|
|
|
|
|
var dimension_key = "custom_css_width" if is_width else "custom_css_height"
|
|
|
|
|
var percentage_key = "custom_css_width_percentage" if is_width else "custom_css_height_percentage"
|
|
|
|
|
var fill_key = "should_fill_horizontal" if is_width else "should_fill_vertical"
|
|
|
|
|
|
|
|
|
|
if has_meta(dimension_key):
|
|
|
|
|
return float(get_meta(dimension_key))
|
|
|
|
|
elif has_meta(percentage_key):
|
|
|
|
|
var percentage_str = get_meta(percentage_key)
|
|
|
|
|
var percentage = float(percentage_str.replace("%", "")) / 100.0
|
|
|
|
|
var parent_size = get_parent_size(is_width)
|
|
|
|
|
if parent_size > 0:
|
|
|
|
|
return parent_size * percentage
|
|
|
|
|
elif (size.x if is_width else size.y) > 0:
|
|
|
|
|
return (size.x if is_width else size.y) * percentage
|
|
|
|
|
else:
|
|
|
|
|
return 0.0
|
|
|
|
|
elif has_meta(fill_key):
|
|
|
|
|
return get_parent_or_fallback_size(is_width)
|
|
|
|
|
else:
|
|
|
|
|
return 0.0
|
|
|
|
|
|
|
|
|
|
func get_parent_size(is_width: bool) -> float:
|
|
|
|
|
var parent_container = get_parent()
|
|
|
|
|
if parent_container:
|
|
|
|
|
return parent_container.size.x if is_width else parent_container.size.y
|
|
|
|
|
return 0.0
|
|
|
|
|
|
|
|
|
|
func get_parent_or_fallback_size(is_width: bool) -> float:
|
|
|
|
|
var parent_container = get_parent()
|
|
|
|
|
if parent_container and (parent_container.size.x if is_width else parent_container.size.y) > 0:
|
|
|
|
|
return parent_container.size.x if is_width else parent_container.size.y
|
|
|
|
|
elif (size.x if is_width else size.y) > 0:
|
|
|
|
|
return size.x if is_width else size.y
|
|
|
|
|
else:
|
|
|
|
|
var fallback = custom_minimum_size.x if is_width else custom_minimum_size.y
|
|
|
|
|
return fallback if fallback > 0 else NAN
|