Cross-Library Synthesis: How Fifteen Toolkits Integrate with the Window System
The capstone of the window-system-integration survey. It distils the fifteen deep-dives — Winit, SDL3, GLFW, sokol_app.h, Qt 6, GTK 4, Flutter Engine, Chromium Ozone, Avalonia, .NET MAUI, Uno Platform, Slint, wxWidgets, JUCE, and Smithay SCTK + libdecor — and the shared concepts vocabulary into a single head-to-head comparison along the ten dimensions every deep-dive analyses: window lifecycle, the event loop, input (incl. IME), Wayland specifics, DPI/scaling, multi-window/popups, threading, clipboard/DnD, escape hatches, and history/regrets.
The goal is not to crown a winner — these systems occupy different niches, from a 3-D-app shim (sokol) to a browser's platform layer (Chromium Ozone) — but to surface where the field is unanimous (the consensus standards a new toolkit should simply adopt), where it forks (the genuine architectural decisions, with the strongest argument on each side drawn from the evidence), and what gap a from-scratch framework faces today (the delta table that bridges into recommendations).
NOTE
Scope. This is the synthesis leaf of the survey. It assumes the shared mechanics — client-vs-server decorations, scancode/keysym/virtual-key, logical vs physical coordinates, the scale factor, IME pre-edit, override-redirect vs xdg_popup grab, the Win32 modal resize loop, raw vs accelerated pointer, no-buffer-no-window, frame-callback vsync, and readiness vs completion in the loop — as defined in concepts, and cross-links rather than re-derives them. Each per-subject deep-dive is the source of truth for its own claims.
Last reviewed: June 8, 2026
Part 1 — The systems at a glance
One row per surveyed system. Loop ownership is the readiness-vs-completion loop-ownership fork (poll = app-owned; callback = library/OS-owned; hybrid = both). Native coord unit is the logical-vs-physical design choice. Wayland decoration is the CSD/SSD strategy. Escape hatch is the raw-handle / native passthrough shape (dimension 9).
| Library | Language | Category | Loop ownership | Native coord unit | Wayland decoration | Threading | Escape hatch |
|---|---|---|---|---|---|---|---|
| Winit | Rust | windowing library | callback (own loop) | physical | own-drawn Adwaita (sctk-adwaita) | main-thread; with_any_thread | raw-window-handle + per-platform getters |
| SDL3 | C | windowing + media library | hybrid (poll / callbacks) | mixed (per-platform) | SSD-first → libdecor CSD | main-thread (AppKit/UIKit) | properties bag + message-pump hooks |
| GLFW | C | minimal windowing library | poll (app-owned) | mixed (window vs fb size) | libdecor → SSD → own-drawn fallback | main-thread | typed glfwGet*Window accessors |
| sokol_app.h | C99 | single-header app shim | callback (own loop) | mixed (physical fb) | N/A — no Wayland backend (X11 only) | callbacks one thread | type-erased void* getters |
| Qt 6 (QPA) | C++ | full GUI framework (QPA layer) | hybrid (Qt owns QEventLoop) | logical | own-drawn CSD plugins (predates libdecor) | GUI = main thread | QNativeInterface + native event filter |
| GTK 4 / GDK | C (GObject) | full framework (ref. WL client) | callback/hybrid (GLib source) | logical | own-drawn CSD; only KDE SSD protocol | single main thread | native handle getters + wl_display exposure |
| Flutter Engine | C/C++ | engine / embedder framework | callback/hybrid (embedder) | mixed (logical + ratio) | N/A — delegates to GTK 3 / GDK | platform = embedder main thread | native handle getters + WindowProcDelegate |
| Chromium Ozone | C++ | browser platform-abstraction | hybrid (fd → MessagePump) | mixed (DIP + pixel) | SSD-first → own Views-drawn CSD | UI thread; GPU off-thread | gfx::AcceleratedWidget + mix-in extension interfaces |
| Avalonia | C# (.NET) | from-scratch Skia UI framework | hybrid (managed Dispatcher) | logical (DIPs) | N/A — no native Wayland (X11/XWayland) | UI thread; render off-thread | TryGetPlatformHandle + WndProcHookCallback |
| .NET MAUI | C# (.NET) | native-control-wrapping framework | callback (owns no loop) | logical (DIPs) | N/A — no Linux desktop backend | main-thread only | handler.PlatformView (literal native window) |
| Uno Platform | C# (.NET) | WinUI-over-Skia framework | hybrid (per-platform) | logical (view pixels) | N/A — no native Wayland (XWayland) | dispatcher thread; render off | NativeWindow handle getters + per-window WndProc |
| Slint | Rust | reactive UI on a backend abstr. | hybrid (winit / calloop KMS) | logical | own-drawn CSD via winit (sctk-adwaita) | loop thread; main on macOS | with_winit_window + raw-window-handle |
| wxWidgets | C++ | native-widget-wrapping framework | callback/delegated (native) | logical (DIPs) | N/A — delegated to GTK | main-thread only | GetHandle() + MSWWindowProc + wxNativeWindow |
| JUCE | C++ | audio-focused GUI framework | hybrid (own / host-as-plugin) | mixed (logical Component) | N/A — no Wayland backend (X11 only) | single message thread | getNativeHandle() void* + FD registration |
| Smithay SCTK + libdecor | Rust + C | Wayland client toolkit + CSD helper | poll (caller-owned) | logical (integer only) | three-tier: SSD → FallbackFrame → libdecor | no main-thread rule | raw proxy getters; caller bridges raw-window-handle |
Two structural observations fall straight out of the table and frame the rest of this document:
- The "Wayland decoration" column is mostly empty. Of fifteen, only six speak Wayland natively (Winit, SDL3, GLFW, Qt 6, GTK 4, Chromium Ozone, Slint via winit, Smithay); the rest either have no Linux backend (MAUI), run under XWayland as an X11 client (Avalonia, Uno, JUCE, sokol), or delegate to GTK (Flutter, wxWidgets). Native Wayland is the single biggest gap in the field — and the headline finding for a new toolkit (see §Dimension 4 and §Part 5).
- Loop ownership and native coordinate unit are the two axes that genuinely fork. Every other dimension trends toward a consensus (Part 3); these two are real decisions with strong arguments on both sides (Part 4).
Part 2 — Per-dimension comparison
Each subsection cuts one of the ten analysis-spine dimensions across all fifteen subjects.
Dimension 1 — Window creation & lifecycle
The fork is synchronous map (X11/Win32) vs the asynchronous no-buffer-no-window handshake (Wayland). On X11/Win32, CreateWindowEx+ShowWindow / XMapWindow puts the window on screen immediately; on Wayland a wl_surface is invisible until a buffer is committed and the compositor has sent the initial xdg_surface.configure.
| Library | Create-window model |
|---|---|
| Winit | constructor blocks in roundtrip+blocking_dispatch until is_configured() |
| SDL3 | Wayland_ShowWindow spins on libdecor_dispatch until the first configure |
| GLFW | xdg_toplevel created only if visible/monitor; commit+roundtrip before first buffer |
| Smithay SCTK | draws the first frame inside WindowHandler::configure, never on creation |
| Chromium Ozone | surface unmapped until buffer; set_window_geometry silently dropped without one |
| Slint | destroys the window on hide (winit won't create an invisible Wayland window) |
| Qt 6 | QWaylandWindow::isExposed() false until configure; expose fires only after it |
| GTK 4 | gdk_toplevel_present() single async entry; freeze/thaw on first configure |
| Flutter | lifecycle delegated to GDK; engine itself owns no window |
| Avalonia, Uno, JUCE, sokol | synchronous X11 map (no Wayland to negotiate) |
| MAUI, wxWidgets | delegated to the native/host stack (WinUI, GTK, UIKit) |
The recurring lesson, recorded most sharply in SDL3's bug tail (KDE bug 448856): retrofitting a synchronous "create then show" API onto the async handshake is fragile — model the lifecycle as explicit state from the start. The same rule produces the universal Wayland no-op: clients cannot set their own toplevel position (SDL3, GLFW, GTK 4, Chromium Ozone, Smithay all record SetWindowPosition as a silent no-op), because the compositor owns placement.
Dimension 2 — The event loop
Three loop-ownership models, mapped to readiness vs completion in the loop:
| Model | Who drives | Subjects |
|---|---|---|
| Poll (app-owned) | app calls PollEvents / dispatches the loop | GLFW, default SDL3, Smithay SCTK |
| Callback (library/OS-owned) | control inverted; the OS/library calls the app | Winit (ApplicationHandler), sokol, MAUI, opt-in SDL3 |
| Hybrid (own loop over native source) | toolkit owns a dispatcher wrapping the native pump | Qt 6, GTK 4, Avalonia, Uno, JUCE, Flutter, Chromium Ozone, wxWidgets, Slint |
On Linux the loop is, underneath, a readiness reactor over the display socket fd. Winit wraps the Wayland socket as a calloop WaylandSource and exposes AsFd so embedders can poll it; Smithay integrates via calloop-wayland-source; Chromium Ozone plugs the fd into base::MessagePump and runs a dedicated fd-watch thread to dodge the libwayland prepare_read single-reader deadlock when a GPU/EGL also reads it; GTK 4 integrates as a GLib GSource (and inverts on macOS so CFRunLoop drives GLib via a select thread).
Two pervasive event-loop hazards recur:
- The Win32 modal resize/move loop freezes redraws during a titlebar drag. The survey-wide fix is identical:
SetTimeronWM_ENTERSIZEMOVE, drive a frame fromWM_TIMER(SDL3, GTK 4, sokol, Uno). Winit and sokol add the dummy-WM_MOUSEMOVEtrick to cancel the ~500 ms first-click pause. Qt 6, JUCE, wxWidgets track an in-size-move flag and accept the loop is mitigated, not eliminated. - Nested native loops on macOS. wxWidgets and Avalonia document at length that
[NSApp run]may only be the outermost loop; nested loops must usenextEventMatchingMask:. Flutter adds a customCFRunLoopMode(FlutterRunLoopMode) to common+private modes to survive modal run loops — the cleanest recorded fix for a guest-on-host-loop design.
Dimension 3 — Input (keyboard, pointer, IME)
Keyboard input splits the scancode / keysym / virtual-key identities. On Linux the consensus translator is xkbcommon; the cautionary cases are the toolkits that ship their own table and inherit layout bugs.
| Concern | Consensus / fork |
|---|---|
| Keymap state machine | xkbcommon everywhere on Linux (Winit, SDL3, GTK 4, Chromium Ozone, Smithay); own table = bug (Avalonia's X11KeyTransform, JUCE's XLookupString-only, wxWidgets hardcoded us) |
| Wayland keycode offset | universal +8 before every xkb call (wl_keyboard delivers raw evdev) |
| Key repeat (Wayland) | client-synthesised (timer per held key) everywhere — the protocol only sends rate/delay |
| High-res scroll | universal axis_value120 / WHEEL_DELTA=120 accumulator (1/120-detent) (SDL3, Smithay, GLFW, Qt 6, GTK 4) |
| Raw/relative pointer | separate event source (zwp_relative_pointer_v1 / WM_INPUT); GTK 4 binds none (not a game toolkit) |
IME (pre-edit/composition) is the most uneven dimension and produces two field-wide findings:
| Platform | Modern protocol | What the field actually does |
|---|---|---|
| Wayland | zwp_text_input_v3 | SDL3, Winit, Qt 6 bind it; GDK does not (GTK 4 does IME above GDK); Smithay ships no consumer |
| Win32 | Text Services Framework | nobody uses TSF — SDL3, Winit, Qt 6, GTK 4, JUCE, Avalonia, Uno all use legacy IMM32 |
| macOS | NSTextInputClient | SDL3, Winit, JUCE implement it; GLFW mispositions the candidate window |
| X11 | XIM (or D-Bus IBus/Fcitx) | Winit/SDL3 use XIM; Uno uses D-Bus IBus/Fcitx; GLFW, JUCE, sokol omit pre-edit entirely |
Finding 1: the universal IMM32-over-TSF choice on Windows — not one toolkit uses the modern TSF. Finding 2: IME is frequently absent — GLFW (issue #41 open since 2013), JUCE's X11, sokol, Smithay, and MAUI all punt it, confirming that pre-edit is table-stakes-but-tedious and a reusable windowing layer should own the consumer rather than push it upward (as GTK 4 does to GDK).
Dimension 4 — Wayland specifics
The dimension that most sharply separates the field. Only six subjects are native Wayland clients; the rest are absences-as-findings.
| Wayland posture | Subjects |
|---|---|
| Native Wayland client | Winit, SDL3, GLFW, Qt 6, GTK 4, Chromium Ozone, Smithay SCTK (+ Slint via winit) |
| Delegates to GTK/GDK | Flutter (GTK 3), wxWidgets (GTK 3) |
| XWayland only (X11 client) | Avalonia, Uno, JUCE, sokol |
| No Linux desktop backend at all | MAUI |
Even among native clients, the protocol coverage is patchy and itself a finding:
- Decorations: GTK 4 binds neither libdecor nor
zxdg-decoration-v1(only the KDE protocol), so on wlroots it always falls back to own CSD. Chromium Ozone and SDL3 prefer SSD then fall back (Ozone to own Views-drawn CSD, SDL3 to libdecor) because GNOME/Mutter advertises noxdg-decorationat all. - Fractional scale: Smithay SCTK implements none (integer only) — the cautionary tale; GTK 4/Qt 6/GLFW bind
wp_fractional_scale_v1+viewporter. - Pointer constraints / relative motion: GTK 4 binds none — no FPS-style input.
- layer-shell / idle-inhibit: unbound by GTK 4 and upstream Qt 6 6.8.
Smithay SCTK + libdecor is the catalog authority for this dimension: it is the only Wayland-only subject, and its three-tier decoration story (compositor SSD → FallbackFrame → libdecor dlopen plugin) is the reference model.
Dimension 5 — DPI & scaling
The native-coordinate fork (dimension covered in §Part 4) plays out here as where the scale comes from and two hazards. See the scale factor.
| Platform | Scale source (consensus) |
|---|---|
| Wayland | wp_fractional_scale_v1 (preferred scale as integer 1/120ths) + wp_viewporter |
| Win32 | Per-Monitor-V2 awareness; WM_DPICHANGED (scale = dpi/96) |
| macOS | NSWindow.backingScaleFactor (integer 1×/2× only) |
| X11 | no per-surface protocol — scrape global Xft.dpi or guess (the universal weak spot) |
- Created-at-wrong-scale: Winit's
ScaleFactorChangedships asurface_size_writer; Slint dispatchesScaleFactorChangedon creation before first paint — the two cleanest cures. GTK 4/wxWidgets correct after realization on the first configure. - Mixed-DPI migration: Win32 re-fires
WM_DPICHANGEDand Wayland re-firespreferred_scale; X11 single-global-DPI cannot — so every XWayland/X11-only toolkit (Avalonia (#13450), Uno, JUCE integer-only +dconfshell-out, sokol high-DPI TODO) inherits blurry mixed-DPI.
Dimension 6 — Multi-window & popups
Two independent capabilities: multiple top-levels, and transient override-redirect / xdg_popup grab surfaces (menus, tooltips, dropdowns).
| Capability | Subjects |
|---|---|
| Multi-window first-class | most frameworks (Qt 6, GTK 4, Avalonia, Uno (5.2), Flutter (settling), wxWidgets, JUCE) |
| Single window only | sokol (static _sapp precludes it architecturally) |
| No cross-platform popup grab | Winit (only X11 _NET_WM_WINDOW_TYPE hints — punts entirely) |
| Real OS popups (X11 override-redirect) | SDL3, Avalonia (ManagedPopupPositioner), Qt 6 (BypassWindowManagerHint) |
Real OS popups (xdg_popup grab) | SDL3, Smithay (Popup/XdgPositioner), libdecor window-menu grab |
| In-canvas popups (no OS sub-surface) | Uno, Slint (winit backend) — sidestep the fork, lose compositor dismiss |
The cross-platform consequence the survey keeps hitting: a popup abstraction cannot be a simple "place this window at (x, y)" call, because that maps onto neither the X11 override-redirect model nor the Wayland parent-relative xdg_positioner model cleanly. Winit punts it to consumers; the framework toolkits either draw in-canvas or carry both native paths.
Dimension 7 — Threading
The most unanimous dimension in the survey: GUI = main thread, forced by macOS AppKit.
| Threading posture | Subjects |
|---|---|
| Main-thread-only for windows/events | SDL3, GLFW, Qt 6, GTK 4, Avalonia, MAUI, wxWidgets, JUCE, Flutter, Chromium Ozone (UI thread) |
| Main-thread on macOS, relaxed elsewhere | Winit (MainThreadMarker panics off-main; with_any_thread on Linux/Win), Slint |
| No main-thread rule | Smithay SCTK (Wayland has no such constraint; events on the dispatch thread) |
The constraint is driven by one platform — every deep-dive that explains why it is main-thread-only (JUCE, Qt 6, wxWidgets, Avalonia) names AppKit. Rendering is commonly off-thread (Avalonia, Uno, Chromium Ozone's sandboxed GPU process); cross-thread work marshals back via a dispatcher/CallAfter/MessageManagerLock. The only escapee is Smithay, because it never touches AppKit.
Dimension 8 — Clipboard & drag-and-drop
The consensus is that clipboard is a negotiated, MIME/atom-typed, chunked transfer, and that doing it well (large transfers, rich formats, DnD) is where toolkits cut corners.
| Behaviour | Field finding |
|---|---|
X11 large transfers (INCR) | done right by Qt 6, GTK 4; deliberately skipped by JUCE (~1 MB cap, "a pain in the *ss"), sokol (detected, not implemented), Uno (read-only) |
| Async MIME-stream clipboard | GTK 4 (with delayed rendering) is the model |
| Wayland MIME mapping | wxWidgets needs an X11-atom→Wayland-MIME table (wxGTKGetAltWaylandFormat) |
| Text-only clipboard | Slint, Flutter (Linux text/plain only) — DnD/rich formats punted |
| No portable drag-source | SDL3 (drop-target only); DnD is plugin territory for Flutter |
DnD is consistently the weakest corner: no surveyed minimal/windowing library offers a portable drag source, and frameworks that have it inherit it from the wrapped native toolkit.
Dimension 9 — Escape hatches
Universal: every toolkit exposes the native handle. The fork is how typed it is, and whether raw native-event passthrough exists. The consensus modern shape is raw-window-handle-style typed handoff for GPU interop.
| Escape-hatch shape | Subjects |
|---|---|
Typed raw-window-handle (rwh 0.6) | Winit, Slint (caller bridges in Smithay) |
| Typed per-platform accessors | GLFW (glfwGet*Window), Qt 6 (QNativeInterface), Avalonia (IMacOSTopLevelPlatformHandle) |
| ABI-stable property/struct lookup | SDL3 (properties bag), sokol (sapp_get_swapchain value struct) |
Type-erased void* | sokol, JUCE ("no guarantees what you'll get back") |
| Literal native window object | MAUI (handler.PlatformView), Flutter (HWND/NSView/GtkWidget) |
| Raw message-pump passthrough | SDL3 (SetWindowsMessageHook/SetX11EventHook), Qt 6 (native event filter), Avalonia/Uno/wxWidgets/JUCE (WndProc); Winit (Win32 msg_hook); GLFW has none |
| Capability probing (not a fat interface) | Avalonia (TryGetFeature<T>), Qt 6 (hasCapability), Slint (WindowAdapter trait), Chromium Ozone (mix-in extension interfaces) |
The strongest recorded pattern is naming the leak: Chromium Ozone's X11Extension and Avalonia's [PrivateApi]/TryGetFeature<T> document exactly where the abstraction is incomplete instead of pretending completeness — the opposite of JUCE's un-typed void*.
Dimension 10 — History, redesigns & known regrets
The field's collective scar tissue, and the most direct input to a new design:
| Library | Defining redesign / standing regret |
|---|---|
| Winit | Event Loop 2.0 (iterator → callback, 2018-20); 0.30 ApplicationHandler; recurring renames |
| SDL3 | main-callbacks model; properties bag replaces typed getters; gestures removed (no replacement) |
| GLFW | Wayland 3.2 (2016), libdecor 3.4 (2024); IME still omitted (issue #41 since 2013) |
| sokol | window/swapchain decoupled (2025); MTKView → CAMetalLayer+CADisplayLink (2026); Wayland open since 2020 |
| Qt 6 | QPA "Lighthouse" (2012); high-DPI always-on in 6.0; QPA contract kept \internal |
| GTK 4 | GdkWindow removed → one GdkSurface per toplevel (4.0); Wayland-first protocol expansion |
| Flutter | GLFW prototype → GObject GTK 3; multi-view (2022-23); multi-window (2024-25, settling); GTK 3 pin |
| Chromium Ozone | "Interfaces not ifdefs" (2013-16); dedicated Wayland fd thread (2021); legacy X11 removed (2021) |
| Avalonia | Dispatcher rewrite (2023, WPF model); platform interfaces marked [PrivateApi] (v11); Wayland open since 2017 (#1243) |
| MAUI | Xamarin.Forms renderers → handler/PropertyMapper (2022); Linux desktop repeatedly declined |
| Uno | from-scratch X11/Win32 hosts replace GTK+3 (5.2, 2024, −200 MB); multi-window (5.2) |
| Slint | winit 0.30 forced ApplicationHandler + lazy WinitWindowOrNone (1.7); winit default everywhere (1.16) |
| wxWidgets | GTK1→2→3 migrations; GTK4 still not shipping (no --with-gtk=4); own wl_display for pointer-warp |
| JUCE | Direct2D backend (JUCE 8, 2024); X11-only on Linux (Wayland gap since 2019, #549) |
| Smithay | rewritten onto wayland-rs 0.30+ handler-trait model (~2020); delegate_dispatch2! collapse (2025-26) |
The two regrets that every verdict converges on: (a) deferring native Wayland inherits XWayland's ceiling (no fractional scale, no frame-callback vsync, no xdg_popup grabs) — voiced by Avalonia (8-year delay), JUCE, Uno, sokol, MAUI; and (b) under-specifying the windowing contract (pushing title/decoration/popups/fullscreen onto embedders) yields "silently unsupported per platform" — voiced by Flutter and MAUI.
Part 3 — The consensus standards
Where the field is unanimous, a new toolkit should simply adopt the standard and move on. The survey establishes these:
- xkbcommon for keymaps on Linux. Every native Linux backend uses xkbcommon for scancode→keysym translation; the toolkits that ship their own table (Avalonia, JUCE, wxWidgets) are explicitly recorded as inheriting layout bugs. Corollary standards: the
+8evdev→xkb keycode offset and client-side key-repeat synthesis on Wayland. - GUI on the main thread. Forced by macOS AppKit and adopted uniformly (§Dimension 7); rendering goes off-thread, cross-thread work marshals back through a dispatcher.
raw-window-handle-style typed handle handoff for windowing↔rendering decoupling. Winit pioneered drawing nothing and handing a typed handle to the GPU layer; it is now the reference shape (Slint, and the pattern JUCE'svoid*is critiqued against).- The
axis_value120/WHEEL_DELTA=120high-resolution scroll accumulator — the 1/120-detent convention shared verbatim between Wayland and Win32, accumulated per frame (SDL3, Smithay, GLFW, Qt 6, GTK 4). - Per-Monitor-V2 DPI awareness +
WM_DPICHANGED(scale = dpi/96) on Win32, andwp_fractional_scale_v1+viewporteron Wayland, as the scale sources. - "SSD is only a hint." Per the
xdg-decorationprotocol there is no reliable way to force or forbid server-side decorations, so every serious toolkit must carry a CSD path (§Dimension 4) — the lesson GNOME forced on the ecosystem. - Model the Wayland window lifecycle as explicit async state — the no-buffer-no-window handshake (commit-without-buffer → wait for configure → ack → attach buffer) cannot be hidden behind a synchronous "create then show" without fragility.
- The Win32
SetTimer-driven live-resize workaround for the modal resize loop — identical in spirit across every Win32 backend.
These are not decisions; they are table stakes.
Part 4 — The architectural trade-offs (where the field forks)
Four genuine forks, each presented as a decision with the strongest argument for each side drawn from the evidence.
Fork A — Poll vs callback event loop
| Side | Strongest argument (from the evidence) |
|---|---|
| Poll (app-owned) | The smallest honest API; the app stays in control and can add its own fd sources. GLFW's PollEvents/WaitEvents/PostEmptyEvent is "the smallest honest event-loop API." |
| Callback (library-owned) | The only model that survives Web/iOS/macOS, where the OS owns the loop. Winit abandoned its poll_events() iterator for exactly this reason; SDL3 added opt-in SDL_MAIN_USE_CALLBACKS to run where the OS owns the loop without #ifdefs. |
Resolution the evidence points to: the hybrid (SDL3) — a dead-simple app-owned pump as the default plus an opt-in inverted callback model — is the design most verdicts praise, because the callback model is mandatory on some platforms but the poll model is the more ergonomic default everywhere else. The deeper requirement, voiced by GLFW and sokol's verdicts and the readiness concept, is an external-fd integration point so an async runtime and the windowing loop can share the process's blocking wait — Winit's calloop/AsFd story is the reference; its absence in GLFW/SDL3 is the recorded gap.
Fork B — Logical vs physical native coordinates
| Side | Strongest argument (from the evidence) |
|---|---|
| Physical (device pixels) | No hidden conversion; the app sees exactly what the GPU rasterizes and converts itself. Winit's PhysicalSize/PhysicalPosition are the API-native unit; its surface_size_writer lets the app rewrite the post-rescale physical size in-callback. |
| Logical (DIPs) | The app lays out resolution-independently; physical pixels appear only at the OS seam. The majority of frameworks (Qt 6, GTK 4, Avalonia, JUCE, Slint, MAUI) choose this, converting at the QPA-style seam (QHighDpiScaling). |
| Mixed (per-platform unit) | Honest to each OS but inconsistent. SDL3 uses each platform's native unit and exposes scale rather than hiding it (distinct pixel-density / content-scale / display-scale with guaranteed change events); GLFW splits window-size vs framebuffer-size. |
Resolution the evidence points to: for an application framework, logical is the majority and the ergonomic default — but the universal corollary, learned the hard way across the survey, is never size the render target off the logical window size; size it off the physical/framebuffer size (or the pixel-size-changed event), or you render blurry. The cleanest cure for the created-at-wrong-scale transient is Slint's "learn the scale before first paint."
Fork C — Wayland decorations: libdecor vs own-drawn CSD vs punt
| Side | Strongest argument (from the evidence) |
|---|---|
| libdecor (delegate CSD) | No per-toolkit decoration maintenance; native-looking frames via the desktop's plugin. Chromium Ozone's verdict: "Don't reimplement CSD by hand if you can integrate libdecor — Chromium's own-drawn path is a maintenance tax." SDL3, GLFW, Smithay take this route. |
| Own-drawn CSD | Consistent look, no C dependency, full control. Winit (sctk-adwaita, no libdecor C dep), Qt 6 (predates libdecor), GTK 4, Chromium Ozone draw their own — but Qt 6's own verdict concedes its plugin approach "now diverges from the desktop." |
| Punt (delegate to GTK / X11-only) | Zero Wayland decoration code at all. Flutter/wxWidgets delegate to GTK; Avalonia/Uno/JUCE/sokol are X11-only and inherit the WM's frame. The cost: XWayland's ceiling (§Dimension 10 regret (a)). |
Resolution the evidence points to: carry a CSD path regardless (SSD is only a hint), and prefer libdecor over hand-rolled CSD unless you have a strong reason for a bespoke frame — multiple verdicts (Chromium Ozone, Qt 6) treat own-drawn CSD as a tax that drifts from the desktop. Smithay + libdecor's three-tier model (SSD → minimal FallbackFrame → libdecor) is the reference fallback chain.
Fork D — Own the windowing layer vs delegate to native
| Side | Strongest argument (from the evidence) |
|---|---|
| Own the pixels (draw everything, OS window is just a surface) | Full control of frame pacing, raw pointer, inline pre-edit, Wayland popups, custom chrome — none of which a wrapper can add. Avalonia, Flutter, Uno, Slint, and winit-based stacks fill exactly this niche; MAUI's own verdict says a wrapper "cannot get there." |
| Delegate to native (wrap the platform toolkit/controls) | Native fidelity, accessibility, and IME "for free"; far less code. wxWidgets/MAUI wrap native controls; Flutter/wxWidgets delegate Linux to GTK to reuse its mature Wayland/IME/clipboard stack. |
The cost of delegating, recorded repeatedly: you are perpetually a step behind on the display server (wxWidgets is "perpetually a step behind on Wayland and must hack around its own dependency"), you inherit every native quirk, and capabilities silently no-op where the wrapped stack lacks them (MAUI's size/position/decorations work on only two of four platforms). The cost of owning: you must build Wayland, IME, frame pacing, and popup grabs yourself — which is precisely the gap analysed next.
Resolution the evidence points to: for a toolkit that wants modern frame pacing, raw pointer, inline pre-edit, Wayland-native popups, and custom chrome on every OS, owning the windowing layer is the only path — but it must own the CSD layer, the popup/grab model, and the frame-clock from day one, not defer them (the Avalonia/winit stance the verdicts endorse).
Part 5 — The delta table
Where a brand-new from-scratch windowing framework stands today against the field's consensus, per dimension — the gap analysis that bridges into recommendations. "Field consensus" is the standard from Part 3 and the resolution from Part 4; "Gap for a new framework" is the work that consensus implies a green-field toolkit must do.
| Dimension | Field consensus / best-in-class | Gap for a new framework (build this) | Reference to steal from |
|---|---|---|---|
| Window lifecycle | Async no-buffer-no-window state on Wayland; sync map on X11/Win32 | Model lifecycle as explicit async state from day one; one async present() entry | GTK 4 gdk_toplevel_present, Smithay |
| Event loop | Hybrid poll+callback; external-fd integration; Win32 SetTimer live-resize | A pollable-fd loop (calloop-style) that an async runtime can drive | Winit calloop/AsFd, SDL3 hybrid |
| Input — keymaps | xkbcommon + +8 offset + client-side repeat | Use xkbcommon (don't ship your own table); synthesise Wayland key repeat | Chromium Ozone, Smithay |
| Input — IME | Separate pre-edit event stream; own the consumer; IMM32 on Win32, text_input_v3 on WL | Own zwp_text_input_v3 + NSTextInputClient + IMM32 (TSF is the open opportunity) | SDL3, Winit |
| Wayland specifics | Native client; SSD-is-a-hint + CSD path; fractional scale; popups | Build a native Wayland client (the field's biggest gap) — libdecor CSD, fractional scale | Smithay + libdecor (authority) |
| DPI / scaling | Logical native unit; learn scale before first paint; re-fire on migration | Protocol-driven scale (not heuristic Xft.dpi); size render target off physical size | Slint, Winit surface_size_writer |
| Multi-window / popups | First-class window handles; both override-redirect and xdg_popup popup paths | A popup primitive that maps onto both servers (not "place at (x,y)"); avoid in-canvas-only | SDL3, Smithay XdgPositioner |
| Threading | GUI = main thread (AppKit-forced); render off-thread; marshal back | Adopt main-thread rule; provide a dispatcher for cross-thread marshalling | Avalonia two-layer Dispatcher |
| Clipboard / DnD | Async MIME-typed transfer; INCR chunking; portable drag source (rare) | Do INCR/async clipboard right; provide a real drag-source (the field's weakest corner) | GTK 4 async MIME-stream clipboard |
| Escape hatches | Typed raw-window-handle handoff; capability probing; name the leaks | Typed handle accessor + TryGetFeature-style probe; raw message-pump hook | Winit rwh, Avalonia TryGetFeature |
| Frame pacing (cross-cutting) | Per-platform vsync source folded into one frame clock; CADisplayLink plan | One frame-clock abstraction over wl_surface.frame/CVDisplayLink→CADisplayLink/DXGI vblank | GTK 4 GdkFrameClock, Slint FrameThrottle |
The single highest-leverage finding for a new framework: native Wayland is the field's biggest collective gap (only ~6 of 15 even attempt it, and the universal regret is having deferred it), so on Linux a green-field toolkit's strongest differentiator is to be Wayland-first — treating X11 as the legacy path, owning the CSD/popup/frame-clock layers itself, and using Smithay + libdecor as the implementation authority. This is a Linux-backend stance within an otherwise cross-platform design (Win32 and AppKit are first-class, not afterthoughts); the concrete, milestoned plan for that build-out is recommendations.
Sources
- Shared vocabulary: concepts (decorations, coordinates, scale, IME, popups, the loop) — every term cross-linked above is defined and grounded there.
- Per-subject deep-dives: Winit, SDL3, GLFW, sokol, Qt 6, GTK 4, Flutter Engine, Chromium Ozone, Avalonia, .NET MAUI, Uno Platform, Slint, wxWidgets, JUCE, Smithay SCTK + libdecor — each carries its own primary-source citations.
- Cross-tree siblings: the async-io survey (the readiness loop concepts the windowing loop shares), the ui-layout catalog (the rendering/layout layers that sit on top of a windowing substrate), and Sparkles' Design by Introspection guideline (the capability-trait ethos Slint's
WindowAdaptermirrors). - Survey index: the window-system-integration index; the design plan this synthesis bridges into: recommendations.