Skip to main content

win_win/
window.rs

1#![allow(non_snake_case)]
2
3use std::ffi::OsStr;
4use std::mem;
5use std::ptr::{null, null_mut};
6use std::rc::Rc;
7
8use winapi::ctypes::c_int;
9use winapi::shared::minwindef::{ATOM, DWORD, HINSTANCE, LPARAM, LPVOID, LRESULT, UINT, WPARAM};
10use winapi::shared::windef::{HBRUSH, HCURSOR, HICON, HMENU, HWND};
11use winapi::um::winnt::LPCWSTR;
12use winapi::um::winuser::{
13    CreateWindowExW, DefWindowProcW, GetWindowLongPtrW, RegisterClassExW, SetWindowLongPtrW,
14    CREATESTRUCTW, CW_USEDEFAULT, GWLP_USERDATA, WM_CREATE, WM_NCDESTROY, WNDCLASSEXW,
15};
16
17use wio::wide::ToWide;
18
19use crate::error::Error;
20
21/// A Rust wrapper for the winapi "window procedure".
22///
23/// See the Microsoft documentation on [Window Procedures] for more information. The details of
24/// the window procedure are up to the application, though this wrapper does a bit of lifetime
25/// management for the trait object, dropping it on [`WM_NCDESTROY`].
26///
27/// The window procedure will only be called from the message loop of the thread on which it was
28/// created, which is why there is no `Sync` or `Send` bound on the trait object. However, it is
29/// definitely possible for it to be called [reentrantly], which is a primary reason the method is
30/// `&self`. Common ways to observe reentrant calls include:
31///
32/// * Calling [`DestroyWindow`].
33///
34/// * Calling [`SendMessage`].
35///
36/// * Calling a synchronous dialog, including a file dialog.
37///
38/// [Window Procedures]: https://docs.microsoft.com/en-us/windows/win32/winmsg/window-procedures
39/// [`DestroyWindow`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-destroywindow
40/// [`SendMessage`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessage
41/// [reentrantly]: https://www-user.tu-chemnitz.de/~heha/viewchm.php/hs/petzold.chm/petzoldi/ch03c.htm
42/// [`WM_NCDESTROY`]: https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-ncdestroy
43pub trait WindowProc {
44    /// The Rust-side implementation of the window procedure.
45    ///
46    /// When return value is `None`, [`DefWindowProc`] is called.
47    ///
48    /// [`DefWindowProc`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowprocw
49    fn window_proc(&self, hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM)
50        -> Option<LRESULT>;
51}
52
53/// A window class.
54pub enum WindowClass {
55    Atom(ATOM),
56    Name(Vec<u16>),
57}
58
59/// A builder for registering new window classes.
60pub struct WindowClassBuilder {
61    style: UINT,
62    cbWndExtra: c_int,
63    hInstance: HINSTANCE,
64    hIcon: HICON,
65    hCursor: HCURSOR,
66    hbrBackground: HBRUSH,
67    menu_name: Vec<u16>,
68    class_name: Vec<u16>,
69    hIconSm: HICON,
70}
71
72/// A builder for creating new windows.
73pub struct WindowBuilder<'a> {
74    window_proc: Rc<Box<dyn WindowProc>>,
75    dwExStyle: DWORD,
76    window_class: &'a WindowClass,
77    window_name: Vec<u16>,
78    dwStyle: DWORD,
79    x: c_int,
80    y: c_int,
81    nWidth: c_int,
82    nHeight: c_int,
83    hWndParent: HWND,
84    hMenu: HMENU,
85    hInstance: HINSTANCE,
86}
87
88impl<'a> WindowBuilder<'a> {
89    /// Create a new window builder.
90    ///
91    /// The window procedure and window class are set here.
92    ///
93    /// Discussion question: would it ever make sense to create a window
94    /// without a window procedure?
95    pub fn new(
96        window_proc: impl WindowProc + 'static,
97        window_class: &WindowClass,
98    ) -> WindowBuilder {
99        WindowBuilder {
100            window_proc: Rc::new(Box::new(window_proc)),
101            dwExStyle: 0,
102            window_class,
103            window_name: Vec::new(),
104            dwStyle: 0,
105            x: CW_USEDEFAULT,
106            y: CW_USEDEFAULT,
107            nWidth: CW_USEDEFAULT,
108            nHeight: CW_USEDEFAULT,
109            hWndParent: null_mut(),
110            hMenu: null_mut(),
111            hInstance: null_mut(),
112        }
113    }
114
115    /// Build a window.
116    ///
117    /// The return value is the HWND for the window, or 0 on error.
118    ///
119    /// The lifetime of the window is until `WM_NCDESTROY` is called,
120    /// at which point the window procedure is dropped.
121    ///
122    /// [`WM_NCDESTROY`]: https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-ncdestroy
123    pub fn build(self) -> HWND {
124        unsafe {
125            let wnd_proc_ptr = Rc::into_raw(self.window_proc) as LPVOID;
126            let hwnd = CreateWindowExW(
127                self.dwExStyle,
128                self.window_class.as_lpcwstr(),
129                pointer_or_null(&self.window_name),
130                self.dwStyle,
131                self.x,
132                self.y,
133                self.nWidth,
134                self.nHeight,
135                self.hWndParent,
136                self.hMenu,
137                self.hInstance,
138                wnd_proc_ptr,
139            );
140            if hwnd.is_null() {
141                std::mem::drop(Rc::from_raw(wnd_proc_ptr));
142            }
143            hwnd
144        }
145    }
146
147    /// Set the window name.
148    ///
149    /// This becomes the `lpWindowName` parameter to [`CreateWindowEx`].
150    ///
151    /// [`CreateWindowEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
152    pub fn name(mut self, name: impl AsRef<OsStr>) -> Self {
153        self.window_name = name.to_wide_null();
154        self
155    }
156
157    /// Set the window style.
158    ///
159    /// The argument is the bitwise OR of a number of `WS_` values from the [Window Styles] enumeration.
160    /// It becomes the `dwStyle` parameter to [`CreateWindowEx`].
161    ///
162    /// [`CreateWindowEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
163    /// [Window Styles]: https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles
164    pub fn style(mut self, style: DWORD) -> Self {
165        self.dwStyle = style;
166        self
167    }
168
169    /// Set the extended window style.
170    ///
171    /// The argument is the bitwise OR of a number of `WS_EX` values from the [Extended Window Styles] enumeration.
172    /// It becomes the `dwExStyle` parameter to [`CreateWindowEx`].
173    ///
174    /// An interesting parameter is `WS_EX_NOREDIRECTIONBITMAP`, which disables the redirection bitmap.
175    /// It is useful to set when the window will contain a swapchain and no GDI content (in particular, no
176    /// menus). There is a particular source of artifacting on window resize that is reduced when the
177    /// redirection bitmap is disabled. It should almost always be set when using DirectComposition,
178    /// see this [article by Kenny Kerr].
179    ///
180    /// [`CreateWindowEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
181    /// [Extended Window Styles]: https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
182    /// [article by Kenny Kerr]: https://docs.microsoft.com/en-us/archive/msdn-magazine/2014/june/windows-with-c-high-performance-window-layering-using-the-windows-composition-engine
183    pub fn ex_style(mut self, style: DWORD) -> Self {
184        self.dwExStyle = style;
185        self
186    }
187
188    /// Set the window position.
189    ///
190    /// The arguments become the `x` and `y` parameters to [`CreateWindowEx`]. To set one but not the other,
191    /// use `CW_USEDEFAULT`. These are in raw pixel values.
192    ///
193    /// The position is relative to the top left corner of the primary monitor. See [`EnumDisplayMonitors`]
194    /// for more information about multiple monitors.
195    ///
196    /// [`CreateWindowEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
197    /// [`EnumDisplayMonitors`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors
198    pub fn position(mut self, x: c_int, y: c_int) -> Self {
199        self.x = x;
200        self.y = y;
201        self
202    }
203
204    /// Set the window size.
205    ///
206    /// The arguments become the `nWidth` and `nHeight` parameters to [`CreateWindowEx`]. To set one but not
207    /// the other, use `CW_USEDEFAULT`. These are in raw pixel values.
208    ///
209    /// [`CreateWindowEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
210    pub fn size(mut self, width: c_int, height: c_int) -> Self {
211        self.nWidth = width;
212        self.nHeight = height;
213        self
214    }
215
216    /// Set the parent window.
217    ///
218    /// The argument becomes the `hWndParent` parameter to [`CreateWindowEx`].
219    ///
220    /// # Safety
221    ///
222    /// The argument must be a valid HWND reference.
223    ///
224    /// [`CreateWindowEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
225    pub unsafe fn parent_hwnd(mut self, parent: HWND) -> Self {
226        self.hWndParent = parent;
227        self
228    }
229
230    /// Set the menu.
231    ///
232    /// The argument becomes the `hMenu` parameter to [`CreateWindowEx`].
233    ///
234    /// # Safety
235    ///
236    /// The argument must be a valid HMENU reference.
237    ///
238    /// [`CreateWindowEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
239    pub unsafe fn menu(mut self, menu: HMENU) -> Self {
240        self.hMenu = menu;
241        self
242    }
243
244    /// Set the instance handle.
245    ///
246    /// The argument becomes the `hInstance` parameter to [`CreateWindowEx`].
247    ///
248    /// Instance handles are a namespace mechanism, so that components (in a DLL, for example) don't
249    /// interfere with each other. For a top-level application, it is safe to leave this unset.
250    ///
251    /// [Raymond Chen's blog](https://devblogs.microsoft.com/oldnewthing/20040614-00/?p=38903) has
252    /// a bit of information about HINSTANCE, including its historical distinction from HMODULE
253    /// (they are now the same).
254    ///
255    /// # Safety
256    ///
257    /// The argument must be a valid HINSTANCE reference.
258    ///
259    /// [`CreateWindowEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
260    pub unsafe fn instance(mut self, instance: HINSTANCE) -> Self {
261        self.hInstance = instance;
262        self
263    }
264}
265
266#[cfg(target_arch = "x86_64")]
267type WindowLongPtr = winapi::shared::basetsd::LONG_PTR;
268#[cfg(target_arch = "x86")]
269type WindowLongPtr = winapi::shared::ntdef::LONG;
270
271unsafe extern "system" fn raw_window_proc(
272    hwnd: HWND,
273    msg: UINT,
274    wparam: WPARAM,
275    lparam: LPARAM,
276) -> LRESULT {
277    if msg == WM_CREATE {
278        let create_struct = &*(lparam as *const CREATESTRUCTW);
279        let window_state_ptr = create_struct.lpCreateParams;
280        SetWindowLongPtrW(hwnd, GWLP_USERDATA, window_state_ptr as WindowLongPtr);
281    }
282    let window_proc_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *const Box<dyn WindowProc>;
283    let result = {
284        if window_proc_ptr.is_null() {
285            None
286        } else {
287            // Hold a reference for the duration of the call, in case there's a
288            // reentrant call to WM_NCDESTROY (as would happen if the window
289            // procedure called DestroyWindow).
290            let reference = Rc::from_raw(window_proc_ptr);
291            mem::forget(reference.clone());
292            (*window_proc_ptr).window_proc(hwnd, msg, wparam, lparam)
293        }
294    };
295
296    if msg == WM_NCDESTROY && !window_proc_ptr.is_null() {
297        SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
298        mem::drop(Rc::from_raw(window_proc_ptr));
299    }
300    result.unwrap_or_else(|| DefWindowProcW(hwnd, msg, wparam, lparam))
301}
302
303impl WindowClass {
304    /// A builder for creating a new window class.
305    ///
306    /// The class name should be unique, otherwise creation will fail.
307    pub fn builder(class_name: impl AsRef<OsStr>) -> WindowClassBuilder {
308        WindowClassBuilder {
309            class_name: class_name.to_wide_null(),
310            style: 0,
311            cbWndExtra: 0,
312            hInstance: null_mut(),
313            hIcon: null_mut(),
314            hCursor: null_mut(),
315            hbrBackground: null_mut(),
316            menu_name: Vec::new(),
317            hIconSm: null_mut(),
318        }
319    }
320
321    /// Create a window class reference from a name.
322    ///
323    /// This function is useful if the window class has already been registered, either
324    /// through a successful builder or some other means.
325    pub fn from_name(class_name: impl AsRef<OsStr>) -> WindowClass {
326        WindowClass::Name(class_name.to_wide_null())
327    }
328
329    fn as_lpcwstr(&self) -> LPCWSTR {
330        match self {
331            WindowClass::Atom(atom) => *atom as LPCWSTR,
332            WindowClass::Name(name) => name.as_ptr(),
333        }
334    }
335}
336
337impl WindowClassBuilder {
338    /// Create the window class.
339    ///
340    /// Note: the window class is leaked, as its lifetime is most commonly that of
341    /// the application. Somebody who really wants to reclaim that memory can call
342    /// [`UnregisterClass`] manually and deal with the soundness consequences.
343    ///
344    /// [`UnregisterClass`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterclassw
345    pub fn build(self) -> Result<WindowClass, Error> {
346        unsafe {
347            let wnd = WNDCLASSEXW {
348                cbSize: mem::size_of::<WNDCLASSEXW>() as u32,
349                style: self.style,
350                lpfnWndProc: Some(raw_window_proc),
351                cbClsExtra: 0,
352                cbWndExtra: 0,
353                hInstance: self.hInstance,
354                hIcon: self.hIcon,
355                hCursor: self.hCursor,
356                hbrBackground: self.hbrBackground,
357                lpszMenuName: pointer_or_null(&self.menu_name),
358                lpszClassName: self.class_name.as_ptr(),
359                hIconSm: self.hIconSm,
360            };
361            // TODO: probably should be RegisterClassExW so we can set small icon
362            let class_atom = RegisterClassExW(&wnd);
363            if class_atom == 0 {
364                // This should probably be GetLastError.
365                Err(Error::RegisterClassFailed)
366            } else {
367                Ok(WindowClass::Atom(class_atom))
368            }
369        }
370    }
371
372    /// Set the window class style.
373    ///
374    /// The argument is the bitwise OR of a number of `CS_` values from the [Window Class Styles] enumeration.
375    /// It becomes `style` field in the [`WNDCLASSEX`] passed to [`RegisterClassEx`]. See [Class Styles] for
376    /// more explanation.
377    ///
378    /// [`RegisterClassEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw
379    /// [`WNDCLASSEX`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw
380    /// [Class Styles]: https://docs.microsoft.com/en-us/windows/win32/winmsg/about-window-classes#class-styles
381    /// [Window Class Styles]: https://docs.microsoft.com/en-us/windows/win32/winmsg/window-class-styles
382    pub fn class_style(mut self, style: DWORD) -> Self {
383        self.style = style;
384        self
385    }
386
387    /// Allocate extra bytes in window instances.
388    ///
389    /// The argument becomes the `cbWndExtra` field in the [`WNDCLASSEX`] passed to [`RegisterClassEx`].
390    ///
391    /// Generally this isn't that useful unless creating a dialog, in which case it should be
392    /// [`DLGWINDOWEXTRA`](#associatedconstant.DLGWINDOWEXTRA).
393    ///
394    /// Note: there is no corresponding method to set `cbClsExtra`, as I can't think of a good reason
395    /// why it would ever be needed.
396    ///
397    /// # Safety
398    ///
399    /// The argument must be a reasonable size (no idea what happens if negative, for example).
400    ///
401    /// [`RegisterClassEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw
402    /// [`WNDCLASSEX`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw
403    pub unsafe fn wnd_extra_bytes(mut self, extra_bytes: c_int) -> Self {
404        self.cbWndExtra = extra_bytes;
405        self
406    }
407
408    /// Set the instance handle.
409    ///
410    /// The argument becomes the `hInstance` field in the [`WNDCLASSEX`] passed to [`RegisterClassEx`].
411    ///
412    /// See the [`instance`](struct.WindowBuilder.html#method.instance) method on `WindowBuilder` for
413    /// more details.
414    ///
415    /// # Safety
416    ///
417    /// The argument must be a valid HINSTANCE reference.
418    ///
419    /// [`RegisterClassEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw
420    /// [`WNDCLASSEX`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw
421    pub unsafe fn instance(mut self, instance: HINSTANCE) -> Self {
422        self.hInstance = instance;
423        self
424    }
425
426    /// Set the icon.
427    ///
428    /// The argument becomes the `hIcon` field in the [`WNDCLASSEX`] passed to [`RegisterClassEx`].
429    ///
430    /// # Safety
431    ///
432    /// The argument must be a valid HICON reference.
433    ///
434    /// [`RegisterClassEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw
435    /// [`WNDCLASSEX`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw
436    pub unsafe fn icon(mut self, icon: HICON) -> Self {
437        self.hIcon = icon;
438        self
439    }
440
441    /// Set the small icon.
442    ///
443    /// The argument becomes the `hIconSm` field in the [`WNDCLASSEX`] passed to [`RegisterClassEx`].
444    ///
445    /// # Safety
446    ///
447    /// The argument must be a valid HICON reference.
448    ///
449    /// [`RegisterClassEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw
450    /// [`WNDCLASSEX`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw
451    pub unsafe fn small_icon(mut self, icon: HICON) -> Self {
452        self.hIconSm = icon;
453        self
454    }
455
456    /// Set the cursor.
457    ///
458    /// The argument becomes the `hCursor` field in the [`WNDCLASSEX`] passed to [`RegisterClassEx`].
459    ///
460    /// The default implementation of [`WM_SETCURSOR`] applies this cursor. In the old-school approach
461    /// where each control has its own HWND, it's reasonable to use this to set the cursor, then
462    /// everything should just work (even without explicit handling of `WM_SETCURSOR`). However, in
463    /// the modern approach where there's a single window for the application, probably a more useful
464    /// strategy is to set the cursor on `WM_MOUSEMOVE`, which reports the cursor position (rather than
465    /// relying on hit testing with the HWND bounds). In that case, setting a default cursor on the
466    /// window will likely result in flashing, as the two window message handlers will compete.
467    ///
468    /// This [Stack overflow question](https://stackoverflow.com/questions/19257237/reset-cursor-in-wm-setcursor-handler-properly)
469    /// contains more details.
470    ///
471    /// Of course, if the entire window is to have a single cursor, setting it here is quite reasonable.
472    ///
473    /// # Safety
474    ///
475    /// The argument must be a valid HCURSOR reference.
476    ///
477    /// [`RegisterClassEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw
478    /// [`WNDCLASSEX`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw
479    /// [`WM_SETCURSOR`]: https://docs.microsoft.com/en-us/windows/win32/menurc/wm-setcursor
480    /// [`WM_MOUSEMOVE`]: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousemove
481    pub unsafe fn cursor(mut self, cursor: HCURSOR) -> Self {
482        self.hCursor = cursor;
483        self
484    }
485
486    /// Set the background brush.
487    ///
488    /// The argument becomes the `hBrBackground` field in the [`WNDCLASSEX`] passed to [`RegisterClassEx`].
489    ///
490    /// # Safety
491    ///
492    /// The argument must be a valid HBRUSH reference.
493    ///
494    /// [`RegisterClassEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw
495    /// [`WNDCLASSEX`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw
496    pub unsafe fn background(mut self, brush: HBRUSH) -> Self {
497        self.hbrBackground = brush;
498        self
499    }
500
501    /// Set the default menu.
502    ///
503    /// The argument becomes the `lpszClassName` field in the [`WNDCLASSEX`] passed to [`RegisterClassEx`].
504    ///
505    /// The string references the resource name of the class menu. There is no mechanism to support
506    /// the MAKEINTRESOURCE macro.
507    ///
508    /// [`RegisterClassEx`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw
509    /// [`WNDCLASSEX`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw
510    pub fn menu_name(mut self, menu_name: impl AsRef<OsStr>) -> Self {
511        self.menu_name = menu_name.to_wide_null();
512        self
513    }
514
515    /// The number of extra bytes needed for dialogs.
516    ///
517    /// See [`wnd_extra_bytes`](#method.wnd_extra_bytes).
518
519    // Note: this arguably should be defined in the winapi crate. In any case,
520    // probably not that important.
521    pub const DLGWINDOWEXTRA: c_int = 30;
522}
523
524/// A convenience function for an optional string, on which an empty slice
525/// returns a null pointer.
526fn pointer_or_null(slice: &[u16]) -> *const u16 {
527    if slice.is_empty() {
528        null()
529    } else {
530        slice.as_ptr()
531    }
532}