Files
leonwww/addons/godot-flexbox/flex_container.gd
2025-07-28 15:22:34 +03:00

244 lines
6.3 KiB
GDScript

@tool
class_name FlexContainer
extends Container
@export var debug_draw = false
var _draw_rects = []
const EDGES = [1, 2, 3, 0]
const _PADDING_WRAPPER_NAME = "__FlexPaddingWrapper"
enum FlexDirection {Column,ColumnReverse,Row,RowReverse}
enum FlexWrap {NoWrap,Wrap,WrapReverse}
enum JustifyContent {FlexStart,Center,FlexEnd,SpaceBetween,SpaceAround,SpaceEvenly}
enum AlignItems {Auto,FlexStart,Center,FlexEnd,Stretch,Baseline,SpaceBetween,SpaceAround}
enum AlignContent {Auto,FlexStart,Center,FlexEnd,Stretch,Baseline,SpaceBetween,SpaceAround}
const DEFAULT_VALUE = {
reverse = 0,
flex_wrap = FlexWrap.NoWrap,
flex_direction = FlexDirection.Row,
justify_content = JustifyContent.FlexStart,
align_items = AlignItems.Stretch, # Note: Stretch is the default
align_content = AlignContent.FlexStart
}
var _root: Flexbox
var _initialized = false
var _flex_list = []
enum FlexDataType { CID = 0, FLEXBOX, CONTROL }
var direction_reverse = DEFAULT_VALUE.reverse
@export var flex_direction:FlexDirection = DEFAULT_VALUE.flex_direction
@export var flex_wrap:FlexWrap = DEFAULT_VALUE.flex_wrap
@export var justify_content:JustifyContent = DEFAULT_VALUE.justify_content
@export var align_items:AlignItems = DEFAULT_VALUE.align_items
@export var align_content:AlignContent = DEFAULT_VALUE.align_content
var state:Dictionary = {flex_direction=null, flex_wrap=null,justify_content=null,align_items=null,align_content=null}
func _init():
_root = Flexbox.new()
update_state()
func update_state():
for property in state:
var value = get(property)
state[property] = value
func _ready() -> void:
_root.set_flex_direction(get("flex_direction"))
_root.set_flex_wrap(get("flex_wrap"))
_root.set_justify_content(get("justify_content"))
_root.set_align_items(get("align_items"))
_root.set_align_content(get("align_content"))
update_state()
set_process_input(false)
_initialized = true
func _notification(what: int) -> void:
match what:
NOTIFICATION_SORT_CHILDREN:
_resort()
[NOTIFICATION_TRANSLATION_CHANGED, NOTIFICATION_LAYOUT_DIRECTION_CHANGED]:
queue_sort()
func _resort() -> void:
if not is_instance_valid(_root):
return
_root.remove_all_children()
_flex_list.clear()
var root_size = get_size()
_root.set_width(root_size.x)
_root.set_height(root_size.y)
if debug_draw:
_draw_rects.clear()
_draw_debug_rect(Rect2(Vector2.ZERO, root_size), Color(0, 0.8, 0.5, 1))
for i in range(get_child_count()):
var c = get_child(i)
if not c.is_class("Control") or c.is_set_as_top_level() or not c.is_visible_in_tree():
continue
var flexbox = Flexbox.new()
_root.insert_child(flexbox, _flex_list.size())
_flex_list.append([c.get_instance_id(), flexbox, c])
_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"))
_root.mark_dirty_and_propogate()
_root.calculate_layout(root_size.x, root_size.y, 1)
for flex_data in _flex_list:
var flexbox = flex_data[FlexDataType.FLEXBOX]
var c = flex_data[FlexDataType.CONTROL]
if not is_instance_valid(c):
continue
var offset = Vector2(flexbox.get_computed_left(), flexbox.get_computed_top())
var size = Vector2(flexbox.get_computed_width(), flexbox.get_computed_height())
_fit_child_in_rect(c, Rect2(offset, size))
if debug_draw:
_draw_debug_rect(Rect2(offset, size), Color(1, 0, 0, 0.8))
queue_redraw()
func padding_wrapper(node:Control,spacing_value:Array):
var wrapper_node = node.get_node_or_null(_PADDING_WRAPPER_NAME)
if not wrapper_node:
wrapper_node = MarginContainer.new()
wrapper_node.name = _PADDING_WRAPPER_NAME
var children_to_wrap = node.get_children()
for child in children_to_wrap:
if child != wrapper_node:
child.reparent(wrapper_node)
node.add_child(wrapper_node)
wrapper_node.add_theme_constant_override("margin_left", spacing_value[0])
wrapper_node.add_theme_constant_override("margin_top", spacing_value[1])
wrapper_node.add_theme_constant_override("margin_right", spacing_value[2])
wrapper_node.add_theme_constant_override("margin_bottom", spacing_value[3])
func _find_index_from_flex_list(flex_list: Array, cid: int) -> int:
for i in range(flex_list.size()):
if flex_list[i][FlexDataType.CID] == cid:
return i
return -1
func _set_control_min_size(c: Control, flexbox: Flexbox):
var min_size = c.get_combined_minimum_size()
flexbox.set_min_width(min_size.x)
flexbox.set_min_height(min_size.y)
func _fit_child_in_rect(child: Control, rect: Rect2) -> void:
if not is_instance_valid(child):
return
child.set_position(rect.position)
child.set_size(rect.size)
child.set_rotation(0)
child.set_scale(Vector2.ONE)
func apply_flex_meta(node, metas):
for key in metas:
var value = metas[key]
apply_child_property(node, key, value)
func apply_child_property(node, prop, value):
match prop:
"basis":
if typeof(value) == TYPE_STRING and value == "auto":
node.set_flex_basis_auto()
else:
node.set_flex_basis(value)
"grow":
node.set_flex_grow(value)
"padding":
for i in range(4):
var edge = EDGES[i]
node.set_padding(edge, value[i])
"margin":
for i in range(4):
var edge = EDGES[i]
var value1 = value[i]
if typeof(value1) == TYPE_STRING and value1 == "auto":
node.set_margin_auto(edge)
else:
node.set_margin(edge, value1)
"align_self":
node.set_align_self(value)
func flex_property_changed(property, value):
value = process_value(property, value)
state[property] = value
set(property, value)
match property:
"flex_direction":
_root.set_flex_direction(value)
"flex_wrap":
_root.set_flex_wrap(value)
"justify_content":
_root.set_justify_content(value)
"align_items":
_root.set_align_items(value)
"align_content":
_root.set_align_content(value)
func update_layout():
queue_sort()
func edit_set_state(p_state):
for property in p_state:
var value = p_state[property]
flex_property_changed(property, value)
update_layout()
func edit_get_state():
return state.duplicate()
func _draw():
for r in _draw_rects:
draw_rect(r.rect, r.color, false, 2)
func _draw_debug_rect(rect, color):
_draw_rects.append({rect = rect, color = color})
func process_value(key, value):
if DEFAULT_VALUE.has(key) && value == -1:
return DEFAULT_VALUE[key]
return value
func get_class():
return "FlexContainer"