Skip to main content

Desktop integration on Windows: what works, and what does not

What an app on the win32 backend can reach today, what it cannot, and which of the gaps have a route. Every row was measured on this backend rather than inferred from the API's existence: "works" means a test asserts it or it was driven and seen.

Works​

whatthe APIhow
a window, composed<window>DirectComposition surface per window, Direct2D verbs
text and layout<text>, app.fontsDirectWrite: layout, line metrics, hit testing, carets
glyph runsctx.drawGlyphs, face.glyphIdFor/advanceOfDrawGlyphRun — what <Terminal backend="vt"> positions its grid with
variable font axesstyle.variationsIDWriteTextLayout4::SetFontAxisValues
app-supplied fontsloadFont, openFonta DirectWrite font set of the app's own, with the file's axes kept
OpenGL<glarea>WGL, and WGL_NV_DX_interop2 to reach the composed surface
keyboardonKeyDown, focusToUnicodeEx without consuming dead keys; X keysym vocabulary
pointeronMouseDown, wheel, captureall five buttons, SetCapture for drags leaving the window
focusonFocus/onBlurWM_ACTIVATE
window statesuseWindowState, <window maximized fullscreen alwaysOnTop>ShowWindow, SetWindowPos, a remembered frame for fullscreen
screens and DPIuseScreens, useScaleper-monitor v2; no live per-window re-scale yet (windows.md)
light/dark and accentuseSystemAppearanceUISettings and the theme registry; the frame follows through DWMWA_USE_IMMERSIVE_DARK_MODE
native control bezels<button>, <checkbox>…the visual styles engine, on a light desktop; dark ones are drawn (Windows' are not public)
file dialogsuseFileDialogthe Common Item Dialog, parented, modal on the UI thread
clipboarduseClipboardtext only, and format enumeration
tray icon and menuuseTrayShell_NotifyIcon
taskbar progressuseProgressITaskbarList3::SetProgressValue
a badge on the iconuseBadge, setBadgethe count drawn into an overlay icon — Windows has no badge API
demanding attentionsetUrgentFlashWindowEx
global hotkeysuseAcceleratorRegisterHotKey
notificationsnotify, useNotifiera tray balloon, which the shell shows as a toast
idle timeuseIdleGetLastInputInfo, polled on the same adaptive wait the X rung uses
keeping the screen awakekeepAwake, useKeepAwakeSetThreadExecutionState, counted
reading a window backwindow.snapshot()PrintWindow with PW_CLIENTONLY and PW_RENDERFULLCONTENT
localesystemLocale, useLocaleICU's own resolution, which reads the OS on Windows — no backend code
drag and dropuseDropTarget, useDragSourceIDropTarget and DoDragDrop over OLE, both directions
a thumbnail toolbaruseThumbnailToolbarITaskbarList3::ThumbBarAddButtons — Windows only
a jump listuseJumpListICustomDestinationList — Windows only
recent documentsuseRecentDocumentSHAddToRecentDocs — Windows only
input methodsonCompositionStart/Update/End, <textinput>IMM32: the preedit is drawn in the field, the candidate list follows the caret
screen readersrole, aria-*, announce()a UI Automation provider over a pushed mirror of the tree
window identity<window appId>the AppUserModelID on the window's property store — taskbar grouping, pinning, the jump list

Built, with limits worth knowing​

Input methods — built on IMM32, with the gaps that layer has​

CJK input works, through the same composition events a Wayland text-input drives, so an application handles one composition and gets both. The preedit is drawn in the field rather than in a floating box, and the candidate list follows the caret.

What it does not carry is what IMM32 itself no longer carries. Modern input methods — the new Microsoft Japanese and Chinese IMEs included — are TSF text services reaching an IMM32 window through a compatibility layer, which does not pass on voice typing (Win+H inserts nothing), the shell's handwriting panel, or shape-writing on the touch keyboard. A TSF text store would close those; it is not built. Reconversion (asking the IME to re-open a committed word) is not wired up either, though IMM32 carries it.

Screen readers — built, without text ranges​

Screen readers are served by a UI Automation provider of this project's own, answering from a copy of the tree the app pushes, so a screen reader is never paced by the app's busiest moment. Roles, names, descriptions, states, bounding rectangles, focus and the Invoke, Toggle, Value and RangeValue patterns all cross; announce() goes out as a UIA notification. Everything an app writes — role, aria-label, aria-checked, aria-valuenow — is read by the same code the AT-SPI bridge reads, so the two backends cannot disagree about what a node is.

What is missing is ITextProvider. A <textinput> exposes its value, so a screen reader reads the field and hears edits, but caret-by-caret and word-by-word navigation inside it is not there.

Not implemented​

Ordered by what it costs an app today.

useLauncherMenu is inert​

The jump list is built (useJumpList above) but it is not what useLauncherMenu means, and is deliberately not wired to it. A launcher-menu item carries a callback; a jump-list task starts a new process with arguments, because the shell launches the program rather than calling into the running one. Mapping one onto the other would quietly change what a click does.

Activation — file associations, URL schemes, second launches​

useAppOpen and useAppActivate never fire on Windows: there are no file associations, no URL schemes and no single-instance handoff yet, so a second launch is a second process. uri-schemes.md documents what the other platforms do.

Eyedropper — the pixel is read, the ladder has no rung​

The bridge reads a screen pixel (app.screenColorAt, app.pointerPosition), but useEyedropper() reports supported: false here until a rung draws the loupe over it.

Calendar — blocked, not missing​

useDesktopCalendarEvents wants the Windows appointments API, which requires package identity. Unpackaged, there is nothing to call, and the hook reports supported: false rather than guessing.

Permissions — nothing to ask​

usePermission has no Windows meaning for an unpackaged app: the capability API is for packaged apps, and everything else sits under one per-device switch. The answer is 'unknown' until something is attempted, and the attempt's own error is the answer.

useGlobalMenu is a menu bar owned by the desktop. Windows has none; a <MenuBar> drawn in the window is the whole story here, and that works.

Keyboard grabs​

grabKeyboard accepts and does nothing: there is no Windows mechanism for it. A popup that asked for keys gets them only while it is the foreground window.

grabPointer is a different story — see below.

Built, with limits worth knowing: <popup grab>​

Windows has no pointer grab to give — SetCapture sends a window the mouse only while a button is already down — so the dismissal is reproduced from two signals, and <Select>, <Menu> and any <popup grab> close the way they do everywhere else:

  • a press delivered to another window of this application — a click in the owner behind the menu, in a second window, or in the menu a submenu came from;
  • the application losing activation — a press in another application or on the desktop. A popup never takes activation itself, so opening one raises no blur.

The gap: a press on the non-client area of one of our own windows — a title bar, a resize border — is not reported, so a menu left open while the user drags the window behind it stays open. X11's grab covers that and this does not.

What Windows has that nothing else does​

Some Windows surfaces have no cross-platform API to fit into, so they have APIs of their own, and a backend installs a method, and its presence is the capability. They are reported as features of the launcher, beside the badge and the progress bar:

const launcher = useDesktopCapability('launcher');
launcher.backend; // 'taskbar' here, 'cocoa' on a Dock
launcher.features.thumbnailToolbar; // the buttons under the hover preview
launcher.features.tasks; // the jump list's Tasks category
launcher.features.recentDocuments; // the shell's Recent lists

They live there rather than in useSupports() because useSupports answers for the display — transparency, shaders, embedding — and a taskbar button is not a display property; and because a boolean cannot say which launcher answered. Every other backend answers false for the three, and the hooks do nothing where the method is absent. examples/taskbar.jsx is the shape; there is no process.platform in it.

  • Thumbnail toolbars — useThumbnailToolbar: up to seven buttons under the taskbar thumbnail, play/pause/next for a media app.
  • Recent documents — useRecentDocument.
  • A jump list — useJumpList (see useLauncherMenu).
  • Taskbar overlay as a status light: setBadge takes a label, and an app that wants a dot rather than a number can pass one character.

Ideas not built yet are in the design notes.