Files
settled-reach/client/ui/implant/implant_pending.gd
T
jpmschweitzerandClaude Opus 4.7 25ef9d2f63 feat(ui): ImplantPending — diegetic Layer-1 "generating" indicator (#960)
A reusable implant-layer busy indicator (Araminta's spec): an animated
bracket sweep `[ >>>>···· ]` with a caption, themed entirely via ImplantTheme.
The Atlas shows it centered over the canvas while the Layer-1 proxy is Pending
and hides it on Ready — so cache hits never flash it and serverless mode never
shows it.

Layer separation (per direction): ImplantPending belongs to the diegetic
implant UI only. The global, non-diegetic UI layer must use its own busy
indicator with its own visuals — sharing the sweep logic is fine, resting on
this theme/these glyphs is not. Documented in the component header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:05:39 +02:00

107 lines
3.3 KiB
GDScript

class_name ImplantPending
extends Control
## Diegetic "pending / working" indicator for the implant overlay (D-169).
##
## An animated bracket sweep — `[ >>>>········ ]` — with a caption above it.
## Linear left-to-right fill that loops; reads as a bounded machine process,
## not an OS spinner (Araminta's spec, #960).
##
## LAYER SEPARATION: this belongs to the IMPLANT (diegetic, in-fiction) UI layer
## only. It styles exclusively from ImplantTheme. The global / non-diegetic UI
## layer must NOT reuse these visuals — if that layer needs a busy indicator it
## gets its own component with its own look. Sharing the sweep *logic* is fine;
## resting on the same theme or glyphs is not.
##
## API: start(label) shows + animates; stop() hides + resets. Starts hidden, so
## at rest the player sees nothing.
const SWEEP_PERIOD: float = 1.2 # seconds per left-to-right pass (linear, looping)
const INTERIOR_WIDTH: float = 160.0 # px between the brackets
const SWEEP_WIDTH: float = 28.0 # px width of the moving fill
const DEFAULT_WIDTH: float = 220.0
var _label_text: String = "WORKING"
var _sweep_t: float = 0.0 # normalized 0..1 sweep position
var _theme: ImplantTheme = null
var _font: Font = null
func _ready() -> void:
_font = ThemeDB.fallback_font
custom_minimum_size = Vector2(DEFAULT_WIDTH, 40.0)
mouse_filter = Control.MOUSE_FILTER_IGNORE
visible = false
set_process(false)
## Show the indicator and begin animating. `label` is uppercased for display.
func start(label: String = "WORKING") -> void:
_label_text = label.to_upper()
_sweep_t = 0.0
visible = true
set_process(true)
queue_redraw()
## Hide the indicator and stop animating.
func stop() -> void:
visible = false
set_process(false)
_sweep_t = 0.0
func apply_implant_theme(t: ImplantTheme) -> void:
_theme = t
queue_redraw()
func _process(delta: float) -> void:
_sweep_t = fmod(_sweep_t + delta / SWEEP_PERIOD, 1.0)
queue_redraw()
func _draw() -> void:
if _font == null:
return
var dim: Color = _theme.text_dim if _theme else Color("#667788")
var accent: Color = _theme.accent_active if _theme else Color("#f0d060")
accent.a = 0.6
var fs: int = _theme.font_body if _theme else 11
var line_h: float = float(_theme.line_height if _theme else 18)
var w: float = size.x if size.x > 0.0 else DEFAULT_WIDTH
# Caption — centered, dim.
var cap_w: float = _font.get_string_size(_label_text, HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x
draw_string(
_font,
Vector2((w - cap_w) * 0.5, float(fs)),
_label_text,
HORIZONTAL_ALIGNMENT_LEFT,
-1,
fs,
dim
)
# Bracket row — centered group: [ <interior> ]
var glyph_w: float = _font.get_string_size("[", HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x
var group_w: float = glyph_w * 2.0 + INTERIOR_WIDTH
var gx: float = (w - group_w) * 0.5
var baseline_y: float = line_h + float(fs)
draw_string(_font, Vector2(gx, baseline_y), "[", HORIZONTAL_ALIGNMENT_LEFT, -1, fs, dim)
draw_string(
_font,
Vector2(gx + glyph_w + INTERIOR_WIDTH, baseline_y),
"]",
HORIZONTAL_ALIGNMENT_LEFT,
-1,
fs,
dim
)
# Sweep fill — a thin bar travelling across the bracket interior.
var interior_x: float = gx + glyph_w
var travel: float = INTERIOR_WIDTH - SWEEP_WIDTH
var sweep_x: float = interior_x + _sweep_t * travel
var sweep_y: float = baseline_y - float(fs) * 0.35
draw_rect(Rect2(sweep_x, sweep_y, SWEEP_WIDTH, 1.0), accent)