Windowing — Windows

Overview

WIP — main lifecycle is stable, the per-monitor DPI path and IME composition window have rough edges. The Windows backend is Win32Window. It uses the Win32 API loaded entirely via dlopen (LoadLibraryA + GetProcAddress) so the binary cross-compiles from macOS without a Win32 SDK present. The struct holds the HWND, HINSTANCE, render mode (GPU via HGLRC or CPU), the embedded event::CommonWindowState, the Win32Libraries function-pointer table, multi-window state (pending_window_creates), the menu bar and active context menu, the DPI helpers, the IME composition string, the tooltip window, and the optional accessibility adapter.

Win32Libraries and dlopen

Win32Libraries declares one struct per DLL — User32, Gdi32, Kernel32, OpenGL32, Imm32, Shcore, UxTheme, Dwmapi — each holding extern "system" function pointers. Win32Libraries::load calls load_dll("user32.dll"), load_dll("gdi32.dll"), etc. and populates each struct. There is no shared DynamicLibrary trait implementation here — the autoreview report flags this as an inconsistency with the Linux backends; load_first_available is unreachable on Windows.

The Win32 type aliases map every opaque handle to *mut c_void:

pub type HWND       = *mut c_void;
pub type HDC        = *mut c_void;
pub type HGLRC      = *mut c_void;
pub type HMENU      = *mut c_void;
pub type HINSTANCE  = *mut c_void;
pub type HMONITOR   = *mut c_void;
pub type HIMC       = *mut c_void;   // IME context handle
pub type WPARAM     = usize;
pub type LPARAM     = isize;
pub type LRESULT    = isize;

Win32 structs (MSG, WNDCLASSW, RECT, POINT, TRACKMOUSEEVENT, COMPOSITIONFORM, etc.) are #[repr(C)] mirrors of the headers in Windows.h. Windows-specific dlopen also exposes encode_wide(s: &str) -> Vec<u16> for converting Rust strings to UTF-16 zero-terminated buffers (every Win32 API takes wide strings).

Window creation

Win32Window::new is the constructor:

  1. Win32Libraries::load() — load every required DLL.
  2. dpi::DpiFunctions::new() — load the DPI APIs (SetProcessDpiAwarenessContext, GetDpiForWindow, AdjustWindowRectExForDpi, etc.). All five of these are version-gated; older Windows falls back to SetProcessDpiAware from XP / Vista.
  3. dpi.set_process_dpi_aware() — register per-monitor V2 DPI awareness so the window won't get bilinearly scaled by the OS.
  4. wcreate::register_window_class(hinstance, window_proc, &win32) — registers the "AzulWindowClass" window class with a null background brush (we paint the entire window with OpenGL or the CPU compositor; letting Windows paint a brush flashes black/white during creation).
  5. wcreate::create_hwnd(hinstance, options, parent, user_data, &win32)CreateWindowExW with WS_OVERLAPPEDWINDOW. The window is not shown yet — ShowWindow is deferred until after the first frame is presented.
  6. wcreate::create_gl_context(hwnd, &win32) — see below.
  7. WebRender + image cache + renderer setup, identical to other backends.
  8. SetWindowLongPtrW(hwnd, GWLP_USERDATA, window_ptr as isize) — stash the *mut Win32Window in the window's user data so WindowProc can recover it.

The deferred ShowWindow call lives in render_and_present after the first SwapBuffers succeeds; this avoids the classic „window appears white then content paints“ flash.

GL context creation — three-step fallback

wcreate::create_gl_context tries three OpenGL versions in order:

  1. OpenGL 3.2 Core via wglCreateContextAttribsARB — preferred. Requires WGL extensions, which require a dummy GL 1.x context to exist first so wglGetProcAddress("wglCreateContextAttribsARB") returns non-null. The dummy context is created on a hidden window, queried for the extension, then destroyed.
  2. OpenGL 3.0 via wglCreateContextAttribsARB — same path, WGL_CONTEXT_MAJOR_VERSION_ARB = 3, MINOR = 0.
  3. Legacy wglCreateContext — produces a 1.x compatibility context. WebRender then runs in compatibility mode.

If all three fail, the path falls through to CPU rendering (RenderMode::Cpu) and the window paints via StretchDIBits — see the cpurender feature flag.

