UI Design Standards (desktop / Qt)
This is a rulebook of taste. When you build or change a desktop UI, satisfy
these rules by default — don't wait to be asked. When you review a screen, score
it against the checklist at the bottom and cite rule IDs (e.g. L1, A2).
Every rule below exists because a real screen failed it. The fixes are concrete
on purpose: prefer the specific number over "make it nicer."
These are defaults, not dogma. This file is meant to be edited as the
author's preferences sharpen. Treat it as the single source of truth for "what
good looks like" in this codebase.
L — Layout & width
- L1 — Inputs do not stretch to fill the panel. Give line edits, spin boxes,
and combos a sensible maximum width (≈ 220–320 px) or put them in a form
whose field column is capped. A 600 px-wide text box for "2.0" looks broken.
- L2 — Fixed label column. In a
QFormLayout, keep labels right-aligned in a
consistent column so values line up. Don't let one long label shove the column.
- L3 — Cap the form's overall width. A settings form should sit in a column
~360–480 px wide, left-aligned, not sprawl across the whole pane.
- L4 — No horizontal scrollbar on a settings panel, ever. If content scrolls
sideways, the layout is wrong (usually an un-capped input — see L1). Fix the
width, don't add a scrollbar.
- L5 — Cap content width on big screens. When the window is maximized,
content must not stretch edge to edge. Hold forms to a ~360–480 px column and
data tables/lists to a sensible max (~900–1200 px), left-aligned or centered;
let the extra width fall to margins or a deliberate pane, never to a
two-row table that sprawls across a 1900 px window.
- L6 — Tables size to content, never to a stretch. A trivial column ("1",
"2") must never be wide; don't make
stretchLastSection/a single Stretch
your sizing strategy. Any table work → use the ui-tables skill — it owns
the content-aware sizing details (caps + a flexible column) so this rulebook
stays lean.
R — Responsive / splitter behavior
- R1 — Stay usable at the minimum width. Dock/settings panels must survive
being dragged narrow (test at ~280–320 px). Set a sane
minimumWidth;
below it, content wraps or elides — it never clips.
- R2 — Primary actions are always visible. Back/Next/Apply must never slide
off-screen when the panel narrows. Anchor them in a footer bar (see A1), not in
a wide row that gets cut off.
- R3 — Wrap or elide, don't overflow. Long captions get
setWordWrap(True);
long values get elided. Width pressure resolves inside the panel.
S — Space & hierarchy
- S1 — No unexplained dead space. Big empty gaps mean a stray
addStretch()
or wrong size policy. Pin content to the top with one trailing
addStretch(1) and a footer pinned to the bottom — not stretches scattered
between fields. (This is the #1 cause of the "floating in a void" look.)
- S2 — Design the empty state. When the list/data is empty, show a short
helper line ("No files yet — add one on the Quick Setup tab"), not a blank
void with a disabled button stranded at the bottom.
- S3 — Consistent spacing scale. Use multiples of 4 px (4/8/12/16).
Section gap 12–16, control gap 6–8, content margins 12–16. No magic numbers.
- S4 — Group with intent. Related fields sit together under a heading or in a
card/
QGroupBox; unrelated ones get a separator or their own group.
A — Action & affordance
- A1 — Footer button bar. Put Back/Next (or Cancel/OK) together in a single
bottom row: secondary on the left, primary on the right, both always
visible. Don't separate Back and Next to opposite ends of a tall panel.
- A2 — The primary action is unmistakable. The step-advancing button (Next /
Run / Apply) is filled and colored (use the theme's primary/
success
green), not a flat grey twin of Back. One primary per screen.
- A3 — Disabled means explained. If a button is disabled, the reason is
visible nearby ("select at least one channel"), not a mystery.
I — Interaction & feedback
These are behaviors, not pixels — a single screenshot won't reveal them, so
reason about them explicitly and exercise them (see ui-visual-review).
- I1 — An action works for every valid selection. If "Assign to checked
files" works with all files checked, it must also work with one or some.
Enable it exactly when it's valid (≥ 1 selected) — never gate it on "all".
- I2 — No silent no-ops. Every action produces visible feedback: the affected
rows/status update, a log line, a toast, or a clear state change. "I clicked
Assign and nothing happened" is a bug even if the work technically succeeded.
- I3 — Disabled controls say why (see A3). A greyed "Assign geometry to
checked files" must show the reason ("check at least one file"), not leave the
user guessing whether it's broken.
- I4 — One consistent selection model. Checkboxes, row-highlight, and "Select
all" must agree; the action applies to that set, and the set is obvious.
- I5 — Enumerate states, then verify them. Before calling a screen done, walk
the states — empty / one / some / all / max selection, and the before→after of
each action — and check each. Don't assume the happy path covers them. This is
what "be intelligent about it" means: anticipate the edge cases.
C — Controls & information architecture
- C1 — Small exclusive choice → radio buttons, not a dropdown. For 2–4
mutually exclusive options the user must compare (e.g. "derive by"), show
radio buttons. Reserve combos for long lists where space is tight.
- C2 — Mode/method gets its own step. When a wizard branches on a choice
(method, mode, target), make that selection its own clear step, then show only
the fields that the chosen branch needs. Don't render all branches at once.
- C3 — Tame density with collapsibles. Dense "advanced" settings go into
collapsible groups, collapsed by default, with the common case visible. But a
collapsed group must not leave the panel looking empty (see S1/S2).
- C4 — No cryptic enum labels. Replace raw identifiers (
exterior_only,
both_sides_priority) with plain language ("Outer shots only") and a one-line
caption. If you can't explain an option in a sentence, reconsider having it.
- C5 — No orphan checkboxes. A lone checkbox with a jargon label and no
context is noise. Fold it into the relevant group, label it as an outcome
("Require shots on both sides"), or cut it.
- C6 — Progressive disclosure over a "strategy" grab-bag. Prefer asking what
the user wants (the goal) and deriving settings, over exposing a pile of
interacting expert knobs. If a section feels like a mess, redesign around the
decision the user is actually making — touch the core/model if needed.
ST — State separation
- ST1 — Quick and Advanced are isolated. A "quick setup" and an "advanced"
view must not share or mirror each other's widgets/state. Picking one path
does not silently mutate the other. Make the active path the single source of
truth.
T — Theme & consistency
- T1 — Use theme tokens, not ad-hoc styles. Pull colors/spacing from the
project's theme/QSS (e.g.
role="muted", primary="true"), never hardcode a
one-off hex or inline stylesheet that drifts from the rest of the app.
- T2 — Consistent control sizing. Buttons share a min-height; inputs share a
height; rows align. Mismatched control heights read as unfinished.
Review checklist (score every screen)
Render the screen (see ui-visual-review), then check each. Cite the IDs that fail.
A screen "passes" only when every rule is satisfied or has a noted, deliberate
exception.