dot-ui

Screens, HUD and menus with no art assets — a screen stack that derives z-order, input blocking and mouse mode from one place, a settings panel generated from any DotConfig, and a rebinder with conflict detection.

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_pop sparingly — 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 the Callable that reads it. DotStatBar.max_source is the second one.

DotUiTheme, or your own Theme

A palette and a scale, or replace it entirely. dot-ui ships 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.

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.