Magic Apps

Documentation

macmagic.app

Menu bar status item and popover

Both apps put a status item in the menu bar, and both open a native popover from it. The mechanism is shared; what each app shows in the popover is not. Magimail's unread feed is in ../magimail/menu-bar.md, Magical's agenda in ../magical/menu-bar.md.

Status item and the popover run in the helper

The NSStatusItem and its NSPopover both live in the Swift helper process, not in Electron. The main process serialises the state the item and popover need into one tray.update push and the helper renders from it:

flowchart LR
    Main["Main process (poller)"] -->|tray.update| Helper["Swift helper"]
    Helper --> Item["NSStatusItem title"]
    Helper --> Popover["NSPopover → TrayPopoverView"]
    Popover -->|tray.* events| Main

Opening the popover never waits on a request, because everything it shows arrives in the push. A helper that is missing or has crashed leaves the status item absent for the run, which is a packaging failure rather than a runtime one.

Glyph

AppKit inverts a template image to suit the menu bar it lands in, over a light desktop, a dark desktop or a fullscreen menu bar, and keeps only the alpha. Rendering the brand's full-colour artwork with isTemplate = true is pixel-identical to rendering a black copy of it, so neither app keeps a black copy: each has one MarkArtwork, and its surfaces differ only in that flag and in width. Everywhere but the menu bar the flag stays false and the image is pinned .renderingMode(.original), or SwiftUI flattens the brand's colours to a single tint.

Each app's glyph is compiled into the Swift helper binary (MenuBarGlyph.swift), so there is no external resource bundle to load it from. If vector rasterisation fails, the status controllers fall back to a standard SF Symbol such as envelope or calendar.

The box sizes, the glyph grid and the shared line weight are in brand-and-icons.md.

Lining the glyph and the bar up with the text

A status item's title sits beside the glyph, and Magical adds an account-colour bar inline. Both sit on the middle of the title's capitals, which is the line that makes the two menu bar items look level with each other.

An inline bar shares the line with the words rather than the button, so AppKit places it from the baseline up instead of centring it. The attachment drops it below the baseline by half the difference between the bar's height and the font's cap height.

Sizing the status item

The status item is variableLength, so every item to the left of it moves whenever the title's width changes. A title that changes on its own therefore has to be drawn into a field of a constant width, with whatever the reading leaves over appended as a transparent NSTextAttachment of the measured difference. The reserve trails the title rather than leading it: in front it would hold the item's width just as well, but the words would slide rightwards as the text shortened; at the end it is empty space against the neighbouring item and nothing visible moves.

The item keeps variableLength rather than taking a fixed length. A fixed width has to fit the longest title the mode can draw, and macOS hides menu bar extras when it needs the room for app menus, so that width comes out of whatever else the user keeps up there.

Magical's countdown is the title that changes on its own, and how its constant width is measured and why is in ../magical/menu-bar.md.

Popover

The popover is a native NSPopover with .transient behaviour, so clicking outside dismisses it, and it takes the system .popover material, which follows the light and dark appearances on its own.

The width is a constant, set both on the NSPopover and on the popover view's own frame. Left to disagree, the smaller of the two wins and AppKit clips what the other asked for.

Nothing sets a height. The hosting controller is given sizingOptions = [.preferredContentSize], so SwiftUI reports the height of what it has laid out and the popover takes it:

let host = NSHostingController(rootView: view)
host.sizingOptions = [.preferredContentSize]
pop.contentViewController = host

A popover whose width would vary with its content measures its widest label to size the window, which is why both apps filter by a control that costs a fixed width rather than by a segmented control that grows with the names.