Per-Platform Windowing Gotchas
The cross-cutting landmine catalog for the window-system-integration survey: every quirk, workaround, and platform trap that surfaced across the fifteen deep-dives, collected per display server / OS and deduplicated, with the most authoritative source cited and a link back to the deep-dive(s) where it appears. Where a concept has a definition in the shared concepts vocabulary, the row links there; where the authority is an upstream spec or source file, it links to that.
This is the companion to concepts (which defines the ideas) and the per-subject deep-dives (which document each toolkit's implementation). Here the cut is by platform: if you are bringing up a Wayland backend, read § Wayland; a Win32 backend, read § Windows / Win32; and so on. Each landmine is phrased as "the trap, and what every serious toolkit does about it."
NOTE
Scope. Fifteen subjects were surveyed: the windowing libraries Winit, SDL3, GLFW, sokol_app.h; the frameworks Qt 6, GTK 4, Flutter, Chromium Ozone, Avalonia, .NET MAUI, Uno, Slint, wxWidgets, JUCE; and the Wayland-only reference pair Smithay SCTK + libdecor. A blank cell or "N/A" in the tables below is itself a finding — e.g. half the frameworks ship no native Wayland backend at all.
Last reviewed: June 8, 2026
How to read this page
The landmines cluster into a few recurring areas; within each platform section the tables are grouped by area:
- Lifecycle — window creation, mapping, destruction ordering.
- Loop — who owns the event loop, the blocking-wait primitive, cross-thread wakeups.
- Input — keyboard layout, key repeat, IME/composition, pointer, scroll.
- DPI — scale factor, per-monitor scaling, the rescale-after-creation hazard.
- Decorations — who draws the titlebar/border.
- Clipboard / DnD — selection model, large-transfer handling.
The single loudest signal across the whole survey: of the eleven cross-platform subjects, only GTK 4, Qt 6, SDL3, Winit, GLFW, Chromium Ozone, and Slint ship a native Wayland client. Flutter and wxWidgets reach Wayland only by delegating to GTK/GDK; sokol, JUCE, Avalonia, and Uno are X11-only (Wayland via XWayland); and .NET MAUI has no Linux desktop backend at all.
Wayland
The newest and least forgiving backend — almost every landmine here is "Wayland made a deliberate design choice that a toolkit built for X11/Win32 assumptions does not expect." Smithay SCTK + libdecor is the catalog authority for this section.
Lifecycle
| Landmine | What every toolkit does | Authority |
|---|---|---|
No buffer ⇒ no window. A wl_surface is invisible until a buffer is committed, and a toplevel cannot use its role until the compositor sends the initial xdg_surface.configure. "Create window" is not synchronous. | Draw the first frame inside the configure handler after ack_configure; never on creation. Winit blocks its constructor in roundtrip+blocking_dispatch until is_configured(); SDL3's Wayland_ShowWindow spins until the configure; GTK 4 freezes updates before the bare commit and thaws on first configure; SCTK draws inside WindowHandler::configure. | no-buffer-no-window |
Some state is silently dropped if committed without a buffer — e.g. xdg_surface.set_window_geometry. | Order the dance create → configure → ack → attach-buffer → commit; never set geometry on an unmapped surface. | Ozone wayland_surface.h (~L223) |
Protocol objects must be destroyed children-first (decoration before xdg_toplevel before xdg_surface before wl_surface); wrong order is a fatal protocol error that kills the connection. | Encode the order in the destructor. SCTK does it in Drop for WindowInner; Qt 6 honors "the decoration object is deleted before xdg_toplevel". | SCTK window/inner.rs |
Clients cannot set their own toplevel position — there is no global coordinate space; the compositor owns placement. SetWindowPosition on a toplevel is a silent no-op. | Document it as unsupported; expose only begin_move/begin_resize that delegate to the compositor. Hit in SDL3, GLFW, GTK 4, Qt 6 (QTBUG-76902), Ozone, SCTK. | no-buffer-no-window |
Always-on-top is unsupported by core/xdg-shell — it needs wlr-layer-shell (a different surface role). | Ignore the request, or bind layer-shell. None of the surveyed app-toolkits bind layer-shell (SDL3, Qt 6, GTK 4 all lack it). | SCTK (attributes table) |
Connection loss is abrupt: G_IO_ERR/G_IO_HUP (GLib) or a broken read means the compositor died. | Handle it explicitly — GTK 4 calls _exit(1); Qt 6 has a reconnect path then _exit(-1). | GTK 4 gdkeventsource.c |
Loop & frame pacing
| Landmine | What every toolkit does | Authority |
|---|---|---|
libwayland has a single-reader prepare_read contract. Naively watching the display fd while an in-process GPU/vendor-EGL also calls wl_display_prepare_read deadlocks. | The exact prepare_read/flush/read_events/dispatch_pending dance, run from exactly one place. Chromium Ozone moved the fd watch to a dedicated thread to break the UI-thread↔GPU-thread deadlock; GTK 4's poll source is pinned G_MININT priority so it "must run FIRST after every poll." | Ozone dedicated-thread CL |
Frame-callback vsync, not a clock. Smooth animation requires requesting wl_surface.frame before commit and drawing only when the callback fires; this also throttles to zero when occluded. | Funnel the callback into one redraw scheduler. GTK 4 freezes its GdkFrameClock while a callback is outstanding; Qt 6 runs a dedicated second event thread for frame callbacks to avoid starvation; GLFW gates the EGL swap on a frame callback so a hidden-surface swap can't block forever; Winit exposes pre_present_notify solely for this. | frame-callback vsync |
The compositor owns sizing/state; the client only requests. Size, maximized, fullscreen, tiled, activated arrive in the configure; the client must redraw to whatever it is given. | Treat WindowConfigure.state as the truth, never your last setter. | SCTK (configure handshake) |
Input
| Landmine | What every toolkit does | Authority |
|---|---|---|
Key repeat is the client's job. The protocol delivers only press/release plus a repeat_info { rate, delay }; the client must synthesize repeats itself. | Arm a per-key timer (calloop Timer, timerfd, QTimer, g_timeout_add). Done by Winit, SDL3, GLFW, Qt 6, GTK 4, Ozone, SCTK. GTK 4 even wl_display_syncs before each repeat to guard against a hung compositor. SCTK silently disables repeat without the calloop feature. | SCTK repeat.rs |
wl_keyboard delivers raw evdev codes that need a +8 offset before every xkbcommon call (the X11 keycode base). | Apply +8 at the seam. Explicit in SCTK (KeyCode::new(key + 8)). | scancode/keysym/virtual-key |
No client-side text_input_v3 consumer is "free." GTK 4's GDK binds no zwp_text_input_v3 at all (IME lives in gtk/ above GDK); SCTK ships only the IME-author side (input_method_v2), no consumer; GLFW binds no text-input protocol → zero IME. | Own the zwp_text_input_v3 consumer at the windowing layer. SDL3, Winit, Qt 6, Ozone do (Ozone keeps both v1 and v3 wrappers). | pre-edit/composition |
No cursor-shape-v1 ⇒ draw cursors client-side with libwayland-cursor + a timerfd for animation. | Prefer wp_cursor_shape_v1, fall back to a client surface. GLFW has no cursor_shape_v1 (issue #2679); SDL3/Winit/Qt 6/GTK 4/Ozone/SCTK prefer it with fallback. | raw-vs-accelerated-pointer |
High-resolution scroll arrives as axis_value120 (1/120 of a detent) and must be accumulated per wl_pointer.frame. | Merge value120 across the frame; emit one notch per 120 units, carry the remainder. SDL3, SCTK, GLFW, Qt 6, GTK 4. | raw-vs-accelerated-pointer |
| No pointer-constraints/relative-pointer is not automatic. GTK 4 binds none of them — it cannot do FPS-style raw/locked input at all (a deliberate non-game-toolkit gap). | Bind zwp_relative_pointer_v1 + zwp_pointer_constraints_v1. Done by Winit, SDL3, Qt 6, Ozone, SCTK. | GTK 4 §3 |
DPI
| Landmine | What every toolkit does | Authority |
|---|---|---|
Created-at-wrong-scale. A surface has no scale until it enters an output (pre-v6) or gets its first preferred_scale, so the first frame can render at 1× and must be re-rendered. | Treat the first scale as a configure-driven event and re-lay-out. Winit's ScaleFactorChanged ships a surface_size_writer; Slint dispatches scale before first paint; SCTK seeds scale 1 then corrects via scale_factor_changed. | scale-factor |
Fractional scale = wp_fractional_scale_v1 (in 1/120ths) + wp_viewporter, not wl_surface.set_buffer_scale (integer only). | Render at the fractional scale, size the buffer with a viewport. GTK 4, Qt 6, SDL3, GLFW, Ozone do. SCTK has NO fractional-scale support — integer only (the cautionary tale). | scale-factor |
| A surface spanning two outputs takes the MAX integer scale (pre-v6), over-rendering on the lower-DPI monitor. | Accept it, or use fractional scale. SCTK reduces with .max(...). | SCTK §5 |
Decorations
| Landmine | What every toolkit does | Authority |
|---|---|---|
SSD is only a hint. Per xdg-decoration v1 there is "no reliable way of disabling all decorations," and GNOME/Mutter never advertises zxdg_decoration_manager_v1 at all — so every serious toolkit must carry a CSD path. | Request SSD, react to the configure mode, always have a CSD fallback. The CSD strategies diverge sharply (see table). | client-vs-server-decoration, libdecor set_visibility |
| CSD strategy is a real fork and no two toolkits agree. | Winit: own-drawn Adwaita via sctk-adwaita (no libdecor). SDL3: delegates to libdecor. GLFW: libdecor → SSD → own subsurface edges. Qt 6: own decoration plugins (adwaita/bradient), predating libdecor. GTK 4: own CSD, binds only KDE org_kde_kwin_server_decoration (neither libdecor nor xdg-decoration). Ozone: SSD-first, own Views CSD fallback (no libdecor). SCTK: SSD → spartan FallbackFrame → libdecor. | client-vs-server-decoration |
libdecor is effectively mandatory on GNOME for non-GTK clients, and its gtk plugin refuses to load if host symbols clash (png_free, gdk_get_use_xshm) — a guard for clients already linking GTK/libpng. | Carry libdecor (or own CSD) for GNOME; respect the symbol-conflict guard. | SCTK/libdecor |
Clipboard / DnD
| Landmine | What every toolkit does | Authority |
|---|---|---|
No INCR dance — Wayland passes selection data over a pipe fd on demand, so large transfers "just work" (the lazy-callback model), unlike X11. | Offer MIME types via wl_data_source; the consumer receives an fd. SDL3's lazy-provider clipboard matches this exactly; SCTK, GTK 4, Qt 6, Ozone. | SCTK §8 |
| No portable drag source / no layer-shell. SDL3 is drop-target only and has no layer-shell (it is an app toolkit, not a shell). | Accept the scope limit, or bind layer-shell directly via the raw wl_display. | SDL3 §4 |
X11
X11 maps windows immediately (the inverse of Wayland), but carries its own legacy traps — single global DPI, the INCR selection protocol, and override_redirect for popups.
Lifecycle & popups
| Landmine | What every toolkit does | Authority |
|---|---|---|
Popups/menus/tooltips are override_redirect windows the WM ignores; the client positions, stacks, and grabs them itself, and is responsible for dismiss-on-click-outside. | Set override_redirect for transient surfaces; do your own pointer grab. SDL3 (xattr.override_redirect), Qt 6 (XCB_CW_OVERRIDE_REDIRECT + BypassWindowManagerHint), JUCE (windowIsTemporary), Avalonia (ManagedPopupPositioner), Uno (only for embedded children). | override-redirect vs xdg_popup grab |
Window decoration control is non-standard. It relies on the undocumented Motif _MOTIF_WM_HINTS / _MOTIF_WM_FUNCTIONS atoms — "aren't standardized or documented anywhere." | Set _MOTIF_WM_HINTS and hope; SetIsResizable can't actually block XResizeWindow. Uno (verbatim comment), JUCE. | Uno X11NativeOverlappedPresenter.cs (~L17) |
Frame extents are unknown for a transient after creation. A window created 1×1 and resized later has no border size until the WM answers _NET_REQUEST_FRAME_EXTENTS. | Defer or return "unknown." wxWidgets defers gtk_widget_show() until the WM answers (1 s timeout fallback); JUCE returns an OptionalBorderSize that "may be missing for a short time after window creation." | JUCE juce_ComponentPeer.h |
DPI
| Landmine | What every toolkit does | Authority |
|---|---|---|
No per-surface scale protocol exists. Toolkits scrape a single global Xft.dpi (or env vars, or guess). | Read Xft.dpi / _XSETTINGS. GLFW and JUCE read Xft.dpi (JUCE will literally shell out to dconf for Ubuntu's per-display scale); Avalonia guesses via a heuristic chain snapping to 1/1.25/1.5/1.75/2; Uno reads XRandR + Xft.dpi. | scale-factor |
Mixed-DPI multi-monitor is the universal weak spot. A single global Xft.dpi means a window dragged to a differently-scaled monitor does not rescale. | Best-effort: re-evaluate the best-overlap CRTC on move (Uno); otherwise it stays wrong. GLFW, Avalonia (#13450), JUCE all hit this. | scale-factor |
DPI is often integer-only. Scraped _XSETTINGS/Gdk/WindowScalingFactor is an integer scale — X11 cannot express fractional DPI. | Accept integer scale on X11. JUCE, Flutter (GTK 3 integer scale). | JUCE §5 |
Input
| Landmine | What every toolkit does | Authority |
|---|---|---|
| The X server owns key-repeat (detectable autorepeat) — so an X11 backend gets repeat "for free," but a shared codebase that later adds Wayland must add client-side repeat. | Call XkbSetDetectableAutoRepeat(true) to suppress synthetic releases. SDL3, Avalonia, sokol; JUCE instead peeks the queue for a same-timestamp release+press pair. | scancode/keysym/virtual-key |
IME is fragmented: XIM vs D-Bus IBus/Fcitx, and many toolkits omit it. GLFW requests XIMPreeditNothing (no client pre-edit); JUCE uses XLookupString only — no IME at all on X11; sokol uses XLookupString (no XIM). | Modern choice is D-Bus IBus/Fcitx, not legacy XIM: Uno and Avalonia probe *_IM_MODULE/XMODIFIERS and talk D-Bus, falling back to XIM. Winit/SDL3 use XIM. | pre-edit/composition |
Ships-its-own-keymap toolkits inherit layout bugs. Not using xkbcommon and shipping a hard-coded scancode table (Avalonia's X11KeyTransform, JUCE's XLookupString-only path) misses layout nuance. | Delegate to xkbcommon (Wayland) / XKB (X11). GTK 4, Qt 6, Ozone, Winit, SDL3 do. | scancode/keysym/virtual-key |
Keyboard input can fail without setlocale + XSetLocaleModifiers at startup — and XInitThreads is needed on WSL. | Call them at init. Uno ("not sure why this works but Avalonia and xev make similar calls"). | Uno X11ApplicationHost.cs (~L50) |
| No high-resolution scroll — wheel arrives as button-press events (buttons 4–7) unless you use XI2 valuators. | Convert buttons 4–7 to a fixed delta, or read XIScrollClassInfo valuators (Uno). JUCE uses the button path (no hi-res axis). | raw-vs-accelerated-pointer |
Clipboard / DnD
| Landmine | What every toolkit does | Authority |
|---|---|---|
The INCR protocol is mandatory for large transfers — payloads over a property-size limit must be chunked, deleting the property between chunks. | Implement INCR on both read and write. SDL3, Qt 6, GTK 4, Avalonia do. Deliberately omitted by JUCE ("a pain in the *ss", ~1 MB cap), sokol (detected, returns NULL), and on the write side by Uno (// TODO: implement INCR). | ICCCM spec |
Windows / Win32
The Win32 traps are remarkably consistent across toolkits: the modal resize loop, legacy IMM32 over TSF, and the WM_DPICHANGED reposition dance.
Loop
| Landmine | What every toolkit does | Authority |
|---|---|---|
The modal resize/move loop freezes everything. Grabbing the titlebar or a resize edge enters a nested DefWindowProc loop; GetMessage never returns, timers stop, redraws freeze for the whole drag. Bracketed by WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE. | On WM_ENTERSIZEMOVE, install a SetTimer and drive a frame from each WM_TIMER. SDL3, sokol, GTK 4 (pumps g_main_context_iteration); Qt 6, JUCE, wxWidgets, Avalonia, Uno track an "in size-move" flag and mitigate. GLFW does not mitigate (documented as not-a-bug). | Win32 modal resize loop |
| The ~500 ms title-bar-click pause at the start of a move. | Post a dummy WM_MOUSEMOVE (lparam = 0) on WM_NCLBUTTONDOWN to cancel the modal loop early. Winit (with a block comment on the empirically-found trick), sokol. | Winit event_loop.rs |
GetQueueStatus only counts "new" input — using it for a has-pending-input check "results in very hard to find bugs." | Use MsgWaitForMultipleObjectsEx(QS_INPUT|QS_EVENT|QS_POSTMESSAGE, MWMO_INPUTAVAILABLE). Avalonia (verbatim in-source comment), GLFW. | Avalonia Win32DispatcherImpl.cs |
EventLoopProxy::wake_up may be silently dropped under high contention (#3687). | Documented live caveat; no clean fix. Winit. | Winit §2 |
WinUI fires OnActivated twice when maximizing (microsoft-ui-xaml#7343). | Dedupe with an _isActivated flag. .NET MAUI. | .NET MAUI §1 |
DPI
| Landmine | What every toolkit does | Authority |
|---|---|---|
| Per-Monitor-V2 awareness must be opted into, falling back to V1 then system-DPI on older OSes. | SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2) at startup, with fallbacks. Winit, SDL3, GLFW, Qt 6, GTK 4, sokol, Avalonia, JUCE, Uno, Flutter. | scale-factor, WM_DPICHANGED |
WM_DPICHANGED must reposition the window to the OS-suggested rect (in lParam), or the window won't track the cursor across the DPI boundary. wParam packs X DPI in the low word, Y in the high. | Read the DPI from wParam, SetWindowPos to the lParam rect. Everyone above. Qt 6 additionally handles the prefatory WM_GETDPISCALEDSIZE and separates the spontaneous-drag case from setGeometry-induced to avoid double-scaling; Flutter handles WM_DPICHANGED_BEFOREPARENT for child windows. | WM_DPICHANGED |
DPI-unaware-on-purpose for GL/Vulkan. sokol deliberately sets the process DPI-unaware when high_dpi is not requested so Windows upscales. | A pragmatic choice for GL backends without HiDPI plumbing. sokol. | sokol §5 |
Min/max window size needs hand-rolled WM_GETMINMAXINFO — WinUI 3 has no portable API. | Subclass the WndProc and Marshal.PtrToStructure a MinMaxInfo. .NET MAUI. | .NET MAUI §1 |
Input
| Landmine | What every toolkit does | Authority |
|---|---|---|
IME is legacy IMM32, never TSF. Not one surveyed toolkit uses the modern Text Services Framework — all use IMM32 (WM_IME_COMPOSITION with GCS_COMPSTR/GCS_RESULTSTR). | Handle the WM_IME_* messages; position the candidate window with ImmSetCompositionWindow/ImmSetCandidateWindow. SDL3, Winit, Qt 6 ("No key events are sent during composition"), GTK 4, JUCE, Avalonia, Uno, Flutter. Note: some IMEs ignore ImmSetCandidateWindow (Flutter). | pre-edit/composition, TSF |
High-resolution scroll: accumulate WM_MOUSEWHEEL deltas (WHEEL_DELTA = 120). | Divide by 120, carry the remainder for precision touchpads. Most toolkits. JUCE does not accumulate sub-WHEEL_DELTA fractions; sokol had wheel scaling 4× too fast for years (fixed PR #1442). | raw-vs-accelerated-pointer |
Raw/relative motion is a separate source — WM_INPUT via RegisterRawInputDevices. | Register raw input for FPS-style locked-cursor mode. Winit, GLFW, sokol. | raw-vs-accelerated-pointer |
Lifecycle & clipboard
| Landmine | What every toolkit does | Authority |
|---|---|---|
WM_NCDESTROY can arrive after the userdata pointer is freed. | Zero GWL_USERDATA and set a removed-flag. Winit; .NET MAUI subclasses the WndProc (SetWindowLongPtr GWL_WNDPROC) for min/max clamping WinUI doesn't expose. | Winit event_loop.rs |
WM_CLOSE can destroy the HWND before the C++ object in a plugin host. | Swallow WM_CLOSE (mark processed) so DefWindowProc can't destroy the window early. JUCE (plus gs_modalEntryWindowCount for nested modal pumps). | JUCE §10 |
Clipboard delayed-rendering is mostly not worth it. SDL3 investigated it and chose eager materialization; Qt 6/GTK 4/Avalonia do OLE/delayed rendering via IDataObject. | Eager for simple text, OLE IDataObject for delayed. SDL3 (candid in-source comment). | SDL3 SDL_windowsclipboard.c |
macOS / AppKit
Almost every macOS landmine traces back to one fact: AppKit is main-thread-only and [NSApp run] never returns — which is precisely why the entire survey standardizes on "GUI = main thread" across all platforms.
Loop & threading
| Landmine | What every toolkit does | Authority |
|---|---|---|
[NSApp run] never returns — cleanup can't run after it. | Put teardown in applicationWillTerminate; set releasedWhenClosed = NO. sokol (verbatim comment), Avalonia, Uno. | sokol §1 |
| The OS owns the loop (CFRunLoop), so a toolkit can't run its own. | Either cede the loop to [NSApp run] and hang CFRunLoop observers (AfterWaiting→new-events, BeforeWaiting→about-to-wait), or drive NSRunLoop manually with nextEventMatchingMask. Winit (observers), GLFW (manual, admits dequeue:NO "mysteriously hangs"), Qt 6, GTK 4, Avalonia, Uno. | readiness-vs-completion windowing |
AppKit cannot wait on fds and NSEvents simultaneously. | Push the blocking select() to a helper "select thread." GTK 4 (documented at length in gdkmacoseventsource.c). | GTK 4 gdkmacoseventsource.c |
MainThreadMarker::new() / isMainThread panics or asserts off-main — window/loop creation must be on thread 0. | Enforce it at runtime. Winit (MainThreadMarker, the only sanctioned cross-thread channel is EventLoopProxy), SDL3 (SDL_RunOnMainThread), Flutter (runs_task_on_current_thread = isMainThread + NSAssert), JUCE, Qt 6, Avalonia, Uno. | Winit event_loop.rs |
| Nested/modal run loops (menu tracking, modal sheets) starve a guest loop. | Service tasks in a private run-loop mode too. Flutter's FlutterRunLoop adds a source+timer to both common and a private mode; Avalonia re-creates a CFRunLoopObserver when already inside a callback; wxWidgets notes [NSApp run] may only be the outermost loop, nested loops must use nextEventMatchingMask. | Flutter FlutterRunLoop.swift (L9-13) |
Frame pacing
| Landmine | What every toolkit does | Authority |
|---|---|---|
CVDisplayLink is deprecated since macOS 15 with CADisplayLink as the replacement. | Use a per-screen CVDisplayLink for now, plan the CADisplayLink migration. JUCE (per-screen CVDisplayLink), GTK 4 & Qt 6 (both note the deprecation in-source), Avalonia, Flutter; sokol migrated to CADisplayLink (PR #1444) and gets jitter-free frame duration straight from the timestamp. | frame-callback vsync, CVDisplayLink |
CADisplayLink stops when the window is minified/obscured (unlike the old MTKView). | Add a 60 Hz fallback NSTimer. sokol (issue #1446 open). | sokol §2 |
Depending on platform middleware (MTKView) is brittle — macOS updates change swapchain behaviour. | Drop MTKView for CAMetalLayer + CADisplayLink. sokol (the clearest "regret" in the survey). | sokol §10 |
Input & lifecycle
| Landmine | What every toolkit does | Authority |
|---|---|---|
A rendering view (MTKView/MTKView-style) may not provide an NSTextInputContext, so interpretKeyEvents: bypasses the IME entirely. | Override inputContext. Uno (UNOMetalFlippedView); GLFW mispositions the candidate window because firstRectForCharacterRange: returns a zero rect; sokol reads event.characters directly (no NSTextInputClient → no CJK). | pre-edit/composition |
| No direct analogue to keysyms/virtual-keys — the platform gives only a "scancode." | Report the raw scancode. Winit (verbatim doc comment). | scancode/keysym/virtual-key |
| Plugin hosts can close the window unexpectedly. | Create with setReleasedWhenClosed: YES and explicitly retain. JUCE (destruction-ordering hazard from plugin embedding); Avalonia uses setReleasedWhenClosed: false + defer:false to dodge a resize bug. | JUCE juce_NSViewComponentPeer_mac.mm |
| Non-precise scroll deltas are ~10× coarser than trackpad precise deltas. | Multiply non-precise deltas by 10 / branch on hasPreciseScrollingDeltas. Uno, JUCE. | Uno UNOWindow.m |
| macOS via Mac Catalyst (UIKit-on-Mac), not AppKit carries Catalyst compromises. | A framework choice with consequences. .NET MAUI (Catalyst, not AppKit). | .NET MAUI §10 |
Cross-platform & architectural traps
These are not tied to one display server — they are the recurring shape of the abstraction itself.
| Landmine | What the survey found | Where it shows |
|---|---|---|
No portable popup/grab primitive. A toolkit's popup API cannot be "place this window at (x, y)" — that maps cleanly onto neither X11 override_redirect nor Wayland xdg_popup grab. | Winit punts entirely (no cross-platform popup grab, only X11 _NET_WM_WINDOW_TYPE hints); GLFW/sokol have no popup API at all; framework toolkits (Uno, Slint's winit backend, Avalonia) render popups in-canvas to sidestep the fork — at the cost of no compositor-enforced dismiss. | override-redirect vs xdg_popup grab |
| No portable external-fd injection into the wait set. An async runtime and a windowing loop both want to own the blocking wait. | GLFW and SDL3 have no way to add an fd; you poll separately or run windowing on its own thread. Winit's EventLoop implements AsFd (Linux) so it can be polled inside a larger reactor; SCTK/Slint use calloop. | readiness-vs-completion windowing |
No frame-pacing telemetry without a dedicated source. Toolkits relying only on the GL/EGL swap interval (GLFW) or a sleep-timer (Avalonia X11's 60 Hz SleepLoopRenderTimer, JUCE/Uno Linux timers) tear on high-refresh displays. | Pick a per-platform vsync source (frame callback / CVDisplayLink / DXGI WaitForVBlank) and fold them into one frame clock. JUCE runs a dedicated highest-priority WaitForVBlank thread on Windows. | frame-callback vsync |
Windows timer-derived vsync, not a DXGI waitable swapchain. Flutter snaps to a tick over DwmGetCompositionTimingInfo (16.6 ms fallback) — variable-refresh edge cases. | A pragmatic but imperfect choice; prefer a waitable swapchain. | Flutter §2 |
| Native-handle escape hatches are where the abstraction admits it leaks. The set of getters is the map of unsupported features. | Typed handles (Winit raw-window-handle, Qt 6 QNativeInterface, GLFW GLFW_EXPOSE_NATIVE_*) beat type-erased void* (sokol, JUCE getNativeHandle with "no guarantees what you'll get back"). Message-pump hooks: SDL3 (SetWindowsMessageHook/SetX11EventHook), Qt 6, Winit (msg_hook), Avalonia (WndProcHookCallback), Flutter (WindowProcDelegate); GLFW has none — you must subclass the native window yourself. | (per deep-dive §9) |
| Gestures: removed or absent. SDL3 removed gesture recognition in SDL3 with no replacement; GLFW/sokol never had it. | Derive gestures from raw touch events. | SDL3 §3 |
| Wrapping native controls inherits every native quirk and yields zero windowing control. | .NET MAUI (handler.PlatformView is the literal native window; WindowHandler.Standard.cs throws NotImplementedException on Linux); wxWidgets (GTK quirks leak through, e.g. the alt-Wayland-format clipboard table). | .NET MAUI / wxWidgets |
| GTK 3 / GTK-as-the-Linux-layer caps you at integer scale and GDK's protocol coverage. | Flutter (GTK 3 → integer scale only, same-scale monitor migration undetectable, blur on fractional desktops, issue #41980); wxWidgets (two layers from the display server; GTK4 port unfinished because it removes GdkWindow and the X11 escape hatches wx relies on). | Flutter fl_view.cc (L224-227) |
| Managed-runtime UI threads add GC-pause latency on the hot input path. | Avalonia, Uno, .NET MAUI — every native event crosses the managed boundary and is subject to GC pauses (marked [inference] in the deep-dives). | Avalonia §7 |
The "every serious toolkit does X" cheat sheet
The handful of workarounds that recur so often they are effectively table-stakes for a new windowing layer:
- Wayland: draw the first frame in the configure handler (no-buffer-no-window) — never assume "create window" is synchronous.
- Wayland: synthesize key repeat client-side with a timer; the protocol won't do it.
- Wayland: always carry a CSD path — SSD is a hint and GNOME never offers it.
- Win32: drive frames from a
SetTimer/WM_TIMERduring the modal resize loop, or live-resize freezes. - Win32: reposition to the OS-suggested rect on
WM_DPICHANGED, and opt into Per-Monitor-V2. - Win32: use IMM32 for IME — nobody ships TSF, so match the field or surpass it.
- macOS: do everything UI on the main thread, cede the loop to
[NSApp run]/CFRunLoop, and service tasks in a private run-loop mode for nested loops. - macOS: plan the
CVDisplayLink→CADisplayLinkmigration (deprecated since macOS 15). - X11: implement
INCRfor large clipboard transfers — the toolkits that skipped it (JUCE, sokol, Uno write-side) all regret it. - Everywhere: own the IME pre-edit consumer at the windowing layer rather than punting it upward — the toolkits that punt (GLFW, GTK 4's GDK, SCTK, Slint's winit backend) cannot be reused standalone for text input.
Sources
This page is a synthesis of the fifteen deep-dives' own Gotchas/Weaknesses/dimension sections; each landmine's authoritative primary source (a repository file, an in-source comment, an upstream spec, or a tracking issue) is cited inline above and carried in full by the linked deep-dive's own Sources block. The cross-cutting concept definitions and the verified external references (Wayland protocols, the ICCCM spec, WM_DPICHANGED/TSF on learn.microsoft.com, NSTextInputClient/CVDisplayLink on developer.apple.com, all pinned to Wayback snapshots for the bot-hostile hosts) live in concepts.
- Windowing libraries: Winit, SDL3, GLFW, sokol_app.h
- Frameworks: Qt 6 / QPA, GTK 4 / GDK, Flutter Engine, Chromium Ozone, Avalonia, .NET MAUI, Uno Platform, Slint, wxWidgets, JUCE
- Wayland reference: Smithay SCTK + libdecor
- Shared vocabulary: concepts; survey umbrella: index
- Sibling trees: ui-layout, async-io