--- name: makepad-game-ui-designer description: "Design and build Makepad game UI: HUD, menus, pause and win/lose overlays, settings, responsive and touch layout, XR-safe interfaces, and splash-DSL styling. Use for HUD, menu, overlay, scoreboard, minimap, prompt, controls screen, mobile layout, and UI polish work in makepad-widgets and makepad-game-render's hud module." --- # Makepad Game UI Designer Game UI is not app UI. It is read in peripheral vision, during motion, while the player is doing something else. Legibility and latency beat elegance. ## Two surfaces **In-world / overlay HUD** — drawn by the renderer, inside the 3D pass: `makepad-game-render::hud` (`draw_hud_overlay`, `draw_billboard_labels`, `thermometer`), with sim-side HUD state in `makepad-game-sim::hud`. Script verbs: `text`, `bar`, `crosshair`, `label`, `label_text`. Use this for anything that must sit *with* the world: damage numbers, billboard nameplates, interaction prompts, crosshair, world-anchored markers. It is also the only correct choice in XR. **Widget UI** — ordinary `makepad-widgets` composed in `script_mod!`. Use for menus, settings, pause, results screens, chat, and anything with real interaction and focus. Do not put a health bar in a widget layer that must update every frame if the HUD path already does it — you will pay a layout pass for a number. ## Styling with `script_mod!` ```rust script_mod! { use mod.prelude.widgets_internal.* use mod.widgets.* mod.widgets.GameHudBase = #(GameHud::register_widget(vm)) mod.widgets.GameHud = set_type_default() do mod.widgets.GameHudBase { width: Fill height: Fill draw_bg += { /* look only */ } } } ``` Look lives in the DSL, structure and behaviour in Rust. Inheritance (`+=` / `do`) is how a variant is made — copy-pasting a whole widget definition to change one colour is how a theme becomes unmaintainable. The splash DSL resolves **at runtime**: a malformed style or a missing widget id survives `cargo check` and fails on launch. Always run the app after a UI change. ## The states that must exist Every one needs a visible surface. Missing states are the most common UI gap in a game that "works". | State | Must show | | --- | --- | | Boot / loading | that something is happening, and progress if it takes > 1 s | | Menu | start, and how to control the game | | Playing | the HUD, and nothing else | | Paused | that it is paused, resume, restart, quit | | Win | the outcome, the score, what to do next | | Lose | the outcome, why, retry | | Settings | audio, controls, quality | Two recurring bugs: pause that does not stop the sim step, and retry that leaves the previous run's score, timers or audio voices alive. Reset through one `reset()` and test by winning twice in a row. ## HUD principles - **Only what changes the next decision.** Everything else is clutter. A stat card of nine numbers is a placeholder, not a HUD. - **Position by urgency.** Critical and instantaneous → near the centre or the crosshair. Ambient → corners. - **Readable in motion.** High contrast against the *busiest* frame in the game, not the menu background. Outline or shadow text over the world — a thin light font on a bright sky is invisible exactly when it matters. - **Animate value changes.** A health bar that jumps is missed; one that drains over ~0.2 s with a lagging damage ghost is felt. - **Diegetic where it fits** — a speedometer on the dashboard beats a corner number in a driving game. - **Nothing decorative that moves.** Motion in the periphery reads as a threat; spend that signal on real events. ## Responsive and touch Layout with `Fill`, `Fit` and fractional sizing; the fork's turtle layout also has `Grid` (added in recent upstream `work`). Do not hardcode pixel positions for anything that must survive an aspect-ratio change. - Test at 16:9, 4:3, and a tall phone ratio at minimum. Anchor to edges, not to absolute coordinates. - **Safe area**: keep essential UI out of the outer ~5%, and away from notches and rounded corners. - **Touch targets ≥ 44 px** at the real device DPI. Thumbs occlude what they touch — put virtual controls where the hand already is (lower corners), and make virtual sticks relative to first touch, not fixed. - No hover states on touch. If information only appears on hover, it does not exist on mobile. ## XR `xr/` and `apps/arcade/src/xr_input.rs`. Rules that are not optional: - **Never face-lock UI.** Head-locked panels cause sickness. Anchor to the world, to the wrist, or to a comfortable body-relative position that lags the head. - Keep panels ~1–2 m away; nearer than ~0.5 m cannot be focused. - Text must be far larger than a screen equivalent; thin fonts vanish. - Pointing is coarse — targets need to be much bigger than on desktop. - No shake, no rapid full-field flashes. ## Accessibility Cheap, and it improves the game for everyone: never encode meaning in hue alone (pair with shape/icon/position); keep body text ≥ 16 px equivalent; provide a way to reduce shake and flashes; make control prompts show the *active* device's glyphs, not a hardcoded keyboard key. ## Verification `makepad-test` drives the real UI: ```rust app.widget_dump(); // the tree, for finding ids app.widget_snapshot(); // Vec - assert structure/state app.screenshot(); // look at it app.press_key(KeyCode::Escape); app.touch_down(x, y); ``` Capture every state at the target aspect ratio and inspect the images. A UI change verified only by `cargo check` is unverified — the DSL resolves at runtime. ## Required reading - `references/hud-patterns.md` — layouts per genre, anti-patterns, animation timings. - `references/menus-and-flow.md` — state machine, focus, settings, results screens.