How menus work: macOS vs GNUstep

Both macOS and GNUstep hand menu ownership to the frontmost application: the app itself builds the menu and draws it. But the two systems arrange those menus on screen in very different ways. This note walks through how each one actually puts pixels on the glass, and what the consequences are.

How menus work on macOS

On macOS the frontmost application draws its own menu bar. There is no system process that renders the bar on the app's behalf. When an app becomes active, it creates (or unhides) a borderless window that is exactly the width of the screen and roughly 24 points tall, and it draws the apple logo, the app's menu titles, and the clock area into that window itself.

What makes the bar look like a single shared surface is the window server. WindowServer (internally SkyLight on modern macOS) composites every app's windows onto the screen. Menu bar windows are placed at a reserved window level, kCGMainMenuWindowLevel, which sits above normal windows and below alerts. Only one app's menu bar window is visible at a time; the others are ordered out.

An application switch is therefore mostly a visibility swap. When app A deactivates and app B activates, A hides its menu bar window and B shows its own. Both windows occupy the same screen rectangle at the same window level, so the user sees a continuous strip even though the owner changed.

Pull-downs are separate windows. When you click a menu title, the owning app opens a new borderless window at a higher popup window level and runs a local event-tracking loop until you pick an item or dismiss. The menu bar window itself does not expand; the pull-down is an independent surface that the compositor lays on top.

The right-hand side of the bar is more interesting. The clock, Wi-Fi, volume, and battery icons you see there are not drawn by the frontmost app. They are status items, and they come from several different processes at once. Legacy extras are hosted by SystemUIServer. The modern icons (Control Center, Focus, Sound, Now Playing) come from ControlCenter.app. Any app that creates an NSStatusItem draws its own icon from its own process. Each of those processes creates a small borderless window and places it at the menu bar window level, aligned with the same y-coordinate as the frontmost app's bar.

The result is that the menu bar strip you see is a cooperative illusion. Many processes draw into it, none of them owns it, and the rule that keeps them aligned is simply the shared window level plus the convention that everyone uses the same height.

  Frontmost App                                 Screen
  +-------------+     +------------------+     +---------+
  | NSMenu tree | --> | menu-bar NSWindow| --> |         |
  +-------------+     | level: MainMenu  |     |         |
                      +------------------+     |         |
                               |               |  Window |
                               v               | Server  | --> display
                      +------------------+     |(SkyLight|
                      |  pull-down window|     |  /WS)   |
                      |  level: Popup    |     |         |
                      +------------------+     +---------+
                                                    ^
  Right side of the bar (same MainMenu level):      |
                                                    |
  SystemUIServer  --+                                |
  ControlCenter.app-+--> status-item NSWindows ------+
  Any app's item ---+    (each app draws its own)

How menus work in GNUstep

GNUstep inherits its menu model from NeXTstep, and the default presentation is still the NeXTstep one: a vertical, floating menu panel that the user can drag around the screen and tear off submenus from. The menu is a real window, owned by the app, and it lives wherever the user parked it.

There is no system menu bar in stock GNUstep. Every menu belongs to one app and is drawn by that app. When you switch apps, the previous app's menu panel is hidden (or stays visible but inactive, depending on the style) and the new app's menu panel appears. No other process is involved.

GNUstep offers three interface styles, selected by the NSMenuInterfaceStyle default. In the NeXTstep style the menu is the floating vertical panel described above. In the Windows95 style the horizontal menu is embedded inside each of the application's own document windows, the way GTK or Win32 apps traditionally work. In the Macintosh style the app places a horizontal menu strip at the top of the screen, which looks superficially like macOS but is still just that one app's panel; no system bar exists and no other app shares the strip.

Under the hood all three styles are the same object. Every menu is an NSMenu rendered by an NSMenuView hosted inside an NSMenuPanel, which is a borderless NSWindow subclass the app creates and owns. The style setting changes layout (vertical vs horizontal), placement (floating vs embedded vs top-of-screen), and attach behaviour, but the window is always an app-owned panel.

Keyboard shortcuts do not need the menu to be visible. Key equivalents are dispatched inside the app by NSApplication walking the menu tree when a key event arrives, so Command-S works whether the menu is a floating panel, embedded in a document window, or a strip at the top of the screen.

GNUstep has an NSStatusItem class for API compatibility with Cocoa, but there is no shared presentation surface for it to appear in. Without a system-owned bar region, status items have nowhere canonical to live.

  App
  +-------------+     +---------------+     +----------+     +--------+
  | NSMenu tree | --> | NSMenuView    | --> | NSMenu-  | --> | X11 /  | --> screen
  +-------------+     | (vert or horiz)|    | Panel    |     | Wayland|
                      +---------------+     | (borderl.|     +--------+
                                            |  NSWindow)
                                            +----------+

  Three styles, same panel object:

    NeXTstep                Windows95                Macintosh
    (default)

    +----------+            +==============+         +=======================+
    | App      |            | Document win |         | App menu strip (top)  |
    +----------+            | +----------+ |         +=======================+
    | File     |            | | File Edit||               (this app only;
    | Edit     |            | +----------+|                no system bar)
    | View     |            | | content  ||
    | ...      |            | |          ||
    +----------+            | +----------+|
    (floats, drag)          +=============+
                            (menu inside
                             each window)

Processes and components

It helps to see the actual moving parts. Below are tree views of what is running and which framework or object does what, in each system.

macOS. Several processes cooperate. The frontmost app and every status-item owner each contribute a window to the bar region; WindowServer composites them.

macOS menu system
├── WindowServer            (process: _windowserver)
│   ├── SkyLight.framework           compositor + input routing
│   └── CoreGraphics window levels   kCGMainMenuWindowLevel (~24)
│                                    kCGPopUpMenuWindowLevel
│                                    kCGStatusWindowLevel
│
├── Frontmost application   (process: the app itself)
│   ├── AppKit
│   │   ├── NSApplication            setMainMenu:, activation events
│   │   ├── NSMenu / NSMenuItem      the menu model
│   │   ├── NSCarbonMenuImpl         tracking glue (legacy name)
│   │   └── _NSMenuWindowManagerWindow
│   │       ├── menu-bar window      level: MainMenu
│   │       └── pull-down windows    level: PopUp
│   └── HIToolbox.framework          legacy Menu Manager events
│
├── SystemUIServer          (process: SystemUIServer)
│   ├── SystemUIPlugin.framework     loads .menu bundles
│   └── /System/Library/CoreServices/Menu Extras/*.menu
│       (legacy status extras, drawn into SystemUIServer's own
│        windows at MainMenu level)
│
├── ControlCenter.app       (process: ControlCenter)
│   ├── ControlCenter.framework      modern status cluster host
│   ├── BatteryUIKit, NetworkMenusCommon, etc.
│   └── per-item NSWindows at MainMenu level (Wi-Fi, BT, Sound,
│                                              Focus, Clock, ...)
│
└── Any app with NSStatusItem
    └── NSStatusBarWindow             app draws its own icon
                                      at MainMenu level

GNUstep. One process, one app. Everything menu-related lives inside the application's own address space; the window system just displays the panels the app creates.

GNUstep menu system
└── The application         (one process, per app)
    ├── libs-gui (AppKit)
    │   ├── NSApplication             setMainMenu:, key-equiv dispatch
    │   ├── NSInterfaceStyle          picks NeXTstep / Win95 / Mac
    │   ├── NSMenu                    the menu model
    │   ├── NSMenuView                vertical or horizontal layout
    │   ├── NSMenuPanel               borderless NSWindow subclass
    │   │   ├── main menu panel       one per app
    │   │   └── submenu panels        one per open submenu
    │   ├── GSTheme / GSThemeMenu     chrome, Win95 in-window embed
    │   └── NSStatusBar / NSStatusItem
    │                                 present in the API but
    │                                 has no presentation surface
    │
    └── libs-back (display backend)
        └── X11 or Wayland            just shows the NSWindows
                                      the app asked for; no menu
                                      awareness, no shared level

The shapes of the two trees are the story. macOS is a forest of cooperating processes held together by one compositor convention. GNUstep is a single tree per app, with no process above it arranging things.

Side-by-side

 macOSGNUstep
Who owns the barFrontmost app draws its own bar windowEach app owns its own menu panel; no shared bar
Where it appearsFixed strip at top of main displayDepends on style: floating, in-window, or top strip
Per-app or globalPer-app window, global-looking stripPer-app, no global convention
App switchWindowServer hides old bar, shows new one at same levelOld panel hides, new panel appears wherever the app draws it
Status itemsMany processes share the bar region via a common window levelNSStatusItem exists but has no presentation surface
Pull-down trackingSeparate borderless window at popup level, app runs local event loopSubmenu is another NSMenuPanel, app runs local event loop

The one sentence takeaway

Both systems give menu ownership to the application: the app builds the menu and draws it into a borderless window of its own. The difference is everything around that window. macOS adds a shared window level plus a compositor that cooperates across processes, so every app's bar and every status item from every process lands in the same reserved strip at the top of the screen.

GNUstep has the ownership model but no shared presentation convention. Without a reserved region and without a compositor that enforces one, each app's menu appears wherever that app chooses to put it, and a cross-process feature like status items has nowhere to live.