Requires dot-core.
var stack := DotScreenStack.new()
add_child(stack)
stack.register(pause_menu)
stack.register(settings_screen)
hud.bind_stack(stack)
settings_panel.bind(server_config)
health_bar.bind(func(): return health.health)
Two ideas
Four things have to agree, and they are usually written in four places.
Z-order, input blocking, mouse capture and the back key are each simple alone.
The bug is a pause menu that is visible but not focused, a scoreboard that
captures the mouse, or an escape key that closes two screens at once.
DotScreenStack derives all four from one stack, every time it changes.
It ships no art and imports nothing. No textures, no fonts, no nine-patches,
and no knowledge of dot-match or dot-combat. The theme is generated from
StyleBoxFlats; the kill feed takes coloured string fragments; the scoreboard
takes rows of dictionaries.
What is in the box
DotScreenStack / DotScreen |
What is open, in what order, and who gets input. |
DotUiTheme |
A whole Theme from a palette and a scale. No assets. |
DotHud / DotHudWidget |
The always-on layer, and widgets that pull rather than being pushed. |
DotStatBar |
Health, armour, ammo. Eased bar, un-eased number. |
DotCrosshair |
Opens by the actual spread angle through the actual camera projection. |
DotStarfield |
A parallax starfield drawn into any CanvasItem. Hashed, so nothing is stored. |
DotFeedView |
A bounded, self-expiring list of coloured fragments. Kill feed and chat. |
DotChatWindow |
A chat box: a log that fades, a line to type in, and a key that opens it. It runs !sm and /sm itself (they are never sent as chat) and drops a speaker the player has muted for themselves; self_mute_enabled turns that off. |
DotTableView |
Rows and columns. Scoreboard, server browser, ban list. |
DotSettingsPanel |
Builds itself from a DotConfig. Never shows a secret. |
DotBindingsPanel |
Rebinding, conflict detection, and a file that survives a restart. |
DotHudPiece and the glance pieces |
DotVitals, DotAmmoCounter, DotScoreStrip, DotHitMarker, DotDamageNumbers, DotKeyPrompt, DotKeyHints: the HUD a player reads at a glance. Values in, theme colours, bold outlined text that stays readable over a pale map. |
DotInputBinding |
One binding as a short string — "Y", "Shift+A", "Mouse 1" — and back. The wheel and the side buttons have names (“Wheel up”, “Mouse back”), and an old saved “Mouse 4” still reads. |
DotBallotPanel |
A vote on the HUD: numbered options picked with a number key or a click, a countdown, and each voter’s avatar sliding to the row they chose. Takes the dictionary dot-vote’s DotVoteBallotView builds and emits chosen; it names no dot-vote class. |
The glance pieces and the dark HUD
The pieces draw from what they are handed (show_vitals, show_ammo, show_clock, flash, add), never from a game object, so a test drives one alone. Their colours are theme colours on the type DotHudPiece (COLOUR_KEYS), and an absent one falls back to DotHudPiece.DEFAULTS, so they need no theme. The default is a dark HUD: deep plates, white words ringed in black, dark key caps. A bright look is a theme a player picks, not the default.
DotHudPiece.theme_from(colours, constants) builds the Theme a game sets on the node above its pieces. Every piece reads its colours when it draws, so setting a new one restyles the whole layer live with nothing rebuilt; plain Labels under that node get the theme’s text colour outlined in its ink as well. DotHud.user_scale multiplies the HUD’s fit to the frame (a player’s size setting), and DotHud.fit_control does the same for a plain Control root.
A piece that meets a visible member of the dot_hud_cover group (a DotBallotPanel joins it) dims, hides or ignores itself according to cover_mode, so a ballot never sits on top of the ammunition.
DotCrosshair fades out while the player aims down sights: give it aim_source or call set_aim(), and tune aim_fade_sec and aim_opacity.
DotSettingsPanel reads the config you already wrote
Every addon already describes its settings — keys, types, ranges, enum choices,
groups, and which are secrets — in @export annotations. A hand-built settings
screen restates all of that and then drifts from it, one field at a time, until
a setting exists that no screen can reach.
So this reads the config and builds the controls. It honours
sensitive_keys(), for the same reason DotConfig refuses secrets from the
environment and argv: they end up in screenshots and pasted bug reports.
Edits are held until apply(), and an invalid set is rolled back entirely —
applying half of them leaves the config in a state nobody chose and the player
cannot tell which half survived.
Widgets pull
A health bar wired to a health_changed signal misses the change that happened
before it was created, and fires four times when four things change in one tick.
A DotHudWidget holds a Callable that reads the value, on a throttle — ten
times a second by default, which is faster than anyone can read, with an
immediate path for anything where lateness is a lie.
The crosshair does the projection
gap_pixels() is half_viewport_height / tan(fov/2) — the focal length in
pixels — times tan(spread). A crosshair that opens by an arbitrary amount is
worse than a fixed one: it tells the player something confident and wrong about
where their shots will go.
Where a game plugs in
- DotScreen subclass
_screen_id,_on_push,_on_pop,_on_cover,_on_reveal,_can_pop. Use_can_popsparingly — a screen that refuses to close is a screen a player can be trapped in, and the escape key not working is indistinguishable from a bug.- DotHudWidget subclass + sourceCallable
_on_value(current)and theCallablethat reads it.DotStatBar.max_sourceis the second one.- DotUiTheme, or your own Theme
A palette and a scale, or replace it entirely.
dot-uiships no art on purpose.- Nothing, for a settings screen
DotSettingsPanel.bind(config). That is the whole integration.- Signals
screen_pushed,screen_popped,top_changed,menu_state_changed,pushed,popped,covered,revealed,close_requested,refreshed,line_added,line_expired,row_activated,setting_changed,applied,apply_failed,capture_started,capture_ended,binding_changed,binding_refused.
`set_anchors_preset` does not set offsets
A Control built in code keeps the zero size it was created with, so a whole
interface lays out inside nothing and is invisible while being, by every
property, correctly configured. This addon shipped five of them at once; every
screen it had ever hosted was 0 × 0 unless its host happened to size it, and
nothing in the suite measured a size.
The general lesson: an interface is the one part of this collection whose bugs are invisible to assertions. Render a frame and look at it.
Open screens must sort after closed ones
The stack indexes from zero, which leaves closed screens after open ones in sibling order. Invisible while closed screens are hidden, and wrong the moment a host adds any other child.
