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:
Win32Libraries::load()— load every required DLL.dpi::DpiFunctions::new()— load the DPI APIs (SetProcessDpiAwarenessContext,GetDpiForWindow,AdjustWindowRectExForDpi, etc.). All five of these are version-gated; older Windows falls back toSetProcessDpiAwarefrom XP / Vista.dpi.set_process_dpi_aware()— register per-monitor V2 DPI awareness so the window won't get bilinearly scaled by the OS.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).wcreate::create_hwnd(hinstance, options, parent, user_data, &win32)—CreateWindowExWwithWS_OVERLAPPEDWINDOW. The window is not shown yet —ShowWindowis deferred until after the first frame is presented.wcreate::create_gl_context(hwnd, &win32)— see below.- WebRender + image cache + renderer setup, identical to other backends.
SetWindowLongPtrW(hwnd, GWLP_USERDATA, window_ptr as isize)— stash the*mut Win32Windowin the window's user data soWindowProccan 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:
- OpenGL 3.2 Core via
wglCreateContextAttribsARB— preferred. Requires WGL extensions, which require a dummy GL 1.x context to exist first sowglGetProcAddress("wglCreateContextAttribsARB")returns non-null. The dummy context is created on a hidden window, queried for the extension, then destroyed. - OpenGL 3.0 via
wglCreateContextAttribsARB— same path,WGL_CONTEXT_MAJOR_VERSION_ARB = 3,MINOR = 0. - 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:
WindowProcis theextern "system"function registered with the window class. Win32 calls it for every message and re-enters the application during certain APIs (SetWindowPos,MoveWindow, etc.). InsideWindowProcthe window pointer is recovered viaGetWindowLongPtrW(hwnd, GWLP_USERDATA), and the standardprocess_window_events()runs after updating the relevant fields oncurrent_window_state.- The main loop uses
PeekMessageW(PM_REMOVE)to drain pending messages for each window, thenWaitMessage()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). Updatesmouse_state.button_downand firesprocess_window_events.WM_MOUSEMOVE. Updates the cursor position. The first move triggersTrackMouseEvent(TME_LEAVE)soWM_MOUSELEAVEis delivered later.WM_MOUSEWHEEL/WM_MOUSEHWHEEL. Converts to a scroll delta and pushes it toscroll_manager.WM_KEYDOWN/WM_KEYUP/WM_CHAR. Callswin_event::handle_keyfor VK code translation, thenprocess_window_events.WM_SIZE. Updatescurrent_window_state.sizeand callsincremental_relayout.WM_DPICHANGED. Refreshes per-window DPI and runs the fullregenerate_layout.WM_PAINT. Callsrender_and_present()and thenValidateRect.WM_COMMAND(low word equals menu command ID). Looks upCoreMenuCallbackin the menu bar'scommand_id → callbackmap.WM_CLOSE. Setsis_open = falseand returns 0 to defeat the defaultDestroyWindow.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:
WM_IME_STARTCOMPOSITION—ImmGetContext(hwnd)returns aHIMC;ImmSetCompositionWindow(himc, &CompositionForm { ... })positions the candidate window at the caret.WM_IME_COMPOSITION— preedit string is read withImmGetCompositionStringW(himc, GCS_COMPSTR, ...). Buffered intoime_composition: Option<String>on the window.- On commit (
GCS_RESULTSTR) the result string is fed intoprocess_text_input. WM_IME_ENDCOMPOSITION— clearime_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,
AppendMenuWwith eitherMF_STRING | command_id(leaf) orMF_POPUP | submenu_handle(submenu). - The unique command IDs come from the global atomic
WINDOWS_UNIQUE_COMMAND_ID_GENERATOR; thecommand_id → CoreMenuCallbackmap is stored onWindowsMenuBar.
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_stylereader reads only thePersonalizekeys; some accent variants (e.g., contrast themes) need additional handling. WM_NCHITTESTfor custom titlebar regions is not yet wired — client-side decoration drag handles need this to make windows draggable from non-titlebar regions.WM_INPUTfor raw mouse / pen / touch is not yet supported; onlyWM_MOUSE*andWM_TOUCHare processed.- The CPU path's
bgra_bufferis 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