ADR 0022: A unified floating-panel design system for editor chrome¶
Status¶
Accepted
Context¶
Phase 13 shipped FindBar as a bar docked full-width at the top of the
window, inside a QVBoxLayout wrapper main.cpp had to introduce
specifically for it. Direct user feedback right after: the bar "looks
so basic," and more windows of this kind are coming (Open/Save-As next,
per docs/ROADMAP.md's Phase 14) — so this needed to become a real,
reusable system now, not another one-off widget shape per phase.
Two decisions were put to the user rather than guessed, since both have real tradeoffs and set precedent for everything after:
- Where do these panels sit? True center of the window (a command-palette/modal feel, consistent everywhere) vs. top-center (stays clear of the text being searched). Chose true center, with the explicit tradeoff noted (a centered find panel does sit on top of the text you're searching) — the user's call, consistent feel across every future panel outweighed it.
- Do future Open/Save-As dialogs join this system, or stay native
QFileDialog? Chose custom floating panels — overrides the native-dialog approach the original Phase 14 plan had sketched; that plan needs updating before Phase 14 starts (a custom file browser is real, non-trivial workQFileDialogwould have skipped entirely).
Decision¶
FloatingPanel (gui/src/floating_panel.{h,cpp}) — the reusable base¶
A plain child widget of whatever host it's given (not a top-level
QWindow) — simpler than real separate windows, no window-manager
compositing/platform quirks to manage, and "floats over things" is
just raise() plus being painted after its host in the normal Qt
child-stacking order. Owns exactly four things, nothing about content:
- Centering:
recenter()sizes tosizeHint()(so subclasses get correct sizing for free from their own layout, no manual size bookkeeping) and centers that over the host's fullrect(), clamped to a margin so a very small window never pushes it off-screen. Installs itself as an event filter on the host to catchQEvent::Resizeand re-center live — no signal needed from the host. - Flat paint: fills its own background with a caller-supplied translucent color and draws a 1px border in a caller-supplied low-alpha color. No shadow, no gradient, no rounded corners — "flat" taken literally, matching the user's explicit ask and this project's existing "one font color, vary only weight/opacity" pillar (ADR 0007) applied here to chrome instead of syntax highlighting.
- Fade open/close: a
QGraphicsOpacityEffect+ oneQPropertyAnimation(140ms,OutCubic) — "fast and smooth," per the user's ask, and short enough that it reads as instant-but-clean rather than a wait.setAnimated(bool)— checked and set fresh by the owning panel on every open/close, not cached at construction — gates it:truefades,falsesnaps instantly. Wired to the sameanimationsconfig key the caret-fade/smooth-scroll already use (ADR 0012 decision 2), rather than a second motion toggle — one lever for "does this app move," matching that decision's own reasoning (a single value already does double duty for two unrelated animation mechanisms there; a third reuses it rather than adding a fourth knob). - No config access of its own — every color is pushed in via
setColors(...)by the subclass. KeepsFloatingPanelreusable for a future non-text panel (Open/Save-As) without anycore/AseConfigcoupling baked into the base class.
LetterBadge (gui/src/letter_badge.{h,cpp}) — the label replacement¶
A small flat square with one bold letter, replacing FindBar's old
"Find:"/"Replace:" text labels — "F"/"R" today, and the obvious pattern
for future panels ("O" for Open, "S" for Save-As). Same
no-config-of-its-own shape as FloatingPanel: colors set explicitly by
the owner. Filled with the text color (high alpha, reads as a solid
opaque chip) with the letter drawn in the background color — a
contrast-inverted micro-badge using only colors the theme already
defines, no new hue.
FindBar rebuilt on FloatingPanel¶
Same public behavior as ADR 0021 (matching/highlighting/replace logic
untouched — this phase only touched chrome), restyled:
FindBar : public FloatingPanel, laid out with LetterBadges instead
of QLabels, openFor/hideBar now call openPanel()/closePanel()
instead of raw setVisible. refreshTheme() (now public, called from
EditorViewport::checkConfigReload too, not just on open) pulls panel/
border/badge colors from new EditorViewport accessors
(panelBackgroundColor, panelBorderColor, textColor,
backgroundColor, panelFieldColor, animationsEnabled) — so an
edited config.ase reaches this panel live, exactly like it already
does for the editor itself.
One fix beyond the ask, found during live verification: QLineEdit's
default native style is a bright white field with a blue focus ring —
jarring against this app's dark palette regardless of how the panel
around it looks. Both fields get setFrame(false) and an explicit
QPalette (base = a new derived panelFieldColor — the background,
lightened, so a field reads as a distinct control without a new
config key; text = the theme's full-contrast text color). Not asked
for explicitly, but "flat... same colors... full contrast" doesn't
hold if the input box itself is still native-white.
main.cpp simplified back¶
The Phase 13 QVBoxLayout wrapper is gone — FindBar is a child of
EditorViewport now, not a layout row, so window.setCentralWidget
(viewport) again, exactly as it was before Phase 13.
New config key¶
panel_background = #282828E6 — the existing background hue at
~90% opacity ("a small transparency that looks clean," per the ask).
panelBorderColor()/panelFieldColor() are deliberately not new
config keys — both are derived in code from colors that already exist,
keeping the config surface from growing one key per cosmetic detail.
Consequences¶
Verified live: Ctrl+F and Ctrl+H both produce a true-centered,
flat, bordered panel with legible badges and full-contrast field text
(screenshotted); replace-one, replace-all, and undo all re-verified
working unchanged through the new container (save-to-file round trips,
same as ADR 0021). Config hot-reload path code-reviewed (calls through
identically to the editor's own background/text hot-reload) but not
independently re-screenshotted this phase.
Not built: the animation's actual frame-by-frame motion wasn't captured
mid-flight (150ms is faster than this project's screenshot tooling can
reliably catch a partial frame at, confirmed again after the addendum
below) — verified by code inspection only, standard
QGraphicsOpacityEffect/QPropertyAnimation usage. Phase 14
Addendum: scale-in polish¶
Immediate follow-up, before Phase 14 started: direct feedback wanted
the open/close motion itself more "clean, fast, and aesthetic," not
just present. Pure opacity fade read as flat; added a synchronized
scale from 96% of full size up to 100% (QParallelAnimationGroup
pairing the existing opacity QPropertyAnimation with a new one
animating geometry, both 150ms/OutCubic), so the panel now pops
gently into place rather than just materializing. targetGeometry()
was factored out of recenter() so both the animated open/close path
and the non-animated live-resize-repositioning path (which
deliberately does not replay the pop — a host resize isn't a user
open/close action) compute the same centered rect from one place.
Still gated by the same animations config key and setAnimated
per-open check as before — this only changes how the motion looks
when it's on, not when it runs. (editor chrome: status bar, dirty
tracking, Open/Save-As) needs its own plan update before it starts:
the original Phase 14 plan still describes native QFileDialog for
Open/Save-As, superseded by
this ADR's second decision — a custom file-browsing floating panel is
real, unscoped work that plan never accounted for.