[RFC] Frameless window retrofit: one 52 px unified title bar with radio tabs, on all three platforms

Open
#4,764 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
38/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Quiet
Tech stack
cpp
Domain
desktop

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

enhancement GUI macOS maintainer-review rfc Windows

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:

  1. 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 against main. main sets
    Qt::FramelessWindowHint on 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.

  2. 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.

  1. macOS has no traffic lights at all. main is 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.

  2. 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 answer WM_NCHITTEST with HTMAXBUTTON, which the
    custom frame does not do. Nothing in #2032 addresses it. Win11 corners are
    also square, because nothing asks DWM for DWMWCP_ROUND.

  3. 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 in docs/,
    AGENTS.md or CONTRIBUTING.md, so it is not repo-canon and the appeal has
    been dropped. The rendering argument stands on its own.)

  4. 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
    the MainWindow.cpp monolith 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)
Image

macOS: (draft, design rendering)
Image

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 testingget titlebar exposes geometry, tabs, audio state,
    chrome variant, and a live linkPulse level, 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.

LinuxQt::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.

macOSQt::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.

WindowsWM_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:

  1. The 52 px bar is approved including its space cost — now understood as
    ~20 px on all three platforms (see Correction).
  2. Windows and Linux verification is a PR gate, not an RFC gate. The PR will
    ship with a /winbuild artifact. #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.
  3. 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-chrome14 files, ~1100 insertions.

New

  • src/gui/BrandMark.{h,cpp} — logo + gradient wordmark
  • src/gui/RadioTabBar.{h,cpp} — tabs, status dots, link indicator, popover
  • src/gui/WindowCaptionButtons.{h,cpp} — all three control styles
  • src/gui/mac/WindowChrome.{h,mm} — corners / shadow / resizable bit (objc++)
  • tests/unified_title_bar_test.cpp — headless
  • resources/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 tab
  • src/gui/MainWindow.{h,cpp} — Snap Layouts + DWM corners; 8 px resize band;
    macOS frameless trim
  • src/gui/MainWindow_Session.cpp — feeds tabs from discovery + SmartLink
  • src/core/AutomationServer.{h,cpp}titlebar model + verb
  • resources/themes/default-{dark,light}.json + regenerated
    ThemeSeedGenerated.cpp
  • docs/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

  1. macOS loses the green-button tiling menu and Stage Manager affordances.
    Accepted per review; alternative 2 is the way back.
  2. 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.
  3. Fractional scaling on Linux is untested for drag jitter.
  4. 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)
  • linkPulse decays 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from aethersdr/AetherSDR

All issues in aethersdr/AetherSDR

Similar issues

More C++ issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.