gl::GlFunctions::initialize loads opengl32.dll. The fill closure passed to common::gl_loader::load_gl_context first asks wglGetProcAddress(name); if that returns null, it falls back to GetProcAddress(opengl32_dll, name)wglGetProcAddress is the only way to get GL >= 1.2 functions, but GetProcAddress is the only way to get GL 1.0–1.1 functions, so both lookups are needed.

DPI awareness

DpiFunctions runtime-loads the entire DPI API surface because it spans Windows Vista (SetProcessDPIAware) through Windows 10 1703 (SetProcessDpiAwarenessContext with PER_MONITOR_AWARE_V2). Each Option<fn> is Some only on Windows versions where the symbol exists in user32.dll.

The activation order in Win32Window::new:

if let Some(f) = dpi.set_process_dpi_awareness_context {
    unsafe { f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); }
} else if let Some(f) = dpi.set_process_dpi_awareness {
    unsafe { f(ProcessDpiAwareness::PROCESS_PER_MONITOR_DPI_AWARE); }
} else if let Some(f) = dpi.set_process_dpi_aware {
    unsafe { f(); }
}

Per-window DPI is queried via GetDpiForWindow(hwnd) on each WM_DPICHANGED message; the layout viewport is updated and regenerate_layout fires.

Event loop — WindowProc + main loop

The Win32 message dispatch is split:

  • WindowProc is the extern "system" function registered with the window class. Win32 calls it for every message and re-enters the application during certain APIs (SetWindowPos, MoveWindow, etc.). Inside WindowProc the window pointer is recovered via GetWindowLongPtrW(hwnd, GWLP_USERDATA), and the standard process_window_events() runs after updating the relevant fields on current_window_state.
  • The main loop uses PeekMessageW(PM_REMOVE) to drain pending messages for each window, then WaitMessage() to block when idle (zero CPU when nothing is happening).

Per-message handling lives in WindowProc's match msg {} arms:

  • WM_LBUTTONDOWN / WM_RBUTTONDOWN / WM_MBUTTONDOWN (and _UP). Updates mouse_state.button_down and fires process_window_events.
  • WM_MOUSEMOVE. Updates the cursor position. The first move triggers TrackMouseEvent(TME_LEAVE) so WM_MOUSELEAVE is delivered later.
  • WM_MOUSEWHEEL / WM_MOUSEHWHEEL. Converts to a scroll delta and pushes it to scroll_manager.
  • WM_KEYDOWN / WM_KEYUP / WM_CHAR. Calls win_event::handle_key for VK code translation, then process_window_events.
  • WM_SIZE. Updates current_window_state.size and calls incremental_relayout.
  • WM_DPICHANGED. Refreshes per-window DPI and runs the full regenerate_layout.
  • WM_PAINT. Calls render_and_present() and then ValidateRect.
  • WM_COMMAND (low word equals menu command ID). Looks up CoreMenuCallback in the menu bar's command_id → callback map.
  • WM_CLOSE. Sets is_open = false and returns 0 to defeat the default DestroyWindow.
  • WM_IME_*. Composition string handling. See below.

win_event (adapted from winit, Apache-2.0 licensed) handles the cluster of small Win32 quirks around extended keys, scancode-based disambiguation (left vs right Shift, numeric keypad keys), AltGr emulation on European keyboards, and the dance between WM_KEYDOWN (virtual key code) and WM_CHAR (translated UTF-16 character).

request_redraw calls InvalidateRect(hwnd, NULL, FALSE) followed by UpdateWindow(hwnd) — Win32 will deliver the resulting WM_PAINT before the next PeekMessage returns.

IME — Imm32

The Imm32 library functions are dlopen-loaded alongside the rest. The flow:

  1. WM_IME_STARTCOMPOSITIONImmGetContext(hwnd) returns a HIMC; ImmSetCompositionWindow(himc, &CompositionForm { ... }) positions the candidate window at the caret.
  2. WM_IME_COMPOSITION — preedit string is read with ImmGetCompositionStringW(himc, GCS_COMPSTR, ...). Buffered into ime_composition: Option<String> on the window.
  3. On commit (GCS_RESULTSTR) the result string is fed into process_text_input.
  4. WM_IME_ENDCOMPOSITION — clear ime_composition, ImmReleaseContext(hwnd, himc).

UTF-16 surrogate pairs from WM_CHAR are reassembled via high_surrogate: Option<u16> (set on the high half, consumed on the low half).

