207 lines
9.3 KiB
GDScript
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))
|