[RFC] Frameless window retrofit: one 52 px unified title bar with radio tabs, on all three platforms
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 38/100
Research direction
Start with MainWindow.cpp:1084 and the prototype branch described in the implementation scope to understand the current frameless-window setup. Review the proposed titlebar model and titlebar bridge verb, then use the PR gate verification to check the unified 52 px bar, platform-specific controls, radio tabs, audio cluster, theming, accessibility, and headless assertions on all three platforms.
Written by the indexing model from the issue text.
Description
Status: approved by @ten9876 (comment).
Revised in place per that review. Changes in this revision:
the space accounting is corrected (see Correction below — it was wrong in a
way the review did not catch, and it is worse than the review assumed), the
Windows/Linux cost is stated, verification is moved to the PR gate, the cited
issues are restated against their actual state, and the appeal to a
non-existent emoji rule is dropped.
Correction to the original filing
Two claims in the first version of this RFC were wrong, and both flattered the
proposal. Correcting them here rather than quietly editing them away:
-
There is no wasted 32 px band on
main. The original text claimed a
reserved-but-empty strip above the title bar, "measurable at 32 px in the
widget tree." That measurement was real but it was taken against my own
abandoned macOS experiment, not againstmain.mainsets
Qt::FramelessWindowHinton everything except Windows
(MainWindow.cpp:1084,#ifndef Q_OS_WIN), so macOS is already frameless
today and there is no native bar and no dead band. The 32 px I measured was
Qt reserving the native title-bar height inside a full-size content view —
an artifact I introduced in alternative 1 and then removed. -
macOS does not gain screen space either. The original claimed "~28 px of
spectrum" back on macOS. Since macOS is already frameless with the same 32 px
strip as the other two, it takes the same 20 px loss. The review accepted the
cost for Windows and Linux on the understanding that macOS gained; it does
not. The correct accounting is a ~20 px loss of vertical space on all three
platforms.
This does not change what is being proposed, but it does change the argument for
it: the case is consistency, native-feeling controls, and the multi-radio
surface — not reclaimed pixels. @ten9876, flagging in case the accounting
changes your read.
Preflight
- I have read GOVERNANCE.md and confirmed this change requires an RFC
- I have searched existing issues and this RFC has not been proposed before
- I have not opened a PR for this change yet
A prototype branch exists and is described under Implementation scope; the PR
will be opened against main now that this is approved.
Problem
The window chrome is inconsistent across platforms, and the strip it draws
carries the wrong things.
-
macOS has no traffic lights at all.
mainis frameless on macOS, so the
window has none of the platform's window controls — just a Discord-style
min/max/close trio at the far right, square corners, and no system edge
resize. This is the substance of #2149 ("UI on Mac not conform Mac"), which
was closed as completed against a narrower complaint; the chrome itself still
does not read as a Mac window. -
Windows has no Snap Layouts. #2032 fixed Aero Snap (drag-to-edge), and
that fix stands. Snap Layouts is a different Windows 11 feature — the
layout flyout that appears when you hover the maximize button — and it
requires the window to answerWM_NCHITTESTwithHTMAXBUTTON, which the
custom frame does not do. Nothing in #2032 addresses it. Win11 corners are
also square, because nothing asks DWM forDWMWCP_ROUND. -
The strip carries the wrong things. It holds an app-name label with a
version number, an unlabelled heartbeat lamp whose meaning has to be learned
from a tooltip, and two 🔊 / 🎧 emoji used as controls. The emoji are the
clearest defect: they render in the platform's own colour and weight, so they
cannot follow the theme and do not match the bar around them. (An earlier
draft of this RFC appealed to a design-system rule prohibiting emoji. That
rule exists in the AetherSDR design-system skill but not indocs/,
AGENTS.mdorCONTRIBUTING.md, so it is not repo-canon and the appeal has
been dropped. The rendering argument stands on its own.) -
No multi-radio surface. #3445 (open) is the live design discussion for
one instance driving two radios, and there is nowhere in the shell to see
the radios. Switching means opening a modal picker; there is no persistent
answer to "which radio am I on, and what else is out there." (The original
filing also cited #3351 as a multi-radio precedent — that was wrong: #3351 is
theMainWindow.cppmonolith refactor, closed as completed, and is unrelated.)
Proposal
Replace the strip with one 52 px unified title bar that is the window's only
title bar on every platform, keeping the window frameless on all three so
exactly one thing draws chrome.
Windows/Linux: (draft, design rendering)
macOS: (draft, design rendering)
Primary features
| # | Feature | Notes |
|---|---|---|
| 1 | Single 52 px bar | Owns window controls, brand, radio tabs, and the audio cluster. Asserted at offsetInWindow == 0 so nothing can silently reserve a strip above it. |
| 2 | Brand mark | 22 px circular logo + "AetherSDR" wordmark; "SDR" filled with the brand gradient. Replaces the AetherSDR v26.x text label. |
| 3 | Radio tabs | One tab per known radio: name over a status line, plus a 7 px state dot. Active tab takes the accent border + fill. The persistent multi-radio surface #3445 needs. |
| 4 | Discovered-radios popover | The + button opens a panel listing every radio on the network with its transport (SmartLink / LAN IP) in mono, and a Connect manually… footer. |
| 5 | Heartbeat folded into the active tab's dot | The standalone lamp is retired. One dot answers both "which radio is this" and "is its link alive." Beat-driven glow, amber while discovering, red on loss. |
| 6 | Audio cluster, de-emojified | PC Audio pill + thin-line SVG speaker/headphone marks + 64 px sliders + JetBrains Mono values. |
| 7 | Per-platform window controls | macOS traffic lights (12 px on a 20 px pitch, glyphs on group hover, grey when inactive); Windows 46 px caption buttons with Snap Layouts; Linux bordered chips. |
| 8 | Themed end to end | 15 new tokens under color.titlebar.* / color.brand.* in both bundled themes; the light theme re-scope still works. |
| 9 | Bridge-drivable | New get titlebar model and titlebar verb so every control is assertable and drivable headlessly. |
Use cases
- "Which radio am I on?" — answered without opening anything: the active tab
names it and its dot shows the link state. - "Is my radio still there?" — the active tab's dot pulses once per discovery
packet, holds amber while searching, and goes red after three missed sweeps.
Same state machine and same 3-miss threshold as today's lamp, including the
safety-critical rule that the alarm holds solid red when the operator has
blinking disabled. - "What else is on the network?" — the
+popover, without leaving the shell. - Multi-radio operating (#3445) — the tab strip is the surface a second
session would attach to; today it is a selector, tomorrow a switcher. - A Mac window that looks like one — traffic lights in their conventional
position and pitch, continuous rounded corners, native edge resize. - Snap Layouts on Windows 11 — hover the maximize button, get the layout
flyout, like every other Win11 app. - Low-vision / colour-blind operators — no state is encoded by dot colour
alone; every tab spells its state in words on the rendered status line and
in its accessible name, and every control is keyboard-reachable and named. - Agent-driven testing —
get titlebarexposes geometry, tabs, audio state,
chrome variant, and a livelinkPulselevel, so the heartbeat is assertable
instead of eyeballed.
What this costs
~20 px of vertical space on all three platforms. Every platform today is
frameless with a 32 px strip; the new bar is 52 px, and there is no native title
bar anywhere to reclaim in exchange. On a 1080p panel that is ~2 % of window
height, spent on the radio tabs and a control cluster that is legible at the
sizes this audience needs. Accepted deliberately, per @ten9876's review — and
per the correction above, accepted on all three platforms, not two.
The FramelessWindow setting stays as the escape hatch everywhere.
Cross-platform impact
All three platforms get the identical bar and identical behaviour; only the
window-control shape and their side of the bar differ.
Linux — Qt::FramelessWindowHint (unchanged from today). Controls are 13 px
bordered chips at the left. Drag via QWindow::startSystemMove(), resize via
startSystemResize() from an 8 px edge band — never hand-rolled geometry, so
Wayland tiling and X11 snapping keep working. Costs 20 px.
macOS — Qt::FramelessWindowHint (unchanged from today) with our own traffic
lights. Rounded corners, the system shadow, and native edge resizing are
restored by putting NSWindowStyleMaskResizable back into the borderless style
mask, so AppKit still owns the resize. Costs 20 px; gains the platform's window
controls, which main does not have at all.
Windows — WM_NCCALCSIZE custom frame (unchanged), plus two additions:
WM_NCHITTEST answers HTMAXBUTTON over the maximize control so Snap Layouts
appears on hover, and DwmSetWindowAttribute with DWMWCP_ROUND gives Win11 its
8 px corners (Win10 falls back to square). The caption cluster is 46 px buttons at
the right, flush to the window edge. Costs 20 px.
Alternatives considered
1. Keep the native NSWindow title bar on macOS and paint the bar into it
(full-size content view + transparent titlebar + repositioned traffic lights —
the "design 2a" approach). Built, measured, abandoned. AppKit's
NSTitlebarContainerView sits above Qt's content view and owns both the pixels
and the mouse events in the top ~28 px of the window. Measured on macOS 15: a
hover 16 px from the window top produced no Qt enter event at all — the top half
of every radio tab and the + button were dead. Qt also placed its client area
32 px down (this is the 32 px from the correction above), so the brand and tabs
rendered a row below the traffic lights. Reconciling the two needed three
separate workarounds: resizing AppKit's private container, carving the strip out
of the window's backdrop paint, and re-asserting the style mask behind Qt's back.
Too fragile for one platform.
2. Fully native macOS via NSToolbar / NSTitlebarAccessoryViewController.
The cleanest macOS answer and it keeps every native affordance — but the bar's
contents (brand, tabs, sliders) would have to be re-implemented as AppKit views,
so macOS would stop sharing the bar with the other two platforms. Rejected on
maintenance grounds, not technical ones. Per @ten9876's review this stays on
the record as the way back if losing the green button's tiling menu and Stage
Manager affordances proves worse in practice than it looks on paper.
3. Leave macOS with a native title bar above a shorter in-app strip. Would
require un-framelessing macOS to gain a bar we would then have to work around —
alternative 1's problem, arrived at deliberately.
4. Glass/translucent bar on macOS (NSVisualEffectView behind a 0.62-alpha
fill). Implemented and backed out: with WA_TranslucentBackground the partial
backdrop fill left bands of raw desktop bleeding through the bar. The bar is
opaque.
5. Keep the 32 px height and fit the tabs into it. Two-line tabs (name over
status) do not fit legibly in 32 px for an audience the project explicitly
designs for at 4.5:1 contrast and larger type. Single-line tabs would fit but
lose the status line, and status would then be carried by dot colour alone —
which the accessibility contract forbids.
Accepted decisions (from review)
Recorded so the trade-offs read as choices:
- The 52 px bar is approved including its space cost — now understood as
~20 px on all three platforms (see Correction). - Windows and Linux verification is a PR gate, not an RFC gate. The PR will
ship with a/winbuildartifact. #4557 (fix(windows): preserve frameless
window top-edge position, merged in v26.8.1) sits in exactly the Windows
frame-handling path this touches, so that gets a deliberate regression
check, not a smoke test. Linux needs someone on a fractional-scaling
Wayland desktop for the drag-jitter question. - The macOS trade is accepted — the green button's tiling menu and Stage
Manager affordances, in exchange for one consistent bar. Alternative 2 is the
documented way back.
Implementation scope
Prototype branch: feat/frameless-window-chrome — 14 files, ~1100 insertions.
New
src/gui/BrandMark.{h,cpp}— logo + gradient wordmarksrc/gui/RadioTabBar.{h,cpp}— tabs, status dots, link indicator, popoversrc/gui/WindowCaptionButtons.{h,cpp}— all three control stylessrc/gui/mac/WindowChrome.{h,mm}— corners / shadow / resizable bit (objc++)tests/unified_title_bar_test.cpp— headlessresources/images/logo-96.png
Modified
src/gui/TitleBar.{h,cpp}— 32 px → 52 px, restructured; heartbeat state
machine retained, its output moved to the active tabsrc/gui/MainWindow.{h,cpp}— Snap Layouts + DWM corners; 8 px resize band;
macOS frameless trimsrc/gui/MainWindow_Session.cpp— feeds tabs from discovery + SmartLinksrc/core/AutomationServer.{h,cpp}—titlebarmodel + verbresources/themes/default-{dark,light}.json+ regenerated
ThemeSeedGenerated.cppdocs/automation-bridge.md,docs/theming/canonical-tokens.md
New dependencies: none. Links AppKit + QuartzCore on macOS and dwmapi
on Windows — all system frameworks.
Known risks
- macOS loses the green-button tiling menu and Stage Manager affordances.
Accepted per review; alternative 2 is the way back. - Windows and Linux are unverified by me — no machine for either. Moves to
the PR gate per review, with #4557 as a named regression target. - Fractional scaling on Linux is untested for drag jitter.
- Right-clicking a radio tab now hosts the "Blink status indicator" toggle that
used to live on the heartbeat lamp — a moved affordance existing users must
rediscover. The View-menu checkbox is unchanged.
Verified so far (macOS, live, via the automation bridge)
- 242/242 tests pass headless, including the new one
offsetInWindow == 0- Tab hover/activation reaches Qt at 10 px from the window top (alternative 1's
dead zone) linkPulsedecays and re-swells on each discovery packet — the heartbeat is
driving the dot- Theme-seed gate and bridge-docs gate both green
- Dominant language
- C++
- Stars
- 221
- Forks
- 117
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 299
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from aethersdr/AetherSDR
-
audio documentation good first issue maintainer-review
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
bug CW maintainer-review safety
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
docs(agents): family PRs must stay in the family tree (shared applet/pan/model edits stall review) Opendocumentation enhancement maintainer-review
Difficulty 1/5 1-3 hours Newbie friendliness 90/100
-
bug good first issue GUI maintainer-review priority: low
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
bug
Difficulty 1/5 Under an hour Newbie friendliness 86/100
All issues in aethersdr/AetherSDR
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
Sensor initialization takes very long when `--initial-sim-time` is set to current UNIX timestamp Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
gazebosim/gz-sensors#662 · 1 comment ·
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
comp-datalake
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
ClickHouse/ClickHouse#121222 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
LadybirdBrowser/ladybird#12123 ·