Menus

menu::WindowsMenuBar wraps a Win32 HMENU:

  • CreateMenu() for the bar root.
  • For each menu item, AppendMenuW with either MF_STRING | command_id (leaf) or MF_POPUP | submenu_handle (submenu).
  • The unique command IDs come from the global atomic WINDOWS_UNIQUE_COMMAND_ID_GENERATOR; the command_id → CoreMenuCallback map is stored on WindowsMenuBar.

set_menu_bar(hwnd, &WindowsMenuBar) calls SetMenu(hwnd, hmenu). A hash-based diff (menu.hash) skips reconstruction when the menu hasn't changed between layouts.

Context menus go through a separate path: TrackPopupMenu(hmenu, ..., hwnd, NULL) is called in response to a right-click; the resulting WM_COMMAND looks up the callback in Win32Window.context_menu.

Tooltips

TooltipWindow creates a tooltip via the Win32 TOOLTIPS_CLASS window class — a real native tooltip, not a custom popup. TTM_ADDTOOL registers the rect; TTM_TRACKACTIVATE shows it; TTM_TRACKPOSITION moves it on cursor motion. Visuals match the user's Windows theme automatically.

Multi-window registry

The Windows registry is a thread-local BTreeMap<HWND, *mut Win32Window>. The pattern matches the Linux and macOS registries documented on those pages — register_window, unregister_window, get_window, get_all_window_handles. The pointer is leaked via Box::into_raw from the run loop; unregister_window returns the pointer for Box::from_raw cleanup.

The main loop iterates get_all_window_handles() each tick. Multi-window fan-out for popup menus and dialogs flows through Win32Window.pending_window_creates, drained by the main loop after event processing.

Clipboard

The Windows clipboard module uses the clipboard-win crate. set_clipboard(formats::Unicode, &text) on copy; get_clipboard::<String, _>(formats::Unicode) on paste. The manager's clear() is only called on successful write so a transient clipboard-busy error doesn't drop the user's selection.

Accessibility

The Windows accessibility module uses accesskit_windows::SubclassingAdapter when the a11y feature is on. The adapter window-procs the HWND to intercept WM_GETOBJECT(OBJID_CLIENT) and present the AzulRoot accessibility tree to UIA. accesskit_windows::SubclassingAdapter::new is wrapped in catch_unwind for the same reasons as the Linux variant — some COM/UIA initialisation failures panic.

CPU rendering path

When RenderMode::Cpu is active, render_and_present rasterises through cpurender into retained_pixmap: AzulPixmap, converts to BGRA into the cached bgra_buffer: Vec<u8>, and uses StretchDIBits to blit onto the window's HDC. The glyph_cache is held on the window so successive frames reuse rasterised glyphs.

gpu_damage_rects: Vec<LogicalRect> is also tracked in GPU mode — when non-empty, only the listed regions need painting; the WebRender transaction sets the same rects so the GPU compositor can skip unchanged tiles.

system_style and dynamic theming

SystemStyle::detect_windows reads from the registry (HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize) plus DwmGetColorizationColor to populate the azul_css::system::SystemStyle struct: dark/light theme, accent colour, window background, system fonts. The result is plumbed into current_window_state.system_style and influences both :dark / :light CSS pseudo-classes and any system-ui font references.

Theme changes arrive via WM_SETTINGCHANGE with lParam == "ImmersiveColorSet" — the message handler re-runs detection and triggers a full regenerate_layout so theme-conditional CSS re-evaluates.

Known issues / TODOs

  • The system_style reader reads only the Personalize keys; some accent variants (e.g., contrast themes) need additional handling.
  • WM_NCHITTEST for custom titlebar regions is not yet wired — client-side decoration drag handles need this to make windows draggable from non-titlebar regions.
  • WM_INPUT for raw mouse / pen / touch is not yet supported; only WM_MOUSE* and WM_TOUCH are processed.
  • The CPU path's bgra_buffer is cached but not damage-clipped — it always rebuilds the full BGRA from the pixmap each frame.

Coming Up Next

  • Common — Shared shell infrastructure across platforms
  • macOS — macOS shell - Cocoa, AppKit, IME, a11y
  • Menus and CSD — Menus and client-side decorations across platforms
  • Windowing Overview — Per-window aggregate, headless variant, and the platform shell layer

Back to guide index