Skip to content

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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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 sourceWM_INPUT via RegisterRawInputDevices.Register raw input for FPS-style locked-cursor mode. Winit, GLFW, sokol.raw-vs-accelerated-pointer

Lifecycle & clipboard

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
[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

LandmineWhat every toolkit doesAuthority
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

LandmineWhat every toolkit doesAuthority
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.

LandmineWhat the survey foundWhere 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:

  1. Wayland: draw the first frame in the configure handler (no-buffer-no-window) — never assume "create window" is synchronous.
  2. Wayland: synthesize key repeat client-side with a timer; the protocol won't do it.
  3. Wayland: always carry a CSD path — SSD is a hint and GNOME never offers it.
  4. Win32: drive frames from a SetTimer/WM_TIMER during the modal resize loop, or live-resize freezes.
  5. Win32: reposition to the OS-suggested rect on WM_DPICHANGED, and opt into Per-Monitor-V2.
  6. Win32: use IMM32 for IME — nobody ships TSF, so match the field or surpass it.
  7. 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.
  8. macOS: plan the CVDisplayLinkCADisplayLink migration (deprecated since macOS 15).
  9. X11: implement INCR for large clipboard transfers — the toolkits that skipped it (JUCE, sokol, Uno write-side) all regret it.
  10. 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.