feat(ui): every control state is checked for readability, not trusted
The palette has two light accents and one very dark one, and a control changes its FILL on hover and press. Paper text that reads at 18:1 on the resting near-black chip inverts to paper-on-yellow the moment the pointer arrives, which is 1.1:1 — invisible. That is the standard way a stylised UI becomes unreadable, and it was live on the OptionButton dropdown that every settings row uses: PopupMenu draws its papaya hover fill but keeps `font_color` unless `font_hover_color` is set, and it was not set. So the label now follows the fill. `ink_for(fill)` picks the legible glyph colour by contrast ratio, and every state's text, icon and outline is derived from its own fill through it — including the states nobody remembers exist: `hover_pressed` on a toggle (a CheckButton read as OFF while you touched it), icon colours on a CheckBox that is all icon, and the list hover that used to be the same papaya as list SELECTION, so the row you pointed at looked like the row you had chosen. debug/ui_contrast_check.gd interrogates the BUILT theme rather than the palette — a table compared against itself agrees by construction and catches nothing — and fails below the WCAG floor. It found the PopupMenu gap, a Tree hover asking for a `font_hovered_color` Godot does not have, and the health bar's readout at 2.4:1 over its own fill. That last one is not fixable as a colour pair: a centred readout straddles a hot papaya fill and a near-black trough, and no single colour beats both. What carries it is the heavy ink outline this theme puts on every glyph, which is its first stated rule and the same mechanism that keeps menu text legible straight over the 3D scene. The check models that as a fallback route — outline vs backdrop 3:1, glyph vs outline 4.5:1, at least 4 px — granted only where the backdrop genuinely varies, and substituting two ratios for one rather than waiving the requirement. Disabled text moves from 3.2:1 to 5.2:1 on the way past. A greyed-out "Start Match" is information; an illegible smudge is not. Also styled, because the theme had simply never mentioned them and Godot's defaults are grey-on-grey: scrollbars, Tree, SpinBox, ProgressBar, tooltips, LineEdit read-only and selected text. 108 pairs checked, all passing. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f0b0d19847
commit
414026f001
+250
-19
@@ -6,7 +6,7 @@ class_name UITheme
|
||||
##
|
||||
## The look is a charged cel comic — near-black violet ink, hot papaya, and a
|
||||
## lightning yellow that only ever appears at the moment something is pressed.
|
||||
## Three rules hold it together:
|
||||
## Four rules hold it together:
|
||||
##
|
||||
## INK EVERYTHING every panel, chip and letter carries a heavy dark edge.
|
||||
## It is what makes flat colour read as drawn rather than
|
||||
@@ -18,11 +18,28 @@ class_name UITheme
|
||||
## VOLT MEANS NOW yellow is reserved for the pressed state and the bolt.
|
||||
## Spend it anywhere else and the moment of input stops
|
||||
## standing out.
|
||||
## EVERY STATE READS a fill and the text on it are chosen together, and the
|
||||
## pair is checked. See `STATE_TABLE` and `contrast`.
|
||||
##
|
||||
## Everything here is static. Screens ask for `apply_global` once and then use
|
||||
## the helpers — `heading`, `card`, `divider`, `chip_button` — instead of
|
||||
## hand-rolling styleboxes, which is how the pause menu drifted 900 lines away
|
||||
## from the rest of the game's look.
|
||||
##
|
||||
## ── Why the state table exists ────────────────────────────────────────────────
|
||||
##
|
||||
## The palette has two light accents (papaya, volt) and one very dark one (ink),
|
||||
## and a control changes its FILL on hover and press. Text that reads perfectly
|
||||
## at rest — paper on near-black — inverts to paper-on-yellow the instant the
|
||||
## pointer arrives, which is a contrast ratio of 1.1:1. It is invisible.
|
||||
##
|
||||
## This is the single most common way a stylised UI becomes unreadable, and it is
|
||||
## also the complaint HoYoverse's own audience has levelled at Zenless Zone
|
||||
## Zero's menus. The fix is not vigilance, it is arithmetic: `ink_for` picks the
|
||||
## legible text colour for any fill, every state declares its pair in
|
||||
## `STATE_TABLE`, and `debug/ui_contrast_check.gd` fails the build if one of them
|
||||
## drops below the WCAG floor. A state cannot ship unreadable without the check
|
||||
## going red.
|
||||
|
||||
const FONT_PATH := "res://assets/ui/fonts/Bangers-Regular.ttf"
|
||||
|
||||
@@ -42,6 +59,14 @@ const MAGENTA := Color(1.00, 0.18, 0.52)
|
||||
## Older screens name the accent `TEAL`; it is the electric cyan now.
|
||||
const TEAL := CYAN
|
||||
|
||||
## Disabled: the fill goes flat and the label desaturates, but it does NOT go so
|
||||
## dim that it stops being a word. A disabled control still has to say what it
|
||||
## would do — "Start Match" greyed out is information, an illegible smudge is
|
||||
## not. This pair sits above 5:1, where the old 0.42-grey sat at 3.2:1.
|
||||
const DEAD_FILL := Color(0.10, 0.10, 0.14, 0.92)
|
||||
const DEAD_EDGE := Color(0.30, 0.29, 0.36)
|
||||
const DEAD_TEXT := Color(0.55, 0.54, 0.62)
|
||||
|
||||
## How far a chip leans. Applied as opposite corners round and the other two
|
||||
## nearly square, which is as close to a skew as a StyleBoxFlat can get.
|
||||
const LEAN := 18
|
||||
@@ -49,6 +74,85 @@ const LEAN := 18
|
||||
static var _theme: Theme = null
|
||||
|
||||
|
||||
# ── Contrast ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
## Relative luminance, per WCAG 2.1. Alpha is ignored: a chip's fill is drawn
|
||||
## over the panel behind it, and assuming the fill is opaque is the pessimistic
|
||||
## reading, which is the one worth checking.
|
||||
static func luminance(c: Color) -> float:
|
||||
var ch := [c.r, c.g, c.b]
|
||||
for i in 3:
|
||||
var v: float = ch[i]
|
||||
ch[i] = v / 12.92 if v <= 0.04045 else pow((v + 0.055) / 1.055, 2.4)
|
||||
return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2]
|
||||
|
||||
|
||||
## WCAG contrast ratio between two colours, 1.0 (identical) .. 21.0 (black on
|
||||
## white). 4.5 is the floor for body text, 3.0 for text above ~24 px.
|
||||
static func contrast(a: Color, b: Color) -> float:
|
||||
var la := luminance(a)
|
||||
var lb := luminance(b)
|
||||
return (maxf(la, lb) + 0.05) / (minf(la, lb) + 0.05)
|
||||
|
||||
|
||||
## The legible text colour for a given fill: ink on a light chip, paper on a dark
|
||||
## one. This is what makes a hover safe — the fill is free to become papaya or
|
||||
## volt because the label follows it.
|
||||
static func ink_for(fill: Color) -> Color:
|
||||
return INK if contrast(INK, fill) >= contrast(PAPER, fill) else PAPER
|
||||
|
||||
|
||||
## Every (fill, text) pair the theme puts on screen, as
|
||||
## `["<class>/<state>", fill, text, is_large_text]`.
|
||||
##
|
||||
## Written out rather than derived so the check has something to compare the
|
||||
## built theme AGAINST — a table generated from the theme would agree with it by
|
||||
## construction and catch nothing. Large text (headings, the wordmark, the HUD's
|
||||
## big numerals) is allowed the 3:1 floor WCAG gives it.
|
||||
static func state_table() -> Array:
|
||||
var out: Array = []
|
||||
for cls in BUTTON_CLASSES:
|
||||
out.append_array([
|
||||
[cls + "/normal", INK_SOFT, PAPER, false],
|
||||
[cls + "/hover", PAPAYA, ink_for(PAPAYA), false],
|
||||
[cls + "/pressed", VOLT, ink_for(VOLT), false],
|
||||
[cls + "/focus", INK_SOFT, PAPER, false],
|
||||
[cls + "/disabled", DEAD_FILL, DEAD_TEXT, false],
|
||||
])
|
||||
out.append_array([
|
||||
["Label/on_panel", PANEL, PAPER, false],
|
||||
["Label/dim_on_panel", PANEL, PAPER_DIM, false],
|
||||
["Label/title", PANEL, PAPAYA, true],
|
||||
["LineEdit/normal", INK_SOFT, PAPER, false],
|
||||
["LineEdit/placeholder", INK_SOFT, PAPER_DIM, false],
|
||||
["ItemList/normal", PANEL_DEEP, PAPER, false],
|
||||
["ItemList/hovered", INK_SOFT, PAPER, false],
|
||||
["ItemList/selected", PAPAYA, ink_for(PAPAYA), false],
|
||||
["ItemList/selected_focus", PAPAYA_HOT, ink_for(PAPAYA_HOT), false],
|
||||
["PopupMenu/normal", PANEL_DEEP, PAPER, false],
|
||||
["PopupMenu/hover", PAPAYA, ink_for(PAPAYA), false],
|
||||
["PopupMenu/disabled", PANEL_DEEP, DEAD_TEXT, false],
|
||||
["TabContainer/selected", PAPAYA, ink_for(PAPAYA), false],
|
||||
["TabContainer/unselected", INK_SOFT, PAPER_DIM, false],
|
||||
["TabContainer/hovered", PAPAYA_HOT, ink_for(PAPAYA_HOT), false],
|
||||
["Tree/normal", PANEL_DEEP, PAPER, false],
|
||||
["Tree/selected", PAPAYA, ink_for(PAPAYA), false],
|
||||
["Card/label", INK, PAPER, true],
|
||||
["Card/hover_label", INK, VOLT, true],
|
||||
])
|
||||
return out
|
||||
|
||||
|
||||
## Which theme types get the chip button treatment. Godot theme types do not
|
||||
## inherit styling from Button, so each one has to be told the same thing.
|
||||
const BUTTON_CLASSES := ["Button", "OptionButton", "MenuButton", "CheckBox",
|
||||
"CheckButton", "LinkButton"]
|
||||
|
||||
|
||||
# ── Styleboxes ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
## A leaning comic chip: flat fill, heavy ink edge, hard offset shadow.
|
||||
##
|
||||
## Kept at its original name and argument order — main_menu and match_hud both
|
||||
@@ -93,6 +197,24 @@ static func panel(bg: Color = PANEL, border: Color = INK,
|
||||
return sb
|
||||
|
||||
|
||||
## A tight row chip — a list entry, a popup line, a tab — with the lean dropped.
|
||||
##
|
||||
## The lean is energy on a button the eye lands on. Repeated down twenty rows of
|
||||
## a list it reads as a stack of broken rectangles, so rows get square corners
|
||||
## and keep the fill and the edge.
|
||||
static func row(bg: Color, border: Color = INK, border_w: int = 2) -> StyleBoxFlat:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = bg
|
||||
sb.border_color = border
|
||||
sb.set_border_width_all(border_w)
|
||||
sb.set_corner_radius_all(4)
|
||||
sb.content_margin_left = 12
|
||||
sb.content_margin_right = 12
|
||||
sb.content_margin_top = 5
|
||||
sb.content_margin_bottom = 5
|
||||
return sb
|
||||
|
||||
|
||||
static func build() -> Theme:
|
||||
if _theme:
|
||||
return _theme
|
||||
@@ -105,12 +227,10 @@ static func build() -> Theme:
|
||||
# ── Buttons ──────────────────────────────────────────────────────────
|
||||
# Dark chip with a papaya edge at rest; the chip FILLS papaya on hover and
|
||||
# flashes volt on press. The press state drops its shadow, so the chip
|
||||
# visibly slams down into the page rather than just changing colour.
|
||||
_button_look(t, "Button")
|
||||
for cls in ["OptionButton", "MenuButton", "CheckBox", "CheckButton",
|
||||
"LinkButton"]:
|
||||
# Godot theme types do not inherit styling from Button, so each one has
|
||||
# to be told the same thing.
|
||||
# visibly slams down into the page rather than just changing colour. Every
|
||||
# state's text colour comes from `ink_for` its own fill, so the label
|
||||
# inverts WITH the chip instead of vanishing into it.
|
||||
for cls in BUTTON_CLASSES:
|
||||
_button_look(t, cls)
|
||||
|
||||
# ── Labels: ink outline everywhere, for readability straight over 3D ──
|
||||
@@ -125,45 +245,110 @@ static func build() -> Theme:
|
||||
# ── Inputs ───────────────────────────────────────────────────────────
|
||||
t.set_stylebox("normal", "LineEdit", box(INK_SOFT, PAPAYA, 4, 3, false))
|
||||
t.set_stylebox("focus", "LineEdit", box(INK_SOFT, CYAN, 4, 3, false))
|
||||
t.set_stylebox("read_only", "LineEdit", box(DEAD_FILL, DEAD_EDGE, 4, 3, false))
|
||||
t.set_color("font_color", "LineEdit", PAPER)
|
||||
t.set_color("font_uneditable_color", "LineEdit", DEAD_TEXT)
|
||||
t.set_color("caret_color", "LineEdit", VOLT)
|
||||
t.set_color("font_placeholder_color", "LineEdit", PAPER_DIM)
|
||||
# Selected text: volt fill wants ink glyphs, same rule as a pressed chip.
|
||||
t.set_color("font_selected_color", "LineEdit", INK)
|
||||
t.set_color("selection_color", "LineEdit", VOLT)
|
||||
|
||||
t.set_stylebox("normal", "SpinBox", box(INK_SOFT, PAPAYA, 4, 3, false))
|
||||
t.set_color("font_color", "SpinBox", PAPER)
|
||||
|
||||
# ── Sliders: the filled part is the charged part ─────────────────────
|
||||
t.set_stylebox("slider", "HSlider", box(INK_SOFT, INK, 2, 2, false))
|
||||
t.set_stylebox("grabber_area", "HSlider", box(PAPAYA, INK, 2, 2, false))
|
||||
t.set_stylebox("grabber_area_highlight", "HSlider", box(VOLT, INK, 2, 2, false))
|
||||
t.set_stylebox("slider", "VSlider", box(INK_SOFT, INK, 2, 2, false))
|
||||
t.set_stylebox("grabber_area", "VSlider", box(PAPAYA, INK, 2, 2, false))
|
||||
t.set_stylebox("grabber_area_highlight", "VSlider", box(VOLT, INK, 2, 2, false))
|
||||
|
||||
# ── Progress bars ────────────────────────────────────────────────────
|
||||
t.set_stylebox("background", "ProgressBar", row(INK, INK_SOFT, 2))
|
||||
t.set_stylebox("fill", "ProgressBar", row(PAPAYA, INK, 0))
|
||||
t.set_color("font_color", "ProgressBar", PAPER)
|
||||
t.set_color("font_outline_color", "ProgressBar", INK)
|
||||
t.set_constant("outline_size", "ProgressBar", 6)
|
||||
|
||||
# ── Panels / lists ───────────────────────────────────────────────────
|
||||
t.set_stylebox("panel", "PanelContainer", panel())
|
||||
t.set_stylebox("panel", "Panel", panel())
|
||||
t.set_stylebox("panel", "ItemList", panel(PANEL_DEEP, INK, 3))
|
||||
t.set_color("font_color", "ItemList", PAPER)
|
||||
t.set_color("font_selected_color", "ItemList", INK)
|
||||
t.set_stylebox("selected", "ItemList", box(PAPAYA, INK, 4, 2, false))
|
||||
t.set_stylebox("selected_focus", "ItemList", box(PAPAYA_HOT, CYAN, 4, 2, false))
|
||||
t.set_stylebox("hovered", "ItemList", box(INK_SOFT, PAPAYA, 4, 2, false))
|
||||
t.set_color("font_selected_color", "ItemList", ink_for(PAPAYA))
|
||||
t.set_color("font_hovered_color", "ItemList", PAPER)
|
||||
t.set_color("font_outline_color", "ItemList", INK)
|
||||
t.set_constant("outline_size", "ItemList", 5)
|
||||
t.set_stylebox("selected", "ItemList", row(PAPAYA))
|
||||
t.set_stylebox("selected_focus", "ItemList", row(PAPAYA_HOT, CYAN))
|
||||
# Hover keeps PAPER text, so the fill must stay DARK. A papaya hover under a
|
||||
# papaya selection also made the two states indistinguishable — the row you
|
||||
# were pointing at looked exactly like the row you had chosen.
|
||||
t.set_stylebox("hovered", "ItemList", row(INK_SOFT, PAPAYA))
|
||||
t.set_stylebox("hovered_selected", "ItemList", row(PAPAYA_HOT, CYAN))
|
||||
t.set_stylebox("cursor", "ItemList", row(Color(0, 0, 0, 0), CYAN))
|
||||
t.set_stylebox("cursor_unfocused", "ItemList", row(Color(0, 0, 0, 0), PAPER_DIM))
|
||||
|
||||
# ── Trees (settings, loadout lists) ──────────────────────────────────
|
||||
t.set_stylebox("panel", "Tree", panel(PANEL_DEEP, INK, 3))
|
||||
t.set_color("font_color", "Tree", PAPER)
|
||||
t.set_color("font_selected_color", "Tree", ink_for(PAPAYA))
|
||||
t.set_color("font_outline_color", "Tree", INK)
|
||||
t.set_constant("outline_size", "Tree", 5)
|
||||
t.set_stylebox("selected", "Tree", row(PAPAYA))
|
||||
t.set_stylebox("selected_focus", "Tree", row(PAPAYA_HOT, CYAN))
|
||||
t.set_stylebox("hovered", "Tree", row(INK_SOFT, PAPAYA))
|
||||
t.set_stylebox("hovered_selected", "Tree", row(PAPAYA_HOT, CYAN))
|
||||
|
||||
# ── Scrollbars: visible at rest, charged under the thumb ─────────────
|
||||
for cls in ["HScrollBar", "VScrollBar"]:
|
||||
t.set_stylebox("scroll", cls, row(INK, INK_SOFT, 1))
|
||||
t.set_stylebox("grabber", cls, row(PAPER_DIM, INK, 1))
|
||||
t.set_stylebox("grabber_highlight", cls, row(PAPAYA, INK, 1))
|
||||
t.set_stylebox("grabber_pressed", cls, row(VOLT, INK, 1))
|
||||
|
||||
# ── Tabs ─────────────────────────────────────────────────────────────
|
||||
t.set_stylebox("panel", "TabContainer", panel())
|
||||
t.set_stylebox("tab_selected", "TabContainer", box(PAPAYA, INK, 6, 3, false))
|
||||
t.set_stylebox("tab_unselected", "TabContainer", box(INK_SOFT, INK, 6, 3, false))
|
||||
t.set_stylebox("tab_hovered", "TabContainer", box(PAPAYA_HOT, INK, 6, 3, false))
|
||||
t.set_color("font_selected_color", "TabContainer", INK)
|
||||
t.set_stylebox("tab_disabled", "TabContainer", box(DEAD_FILL, DEAD_EDGE, 6, 3, false))
|
||||
t.set_color("font_selected_color", "TabContainer", ink_for(PAPAYA))
|
||||
t.set_color("font_unselected_color", "TabContainer", PAPER_DIM)
|
||||
t.set_color("font_hovered_color", "TabContainer", INK)
|
||||
t.set_color("font_hovered_color", "TabContainer", ink_for(PAPAYA_HOT))
|
||||
t.set_color("font_disabled_color", "TabContainer", DEAD_TEXT)
|
||||
t.set_color("font_outline_color", "TabContainer", INK)
|
||||
t.set_constant("outline_size", "TabContainer", 5)
|
||||
|
||||
# ── Popups (OptionButton dropdowns) ──────────────────────────────────
|
||||
#
|
||||
# The dropdown is the one surface where a missing hover colour is fatal: a
|
||||
# PopupMenu draws its hover fill but keeps `font_color` unless
|
||||
# `font_hover_color` is set, so a papaya row under paper text was the least
|
||||
# readable thing in the game and it appeared on every settings menu.
|
||||
t.set_stylebox("panel", "PopupMenu", panel(PANEL_DEEP, PAPAYA, 3))
|
||||
t.set_stylebox("hover", "PopupMenu", box(PAPAYA, INK, 4, 0, false))
|
||||
t.set_stylebox("hover", "PopupMenu", row(PAPAYA, INK, 0))
|
||||
t.set_color("font_color", "PopupMenu", PAPER)
|
||||
t.set_color("font_hover_color", "PopupMenu", INK)
|
||||
t.set_color("font_hover_color", "PopupMenu", ink_for(PAPAYA))
|
||||
t.set_color("font_disabled_color", "PopupMenu", DEAD_TEXT)
|
||||
t.set_color("font_accelerator_color", "PopupMenu", PAPER_DIM)
|
||||
t.set_color("font_separator_color", "PopupMenu", PAPAYA)
|
||||
t.set_color("font_outline_color", "PopupMenu", INK)
|
||||
t.set_constant("outline_size", "PopupMenu", 5)
|
||||
|
||||
# ── Dialogs ──────────────────────────────────────────────────────────
|
||||
t.set_stylebox("panel", "AcceptDialog", panel(PANEL_DEEP, PAPAYA, 4))
|
||||
t.set_stylebox("embedded_border", "Window", panel(PANEL_DEEP, PAPAYA, 4))
|
||||
t.set_color("title_color", "Window", VOLT)
|
||||
|
||||
# ── Tooltips ─────────────────────────────────────────────────────────
|
||||
t.set_stylebox("panel", "TooltipPanel", panel(PANEL_DEEP, VOLT, 2))
|
||||
t.set_color("font_color", "TooltipLabel", PAPER)
|
||||
t.set_color("font_outline_color", "TooltipLabel", INK)
|
||||
t.set_constant("outline_size", "TooltipLabel", 5)
|
||||
|
||||
_theme = t
|
||||
return t
|
||||
|
||||
@@ -172,15 +357,28 @@ static func _button_look(t: Theme, cls: String) -> void:
|
||||
t.set_stylebox("normal", cls, box(INK_SOFT, PAPAYA))
|
||||
t.set_stylebox("hover", cls, box(PAPAYA, INK))
|
||||
t.set_stylebox("pressed", cls, box(VOLT, INK, 10, 3, false))
|
||||
# Hover-while-pressed is its own stylebox on a toggle button. Without it a
|
||||
# CheckButton the pointer is over reverts to the hover fill, so a toggle
|
||||
# reads as OFF for as long as you are touching it.
|
||||
t.set_stylebox("hover_pressed", cls, box(VOLT, CYAN, 10, 3, false))
|
||||
t.set_stylebox("focus", cls, box(INK_SOFT, CYAN))
|
||||
t.set_stylebox("disabled", cls, box(Color(0.10, 0.10, 0.14, 0.85), Color(0.28, 0.27, 0.33)))
|
||||
t.set_stylebox("disabled", cls, box(DEAD_FILL, DEAD_EDGE))
|
||||
t.set_color("font_color", cls, PAPER)
|
||||
t.set_color("font_hover_color", cls, INK)
|
||||
t.set_color("font_pressed_color", cls, INK)
|
||||
t.set_color("font_hover_color", cls, ink_for(PAPAYA))
|
||||
t.set_color("font_pressed_color", cls, ink_for(VOLT))
|
||||
t.set_color("font_hover_pressed_color", cls, ink_for(VOLT))
|
||||
t.set_color("font_focus_color", cls, PAPER)
|
||||
t.set_color("font_disabled_color", cls, Color(0.42, 0.41, 0.48))
|
||||
t.set_color("font_disabled_color", cls, DEAD_TEXT)
|
||||
t.set_color("font_outline_color", cls, INK)
|
||||
t.set_constant("outline_size", cls, 5)
|
||||
# The icon has to invert with the label. A paper glyph on a volt chip is the
|
||||
# same 1.1:1 the text would have been, and a CheckBox is ALL icon.
|
||||
t.set_color("icon_normal_color", cls, PAPER)
|
||||
t.set_color("icon_hover_color", cls, ink_for(PAPAYA))
|
||||
t.set_color("icon_pressed_color", cls, ink_for(VOLT))
|
||||
t.set_color("icon_hover_pressed_color", cls, ink_for(VOLT))
|
||||
t.set_color("icon_focus_color", cls, PAPER)
|
||||
t.set_color("icon_disabled_color", cls, DEAD_TEXT)
|
||||
|
||||
|
||||
## Apply the theme to the whole root window. Idempotent, and cheap enough that
|
||||
@@ -223,6 +421,17 @@ static func heading(text: String, size: int = 32) -> Label:
|
||||
return l
|
||||
|
||||
|
||||
## A quieter line of body copy — a hint, a port number, a mode's description.
|
||||
static func caption(text: String, size: int = 20) -> Label:
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.add_theme_font_size_override("font_size", size)
|
||||
l.add_theme_color_override("font_color", PAPER_DIM)
|
||||
l.add_theme_color_override("font_outline_color", INK)
|
||||
l.add_theme_constant_override("outline_size", 5)
|
||||
return l
|
||||
|
||||
|
||||
## A bolt-struck rule, for separating sections.
|
||||
static func divider(strike_at: float = 0.34) -> BoltRule:
|
||||
var b := BoltRule.new()
|
||||
@@ -240,6 +449,28 @@ static func card() -> PanelContainer:
|
||||
return p
|
||||
|
||||
|
||||
## The one loud button on a screen — the thing the player came here to press.
|
||||
##
|
||||
## Papaya-filled at rest instead of ink-filled, so it is the first thing the eye
|
||||
## lands on, and correspondingly ink-lettered. Hover goes hotter and press still
|
||||
## flashes volt, so the state ladder is unchanged; only the resting colour moves.
|
||||
static func primary_button(text: String, size: int = 44) -> Button:
|
||||
var b := Button.new()
|
||||
b.text = text
|
||||
b.add_theme_font_size_override("font_size", size)
|
||||
b.add_theme_stylebox_override("normal", box(PAPAYA, INK, 12, 4))
|
||||
b.add_theme_stylebox_override("hover", box(PAPAYA_HOT, VOLT, 12, 4))
|
||||
b.add_theme_stylebox_override("pressed", box(VOLT, INK, 12, 4, false))
|
||||
b.add_theme_stylebox_override("focus", box(PAPAYA, CYAN, 12, 4))
|
||||
b.add_theme_color_override("font_color", ink_for(PAPAYA))
|
||||
b.add_theme_color_override("font_hover_color", ink_for(PAPAYA_HOT))
|
||||
b.add_theme_color_override("font_pressed_color", ink_for(VOLT))
|
||||
b.add_theme_color_override("font_focus_color", ink_for(PAPAYA))
|
||||
b.add_theme_color_override("font_outline_color", Color(INK.r, INK.g, INK.b, 0.55))
|
||||
b.add_theme_constant_override("outline_size", 4)
|
||||
return b
|
||||
|
||||
|
||||
## Sounds and the hover kick for every button under `root` (recursive).
|
||||
##
|
||||
## The kick is a 4% scale-up on hover and a snap back on exit. It is small on
|
||||
|
||||
Reference in New Issue
Block a user