Skip to content

feat(desktop): draw the Windows tray menu in the app's font and look - #649

Open
linroid wants to merge 1 commit into
mainfrom
feat/windows-tray-menu
Open

linroid wants to merge 1 commit into
mainfrom
feat/windows-tray-menu

Conversation

@linroid

@linroid linroid commented Oct 24, 2026

Copy link
Copy Markdown
Owner

Refs #600: the tray menu half. The zoom half landed in #643.

Problem

On Windows, Java draws tray menus itself and only maps Java's logical fonts (WFontConfiguration falls back to sansserif for any other name). So on Chinese Windows the menu renders in Arial plus SimSun, unlike the app (Inter, with the system's CJK fallback), and nothing set through AWT can change that.

What changes

  • Windows: a right click on the tray icon opens a Ketch window (TrayMenu.kt) instead of Java's PopupMenu. It shows the same MenuEntry model (trayMenu(...)) as a KetchContextMenu at the pointer, in the app's theme, accent, density and zoom, with command glyphs and the status lines as a text block.
    • The window is undecorated, transparent, always on top and UTILITY (no taskbar or Alt+Tab entry). It covers the work area of the pointer's screen: GraphicsConfiguration.bounds minus Toolkit.getScreenInsets, so never the taskbar.
    • It covers the whole work area because Compose draws popups on the window's canvas. compose.layers.type is read once per process, so a window sized to the root menu would clip its submenus (Speed, Devices → device → Speed, Recent).
    • Placement: opens right of and below the pointer, flipping left or up where it would leave the work area, then moves inside it with an 8 dp margin. With the taskbar at the bottom, that's just above the taskbar, with the menu's right edge at the pointer. Taskbars on any edge work the same way.
    • Dismissal: choosing an item, Esc, a click outside the panel, or the window losing focus (another app, the taskbar, Alt+Tab). Each menu only closes itself, so a right click that reopens it isn't closed by the old window's late focus loss.
    • Keyboard: KetchMenu's ↑ ↓ → ← ↩ Esc.
    • The left click still shows the window, as before.
  • macOS: unchanged. It's still the AWT PopupMenu (an NSMenu), attached before the icon is added, as before.
  • Linux: unchanged (Java's menu with trayMenuFont(zoom)). Java maps its sans-serif to the system font through fontconfig there, so the CJK problem doesn't arise. XEmbed tray proxies also report unreliable pointer positions, and transparent windows need a compositor.
  • app:shared:
    • New KetchContextMenu(position, onDismissRequest, content) opens a KetchMenu at a point, flipping as system menus do.
    • Submenus now keep cascading the way their menu turned: a menu that opened leftward opens its submenus leftward first, so they don't land back over it. This also applies to in-app menus, but only when a parent menu flipped. A menu that didn't turn opens submenus on the end side, as before.
  • main.kt: the tray now sits inside LocalWindowExceptionHandlerFactory, so a failure in the menu window is reported like one in the main window.

Approaches weighed

Compose window (chosen) Native Win32 menu through JNA Swing JPopupMenu
Font App's Inter + system CJK fallback System's Segoe UI / Microsoft YaHei UI Inter
Follows app zoom, theme, accent Yes No (system size; dark only through undocumented uxtheme ordinals) Partly
Submenus, checks, keyboard KetchMenu's Native Swing's
Testable here Placement, keyboard, rendering Nothing runs off Windows (WndProc, message loop, TrackPopupMenu) Partly
Costs A GPU window per open; Windows foreground rules; Compose's accessibility No zoom or app font, a third native code path A third menu style, same focus issues

The Compose window is what the issue asks for (the app's font and zoom). The JNA menu is the fallback if this misbehaves on real Windows machines.

Snapshots

Rendered headlessly on macOS (./gradlew :app:desktop:test -Psnapshots --tests '*TrayMenuSnapshots*') at 2×: the right part of a 1920 dp wide desktop, right-clicked on the tray icon near the taskbar's right end. CJK text here falls back to macOS's PingFang SC; on Windows, Skia falls back through DirectWrite (Microsoft YaHei UI expected).

Before (Java's menu, from the issue) After
Light Dark

Rows: the menu while downloading; Devices → this PC → Speed, cascading left from the screen edge; Speed at 150 % zoom; Recent in Chinese.

Tests

  • TrayMenuPlacementTest (desktop): which screen's work area a pointer belongs to across several screens (negative origins, the shared edge, a pointer outside every screen, no screens).
  • KetchContextMenuTest (shared): placement against a work area. Covers a taskbar below, above, left or right; a pointer inside the area; an offset screen; a menu taller than the area; an area too small for margins; RTL. Also checks the cascade direction of context menus and submenus.
  • TrayMenuContentTest (desktop, headless Compose scene of the real tray entries): ↩ runs the first item and closes; arrows reach Devices → PC → Speed → Slow lane; ← goes back without closing; Esc and a click outside close without running anything; a click runs Quit; with the pointer on a taskbar below, the menu ends just above the bottom edge, left of the pointer.
  • ./gradlew :app:desktop:test :app:shared:jvmTest pass, and app:shared compiles for wasmJs and metadata.

Not verified: needs a Windows machine

All of this was built and tested on macOS. Nothing has run on Windows. Please check:

  1. Transparency: the work-area window renders transparent with Skiko's DirectX renderer, with no black, white or flashing backdrop when it opens.
  2. Pointer and screen coordinates: MouseInfo.getPointerInfo() (falling back to the mouse event's screen point) and getScreenInsets match on HiDPI (125–200 %) and mixed-DPI multi-monitor setups, with the taskbar on each edge, and with an auto-hiding taskbar (insets 0, so the window covers where the taskbar appears).
  3. Focus: the window comes to the front and takes keyboard focus when opened from the tray (Windows' foreground lock). Esc, the arrow keys and closing on focus loss depend on it. A click outside still closes it either way, since the window covers the work area.
  4. Right click: the popup trigger arrives on the tray icon's right button up (isPopupTrigger or BUTTON3), and the left click still shows the main window.
  5. Keyboard opening from the notification area (Win+B, then Shift+F10 or the Menu key): Java may only open its own PopupMenu for that, which Windows no longer has, so that path may do nothing.
  6. Window chrome: the UTILITY window stays off the taskbar and Alt+Tab and above the taskbar's flyouts. Clicking a submenu doesn't close the menu.
  7. Latency: each open creates a GPU window. The time to first frame on a slow GPU is unmeasured.
  8. Fonts: the CJK fallback chosen on Chinese Windows (expected Microsoft YaHei UI).
  9. Screen readers: what Narrator and NVDA announce. This is Compose's accessibility, not a native menu's.

Java draws its tray menus on Windows and only maps its own logical fonts there, so the menu
showed in Arial and SimSun on Chinese Windows instead of the app's Inter with the system's CJK
fallback. On Windows a right click on the tray icon now opens a Ketch window covering the work
area of the pointer's screen, with the same menu entries drawn as a KetchContextMenu at the
pointer: just above the taskbar, in the app's theme and zoom, with submenus, keyboard
navigation and glyphs, closing on a choice, Esc, a click outside or the window losing focus.
macOS keeps its native menu and Linux Java's, which fontconfig already maps to the system font.

KetchContextMenu opens a KetchMenu at a point, flipping as system menus do, and submenus now
keep cascading the way their menu turned instead of opening back over it.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T07:34:30.374829Z 5615733 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Oct 24, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

 2 229 files  + 5   2 257 suites  +5   57m 25s ⏱️ + 1m 25s
15 576 tests +47  15 302 ✅ +43  274 💤 +4  0 ❌ ±0 
18 955 runs  +47  18 681 ✅ +43  274 💤 +4  0 ❌ ±0 

Results for commit 5615733. ± Comparison against base commit be3a16e.

This pull request removes 1 and adds 48 tests. Note that renamed tests count towards both.
com.linroid.ketch.app.desktop.TrayMenuFontTest ‑ trayMenuFont_zoom_growsFromTheSizeOfWindowsMenusToTheNearestPixel
com.linroid.ketch.app.components.KetchContextMenuTest ‑ contextMenuOrigin_areaTooSmallForTheMargins_fitsFlushWithItsEdges[jvm]
com.linroid.ketch.app.components.KetchContextMenuTest ‑ contextMenuOrigin_areaWithOffsetOrigin_staysInIt[jvm]
com.linroid.ketch.app.components.KetchContextMenuTest ‑ contextMenuOrigin_menuTallerThanTheArea_startsAtItsTopWithoutMargin[jvm]
com.linroid.ketch.app.components.KetchContextMenuTest ‑ contextMenuOrigin_pointerInsideTheArea_opensAtThePointer[jvm]
com.linroid.ketch.app.components.KetchContextMenuTest ‑ contextMenuOrigin_rightToLeft_opensLeftOfThePointerFirst[jvm]
com.linroid.ketch.app.components.KetchContextMenuTest ‑ contextMenuOrigin_taskbarAbove_opensBelowIt[jvm]
com.linroid.ketch.app.components.KetchContextMenuTest ‑ contextMenuOrigin_taskbarBelow_opensAboveItLeftOfThePointerAtTheRightEdge[jvm]
com.linroid.ketch.app.components.KetchContextMenuTest ‑ contextMenuOrigin_taskbarBelow_opensRightOfThePointerWhereItFits[jvm]
com.linroid.ketch.app.components.KetchContextMenuTest ‑ contextMenuOrigin_taskbarOnTheLeft_opensRightOfIt[jvm]
com.linroid.ketch.app.components.KetchContextMenuTest ‑ contextMenuOrigin_taskbarOnTheRight_opensLeftOfIt[jvm]
…

This branch has not been deployed

No deployments
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