diff --git a/src/win/drop_target.rs b/src/win/drop_target.rs index 42661c4f..00262d80 100644 --- a/src/win/drop_target.rs +++ b/src/win/drop_target.rs @@ -43,11 +43,8 @@ impl DropTarget { }; unsafe { - let mut window = crate::Window::new(window_state.create_window()); - let event = Event::Mouse(event); - let event_status = - window_state.handler_mut().as_mut().unwrap().on_event(&mut window, event); + let event_status = window_state.handle_event(event); if let Some(pdwEffect) = pdwEffect { match event_status { diff --git a/src/win/hook.rs b/src/win/hook.rs index 061ee5b4..34a67ff3 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -15,7 +15,8 @@ use windows_sys::Win32::{ }, }; -use crate::win::wnd_proc; +use crate::win::BaseviewWindow; +use crate::wrappers::win32::window::wnd_proc; // track all windows opened by this instance of baseview // we use an RwLock here since the vast majority of uses (event interceptions) @@ -128,7 +129,7 @@ unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool { // check if this is one of our windows. if so, intercept it if HOOK_STATE.read().unwrap().open_windows.contains(&HWNDWrapper(msg.hwnd)) { - let _ = wnd_proc(msg.hwnd, msg.message, msg.wParam, msg.lParam); + let _ = wnd_proc::(msg.hwnd, msg.message, msg.wParam, msg.lParam); return true; } diff --git a/src/win/window.rs b/src/win/window.rs index c2447bad..34fceeb2 100644 --- a/src/win/window.rs +++ b/src/win/window.rs @@ -1,4 +1,4 @@ -use windows_core::{ComObject, Interface}; +use windows_core::{ComObject, Interface, Result, HSTRING}; use windows_sys::Win32::{ Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM}, System::Ole::{OleInitialize, RevokeDragDrop}, @@ -12,13 +12,12 @@ use windows_sys::Win32::{ TRACKMOUSEEVENT, }, WindowsAndMessaging::{ - AdjustWindowRectEx, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, - GetMessageW, GetWindowLongPtrW, LoadCursorW, PostMessageW, SetCursor, SetTimer, - SetWindowLongPtrW, SetWindowPos, TranslateMessage, GWLP_USERDATA, HTCLIENT, MSG, - SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOZORDER, WHEEL_DELTA, WM_CHAR, WM_CLOSE, WM_CREATE, - WM_DPICHANGED, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, - WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE, - WM_MOUSEWHEEL, WM_NCDESTROY, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, + AdjustWindowRectEx, DefWindowProcW, DestroyWindow, DispatchMessageW, GetMessageW, + LoadCursorW, PostMessageW, SetCursor, SetTimer, SetWindowPos, TranslateMessage, + HTCLIENT, MSG, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOZORDER, WHEEL_DELTA, WM_CHAR, + WM_CLOSE, WM_DPICHANGED, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, + WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL, + WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SHOWWINDOW, WM_SIZE, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TIMER, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WS_CAPTION, WS_CHILD, WS_CLIPSIBLINGS, WS_MAXIMIZEBOX, WS_MINIMIZEBOX, WS_POPUPWINDOW, WS_SIZEBOX, WS_VISIBLE, @@ -26,10 +25,8 @@ use windows_sys::Win32::{ }, }; -use std::cell::{Cell, Ref, RefCell, RefMut}; +use std::cell::{Cell, Ref, RefCell}; use std::collections::VecDeque; -use std::ffi::OsStr; -use std::os::windows::ffi::OsStrExt; use std::ptr::null_mut; use std::rc::Rc; @@ -45,8 +42,8 @@ const BV_WINDOW_MUST_CLOSE: u32 = WM_USER + 1; use crate::win::hook::{self, KeyboardHookHandle}; use crate::{ - Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, WindowEvent, - WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, + Event, EventStatus, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, + WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, }; use super::cursor::cursor_to_lpcwstr; @@ -55,8 +52,7 @@ use super::keyboard::KeyboardState; #[cfg(feature = "opengl")] use crate::gl::GlContext; -use crate::wrappers::win32::h_instance::HInstance; -use crate::wrappers::win32::window_class::RegisteredClass; +use crate::wrappers::win32::window::*; #[allow(non_snake_case)] fn HIWORD(wparam: WPARAM) -> u16 { @@ -106,33 +102,131 @@ struct ParentHandle { is_open: Rc>, } -impl ParentHandle { - pub fn new(hwnd: HWND) -> (Self, WindowHandle) { - let is_open = Rc::new(Cell::new(true)); - - let handle = WindowHandle { hwnd: Some(hwnd), is_open: Rc::clone(&is_open) }; - - (Self { is_open }, handle) - } -} - impl Drop for ParentHandle { fn drop(&mut self) { self.is_open.set(false); } } -pub(crate) unsafe extern "system" fn wnd_proc( - hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, -) -> LRESULT { - if msg == WM_CREATE { - PostMessageW(hwnd, WM_SHOWWINDOW, 0, 0); - return 0; +type HandlerBuilder = dyn FnOnce(&mut crate::Window) -> Box; + +pub struct BaseviewWindow { + window_state: Rc, + initial_size: Size, + + handler_builder: Cell>>, + + // Things not directly used, but kept so their Drop impl runs when the window is destroyed + _parent_handle: ParentHandle, + _keyboard_hook: Cell>, + _drop_target: Cell>>, + + #[cfg(feature = "opengl")] + pub gl_config: Option, +} + +impl WindowImpl for BaseviewWindow { + fn after_create(&self, window: HWnd) -> Result<()> { + let hwnd = window.as_raw(); + let window_state = &self.window_state; + + self._keyboard_hook.set(Some(hook::init_keyboard_hook(hwnd))); + + unsafe { + // Only works on Windows 10 unfortunately. + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); + + // Now we can get the actual dpi of the window. + let new_rect = if let WindowScalePolicy::SystemScaleFactor = + self.window_state.scale_policy + { + // Only works on Windows 10 unfortunately. + let dpi = GetDpiForWindow(hwnd); + let scale_factor = dpi as f64 / 96.0; + + let current_scale_factor = window_state.current_scale_factor.get(); + + if current_scale_factor != scale_factor { + window_state.current_scale_factor.set(scale_factor); + + let new_size = WindowInfo::from_logical_size(self.initial_size, scale_factor) + .physical_size(); + // Preemptively update so a synchronous WM_SIZE from SetWindowPos below + // doesn't also emit Resized. + window_state.current_size.set(new_size); + + Some(RECT { + left: 0, + top: 0, + // todo: check if usize fits into i32 + right: new_size.width as i32, + bottom: new_size.height as i32, + }) + } else { + None + } + } else { + None + }; + + let drop_target = ComObject::new(DropTarget::new(Rc::downgrade(window_state))); + self._drop_target.set(Some(drop_target.clone())); + + OleInitialize(null_mut()); + + RegisterDragDrop(hwnd, drop_target.as_interface::().as_raw()); + + if let Some(mut new_rect) = new_rect { + // Convert this desired"client rectangle" size to the actual "window rectangle" + // size (Because of course you have to do that). + AdjustWindowRectEx(&mut new_rect, window_state.dw_style, 0, 0); + + // Windows makes us resize the window manually. This will trigger another `WM_SIZE` event, but it happens before GWLP_USERDATA is set, so it is not delivered to the handler + SetWindowPos( + hwnd, + hwnd, + new_rect.left, + new_rect.top, + new_rect.right - new_rect.left, + new_rect.bottom - new_rect.top, + SWP_NOZORDER | SWP_NOMOVE, + ); + + // Send an initial Resized event so users get the correct scale factor and physical size. + self.window_state.send_resized(self.initial_size); + } + } + + #[cfg(feature = "opengl")] + if let Some(gl_config) = self.gl_config.clone() { + let mut handle = Win32WindowHandle::empty(); + handle.hwnd = hwnd; + let handle = RawWindowHandle::Win32(handle); + + let gl_context = unsafe { GlContext::create(&handle, gl_config) } + .expect("Could not create OpenGL context"); + + let Ok(()) = self.window_state.gl_context.set(gl_context) else { + unreachable!(); + }; + }; + + let handler = { + let mut window = crate::Window::new(Window { state: window_state }); + + self.handler_builder.take().unwrap()(&mut window) + }; + *window_state.handler.borrow_mut() = Some(handler); + + Ok(()) } - let window_state_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut WindowState; - if !window_state_ptr.is_null() { - let result = wnd_proc_inner(hwnd, msg, wparam, lparam, &*window_state_ptr); + unsafe fn handle_message( + &self, window: HWnd, msg: u32, wparam: WPARAM, lparam: LPARAM, + ) -> Option { + let hwnd = window.as_raw(); + + let result = unsafe { wnd_proc_inner(hwnd, msg, wparam, lparam, &self.window_state) }; // If any of the above event handlers caused tasks to be pushed to the deferred tasks list, // then we'll try to handle them now @@ -141,30 +235,20 @@ pub(crate) unsafe extern "system" fn wnd_proc( // the borrow of `window_state.deferred_tasks` into the call of // `window_state.handle_deferred_task()` since that may also generate additional // messages. - let task = match (*window_state_ptr).deferred_tasks.borrow_mut().pop_front() { + let task = match self.window_state.deferred_tasks.borrow_mut().pop_front() { Some(task) => task, None => break, }; - (*window_state_ptr).handle_deferred_task(task); - } - - // NOTE: This is not handled in `wnd_proc_inner` because of the deferred task loop above - if msg == WM_NCDESTROY { - RevokeDragDrop(hwnd); - let _ = (*window_state_ptr).window_class.take(); - SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); - drop(Rc::from_raw(window_state_ptr)); + self.window_state.handle_deferred_task(task); } - // The actual custom window proc has been moved to another function so we can always handle - // the deferred tasks regardless of whether the custom window proc returns early or not - if let Some(result) = result { - return result; - } + result } - DefWindowProcW(hwnd, msg, wparam, lparam) + fn before_destroy(&self, window: HWnd) { + unsafe { RevokeDragDrop(window.as_raw()) }; + } } /// Our custom `wnd_proc` handler. If the result contains a value, then this is returned after @@ -174,10 +258,7 @@ unsafe fn wnd_proc_inner( ) -> Option { match msg { WM_MOUSEMOVE => { - let mut window = crate::Window::new(window_state.create_window()); - - let mut mouse_was_outside_window = window_state.mouse_was_outside_window.borrow_mut(); - if *mouse_was_outside_window { + if window_state.mouse_was_outside_window.get() { // this makes Windows track whether the mouse leaves the window. // When the mouse leaves it results in a `WM_MOUSELEAVE` event. let mut track_mouse = TRACKMOUSEEVENT { @@ -189,15 +270,10 @@ unsafe fn wnd_proc_inner( // Couldn't find a good way to track whether the mouse enters, // but if `WM_MOUSEMOVE` happens, the mouse must have entered. TrackMouseEvent(&mut track_mouse); - *mouse_was_outside_window = false; + window_state.mouse_was_outside_window.set(false); let enter_event = Event::Mouse(MouseEvent::CursorEntered); - window_state - .handler - .borrow_mut() - .as_mut() - .unwrap() - .on_event(&mut window, enter_event); + window_state.handle_event(enter_event); } let x = (lparam & 0xFFFF) as i16 as i32; @@ -212,21 +288,18 @@ unsafe fn wnd_proc_inner( .borrow() .get_modifiers_from_mouse_wparam(wparam), }); - window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, move_event); + + window_state.handle_event(move_event); Some(0) } WM_MOUSELEAVE => { - let mut window = crate::Window::new(window_state.create_window()); - let event = Event::Mouse(MouseEvent::CursorLeft); - window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, event); + window_state.handle_event(Event::Mouse(MouseEvent::CursorLeft)); - *window_state.mouse_was_outside_window.borrow_mut() = true; + window_state.mouse_was_outside_window.set(true); Some(0) } WM_MOUSEWHEEL | WM_MOUSEHWHEEL => { - let mut window = crate::Window::new(window_state.create_window()); - let value = (wparam >> 16) as i16; let value = value as i32; let value = value as f32 / WHEEL_DELTA as f32; @@ -243,14 +316,11 @@ unsafe fn wnd_proc_inner( .get_modifiers_from_mouse_wparam(wparam), }); - window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, event); - + window_state.handle_event(event); Some(0) } WM_LBUTTONDOWN | WM_LBUTTONUP | WM_MBUTTONDOWN | WM_MBUTTONUP | WM_RBUTTONDOWN | WM_RBUTTONUP | WM_XBUTTONDOWN | WM_XBUTTONUP => { - let mut window = crate::Window::new(window_state.create_window()); - let mut mouse_button_counter = window_state.mouse_button_counter.get(); #[allow(non_snake_case)] @@ -308,38 +378,20 @@ unsafe fn wnd_proc_inner( }; window_state.mouse_button_counter.set(mouse_button_counter); - - window_state - .handler - .borrow_mut() - .as_mut() - .unwrap() - .on_event(&mut window, Event::Mouse(event)); + window_state.handle_event(Event::Mouse(event)); } None } WM_TIMER => { - let mut window = crate::Window::new(window_state.create_window()); - if wparam == WIN_FRAME_TIMER { - window_state.handler.borrow_mut().as_mut().unwrap().on_frame(&mut window); + window_state.handle_on_frame() } Some(0) } WM_CLOSE => { - // Make sure to release the borrow before the DefWindowProc call - { - let mut window = crate::Window::new(window_state.create_window()); - - window_state - .handler - .borrow_mut() - .as_mut() - .unwrap() - .on_event(&mut window, Event::Window(WindowEvent::WillClose)); - } + window_state.handle_event(Event::Window(WindowEvent::WillClose)); // DestroyWindow(hwnd); // Some(0) @@ -347,18 +399,11 @@ unsafe fn wnd_proc_inner( } WM_CHAR | WM_SYSCHAR | WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP | WM_INPUTLANGCHANGE => { - let mut window = crate::Window::new(window_state.create_window()); - let opt_event = window_state.keyboard_state.borrow_mut().process_message(hwnd, msg, wparam, lparam); if let Some(event) = opt_event { - window_state - .handler - .borrow_mut() - .as_mut() - .unwrap() - .on_event(&mut window, Event::Keyboard(event)); + window_state.handle_event(Event::Keyboard(event)); } if msg != WM_SYSKEYDOWN { @@ -368,32 +413,16 @@ unsafe fn wnd_proc_inner( } } WM_SETFOCUS => { - let mut window = crate::Window::new(window_state.create_window()); - - window_state - .handler - .borrow_mut() - .as_mut() - .unwrap() - .on_event(&mut window, Event::Window(WindowEvent::Focused)); + window_state.handle_event(Event::Window(WindowEvent::Focused)); None } WM_KILLFOCUS => { - let mut window = crate::Window::new(window_state.create_window()); - - window_state - .handler - .borrow_mut() - .as_mut() - .unwrap() - .on_event(&mut window, Event::Window(WindowEvent::Unfocused)); + window_state.handle_event(Event::Window(WindowEvent::Unfocused)); None } WM_SIZE => { - let mut window = crate::Window::new(window_state.create_window()); - let width = (lparam & 0xFFFF) as u16 as u32; let height = ((lparam >> 16) & 0xFFFF) as u16 as u32; @@ -411,12 +440,7 @@ unsafe fn wnd_proc_inner( WindowInfo::from_physical_size(new_size, window_state.current_scale_factor.get()) }; - window_state - .handler - .borrow_mut() - .as_mut() - .unwrap() - .on_event(&mut window, Event::Window(WindowEvent::Resized(new_window_info))); + window_state.handle_event(Event::Window(WindowEvent::Resized(new_window_info))); None } @@ -455,18 +479,11 @@ unsafe fn wnd_proc_inner( if changed { window_state.current_size.set(new_size); - let mut window = crate::Window::new(window_state.create_window()); let new_window_info = WindowInfo::from_physical_size( new_size, window_state.current_scale_factor.get(), ); - - window_state - .handler - .borrow_mut() - .as_mut() - .unwrap() - .on_event(&mut window, Event::Window(WindowEvent::Resized(new_window_info))); + window_state.handle_event(Event::Window(WindowEvent::Resized(new_window_info))); } None @@ -509,25 +526,17 @@ pub(super) struct WindowState { /// struct associated with this HWND through `unsafe { GetWindowLongPtrW(self.hwnd, /// GWLP_USERDATA) } as *const WindowState`. pub hwnd: HWND, - window_class: Cell>, current_size: Cell, current_scale_factor: Cell, - _parent_handle: ParentHandle, keyboard_state: RefCell, mouse_button_counter: Cell, - mouse_was_outside_window: RefCell, + mouse_was_outside_window: Cell, cursor_icon: Cell, // Initialized late so the `Window` can hold a reference to this `WindowState` handler: RefCell>>, - _drop_target: RefCell>>, scale_policy: WindowScalePolicy, dw_style: u32, - // handle to the win32 keyboard hook - // we don't need to read from this, just carry it around so the Drop impl can run - #[allow(dead_code)] - kb_hook: KeyboardHookHandle, - /// Tasks that should be executed at the end of `wnd_proc`. This is needed to avoid mutably /// borrowing the fields from `WindowState` more than once. For instance, when the window /// handler requests a resize in response to a keyboard event, the window state will already be @@ -536,12 +545,50 @@ pub(super) struct WindowState { pub deferred_tasks: RefCell>, #[cfg(feature = "opengl")] - pub gl_context: Option, + pub gl_context: core::cell::OnceCell, } impl WindowState { - pub(super) fn create_window(&self) -> Window<'_> { - Window { state: self } + pub fn new( + hwnd: HWND, current_size: PhySize, scaling: f64, scale_policy: WindowScalePolicy, + style_flags: u32, + ) -> Self { + Self { + hwnd, + current_scale_factor: scaling.into(), + current_size: current_size.into(), + keyboard_state: RefCell::new(KeyboardState::new()), + mouse_button_counter: Cell::new(0), + mouse_was_outside_window: true.into(), + cursor_icon: Cell::new(MouseCursor::Default), + handler: RefCell::new(None), + scale_policy, + dw_style: style_flags, + + deferred_tasks: RefCell::new(VecDeque::with_capacity(4)), + + #[cfg(feature = "opengl")] + gl_context: core::cell::OnceCell::new(), + } + } + + pub(crate) fn handle_on_frame(&self) { + let mut handler = self.handler.borrow_mut(); + let Some(handler) = handler.as_mut() else { return }; + let mut window = crate::window::Window::new(Window { state: self }); + + handler.on_frame(&mut window) + } + + pub(crate) fn handle_event(&self, event: Event) -> EventStatus { + let mut handler = self.handler.borrow_mut(); + + let Some(handler) = handler.as_mut() else { + return EventStatus::Ignored; + }; + + let mut window = crate::window::Window::new(Window { state: self }); + handler.on_event(&mut window, event) } pub(super) fn window_info(&self) -> WindowInfo { @@ -552,20 +599,12 @@ impl WindowState { self.keyboard_state.borrow() } - pub(super) fn handler_mut(&self) -> RefMut<'_, Option>> { - self.handler.borrow_mut() - } - fn send_resized(&self, logical_size: Size) { let window_info = WindowInfo::from_logical_size(logical_size, self.current_scale_factor.get()); self.current_size.set(window_info.physical_size()); - let mut window = crate::Window::new(self.create_window()); - self.handler_mut() - .as_mut() - .unwrap() - .on_event(&mut window, Event::Window(WindowEvent::Resized(window_info))); + self.handle_event(Event::Window(WindowEvent::Resized(window_info))); } /// Handle a deferred task as described in [`Self::deferred_tasks`]. @@ -672,176 +711,85 @@ impl Window<'_> { B: FnOnce(&mut crate::Window) -> H, B: Send + 'static, { - let instance = HInstance::get(); + let title = HSTRING::from(options.title); - unsafe { - let mut title: Vec = OsStr::new(&options.title[..]).encode_wide().collect(); - title.push(0); + let scaling = match options.scale { + WindowScalePolicy::SystemScaleFactor => 1.0, + WindowScalePolicy::ScaleFactor(scale) => scale, + }; - let window_class = RegisteredClass::register_new(instance, Some(wnd_proc)).unwrap(); + let current_size = WindowInfo::from_logical_size(options.size, scaling).physical_size(); - let scaling = match options.scale { - WindowScalePolicy::SystemScaleFactor => 1.0, - WindowScalePolicy::ScaleFactor(scale) => scale, - }; + let mut rect = RECT { + left: 0, + top: 0, + // todo: check if usize fits into i32 + right: current_size.width as i32, + bottom: current_size.height as i32, + }; - let current_size = WindowInfo::from_logical_size(options.size, scaling).physical_size(); + let flags = if parented { + WS_CHILD | WS_VISIBLE + } else { + WS_POPUPWINDOW + | WS_CAPTION + | WS_VISIBLE + | WS_SIZEBOX + | WS_MINIMIZEBOX + | WS_MAXIMIZEBOX + | WS_CLIPSIBLINGS + }; - let mut rect = RECT { - left: 0, - top: 0, - // todo: check if usize fits into i32 - right: current_size.width as i32, - bottom: current_size.height as i32, - }; + if !parented { + unsafe { AdjustWindowRectEx(&mut rect, flags, FALSE, 0) }; + } - let flags = if parented { - WS_CHILD | WS_VISIBLE - } else { - WS_POPUPWINDOW - | WS_CAPTION - | WS_VISIBLE - | WS_SIZEBOX - | WS_MINIMIZEBOX - | WS_MAXIMIZEBOX - | WS_CLIPSIBLINGS - }; + let is_open = Rc::new(Cell::new(true)); - if !parented { - AdjustWindowRectEx(&mut rect, flags, FALSE, 0); - } + let parent_handle = ParentHandle { is_open: is_open.clone() }; - let hwnd = CreateWindowExW( - 0, - window_class.as_atom_ptr(), - title.as_ptr(), + let initializer = move |hwnd: HWnd| { + let window_state = Rc::new(WindowState::new( + hwnd.as_raw(), + current_size, + scaling, + options.scale, flags, - 0, - 0, - rect.right - rect.left, - rect.bottom - rect.top, - parent as *mut _, - null_mut(), - null_mut(), - null_mut(), - ); - // todo: manage error ^ + )); - let kb_hook = hook::init_keyboard_hook(hwnd); + BaseviewWindow { + window_state, + initial_size: options.size, + handler_builder: Cell::new(Some(Box::new(|w| Box::new(build(w))))), - #[cfg(feature = "opengl")] - let gl_context: Option = options.gl_config.map(|gl_config| { - let mut handle = Win32WindowHandle::empty(); - handle.hwnd = hwnd; - let handle = RawWindowHandle::Win32(handle); - - GlContext::create(&handle, gl_config).expect("Could not create OpenGL context") - }); - - let (parent_handle, window_handle) = ParentHandle::new(hwnd); - - let window_state = Rc::new(WindowState { - hwnd, - window_class: Some(window_class).into(), - current_scale_factor: scaling.into(), - current_size: current_size.into(), _parent_handle: parent_handle, - keyboard_state: RefCell::new(KeyboardState::new()), - mouse_button_counter: Cell::new(0), - mouse_was_outside_window: RefCell::new(true), - cursor_icon: Cell::new(MouseCursor::Default), - // The Window refers to this `WindowState`, so this `handler` needs to be - // initialized later - handler: RefCell::new(None), - _drop_target: RefCell::new(None), - scale_policy: options.scale, - dw_style: flags, - - deferred_tasks: RefCell::new(VecDeque::with_capacity(4)), - - kb_hook, + _drop_target: None.into(), + _keyboard_hook: None.into(), #[cfg(feature = "opengl")] - gl_context, - }); - - let handler = { - let mut window = crate::Window::new(window_state.create_window()); - - build(&mut window) - }; - *window_state.handler.borrow_mut() = Some(Box::new(handler)); - - // Only works on Windows 10 unfortunately. - SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); - - // Now we can get the actual dpi of the window. - let new_rect = if let WindowScalePolicy::SystemScaleFactor = options.scale { - // Only works on Windows 10 unfortunately. - let dpi = GetDpiForWindow(hwnd); - let scale_factor = dpi as f64 / 96.0; - - let current_scale_factor = window_state.current_scale_factor.get(); - - if current_scale_factor != scale_factor { - window_state.current_scale_factor.set(scale_factor); - - let new_size = - WindowInfo::from_logical_size(options.size, scale_factor).physical_size(); - // Preemptively update so a synchronous WM_SIZE from SetWindowPos below - // doesn't also emit Resized. - window_state.current_size.set(new_size); - - Some(RECT { - left: 0, - top: 0, - // todo: check if usize fits into i32 - right: new_size.width as i32, - bottom: new_size.height as i32, - }) - } else { - None - } - } else { - None - }; - - let drop_target = ComObject::new(DropTarget::new(Rc::downgrade(&window_state))); - *window_state._drop_target.borrow_mut() = Some(drop_target.clone()); - - OleInitialize(null_mut()); - - RegisterDragDrop(hwnd, drop_target.as_interface::().as_raw()); - - SetWindowLongPtrW(hwnd, GWLP_USERDATA, Rc::into_raw(window_state) as *const _ as _); - SetTimer(hwnd, WIN_FRAME_TIMER, 15, None); - - if let Some(mut new_rect) = new_rect { - // Convert this desired"client rectangle" size to the actual "window rectangle" - // size (Because of course you have to do that). - AdjustWindowRectEx(&mut new_rect, flags, 0, 0); - - // Windows makes us resize the window manually. This will trigger another `WM_SIZE` event, but it happens before GWLP_USERDATA is set, so it is not delivered to the handler - SetWindowPos( - hwnd, - hwnd, - new_rect.left, - new_rect.top, - new_rect.right - new_rect.left, - new_rect.bottom - new_rect.top, - SWP_NOZORDER | SWP_NOMOVE, - ); - - // Send an initial Resized event so users get the correct scale factor and physical size. - { - let window_state_ptr = - GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *const WindowState; - (*window_state_ptr).send_resized(options.size); - } + gl_config: options.gl_config, } + }; - window_handle - } + let hwnd = create_window( + &title, + flags, + rect.right - rect.left, + rect.bottom - rect.top, + parent as *mut _, + initializer, + ) + .unwrap(); + + // FIXME: this SetTimer call could be in after_create, but for some reason it changes the ordering + // for a parent+child window situation, which results in the parent drawing over the child. + // This timer should be replaced by proper window redrawing/damage/vsync handling, but this + // would be a breaking change, so we'll do that later. + unsafe { SetTimer(hwnd, WIN_FRAME_TIMER, 15, None) }; + + unsafe { PostMessageW(hwnd, WM_SHOWWINDOW, 0, 0) }; + + WindowHandle { hwnd: Some(hwnd), is_open: Rc::clone(&is_open) } } pub fn close(&mut self) { @@ -878,7 +826,7 @@ impl Window<'_> { #[cfg(feature = "opengl")] pub fn gl_context(&self) -> Option<&GlContext> { - self.state.gl_context.as_ref() + self.state.gl_context.get() } } diff --git a/src/wrappers.rs b/src/wrappers.rs index abb9280f..3865dcee 100644 --- a/src/wrappers.rs +++ b/src/wrappers.rs @@ -19,5 +19,6 @@ pub mod xlib; #[cfg(all(target_os = "linux", feature = "opengl"))] pub mod glx; +/// Wrappers and utilities around the Win32 API #[cfg(target_os = "windows")] pub mod win32; diff --git a/src/wrappers/win32.rs b/src/wrappers/win32.rs index 957fd141..6365174e 100644 --- a/src/wrappers/win32.rs +++ b/src/wrappers/win32.rs @@ -1,3 +1,3 @@ pub mod h_instance; pub mod uuid; -pub mod window_class; +pub mod window; diff --git a/src/wrappers/win32/h_instance.rs b/src/wrappers/win32/h_instance.rs index 8f2ee843..74748772 100644 --- a/src/wrappers/win32/h_instance.rs +++ b/src/wrappers/win32/h_instance.rs @@ -1,10 +1,11 @@ -use std::ptr::null_mut; +use std::ffi::c_void; +use std::ptr::{null_mut, NonNull}; use windows_core::Error; use windows_sys::Win32::Foundation::HINSTANCE; use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW; #[derive(Copy, Clone, PartialEq)] -pub struct HInstance(HINSTANCE); +pub struct HInstance(NonNull); // SAFETY: This is actually a pointer to the memory image of the executable file. It is guaranteed // to be valid for the process's lifetime. @@ -17,18 +18,19 @@ unsafe impl Sync for HInstance {} impl HInstance { pub fn get() -> Self { let result = unsafe { GetModuleHandleW(null_mut()) }; - if result.is_null() { + + let Some(result) = NonNull::new(result) else { panic!( "Failed to get HInstance pointer: GetModuleHandleW failed: {}", Error::from_win32() - ); - } + ) + }; Self(result) } #[inline] pub fn as_raw(&self) -> HINSTANCE { - self.0 + self.0.as_ptr() } } diff --git a/src/wrappers/win32/window.rs b/src/wrappers/win32/window.rs new file mode 100644 index 00000000..d0d629a5 --- /dev/null +++ b/src/wrappers/win32/window.rs @@ -0,0 +1,81 @@ +mod data; +mod handle; +mod proc; +mod window_class; + +use data::WindowData; +pub use handle::HWnd; +pub use proc::wnd_proc; +use std::ptr::null_mut; +use std::rc::Rc; +use window_class::RegisteredClass; +use windows_core::{Error, Result, HSTRING}; + +use crate::wrappers::win32::h_instance::HInstance; +use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; +use windows_sys::Win32::UI::WindowsAndMessaging::{CreateWindowExW, WINDOW_STYLE}; + +pub trait WindowImpl: 'static { + /// Called during the processing of the WM_CREATE message, but after this type was properly + /// initialized. + /// + /// Note that any messages sent to the window during this function will result in + /// [`handle_message`] to be called immediately. Implementations must be ready for that. + /// + /// If this returns an error, the window creation is canceled. + fn after_create(&self, window: HWnd) -> Result<()>; + unsafe fn handle_message( + &self, window: HWnd, message_code: u32, w_param: WPARAM, l_param: LPARAM, + ) -> Option; + /// Called during the processing of the WM_DESTROY message, but before any other de-initialization + /// takes place. + /// + /// Note that any messages sent to the window during this function will result in + /// [`handle_message`] to be called immediately. Implementations must be ready for that. + /// + /// This function is not fallible. Any errors will be ignored. + fn before_destroy(&self, window: HWnd); +} + +/// Creates a window from the given settings, with a given [`WindowImpl`] type to handle the message, +/// and an initializer function for that type. +/// +/// The initialization function is called during the handling of the WM_CREATE function, which allows +/// it to receive a valid HWND, but before it has to handle any messages. +/// +/// Note that any message sent to the window by the given `initializer` will not be sent to the +/// [`WindowImpl::handle_message`] function. +/// For any non-trivial operations (e.g. window resizing, GL context creation, etc.), put them in +/// [`WindowImpl::after_create`] instead. +pub fn create_window( + title: &HSTRING, flags: WINDOW_STYLE, nc_width: i32, nc_height: i32, parent: HWND, + initializer: impl FnOnce(HWnd) -> W + 'static, +) -> Result { + let instance = HInstance::get(); + let window_class = RegisteredClass::register_new(instance, Some(wnd_proc::))?; + + let data = WindowData::new(initializer, window_class.clone()); + + let hwnd = unsafe { + CreateWindowExW( + 0, + window_class.as_atom_ptr(), + title.as_ptr(), + flags, + 0, + 0, + nc_width, + nc_height, + parent, + null_mut(), + instance.as_raw(), + Rc::into_raw(data).cast(), + ) + }; + + if hwnd.is_null() { + return Err(Error::from_win32()); + } + + Ok(hwnd) +} diff --git a/src/wrappers/win32/window/data.rs b/src/wrappers/win32/window/data.rs new file mode 100644 index 00000000..171838d8 --- /dev/null +++ b/src/wrappers/win32/window/data.rs @@ -0,0 +1,67 @@ +use super::*; +use std::cell::{Cell, OnceCell}; +use std::mem::ManuallyDrop; +use std::ptr::NonNull; +use std::rc::Rc; +use windows_sys::Win32::Foundation::{LPARAM, LRESULT, WPARAM}; + +type Initializer = dyn FnOnce(HWnd) -> W + 'static; + +/// Data owned by the Win32 window. +/// +/// This is the data behind the `GWLP_USERDATA` pointer. +pub struct WindowData { + initializer: Cell>>>, + inner_impl: OnceCell, + // Keep this around to ensure the class is not de-registered while this window is open + _window_class: RegisteredClass, +} + +impl WindowData { + pub fn new(initializer: impl FnOnce(HWnd) -> W + 'static, class: RegisteredClass) -> Rc { + Rc::new(Self { + initializer: Cell::new(Some(Box::new(initializer))), + inner_impl: OnceCell::new(), + _window_class: class, + }) + } + + /// Returns an owned pointer from the given raw pointer, without transferring ownership. + pub unsafe fn from_raw(raw: NonNull>) -> Rc { + let this = ManuallyDrop::new(Rc::from_raw(raw.as_ptr())); + Rc::clone(&this) + } + + pub fn initialize(&self, window: HWnd) -> Result<()> { + let Some(initializer) = self.initializer.take() else { + panic!("WindowData is already initialized"); + }; + + if self.inner_impl.set(initializer(window)).is_err() { + // Should not be possible + unreachable!("WindowData is already initialized"); + } + + if let Some(inner) = self.inner_impl.get() { + inner.after_create(window)?; + } + + Ok(()) + } + + pub fn destroy_started(&self, window: HWnd) { + if let Some(inner) = self.inner_impl.get() { + inner.before_destroy(window); + } + } + + pub unsafe fn handle_message( + &self, window: HWnd, message_code: u32, w_param: WPARAM, l_param: LPARAM, + ) -> Option { + if let Some(inner) = self.inner_impl.get() { + inner.handle_message(window, message_code, w_param, l_param) + } else { + None + } + } +} diff --git a/src/wrappers/win32/window/handle.rs b/src/wrappers/win32/window/handle.rs new file mode 100644 index 00000000..04f4fd3d --- /dev/null +++ b/src/wrappers/win32/window/handle.rs @@ -0,0 +1,50 @@ +use std::marker::PhantomData; +use std::ptr::NonNull; +use windows_core::{Error, Result, HRESULT}; +use windows_sys::Win32::Foundation::{SetLastError, HWND}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + GetWindowLongPtrW, SetWindowLongPtrW, GWLP_USERDATA, +}; + +/// A simple wrapper around a HWND. +/// +/// This type guarantees the HWND is safe to use, but not that it remains valid. (i.e. functions using +/// a handle from this type might still return an "invalid handle" error). +/// +/// The role of this type is to help safely encapsulating most of the unsafe Win32 HWND APIs. +#[derive(Copy, Clone)] +pub struct HWnd<'a>(HWND, PhantomData<&'a ()>); + +impl HWnd<'_> { + pub unsafe fn from_raw(hwnd: HWND) -> Self { + Self(hwnd, PhantomData) + } + + pub fn as_raw(&self) -> HWND { + self.0 + } + + pub fn get_userdata_ptr(&self) -> Option> { + let ptr = unsafe { GetWindowLongPtrW(self.0, GWLP_USERDATA) }; + NonNull::new(ptr as *mut T) + } + + pub fn set_userdata_ptr(&self, data: *const T) -> Result<()> { + // SAFETY: This function is always safe to call + unsafe { SetLastError(0) }; + // SAFETY: This type guarantees the HWND is safe to use. + let previous = unsafe { SetWindowLongPtrW(self.0, GWLP_USERDATA, data as isize) }; + if previous != 0 { + return Ok(()); + } + + // We can't know if a return value of 0 is indicative of an error, or if it's just because the + // previous value was 0. So we check GetLastError instead (called by Error::from_win32). + let error = Error::from_win32(); + if error.code() == HRESULT(0) { + return Ok(()); + } + + Err(error) + } +} diff --git a/src/wrappers/win32/window/proc.rs b/src/wrappers/win32/window/proc.rs new file mode 100644 index 00000000..8656fee0 --- /dev/null +++ b/src/wrappers/win32/window/proc.rs @@ -0,0 +1,94 @@ +use super::*; +use std::ptr::NonNull; +use std::rc::Rc; +use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; +use windows_sys::Win32::UI::WindowsAndMessaging::*; + +/// The wndproc implementation wrapping a given [`WindowImpl`] type. +/// +/// This handles all the lifecycle details of the window and its data created by [create_window]. +pub unsafe extern "system" fn wnd_proc( + window: HWND, message_code: u32, w_param: WPARAM, l_param: LPARAM, +) -> LRESULT { + let handle_default = || unsafe { DefWindowProcW(window, message_code, w_param, l_param) }; + let window = unsafe { HWnd::from_raw(window) }; + + match message_code { + WM_CREATE => { + let create = unsafe { &*(l_param as *const CREATESTRUCTW) }; + let inner_ptr = create.lpCreateParams as *mut WindowData; + + let Some(inner_ptr) = NonNull::new(inner_ptr) else { + // If the state pointer was null for some weird reason, we just abort. + // TODO: log error + return -1; + }; + + if let Err(_e) = window.set_userdata_ptr(inner_ptr.as_ptr()) { + // The call to SetWindowLongPtrW failed for some reason, we cannot continue. + + // Recover and free the received pointer data. + drop(Rc::from_raw(inner_ptr.as_ptr())); + + // TODO: log error + return -1; + } + + // Now the fun begins + let result = { + let inner = unsafe { inner_ptr.as_ref() }; + + inner.initialize(window) + }; + + match result { + // If successful, all good. + // Ownership of the inner state has been passed to the window via the userdata ptr. + Ok(()) => 0, + + // If initializer failed, abort. + Err(_) => { + // First, revoke ownership from the window, we don't want it to be used by any subsequent messages. + let _ = window.set_userdata_ptr(core::ptr::null::()); + + // Try to recover and free the received pointer data. But if this also fails, better to leak + // it than risk crashing + drop(Rc::from_raw(inner_ptr.as_ptr())); + + // TODO: log error + -1 + } + } + } + WM_DESTROY => { + let Some(state_ptr) = window.get_userdata_ptr::>() else { + // State pointer can be null if the WM_DESTROY message got sent before + // the handling of WM_CREATE above finished, or if it failed. + // If that's the case, we have nothing to destroy. + return handle_default(); + }; + + let state = unsafe { Rc::from_raw(state_ptr.as_ptr()) }; + state.destroy_started(window); + let _ = window.set_userdata_ptr(core::ptr::null::()); + drop(state); + + 0 + } + _ => { + let Some(inner_ptr) = window.get_userdata_ptr::>() else { + return handle_default(); + }; + + // This guarantees WindowData remains valid until the end of this scope, + // even if the event handler leads to the window being destroyed + let inner = unsafe { WindowData::from_raw(inner_ptr) }; + + let result = inner.handle_message(window, message_code, w_param, l_param); + + drop(inner); + + result.unwrap_or_else(handle_default) + } + } +} diff --git a/src/wrappers/win32/window_class.rs b/src/wrappers/win32/window/window_class.rs similarity index 91% rename from src/wrappers/win32/window_class.rs rename to src/wrappers/win32/window/window_class.rs index 6f28b0b0..70539035 100644 --- a/src/wrappers/win32/window_class.rs +++ b/src/wrappers/win32/window/window_class.rs @@ -1,5 +1,6 @@ use crate::wrappers::win32::h_instance::HInstance; use crate::wrappers::win32::uuid::Uuid; +use std::num::NonZeroU16; use std::ptr::null_mut; use std::sync::Arc; use windows_core::{Error, Result, HSTRING}; @@ -31,9 +32,10 @@ impl RegisteredClass { }; let class_atom = unsafe { RegisterClassW(&class_info) }; - if class_atom == 0 { + + let Some(class_atom) = NonZeroU16::new(class_atom) else { return Err(Error::from_win32()); - } + }; Ok(Self(Arc::new(RegisteredClassInner(class_atom, instance)))) } @@ -44,7 +46,7 @@ impl RegisteredClass { } } -struct RegisteredClassInner(u16, HInstance); +struct RegisteredClassInner(NonZeroU16, HInstance); impl RegisteredClassInner { // See: @@ -53,7 +55,7 @@ impl RegisteredClassInner { // https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types #[inline] pub fn as_atom_ptr(&self) -> PCWSTR { - self.0 as u32 as PCWSTR + self.0.get() as u32 as PCWSTR } }