300 lines
10 KiB
GDScript
300 lines
10 KiB
GDScript
class_name ABGUI_Tokens
|
|
extends RefCounted
|
|
|
|
## Shared visual/layout vocabulary. These tokens are deliberately not a page
|
|
## layout: a page may choose its own composition while retaining the same
|
|
## semantic controls, readable type and focus language.
|
|
|
|
const DESIGN_ASPECT := 16.0 / 9.0
|
|
const DESIGN_CANVAS_SIZE := Vector2(1280.0, 720.0)
|
|
const DESIGN_SAFE_MARGIN := Vector2(48.0, 32.0)
|
|
const DESIGN_GRID_GUTTER := 16.0
|
|
const MIN_TOUCH_TARGET := Vector2(48.0, 48.0)
|
|
const MIN_FOCUS_GAP := 4.0
|
|
const COLOR_INK := Color("#111921")
|
|
const COLOR_NAVY := Color("#182838")
|
|
const COLOR_NAVY_LIFT := Color("#263e53")
|
|
const COLOR_NAVY_DISABLED := Color("#323b43")
|
|
const COLOR_GOLD := Color("#b99a5d")
|
|
const COLOR_GOLD_LIGHT := Color("#ead49b")
|
|
const COLOR_IVORY := Color("#f5ead0")
|
|
const COLOR_SKY := Color("#73b9e7")
|
|
const COLOR_DANGER := Color("#a94b53")
|
|
const COLOR_MUTED := Color("#73808a")
|
|
# Success ink for parchment surfaces (dark enough for ink-on-paper contrast).
|
|
const COLOR_SUCCESS := Color("#3d7449")
|
|
const COLOR_SURFACE := Color("#182838", 0.90)
|
|
const COLOR_SURFACE_BORDER := Color("#b99a5d", 0.78)
|
|
const COLOR_SURFACE_INK := Color("#f5ead0")
|
|
|
|
## Shared spacing and control vocabulary. These values describe semantic
|
|
## controls, not a page layout; page masters may still choose their own
|
|
## normalized rectangles.
|
|
const SPACING := {
|
|
"micro": 4,
|
|
"tight": 6,
|
|
"control": 8,
|
|
"section": 12,
|
|
"page": 18,
|
|
}
|
|
const BORDER_WIDTHS := {
|
|
"hairline": 1,
|
|
"standard": 2,
|
|
"emphasis": 3,
|
|
}
|
|
const CORNER_RADII := {
|
|
"control": 6,
|
|
"surface": 8,
|
|
"notice": 10,
|
|
}
|
|
const COMMAND_HEIGHTS := {
|
|
"primary": 52,
|
|
"secondary": 48,
|
|
"utility": 48,
|
|
"danger": 48,
|
|
"icon": 48,
|
|
}
|
|
## Authored minimum widths for semantic commands. Pages may still use an
|
|
## expanding normalized slot, but fixed-width commands must share these
|
|
## footprints so labels do not drift between screens.
|
|
const COMMAND_WIDTHS := {
|
|
"primary": 220,
|
|
"secondary": 180,
|
|
"utility": 160,
|
|
"danger": 190,
|
|
"icon": 48,
|
|
}
|
|
const COMMAND_VARIANTS := ["primary", "secondary", "utility", "danger", "icon"]
|
|
const ICON_KINDS := [
|
|
"back", "settings", "company", "coin", "equipment", "party", "risk",
|
|
"confirm", "inspect", "pause", "speed", "retry", "close", "help", "generic",
|
|
]
|
|
|
|
const FONT_SIZES := {
|
|
# caption: the narrow market-card plate opening (name/trait chips) steps
|
|
# below micro; recorded in M52_SHARED_UI_RULES.md before page reuse.
|
|
"caption": 8,
|
|
"micro": 11,
|
|
"label": 14,
|
|
"body": 16,
|
|
"heading": 22,
|
|
"title": 30,
|
|
}
|
|
|
|
const SURFACE_VARIANTS := ["shell", "context", "notice", "overlay"]
|
|
|
|
|
|
static func font_size(role: String, text_scale := 1.0) -> int:
|
|
var base: int = int(FONT_SIZES.get(role, FONT_SIZES.body))
|
|
return maxi(11, int(round(float(base) * maxf(0.8, text_scale))))
|
|
|
|
|
|
static func touch_size(size: Vector2 = Vector2.ZERO) -> Vector2:
|
|
return Vector2(maxf(MIN_TOUCH_TARGET.x, size.x), maxf(MIN_TOUCH_TARGET.y, size.y))
|
|
|
|
|
|
static func design_equal_row(count: int, y: float, height: float, left := DESIGN_SAFE_MARGIN.x, right := DESIGN_SAFE_MARGIN.x, gap := DESIGN_GRID_GUTTER) -> Array:
|
|
var result: Array = []
|
|
if count <= 0:
|
|
return result
|
|
var available := DESIGN_CANVAS_SIZE.x - left - right - gap * float(count - 1)
|
|
var width := available / float(count)
|
|
for index in range(count):
|
|
var x := left + float(index) * (width + gap)
|
|
result.append(Rect2(x / DESIGN_CANVAS_SIZE.x, y / DESIGN_CANVAS_SIZE.y, width / DESIGN_CANVAS_SIZE.x, height / DESIGN_CANVAS_SIZE.y))
|
|
return result
|
|
|
|
|
|
static func is_supported_variant(variant: String) -> bool:
|
|
return variant in COMMAND_VARIANTS
|
|
|
|
|
|
static func spacing(role: String) -> int:
|
|
return int(SPACING.get(role, SPACING.control))
|
|
|
|
|
|
static func border_width(role: String) -> int:
|
|
return int(BORDER_WIDTHS.get(role, BORDER_WIDTHS.standard))
|
|
|
|
|
|
static func corner_radius(role: String) -> int:
|
|
return int(CORNER_RADII.get(role, CORNER_RADII.control))
|
|
|
|
|
|
static func command_height(variant: String) -> int:
|
|
var normalized := normalize_variant(variant)
|
|
return int(COMMAND_HEIGHTS.get(normalized, COMMAND_HEIGHTS.secondary))
|
|
|
|
|
|
static func command_width(variant: String) -> int:
|
|
var normalized := normalize_variant(variant)
|
|
return int(COMMAND_WIDTHS.get(normalized, COMMAND_WIDTHS.secondary))
|
|
|
|
|
|
static func command_size(variant: String, requested := Vector2.ZERO) -> Vector2:
|
|
var height := float(command_height(variant))
|
|
var width := maxf(MIN_TOUCH_TARGET.x, requested.x)
|
|
if requested.x > 0.0:
|
|
width = maxf(width, float(command_width(variant)))
|
|
return Vector2(width, maxf(height, maxf(MIN_TOUCH_TARGET.y, requested.y)))
|
|
|
|
|
|
static func normalize_variant(variant: String) -> String:
|
|
var value := String(variant).to_lower()
|
|
if value in COMMAND_VARIANTS:
|
|
return value
|
|
if value in ["company_primary", "content_primary", "action_primary"]:
|
|
return "primary"
|
|
if value in ["company_utility", "utility", "content_utility", "action_secondary"]:
|
|
return "utility"
|
|
if value in ["company_danger", "content_danger", "danger"]:
|
|
return "danger"
|
|
if value in ["company_tool", "content_tool", "tool", "icon"]:
|
|
return "icon"
|
|
return "secondary"
|
|
|
|
|
|
static func chrome_variant(variant: String, family := "company") -> String:
|
|
var normalized := normalize_variant(variant)
|
|
if family == "content":
|
|
return "content_primary" if normalized == "primary" else "content_secondary" if normalized == "secondary" else "content_destination" if normalized == "utility" else ""
|
|
return "company_primary" if normalized == "primary" else "company_secondary" if normalized == "secondary" else "company_utility" if normalized == "utility" else "company_tool" if normalized == "icon" else ""
|
|
|
|
|
|
static func icon_kind_supported(icon_kind: String) -> bool:
|
|
return String(icon_kind).to_lower() in ICON_KINDS
|
|
|
|
|
|
static func icon_color(disabled := false, hot := false, dark_background := true) -> Color:
|
|
if disabled:
|
|
return COLOR_MUTED
|
|
if dark_background:
|
|
return COLOR_SKY if hot else COLOR_IVORY
|
|
return COLOR_INK if not hot else COLOR_NAVY_LIFT
|
|
|
|
|
|
static func command_colors(variant: String, disabled := false, hot := false, selected := false) -> Dictionary:
|
|
var normalized := normalize_variant(variant)
|
|
var fill := COLOR_NAVY
|
|
var hover_fill := COLOR_NAVY_LIFT
|
|
var pressed_fill := COLOR_NAVY_LIFT
|
|
var border := COLOR_GOLD
|
|
var text := COLOR_IVORY
|
|
match normalized:
|
|
"primary":
|
|
fill = Color("#e8d9b7")
|
|
hover_fill = Color("#fff1c9")
|
|
pressed_fill = Color("#d7c39a")
|
|
border = Color("#806536")
|
|
text = COLOR_INK
|
|
"danger":
|
|
fill = Color("#4d2830")
|
|
hover_fill = Color("#68343d")
|
|
pressed_fill = Color("#3b2027")
|
|
border = COLOR_DANGER
|
|
"icon":
|
|
fill = Color(COLOR_NAVY, 0.72)
|
|
hover_fill = COLOR_NAVY_LIFT
|
|
pressed_fill = COLOR_NAVY
|
|
border = COLOR_SKY
|
|
if selected:
|
|
hover_fill = Color("#34506a")
|
|
border = COLOR_SKY
|
|
if disabled:
|
|
fill = COLOR_NAVY_DISABLED
|
|
hover_fill = fill
|
|
pressed_fill = fill
|
|
border = COLOR_MUTED
|
|
text = Color("#a1a8aa")
|
|
return {
|
|
"fill": fill,
|
|
"hover": hover_fill if hot else hover_fill,
|
|
"pressed": pressed_fill,
|
|
"border": border,
|
|
"text": text,
|
|
"normalized_variant": normalized,
|
|
}
|
|
|
|
|
|
static func flat_command_style(variant: String, state := "normal", selected := false, disabled := false) -> StyleBoxFlat:
|
|
var colors := command_colors(variant, disabled, state == "hover" or state == "focus", selected)
|
|
var fill: Color = colors.fill
|
|
if state == "hover" or state == "focus":
|
|
fill = colors.hover
|
|
elif state == "pressed":
|
|
fill = colors.pressed
|
|
var style := StyleBoxFlat.new()
|
|
style.bg_color = fill
|
|
style.border_color = colors.border
|
|
style.set_border_width_all(border_width("standard") if colors.normalized_variant == "primary" else border_width("hairline"))
|
|
style.set_corner_radius_all(corner_radius("control"))
|
|
style.content_margin_left = float(spacing("section"))
|
|
style.content_margin_right = float(spacing("section"))
|
|
style.content_margin_top = float(spacing("control"))
|
|
style.content_margin_bottom = float(spacing("control"))
|
|
if state == "focus":
|
|
style.shadow_color = Color(COLOR_SKY, 0.32)
|
|
style.shadow_size = 3
|
|
return style
|
|
|
|
|
|
static func hit_area_style(state := "normal") -> StyleBoxFlat:
|
|
var style := StyleBoxFlat.new()
|
|
style.bg_color = Color(COLOR_NAVY, 0.10) if state == "normal" else Color(COLOR_GOLD, 0.14) if state == "hover" else Color(COLOR_SKY, 0.16)
|
|
style.border_color = Color(COLOR_GOLD_LIGHT, 0.28) if state == "normal" else COLOR_GOLD_LIGHT
|
|
style.set_border_width_all(border_width("hairline"))
|
|
style.set_corner_radius_all(corner_radius("control"))
|
|
return style
|
|
|
|
|
|
static func seal_style(color: Color) -> StyleBoxFlat:
|
|
# Small colored identity chip (risk identity markers). The page supplies the
|
|
# semantic token color; border, radius and weight stay shared.
|
|
var style := StyleBoxFlat.new()
|
|
style.bg_color = color
|
|
style.border_color = COLOR_IVORY
|
|
style.set_border_width_all(border_width("hairline"))
|
|
style.set_corner_radius_all(corner_radius("control"))
|
|
return style
|
|
|
|
|
|
static func notice_style(severity := "info") -> StyleBoxFlat:
|
|
var background := Color(COLOR_NAVY, 0.86)
|
|
var border := COLOR_GOLD
|
|
match severity:
|
|
"success":
|
|
background = Color("#214832")
|
|
border = Color("#9ed7aa")
|
|
"warning":
|
|
background = Color("#4d3b23")
|
|
border = Color("#f0ca75")
|
|
"danger":
|
|
background = Color("#4d2830")
|
|
border = COLOR_DANGER
|
|
var style := StyleBoxFlat.new()
|
|
style.bg_color = background
|
|
style.border_color = border
|
|
style.set_border_width_all(border_width("standard"))
|
|
style.set_corner_radius_all(corner_radius("notice"))
|
|
style.content_margin_left = float(spacing("section"))
|
|
style.content_margin_right = float(spacing("section"))
|
|
style.content_margin_top = float(spacing("section"))
|
|
style.content_margin_bottom = float(spacing("section"))
|
|
return style
|
|
|
|
|
|
static func surface_style(variant := "shell") -> StyleBoxFlat:
|
|
var normalized := String(variant).to_lower()
|
|
if normalized not in SURFACE_VARIANTS:
|
|
normalized = "shell"
|
|
var style := StyleBoxFlat.new()
|
|
style.bg_color = COLOR_SURFACE if normalized == "shell" else Color(COLOR_SURFACE, 0.34) if normalized == "overlay" else Color(COLOR_SURFACE, 0.82)
|
|
style.border_color = COLOR_SURFACE_BORDER if normalized != "notice" else COLOR_GOLD_LIGHT
|
|
style.set_border_width_all(border_width("standard"))
|
|
style.set_corner_radius_all(corner_radius("surface"))
|
|
style.content_margin_left = float(spacing("section"))
|
|
style.content_margin_right = float(spacing("section"))
|
|
style.content_margin_top = float(spacing("control"))
|
|
style.content_margin_bottom = float(spacing("control"))
|
|
return style
|