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))