Skip to main content

windows_window/
window.rs

1use crate::bindings::*;
2use std::sync::OnceLock;
3use windows_core::*;
4
5/// Message handler: receives the raw window handle, message code, and
6/// `wparam`/`lparam`. Return `Some(result)` to handle the message, or `None` to
7/// fall through to default processing.
8type MessageHandler = Box<dyn FnMut(*mut core::ffi::c_void, u32, usize, isize) -> Option<isize>>;
9
10/// Resize handler: receives the new client-area width and height in pixels.
11type ResizeHandler = Box<dyn FnMut(i32, i32)>;
12
13struct State {
14    message: Option<MessageHandler>,
15    resize: Option<ResizeHandler>,
16}
17
18/// A top-level window.
19///
20/// The window lives until it is dropped or closed by the user. Its raw `HWND`
21/// is available via [`Window::hwnd`] for interop with other Windows APIs.
22pub struct Window(HWND);
23
24impl Window {
25    /// Begins configuring a new window with the given title.
26    #[allow(clippy::new_ret_no_self)]
27    pub fn new(title: &str) -> WindowBuilder {
28        WindowBuilder {
29            title: title.to_string(),
30            width: CW_USEDEFAULT,
31            height: CW_USEDEFAULT,
32            style: WS_OVERLAPPEDWINDOW as u32,
33            ex_style: 0,
34            state: State {
35                message: None,
36                resize: None,
37            },
38        }
39    }
40
41    /// Returns the raw window handle for interop with other Windows APIs.
42    pub fn hwnd(&self) -> *mut core::ffi::c_void {
43        self.0
44    }
45
46    /// Returns the current client-area size in pixels as `(width, height)`.
47    pub fn client_size(&self) -> (i32, i32) {
48        let mut rect = RECT::default();
49        unsafe {
50            if GetClientRect(self.0, &mut rect).as_bool() {
51                (rect.right - rect.left, rect.bottom - rect.top)
52            } else {
53                (0, 0)
54            }
55        }
56    }
57}
58
59impl Drop for Window {
60    fn drop(&mut self) {
61        unsafe {
62            if IsWindow(self.0).as_bool() {
63                _ = DestroyWindow(self.0);
64            }
65        }
66    }
67}
68
69/// Builder for a [`Window`].
70pub struct WindowBuilder {
71    title: String,
72    width: i32,
73    height: i32,
74    style: u32,
75    ex_style: u32,
76    state: State,
77}
78
79impl WindowBuilder {
80    /// Sets the initial outer window size, including non-client borders, in pixels.
81    pub fn size(mut self, width: i32, height: i32) -> Self {
82        self.width = width;
83        self.height = height;
84        self
85    }
86
87    /// Sets the window style (`WS_*`). Defaults to `WS_OVERLAPPEDWINDOW`.
88    pub fn style(mut self, style: u32) -> Self {
89        self.style = style;
90        self
91    }
92
93    /// Sets the extended window style (`WS_EX_*`). Defaults to none.
94    pub fn ex_style(mut self, ex_style: u32) -> Self {
95        self.ex_style = ex_style;
96        self
97    }
98
99    /// Sets a handler called for every window message. Return `Some(result)` to
100    /// handle the message, or `None` to fall through to default processing.
101    pub fn on_message<F>(mut self, handler: F) -> Self
102    where
103        F: FnMut(*mut core::ffi::c_void, u32, usize, isize) -> Option<isize> + 'static,
104    {
105        self.state.message = Some(Box::new(handler));
106        self
107    }
108
109    /// Sets a handler called when the client area is resized, with the new
110    /// width and height in pixels.
111    pub fn on_resize<F>(mut self, handler: F) -> Self
112    where
113        F: FnMut(i32, i32) + 'static,
114    {
115        self.state.resize = Some(Box::new(handler));
116        self
117    }
118
119    /// Creates and shows the window.
120    pub fn create(self) -> Result<Window> {
121        unsafe {
122            register_class();
123
124            let mut title: Vec<u16> = self.title.encode_utf16().collect();
125            title.push(0);
126
127            let hwnd = CreateWindowExW(
128                self.ex_style,
129                class_name(),
130                PCWSTR(title.as_ptr()),
131                self.style,
132                CW_USEDEFAULT,
133                CW_USEDEFAULT,
134                self.width,
135                self.height,
136                core::ptr::null_mut(),
137                core::ptr::null_mut(),
138                core::ptr::null_mut(),
139                core::ptr::null(),
140            );
141
142            if hwnd.is_null() {
143                return Err(Error::from_thread());
144            }
145
146            let state = Box::new(self.state);
147            SetWindowLongPtrW(hwnd, GWLP_USERDATA, Box::into_raw(state) as _);
148
149            _ = ShowWindow(hwnd, SW_SHOWNORMAL);
150            Ok(Window(hwnd))
151        }
152    }
153}
154
155/// Runs a blocking, event-driven message loop until the window is closed.
156pub fn run() {
157    unsafe {
158        let mut message = MSG::default();
159        while GetMessageW(&mut message, core::ptr::null_mut(), 0, 0).as_bool() {
160            _ = TranslateMessage(&message);
161            DispatchMessageW(&message);
162        }
163    }
164}
165
166/// Runs a message loop driven by `render`. `render` is called whenever no
167/// messages are pending; return `Ok(true)` to keep rendering immediately (for
168/// continuous animation) or `Ok(false)` to wait for the next message before
169/// rendering again (event-driven, e.g. when occluded or idle). Returns when the
170/// window is closed, or early if `render` returns an error.
171pub fn run_with<F>(mut render: F) -> Result<()>
172where
173    F: FnMut() -> Result<bool>,
174{
175    unsafe {
176        let mut message = MSG::default();
177        let mut animating = true;
178        loop {
179            if animating {
180                while PeekMessageW(&mut message, core::ptr::null_mut(), 0, 0, PM_REMOVE as u32)
181                    .as_bool()
182                {
183                    if message.message == WM_QUIT as u32 {
184                        return Ok(());
185                    }
186                    _ = TranslateMessage(&message);
187                    DispatchMessageW(&message);
188                }
189            } else if GetMessageW(&mut message, core::ptr::null_mut(), 0, 0).as_bool() {
190                if message.message == WM_QUIT as u32 {
191                    return Ok(());
192                }
193                _ = TranslateMessage(&message);
194                DispatchMessageW(&message);
195            } else {
196                return Ok(());
197            }
198            animating = render()?;
199        }
200    }
201}
202
203/// Posts a quit message, causing the message loop to exit.
204pub fn quit() {
205    unsafe { PostQuitMessage(0) };
206}
207
208/// Dispatches all currently-pending messages without blocking, then returns.
209///
210/// Returns `false` if a quit message was received (the caller should stop
211/// pumping) or `true` otherwise. Unlike [`run`], this never blocks waiting for
212/// the next message, so callers can drive the message loop while waiting on an
213/// external condition - for example pumping until an asynchronous callback
214/// completes.
215pub fn pump() -> bool {
216    unsafe {
217        let mut message = MSG::default();
218        while PeekMessageW(&mut message, core::ptr::null_mut(), 0, 0, PM_REMOVE as u32).as_bool() {
219            if message.message == WM_QUIT as u32 {
220                return false;
221            }
222            _ = TranslateMessage(&message);
223            DispatchMessageW(&message);
224        }
225        true
226    }
227}
228
229fn class_name() -> PCWSTR {
230    static NAME: OnceLock<Vec<u16>> = OnceLock::new();
231    let name = NAME.get_or_init(|| "windows-window.Window\0".encode_utf16().collect());
232    PCWSTR(name.as_ptr())
233}
234
235unsafe fn register_class() {
236    static REGISTER: OnceLock<()> = OnceLock::new();
237    REGISTER.get_or_init(|| unsafe {
238        _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
239        let wc = WNDCLASSW {
240            style: (CS_HREDRAW | CS_VREDRAW) as u32,
241            lpfnWndProc: Some(wndproc),
242            hCursor: LoadCursorW(core::ptr::null_mut(), IDC_ARROW),
243            lpszClassName: class_name(),
244            ..Default::default()
245        };
246        RegisterClassW(&wc);
247    });
248}
249
250unsafe extern "system" fn wndproc(
251    hwnd: HWND,
252    message: u32,
253    wparam: WPARAM,
254    lparam: LPARAM,
255) -> LRESULT {
256    unsafe {
257        let state = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut State;
258        let mut handled = None;
259
260        if !state.is_null() {
261            // Detach the handlers before invoking them so that any reentrant
262            // dispatch (e.g. a handler that calls SetWindowPos) sees empty slots
263            // and falls through to default processing rather than aliasing a
264            // handler that is already running.
265            let mut message_handler = (*state).message.take();
266            let mut resize_handler = (*state).resize.take();
267
268            // Handlers are invoked directly, without catch_unwind: a panic that
269            // escapes one unwinds to this extern "system" boundary and aborts the
270            // process rather than crossing into the OS frames that called wndproc.
271            // This is intentional.
272            if let Some(handler) = message_handler.as_mut() {
273                handled = handler(hwnd, message, wparam, lparam);
274            }
275
276            if handled.is_none()
277                && message == WM_SIZE as u32
278                && let Some(handler) = resize_handler.as_mut()
279            {
280                let width = (lparam & 0xffff) as i32;
281                let height = ((lparam >> 16) & 0xffff) as i32;
282                handler(width, height);
283                handled = Some(0);
284            }
285
286            // Invoking a handler can synchronously destroy the window (for
287            // example by calling DestroyWindow, or by letting DefWindowProc
288            // handle WM_CLOSE), in which case a reentrant WM_NCDESTROY has
289            // already freed the state below. Re-read the pointer to detect that
290            // and avoid restoring the handlers into freed memory; the taken
291            // handlers then drop here, which is correct.
292            let state = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut State;
293            if !state.is_null() {
294                (*state).message = message_handler;
295                (*state).resize = resize_handler;
296            }
297        }
298
299        if message == WM_NCDESTROY as u32 {
300            let state = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut State;
301            if !state.is_null() {
302                SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
303                drop(Box::from_raw(state));
304            }
305        }
306
307        if let Some(result) = handled {
308            return result;
309        }
310
311        match message as i32 {
312            WM_DESTROY => {
313                PostQuitMessage(0);
314                0
315            }
316            _ => DefWindowProcW(hwnd, message, wparam, lparam),
317        }
318    }
319}