Skip to content

fix(desktop): dev tooling, tray and menu icons, LogBox, rounded clipping and a glyph atlas leak - #17

Merged
ammarahm-ed merged 35 commits into
mainfrom
fix/desktop-dev-issues
Oct 25, 2026
Merged

ammarahm-ed merged 35 commits into
mainfrom
fix/desktop-dev-issues

Conversation

@ammarahm-ed

Copy link
Copy Markdown
Contributor

Fixes a batch of problems found running the tester app, plus rounded overflow: hidden clipping, which needed a gpui-fast change.

Changes

  • Metro: run-desktop starts Metro in a terminal window of its own, so its keys work (--terminal or $REACT_TERMINAL picks it). It reuses a running Metro only if it serves this project, and defaults to port 8081, as the help text and README already said. --no-packager and --packager-log cover the cases that must not open a window; the e2e build uses --packager-log.
  • Developer menu: the menu-bar items now run on the host, like the Dev Menu window's buttons. They used to send a menuPress event that nothing handled.
  • Dock: the app reopens its window from the Dock after the last one closed. A tray turns quit-on-last-window off, and destroying the tray now restores it. A debug run also gets its Dock icon from desktop.json.
  • Scroll indicator: drawn flush with the container's edge.
  • LogBox: <Text> now paints inside its padding and border. The generic monospace font resolves on every platform. Desktop versions of the toast, inspector header and code frame fix the square corners, the white hairline, the collapsed header and the proportional code font.
  • Memory: text inside a scale animation leaked about 3 MB/s, because GPUI keeps a glyph for every font size it draws. Scaled sizes are snapped to quarter points.
  • Icons: Tray.install({ icon, iconTemplate }), Tray.setIcon and Tray.setTitle accept any image source. Menu items take an icon, shown in context menus, tray menus, menus GPUI draws and the macOS menu bar. desktop.json's tray can name one too. The Windows menu bar is not covered yet.
  • Rounded clipping: overflow: hidden with a border radius now clips children to the rounded corners, as on iOS and Android. This bumps gpui-fast to 8da7d89 (streetwriters/gpui-fast rn-gpui-linux), which adds a rounded rectangle to the content mask on Metal, Direct3D and wgpu. Details are in GPUI_PIN.md.

Verification

  • macOS:
    • Full e2e suite passes: 102 tests, 3 skipped.
    • The rounded-clip test was confirmed to fail without the fix.
    • Checked by hand: the Metro terminal window, the Dock icon, the tray icon in the menu bar, and the LogBox screenshots.
    • Memory on the Transforms screen holds flat (296–297 MB over 90 s, where it used to climb 287 → 552 MB).
  • gpui-fast:
    • All gpui unit tests and the Metal pixel test pass on macOS.
    • The wgpu tests pass on macOS, including naga validation of every WGSL variant. The new wgpu pixel test skips there for lack of an adapter.
    • The Windows crate and its tests cross-compile, and the HLSL entry points compile through glslang.
  • Windows and Linux still need this PR's CI run, in particular the new overflow: hidden clips children to the rounded corners e2e test. The gpui-fast pixel tests for Direct3D and wgpu have not run, because the fork has no CI.

…'s Metro

run-desktop started Metro as a hidden background process, so its keys
(r, d, j) were unreachable, and it reused whatever Metro answered on the
port, including another project's. It also defaulted to port 8082 while
the runtime, the help text and the template README all said 8081.

Metro now opens in a new terminal window (--terminal or REACT_TERMINAL
picks it), a running Metro is reused only when it serves this project,
and --no-packager / --packager-log cover the cases that must not open a
window. The tester's start script uses the port its run script uses.
The Developer submenu built its items as MenuAccelerator actions, and that
action only emitted a menuPress event to JS, which nothing handles for the
dev:* ids, so Reload, Open DevTools and the toggles did nothing from the
menu bar while the same items worked from the Dev Menu window.

Menu actions now route through trigger_item, which sends the dev:* ids to
dev_menu::perform on the host.
An installed tray turns quit-on-last-window-closed off, so closing the main
window left a running process with no window, and nothing handled the
Dock's reopen, so clicking the icon did nothing. Destroying the tray also
never turned the app's own policy back on.

Reopening now brings back a hidden window or opens the main component in a
new one, and the tray restores the policy it replaced when it goes. The
test driver gains appReopen so the path is covered end to end.
cargo run starts a bare executable with no bundle and no Info.plist, so
macOS showed a blank Dock icon. run-desktop now passes the icon desktop.json
names (or the CLI placeholder) as RN_GPUI_APP_ICON, and an unbundled run
sets it as the application icon at startup. A .app keeps its bundle icon.
The thumb was inset 2pt from the trailing edge, so it floated inside the
scroll view instead of sitting at its edge. The inset now applies only at
the two ends of the track.
- Text now paints inside its padding and border: Fabric measured it in that
  content box, but it was drawn from the frame's origin at the frame's
  width, so a padded Text sat flush against its left edge.
- The generic monospace family resolves to the platform's monospace font,
  as it does on Android.
