The owner-approved UI redesign (ui-redesign-spec; coordinates from layout-redesign.json at 720p in a 1280 frame). Its aims: one consistent look on every page (BACK bottom-left, the main action bottom-right, sections with accent headers, NAME: VALUE on every cycle), no text smaller than text scale 1.5, nothing off the screen or drawn over something else (the UI audit's bugs), and text that fits by itself in every UI font. No setting, quark.cfg key, file format or network message changed; no gameplay change. (1.18.12 is the model editor.)
Title logo (src/client/renderer.cpp, tools/make_logo.py, src/client/logo_data.h)
- Drawn like the app icon: the menus' title was the UI font at text scale 8 in the team colour. It is now the design's logo: Rubik ExtraBold, every letter on a square black outline, the Q split along make_icon.py's slanted line with a black seam.
- Baked masks: tools/make_logo.py bakes it with make_icon.py's Pillow steps (mask, square dilation, split, seam) at its 2160p size, 4x supersampled, into one 626 x 147 RGBA PNG (16 KB) of masks: R = the friendly half (the Q left of the line and UARK), G = the enemy half, A = the outline. Mipmapped, so it is crisp at 720p, 1080p and 768.
- Team colours (owner): the UI shader tints R with color_friendly and G with color_enemy, so the title follows the player's team colours (defaults: cyan / red, as before). Only the menus' title: the app icon, launcher and website keep the fixed colours.
- Any font (owner): with another UI font picked, the word is drawn in that font at the logo's size (same em and baseline) with its own outline; the Q's quad carries a flag in the vertex's spare byte and the text shader colours it past the same slanted line (9.09 degrees, through the middle of the Q's advance and ink) with a 12/844 em black seam.
- Cost: still one draw call in the existing 2D batch, no extra pass. A sentinel text scale (255 + 255/256) makes the shader sample the logo texture; the texture is bound and the title's uniforms set only by a flush that draws the title. Uniform locations are looked up once at init, like the rest of testing's 2D path (no lookups in the frame loop).
- Sized as designed: em = 7.5 units x the text scale x 0.66 / 0.70 (Rubik's cap height matched to the old font's), baseline 5.7 units under the line top. The settings pages show no title (decision 6).
New widgets (src/client/ui.cpp, ui.h)
- Colour picker (Ui::colorPicker): "LABEL [swatch]"; a click opens a saturation / value square, a hue bar, a hex field and R G B. Right-click: the default. It returns CHANGED or RESET, so a setting whose default is "off" (the name colour) can tell them apart. Colours stay 8 bits a channel, as quark.cfg keeps them. Why: every R / G / B slider trio took three rows and was hard to aim.
- Dropdown (Ui::dropdown): "LABEL: VALUE V" opening its list under it.
- An open picker or dropdown is modal (every other widget inert and unreported; a click outside closes it and does nothing else) and is drawn last, over the page. It closes when its widget is no longer drawn.
- Tabs, hints, right-click: Ui::tab, Ui::hint (0.75 of the UI's text scale, unrounded: 1.5 at 720p), Ui::hintCentered, Ui::rclicked, Ui::openPopup (screenshot runs). Ui::muted now also blocks input.
- Wrapped text (Ui::wrap / hintWrapped): a paragraph broken at spaces, from a 128-entry cache keyed by text, width, size and font (the text kept too, so a hash clash re-wraps): measured once, not every frame, and a cache hit allocates nothing.
- Rect gradient (Renderer::rectGradient) for the picker's ramps, in the same batch.
- No per-frame cost when nothing is open: a couple of flag checks per widget.
Text fitting (src/client/textfit.{h,cpp}: GL-free, so quark_tests uses it)
- Measured once per font: when a UI font is picked (never per frame) every glyph's ink box is measured from its atlas (the distance field gives the edge to a fraction of a texel).
- Line box: if any glyph's ink plus its outline reaches outside the box Rubik uses (the 8-unit line, or Rubik's own ink: -1.21..8.12 units), the whole font is drawn just enough smaller and moved only as far as needed. Bebas Neue (Overwatch) 0.876 (its | , ; fell below the box), Lilita One (TF2) 0.982, Work Sans and Jost only move up 0.05 / 0.13 units, the rest untouched. Why (owner): some bundled fonts touched the line above or below.
- Width (owner): a label that doesn't fit is first closed up (each letter pair only as far as its ink allows: fills at least an outline apart, never more than 15% of a letter), then shrunk, and only cut with "..." when it still doesn't fit at the font's smallest size. A 256-entry cache keyed by text, width, size and font: a fitted label is worked out once, with no per-frame allocation. Merged with testing's no-copy Ui::fitText (buttons and cycles draw the label without copying it, and get its width from the fit).
- Labels fit too: Ui::label / hint / labelCentered take a width or fit the current box (Ui::box; a panel sets its own). The UI test still counts only a "..." as clipped.
- Tests (assets): every built-in font stays inside the line box after its fit (Rubik at exactly 1); a long label in a narrow box (Rubik and QUARK CLASSIC): at 97% it only closes up, at 75% it shrinks with the most closing up that size allows, at 20% it is cut, a fitting one is unchanged.
Settings (src/client/game.cpp)
- One shell, six tabs: every settings page (main menu and pause menu share the code) is one opaque 776 x 656 panel 32 px from the top, with tabs PLAYER / GAMEPLAY / CONTROLS / VIDEO + AUDIO / CROSSHAIR / HUD, BACK bottom-left and the tab's reset bottom-right. Controls 360 wide, 32 high, 8 apart, with a hint under each at 1.5.
- PLAYER: identity, outlines, look (model, name colour), team colours (the preset cycle sets both pickers), INTERFACE > FONT. GAMEPLAY (was VISUALS): view, shots, network smoothing, feedback, other players, chat. VIDEO + AUDIO (new): display, FPS limit, latency, flat map | sound, voice. CROSSHAIR: preview across the top, shape | colour and on hit. HUD: one ELEMENT dropdown with WEAPON MODEL first (decision 7).
- CONTROLS: two sub-tabs (GENERAL + INTERFACE KEYS, MOVE + WEAPON KEYS). The X unbind column is gone: right-click a slot (or click it and press Del) unbinds it. The import list is a dialog over the dimmed tab.
- Every R / G / B trio is one colour picker (team, enemy, name, map, crosshair, hit flash, HUD element).
- HUD hidden in settings (decision 5): in game the panel cut the score and weapon bars. Esc leaves settings from any tab (after closing a picker, dropdown or list).
- Testing's own changes kept: EXCLUSIVE AUDIO (N/A) when the device takes none of its formats, and the FPS limit's right-click default from the cached screen mode.
- Screen paths: settings/gameplay (was settings/visuals), settings/video, settings/controls/movement are new; the same under ingame/pause/.
--settingstab PAGE[+PICKER]opens a page for screenshots. - The HUD widget editor: its toolbar 24 px from the edges and only as tall as its rows, help and captions at 1.5; it says COLOR (was COLOUR).
Menus
- Layout: Game::menuLayout (s = H / 720 around W / 2, narrowed so a 960 page keeps 20 px each side on a squarer window, its top under the title). SetLayout gained a y offset for that.
- MAIN: the 360 px column under the title, rows 40 high, QUIT on its own row after a 20 px gap.
- JOIN: the server list in its panel with headers (SERVER / MAP / PLAYERS / PING), 6 rows that scroll with the wheel, BACK | REFRESH LIST in the footer.
- HOST: the panel sized to its rows (it ran off the bottom: audit). Duel's TEAMS is a 40 px cycle (was a 24 px "CLICK FOR 2V2" button). The network options moved to HOST > ADVANCED (decision 9: UDP PORT, AUTO PORT FORWARD, SERVER LOG), path host/advanced.
- VS BOTS: DIFFICULTY: BEGINNER .. EXPERT (decision 10), TEAMMATE BOTS only in a team mode.
- Pickers (maps, modes, 1v1 rulesets, ranked ladders, in the menus and in game): one card-grid template (Game::drawPicker) in a 960 panel: cards with their description wrapped at 1.5, the current one framed. Maps say FOR <MODE>; in a mode they aren't built for, one legend line replaces the orange two-line warning that sat under almost every map (decision 14; it touched the next card's button: audit).
- Custom mode (decision 8): one 960 x 522 panel on every tab (it grew to the screen's width on WEAPONS and MOVEMENT), five tabs with the current one framed, the same footer on each. GENERAL in three columns (RULES, PHYSICS, ABILITIES); WEAPONS a row per weapon, a hitscan weapon's SPEED greyed out (it was a live slider that changed nothing); MOVEMENT split into MOVEMENT (15 sliders) and a new DASH + GRAPPLE tab (19), path custom/dash. Real units: every scaled slider shows the value it makes in the mode (GRAVITY: 22.0 M/S2, FIRE RATE: 0.83/S); the stored value stays the multiplier, so .qmode files and the network format are unchanged. Testing's PLAYS AS fix (stepping over TOURNEY / ARENA in the direction pressed) is kept.
- INFO: one 960 x 482 panel, text 2 (was 1.2) in one 640 px column, at most 80 characters a line (decision 11), scrolls with the wheel (3 lines a notch) with "LINES x-y OF n". Wrapped once per tab, size and font.
- LEADERBOARD: the buttons inside the panel (they straddled its edge: audit), four 220 px columns, top 15 in rows of 20; MY VIADUCT RUNS in the same panel.
- DEMOS: 8 rows that scroll, RECORDING RATE and its hint, BACK | OPEN FOLDER. CONNECTING: a small centred panel. The save-a-copy question after an edit session uses the dialog template.
quark --menupage PATHopens a menu page by its path for screenshots.
In game
- One band: the pause menu's pages sit between the notices and the weapon bar (Game::ingameLayout), in panels at 95% alpha (decision 12: the HUD under them no longer shows through).
- Notice list: the lines under the score bar (spectator banner, hide and seek / tag banner, objective, bomb timer, ranked's notes, a running vote, the host's port / UPnP / reachability lines with CHECK AGAIN) are one column, each under the last. Why: the tourney vote line, the port line and the reach line were drawn over each other (audit).
- Scoreboard: starts under the notices. TOTAL K / D only shows when someone has a total (bots and offline players never do, so the column was empty) and shows "-" for a row without one.
- F3: the box at (24, 8), at most to x 396, so it stays left of the score bar (it ran under it: audit); four lines, each measured once.
- Cooldown bars: rows 16 apart (WEAPON, DASH, HOOK, SPEED), captions at 1.5 centred on the bar (text 1 was below the redesign's smallest). Testing's dash pips (grey until a dash is allowed) are kept.
- Hide and seek: FACE THE WALL AND COUNT above the countdown (it ran into the captions); the round message in a centre slot, hidden while the scoreboard is held; the tiebreak is compact (560 x 258) and sits under the scoreboard while TAB is held (it covered it: audit).
- Pause: the 360 column, rows of 36, a HOST section, the leave / quit row last. VOTE, QUEUE (two columns of 16 queues) and the MOVE TO SPECTATOR list in the same band and style.
Map editor (src/client/editor.cpp)
- "Don't over-design it: it's the owner's tool": the same tools in the same order, re-flowed, no text under 1.5.
- Side panel: 12 px from the edges, sections with headers: FILE (NAME / TITLE, BUILT FOR, NEW | SAVE | EXIT | TEST in one row, decision 13), MAP + OPEN, HOST EDIT SESSION + BAKE LIGHT, MAPS FOLDER + RESET MAPS, ADD, GROUPS (MAKE / IMPORT / EXPORT, ids unchanged), GRID, MODES (chips 20 tall, names at 1.5, widths measured once per size and font), then the selection. Cycles say what they are (GRID: 1/2 M, MATERIAL: ACCENT).
- Selection: starts at the same height for every kind; ROTATE / MIRROR as a caption and three short buttons; FIND INVISIBLE WALLS: n (was FIND CLIP BOXES (INVISIBLE WALLS): n, shrunk to fit; id unchanged).
- Key bar under the 3D view: PLAYABLE AS, the modes, the team-spawn note and the counts (moved from the panel, where a long selection hid them) over the key help, at 1.5. Lines wider than the view are packed onto another line (a 4:3 window takes one more), re-packed only when width, size or font change. Testing's per-map cache (mode support, clip-box count, the PLAYABLE AS text) feeds it, so nothing is recounted per frame.
- Dialogs on one template, centred in the 3D view (EXPORT's question, IMPORT's list, keep-your-edits / save-a-copy). The session chat's SAY line sits above the key bar (it was drawn over the key help); the top notice fits the view.
- Handle drag fix: a handle on a far face, seen nearly along its axis and dragged towards the horizon, flipped the face inwards: past the axis's vanishing point the nearest point of the mouse ray is behind the camera, so the drag's delta changed sign. The drag now holds while that is so and follows again when the mouse comes back (the edit-host UI test's "[group] > RESIZE -Z" failure).
UI test (src/client/uitest.cpp, docs/UI_TEST.md)
- Learns the new widgets: colour pickers are opened, dragged, given a typed hex value, closed with Esc and right-clicked (default); dropdowns are opened; a keybind slot is right-clicked after its bind and must unbind (unbind_checks in the report).
--uitest-path a,b,-c: explore only the screens under the given paths,-excluding one.- It no longer keeps a transient CONNECTING screen from a game the scenario starts itself (clicking its CANCEL raced the connection, failing about one run in two).
- Replaying the way to a screen, and clicking CHECK AGAIN, first wait (up to 6 s) for a running reachability check to end: the vote page's CHECK AGAIN was sometimes untested or "didn't happen" (the known host-tourney flake).
Provenance
- New UI pages registered as AI-made (the owner approved the redesign): ui/custom/dash, ui/host/advanced, ui/settings/controls/movement, ui/settings/gameplay, ui/settings/video; ui/settings/visuals is gone (renamed GAMEPLAY). The title logo is baked into the code (src/client/logo_data.h), not an exported asset.