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>
107 lines
3.3 KiB
GDScript
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)
|