- Desktop versions of three LogBox files: the toast draws its rounded
  corners on the views that paint (overflow: hidden clips to a rectangle
  here) and drops the white backing that showed as a hairline above it;
  the inspector header gets its 44pt height and the code frame its
  monospace font, both of which were iOS/Android-only Platform.select
  branches.

The tester gains a LogBox screen that raises a warning and an error and
shows a padded, bordered Text.
A transform scales text by shaping it at a scaled font size, and GPUI keeps
the rasterized glyphs of every size it has drawn. An animated scale produced
a new fractional size every frame, so the Transforms screen grew by about
3 MB a second while it animated and never gave the memory back. Scaled sizes
are snapped to quarter points, which keeps the set of sizes bounded; the
same screen now stays flat.
The tray could only show a built-in square (Tray.setIcon took a bare file
path, and on macOS it dropped template mode), and every native menu
rejected an icon field outright.

- Tray.install takes icon and iconTemplate, Tray.setIcon takes any image
  source, and Tray.setTitle sets the text beside it. desktop.json's tray
  accepts icon and iconTemplate too, embedded into the binary.
- MenuItemSpec items and submenus take an icon. It shows in context
  menus, tray menus, menus GPUI draws, and the macOS menu bar (set on the
  NSMenu GPUI installs, since GPUI's items carry no image). A checked item
  keeps its check mark instead.
- Icons are anything <Image source> takes: require() assets (Metro URLs in
  development, bundled files in release), file paths, http(s) and data
  URIs. Each entry point loads them before the menu is built, because the
  native builders are synchronous; a ContextMenuView prefetches its own.

The Windows menu bar still shows items without icons: it is rebuilt from
GPUI's menus, which do not carry them.
cli.js is not in react-native's exports, so resolving it threw and Metro
quietly fell back to the background instead of opening its window.
ContentMask gains a rounded rectangle, so the one struct literal in
rn-gpui becomes ContentMask::new.
A view with overflow: hidden and a border radius clipped its children
square, where iOS and Android clip them to the rounded corners. Views and
scroll views that clip now ask GPUI for the rounded clip.

The border fixture gains a rounded box and a rounded scroll view, and an
e2e test checks their corner pixels, before and after scrolling, so the
composited scroll layer is covered as well.
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

✅ CI passed — c892f06 — run

37 step(s) across 7 job(s), 152m42s total.

job unit rust build e2e clean-install package
clean-install-macos-15 ⏭ ⏭ ⏭ ⏭ ✅ 1368s ⏭
clean-install-ubuntu-24.04 ⏭ ⏭ ⏭ ⏭ ✅ 1412s ⏭
clean-install-windows-2022 ⏭ ⏭ ⏭ ⏭ ✅ 1783s ⏭
lint ✅ 7s ⏭ ⏭ ⏭ ⏭ ⏭
native-macos-15 ⏭ ✅ 495s ✅ 87s ✅ 130s ⏭ ⏭
native-ubuntu-24.04 ⏭ ✅ 290s ✅ 45s ✅ 89s ⏭ ⏭
native-windows-2022 ⏭ ✅ 2351s ✅ 564s ✅ 241s ⏭ ⏭

C++ runtime

  • native-macos-15: built from source: C++ inputs changed (local 1ace36b4 vs pinned 8d816e88)
  • native-ubuntu-24.04: built from source: C++ inputs changed (local 1ace36b4 vs pinned 8d816e88)

This PR changes the prebuilt C++ inputs. A new prebuilt release is needed after merge (dispatch prebuilt-binaries.yml).

Core boundary

  • core names no library
  • // core-private: uses: 3 (packages/core/ReactGpui/rn-gpui/src/builtins/textinput/mod.rs:1, packages/core/ReactGpui/rn-gpui/src/builtins/textinput/mod.rs:2, packages/core/ReactGpui/rn-gpui/src/builtins/textinput/render.rs:1)

… library

The boundary check reads SafeArea in a local name as a library reference;
the component is React Native's own SafeAreaView.
The test mapped Yoga frames onto the whole window, which on Linux has the
menu bar drawn above the surface, so every sample landed a bar too high.
An element capture is cropped where the surface really is. A failure now
names the pixel it read.
…ct the macOS e2e failures"

This reverts commit fe7b18e.
…desktop

Metro always inlines Platform.OS as the platform it builds for, so the bundle saw desktop on every OS. A transform worker that turns the inlining off lets Platform.OS be the macos, windows or linux the shell reports.
The drag handler was registered before the touch and context menu handlers, which GPUI runs in reverse order, so they consumed the press first. Registering it last lets it run first.
The macOS window now has a transparent title bar, the traffic lights get their inset, and the menu button shows only where there is no native menu bar. contextMenu's comment no longer claims Platform.OS is desktop.
…nt batch

The tracked state is the one the view had when its frame last changed. A state update does not move the frame, so it stayed at the old insets and every mount batch sent the same update again, a commit per batch for ever. It happened only with a nonzero top inset, which a transparent title bar gives on macOS, and left the JS thread with no time to run. The latest state of the family is what is compared now.
The header starts under the top inset, so a click at y = 28 missed the button on a transparent title bar. y = 40 is inside it with and without an inset.
@ammarahm-ed
ammarahm-ed merged commit b32d52f into main Oct 25, 2026
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant