Files
aetherbound-guild/runtime/ui/shared/ui_factory.gd
T

207 lines
9.3 KiB
GDScript

class_name ABGUIFactory
extends RefCounted
const Tokens = preload("res://foundation/ui_tokens.gd")
const EntryChrome = preload("res://entry/entry_chrome.gd")
const UIIcon = preload("res://ui/shared/ui_icon.gd")
const UI_FONT = preload("res://assets/fonts/NotoSansSC.ttf")
## Presentation-only factory for repeated MVP controls. Page geometry and
## intent ownership remain with the caller; this object supplies only shared
## semantic styling, typography and owned vector icons.
static func label(value: String, role := "body", text_scale := 1.0, color := Tokens.COLOR_IVORY) -> Label:
var result := Label.new()
result.text = value
result.add_theme_font_override("font", UI_FONT)
result.add_theme_font_size_override("font_size", Tokens.font_size(role, text_scale))
result.add_theme_color_override("font_color", color)
return result
static func command_button(
node_name: String,
text_value: String,
semantic_variant := "secondary",
icon_kind := "generic",
shape := "angular",
family := "company",
requested_size := Vector2.ZERO,
disabled := false,
tooltip := "",
callback := Callable(),
font_size_override := -1
) -> Button:
var button := Button.new()
button.name = node_name
# Angular commands draw their own EntryChrome child. Flat commands still
# need Godot's StyleBox decoration so they remain visible at rest.
button.flat = shape == "angular"
button.focus_mode = Control.FOCUS_ALL
button.disabled = disabled
button.tooltip_text = tooltip if not tooltip.is_empty() else text_value
button.custom_minimum_size = Tokens.command_size(semantic_variant, requested_size)
button.set_meta("ui_factory_control", true)
button.set_meta("ui_semantic_variant", Tokens.normalize_variant(semantic_variant))
button.set_meta("ui_shape", shape)
button.set_meta("ui_family", family)
button.set_meta("ui_icon_kind", icon_kind)
if shape == "angular":
_build_angular_button(button, text_value, semantic_variant, icon_kind, family, font_size_override)
else:
_build_flat_button(button, text_value, semantic_variant, icon_kind, disabled, font_size_override)
if callback.is_valid():
button.pressed.connect(callback, CONNECT_DEFERRED)
return button
static func apply_flat_command(button: Button, semantic_variant := "secondary", icon_kind := "generic", disabled := false, selected := false, family := "content") -> Button:
"""Apply shared command semantics to an existing page-owned hit area.
The caller keeps ownership of children, normalized geometry and intent
wiring; this helper only supplies the shared state, focus and style grammar.
"""
if button == null:
return button
var normalized := Tokens.normalize_variant(semantic_variant)
# Keep the native StyleBox visible for page-owned flat commands. The
# angular family owns a separate chrome layer and is handled by its caller.
button.flat = false
button.focus_mode = Control.FOCUS_ALL
button.disabled = disabled
button.custom_minimum_size = Tokens.command_size(normalized, button.custom_minimum_size)
button.mouse_default_cursor_shape = Control.CURSOR_FORBIDDEN if disabled else Control.CURSOR_POINTING_HAND
button.set_meta("ui_factory_control", true)
button.set_meta("ui_semantic_variant", normalized)
button.set_meta("ui_family", family)
button.set_meta("ui_icon_kind", icon_kind)
button.set_meta("ui_selected", selected)
_apply_flat_styles(button, normalized, selected, disabled)
var label_node := button.get_node_or_null("ButtonLabel") as Label
if label_node != null:
var colors := Tokens.command_colors(normalized, disabled, false, selected)
label_node.add_theme_font_override("font", UI_FONT)
label_node.add_theme_color_override("font_color", colors.text)
var icon_node := button.get_node_or_null("CommandIcon") as ABGUIIcon
if icon_node != null:
icon_node.configure(icon_kind, "dark")
icon_node.set_state(disabled, false)
return button
static func attach_icon(parent: Control, icon_kind: String, node_name := "CommandIcon", tone := "dark", area := Rect2(0.04, 0.12, 0.24, 0.76)) -> ABGUIIcon:
if parent == null:
return null
var existing := parent.get_node_or_null(node_name) as ABGUIIcon
if existing != null:
return existing.configure(icon_kind, tone)
var icon := UIIcon.new().configure(icon_kind, tone)
icon.name = node_name
icon.anchor_left = area.position.x
icon.anchor_top = area.position.y
icon.anchor_right = area.end.x
icon.anchor_bottom = area.end.y
icon.offset_left = 0.0
icon.offset_top = 0.0
icon.offset_right = 0.0
icon.offset_bottom = 0.0
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(icon)
return icon
static func refresh_command_button(button: Button, disabled := false, selected := false) -> void:
if button == null:
return
button.disabled = disabled
button.set_meta("ui_selected", selected)
var variant := String(button.get_meta("ui_semantic_variant", "secondary"))
var shape := String(button.get_meta("ui_shape", "flat"))
if shape != "angular":
_apply_flat_styles(button, variant, selected, disabled)
var label_node := button.get_node_or_null("ButtonLabel") as Label
if label_node != null:
var colors := Tokens.command_colors(variant, disabled, false, selected)
label_node.add_theme_color_override("font_color", colors.text)
var icon_node = button.get_node_or_null("CommandIcon")
if icon_node != null and icon_node.has_method("set_state"):
icon_node.set_state(disabled, false)
static func notice_surface(node_name: String, severity := "info") -> Panel:
var panel := Panel.new()
panel.name = node_name
panel.add_theme_stylebox_override("panel", Tokens.notice_style(severity))
panel.set_meta("ui_notice_severity", severity)
return panel
static func _build_angular_button(button: Button, text_value: String, semantic_variant: String, icon_kind: String, family: String, font_size_override: int) -> void:
for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]:
button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new())
var chrome_variant := Tokens.chrome_variant(semantic_variant, family)
if chrome_variant.is_empty():
chrome_variant = Tokens.chrome_variant("secondary", family)
var chrome = EntryChrome.new().configure(chrome_variant, icon_kind)
chrome.name = "Chrome"
chrome.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
button.add_child(chrome)
var normalized := Tokens.normalize_variant(semantic_variant)
var dark_text := normalized == "utility" or family == "content" and normalized == "primary"
var text_color := Color("#202a31") if dark_text else Tokens.COLOR_IVORY
if button.disabled:
text_color = Color("#89939a")
var label_node := label(text_value, "label", 1.0, text_color)
if font_size_override > 0:
label_node.add_theme_font_size_override("font_size", font_size_override)
label_node.name = "ButtonLabel"
label_node.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
label_node.offset_left = 30.0
label_node.offset_right = -10.0
label_node.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label_node.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label_node.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label_node.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(label_node)
button.set_meta("entry_chrome_variant", chrome_variant)
static func _build_flat_button(button: Button, text_value: String, semantic_variant: String, icon_kind: String, disabled: bool, font_size_override: int) -> void:
_apply_flat_styles(button, semantic_variant, false, disabled)
var has_icon := Tokens.icon_kind_supported(icon_kind) and icon_kind != "generic"
if has_icon:
var icon_node = UIIcon.new().configure(icon_kind, "dark")
icon_node.name = "CommandIcon"
icon_node.anchor_left = 0.0
icon_node.anchor_top = 0.0
icon_node.anchor_right = 0.0
icon_node.anchor_bottom = 1.0
icon_node.offset_left = 10.0
icon_node.offset_right = 42.0
icon_node.offset_top = 8.0
icon_node.offset_bottom = -8.0
button.add_child(icon_node)
var colors := Tokens.command_colors(semantic_variant, disabled)
var label_node := label(text_value, "label", 1.0, colors.text)
if font_size_override > 0:
label_node.add_theme_font_size_override("font_size", font_size_override)
label_node.name = "ButtonLabel"
label_node.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
label_node.offset_left = 44.0 if has_icon else 12.0
label_node.offset_right = -12.0
label_node.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label_node.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label_node.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label_node.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(label_node)
static func _apply_flat_styles(button: Button, semantic_variant: String, selected: bool, disabled: bool) -> void:
button.add_theme_stylebox_override("normal", Tokens.flat_command_style(semantic_variant, "normal", selected, disabled))
button.add_theme_stylebox_override("hover", Tokens.flat_command_style(semantic_variant, "hover", selected, disabled))
button.add_theme_stylebox_override("pressed", Tokens.flat_command_style(semantic_variant, "pressed", selected, disabled))
button.add_theme_stylebox_override("focus", Tokens.flat_command_style(semantic_variant, "focus", selected, disabled))
button.add_theme_stylebox_override("hover_pressed", Tokens.flat_command_style(semantic_variant, "pressed", selected, disabled))
button.add_theme_stylebox_override("disabled", Tokens.flat_command_style(semantic_variant, "normal", selected, true))