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}