From 80b30ce617cf2f3fabad1461f43018609bf10991 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 30 Nov 2022 18:57:56 +0100 Subject: [PATCH] Implement window resizing for Windows --- src/win/window.rs | 555 +++++++++++++++++++++++++++------------------- src/window.rs | 9 +- 2 files changed, 334 insertions(+), 230 deletions(-) diff --git a/src/win/window.rs b/src/win/window.rs index 9c3556b..ef784c9 100644 --- a/src/win/window.rs +++ b/src/win/window.rs @@ -17,6 +17,7 @@ use winapi::um::winuser::{ }; use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; use std::ffi::{c_void, OsStr}; use std::marker::PhantomData; use std::os::windows::ffi::OsStrExt; @@ -29,8 +30,8 @@ use raw_window_handle::{HasRawWindowHandle, RawWindowHandle, Win32Handle}; const BV_WINDOW_MUST_CLOSE: UINT = WM_USER + 1; use crate::{ - Event, MouseButton, MouseEvent, PhyPoint, PhySize, ScrollDelta, WindowEvent, WindowHandler, - WindowInfo, WindowOpenOptions, WindowScalePolicy, + Event, MouseButton, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, WindowEvent, + WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, }; use super::keyboard::KeyboardState; @@ -130,234 +131,261 @@ unsafe extern "system" fn wnd_proc( if !window_state_ptr.is_null() { let window_state = &*window_state_ptr; - match msg { - WM_MOUSEMOVE => { - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); + let result = wnd_proc_inner(hwnd, msg, wparam, lparam, window_state); - let x = (lparam & 0xFFFF) as i16 as i32; - let y = ((lparam >> 16) & 0xFFFF) as i16 as i32; + // 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 + loop { + // NOTE: This is written like this instead of using a `while let` loop to avoid exending + // 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.deferred_tasks.borrow_mut().pop_front() { + Some(task) => task, + None => break, + }; - let physical_pos = PhyPoint { x, y }; - let logical_pos = physical_pos.to_logical(&window_state.window_info.borrow()); - let event = Event::Mouse(MouseEvent::CursorMoved { - position: logical_pos, - modifiers: window_state - .keyboard_state - .borrow() - .get_modifiers_from_mouse_wparam(wparam), - }); + window_state.handle_deferred_task(task); + } - window_state.handler.borrow_mut().on_event(&mut window, event); - - return 0; - } - WM_MOUSEWHEEL | WM_MOUSEHWHEEL => { - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - let value = (wparam >> 16) as i16; - let value = value as i32; - let value = value as f32 / WHEEL_DELTA as f32; - - let event = Event::Mouse(MouseEvent::WheelScrolled { - delta: if msg == WM_MOUSEWHEEL { - ScrollDelta::Lines { x: 0.0, y: value } - } else { - ScrollDelta::Lines { x: value, y: 0.0 } - }, - modifiers: window_state - .keyboard_state - .borrow() - .get_modifiers_from_mouse_wparam(wparam), - }); - - window_state.handler.borrow_mut().on_event(&mut window, event); - - return 0; - } - WM_LBUTTONDOWN | WM_LBUTTONUP | WM_MBUTTONDOWN | WM_MBUTTONUP | WM_RBUTTONDOWN - | WM_RBUTTONUP | WM_XBUTTONDOWN | WM_XBUTTONUP => { - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - let mut mouse_button_counter = window_state.mouse_button_counter.get(); - - let button = match msg { - WM_LBUTTONDOWN | WM_LBUTTONUP => Some(MouseButton::Left), - WM_MBUTTONDOWN | WM_MBUTTONUP => Some(MouseButton::Middle), - WM_RBUTTONDOWN | WM_RBUTTONUP => Some(MouseButton::Right), - WM_XBUTTONDOWN | WM_XBUTTONUP => match GET_XBUTTON_WPARAM(wparam) { - XBUTTON1 => Some(MouseButton::Back), - XBUTTON2 => Some(MouseButton::Forward), - _ => None, - }, - _ => None, - }; - - if let Some(button) = button { - let event = match msg { - WM_LBUTTONDOWN | WM_MBUTTONDOWN | WM_RBUTTONDOWN | WM_XBUTTONDOWN => { - // Capture the mouse cursor on button down - mouse_button_counter = mouse_button_counter.saturating_add(1); - SetCapture(hwnd); - MouseEvent::ButtonPressed { - button, - modifiers: window_state - .keyboard_state - .borrow() - .get_modifiers_from_mouse_wparam(wparam), - } - } - WM_LBUTTONUP | WM_MBUTTONUP | WM_RBUTTONUP | WM_XBUTTONUP => { - // Release the mouse cursor capture when all buttons are released - mouse_button_counter = mouse_button_counter.saturating_sub(1); - if mouse_button_counter == 0 { - ReleaseCapture(); - } - - MouseEvent::ButtonReleased { - button, - modifiers: window_state - .keyboard_state - .borrow() - .get_modifiers_from_mouse_wparam(wparam), - } - } - _ => { - unreachable!() - } - }; - - window_state.mouse_button_counter.set(mouse_button_counter); - - window_state.handler.borrow_mut().on_event(&mut window, Event::Mouse(event)); - } - } - WM_TIMER => { - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - if wparam == WIN_FRAME_TIMER { - window_state.handler.borrow_mut().on_frame(&mut window); - } - return 0; - } - WM_CLOSE => { - // Make sure to release the borrow before the DefWindowProc call - { - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - window_state - .handler - .borrow_mut() - .on_event(&mut window, Event::Window(WindowEvent::WillClose)); - } - - // DestroyWindow(hwnd); - // return 0; - return DefWindowProcW(hwnd, msg, wparam, lparam); - } - WM_CHAR | WM_SYSCHAR | WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP - | WM_INPUTLANGCHANGE => { - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut 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().on_event(&mut window, Event::Keyboard(event)); - } - - if msg != WM_SYSKEYDOWN { - return 0; - } - } - WM_SIZE => { - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - let width = (lparam & 0xFFFF) as u16 as u32; - let height = ((lparam >> 16) & 0xFFFF) as u16 as u32; - - let window_info = { - let mut window_info = window_state.window_info.borrow_mut(); - *window_info = WindowInfo::from_physical_size( - PhySize { width, height }, - window_info.scale(), - ); - - window_info.clone() - }; - - window_state - .handler - .borrow_mut() - .on_event(&mut window, Event::Window(WindowEvent::Resized(window_info))); - } - WM_DPICHANGED => { - // To avoid weirdness with the realtime borrow checker. - let new_rect = { - if let WindowScalePolicy::SystemScaleFactor = window_state.scale_policy { - let dpi = (wparam & 0xFFFF) as u16 as u32; - let scale_factor = dpi as f64 / 96.0; - - let mut window_info = window_state.window_info.borrow_mut(); - *window_info = - WindowInfo::from_logical_size(window_info.logical_size(), scale_factor); - - Some(( - RECT { - left: 0, - top: 0, - // todo: check if usize fits into i32 - right: window_info.physical_size().width as i32, - bottom: window_info.physical_size().height as i32, - }, - window_state.dw_style, - )) - } else { - None - } - }; - if let Some((mut new_rect, dw_style)) = 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, dw_style, 0, 0); - - // Windows makes us resize the window manually. This will trigger another `WM_SIZE` event, - // which we can then send the user the new scale factor. - SetWindowPos( - hwnd, - hwnd, - new_rect.left as i32, - new_rect.top as i32, - new_rect.right - new_rect.left, - new_rect.bottom - new_rect.top, - SWP_NOZORDER | SWP_NOMOVE, - ); - } - } - WM_NCDESTROY => { - unregister_wnd_class(window_state.window_class); - SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); - } - _ => { - if msg == BV_WINDOW_MUST_CLOSE { - DestroyWindow(hwnd); - return 0; - } - } + // 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; } } DefWindowProcW(hwnd, msg, wparam, lparam) } +/// Our custom `wnd_proc` handler. If the result contains a value, then this is returned after +/// handling any deferred tasks. otherwise the default window procedure is invoked. +unsafe fn wnd_proc_inner( + hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM, window_state: &WindowState, +) -> Option { + match msg { + WM_MOUSEMOVE => { + let mut window = window_state.create_window(hwnd); + let mut window = crate::Window::new(&mut window); + + let x = (lparam & 0xFFFF) as i16 as i32; + let y = ((lparam >> 16) & 0xFFFF) as i16 as i32; + + let physical_pos = PhyPoint { x, y }; + let logical_pos = physical_pos.to_logical(&window_state.window_info.borrow()); + let event = Event::Mouse(MouseEvent::CursorMoved { + position: logical_pos, + modifiers: window_state + .keyboard_state + .borrow() + .get_modifiers_from_mouse_wparam(wparam), + }); + + window_state.handler.borrow_mut().on_event(&mut window, event); + + return Some(0); + } + WM_MOUSEWHEEL | WM_MOUSEHWHEEL => { + let mut window = window_state.create_window(hwnd); + let mut window = crate::Window::new(&mut window); + + let value = (wparam >> 16) as i16; + let value = value as i32; + let value = value as f32 / WHEEL_DELTA as f32; + + let event = Event::Mouse(MouseEvent::WheelScrolled { + delta: if msg == WM_MOUSEWHEEL { + ScrollDelta::Lines { x: 0.0, y: value } + } else { + ScrollDelta::Lines { x: value, y: 0.0 } + }, + modifiers: window_state + .keyboard_state + .borrow() + .get_modifiers_from_mouse_wparam(wparam), + }); + + window_state.handler.borrow_mut().on_event(&mut window, event); + + return Some(0); + } + WM_LBUTTONDOWN | WM_LBUTTONUP | WM_MBUTTONDOWN | WM_MBUTTONUP | WM_RBUTTONDOWN + | WM_RBUTTONUP | WM_XBUTTONDOWN | WM_XBUTTONUP => { + let mut window = window_state.create_window(hwnd); + let mut window = crate::Window::new(&mut window); + + let mut mouse_button_counter = window_state.mouse_button_counter.get(); + + let button = match msg { + WM_LBUTTONDOWN | WM_LBUTTONUP => Some(MouseButton::Left), + WM_MBUTTONDOWN | WM_MBUTTONUP => Some(MouseButton::Middle), + WM_RBUTTONDOWN | WM_RBUTTONUP => Some(MouseButton::Right), + WM_XBUTTONDOWN | WM_XBUTTONUP => match GET_XBUTTON_WPARAM(wparam) { + XBUTTON1 => Some(MouseButton::Back), + XBUTTON2 => Some(MouseButton::Forward), + _ => None, + }, + _ => None, + }; + + if let Some(button) = button { + let event = match msg { + WM_LBUTTONDOWN | WM_MBUTTONDOWN | WM_RBUTTONDOWN | WM_XBUTTONDOWN => { + // Capture the mouse cursor on button down + mouse_button_counter = mouse_button_counter.saturating_add(1); + SetCapture(hwnd); + MouseEvent::ButtonPressed { + button, + modifiers: window_state + .keyboard_state + .borrow() + .get_modifiers_from_mouse_wparam(wparam), + } + } + WM_LBUTTONUP | WM_MBUTTONUP | WM_RBUTTONUP | WM_XBUTTONUP => { + // Release the mouse cursor capture when all buttons are released + mouse_button_counter = mouse_button_counter.saturating_sub(1); + if mouse_button_counter == 0 { + ReleaseCapture(); + } + + MouseEvent::ButtonReleased { + button, + modifiers: window_state + .keyboard_state + .borrow() + .get_modifiers_from_mouse_wparam(wparam), + } + } + _ => { + unreachable!() + } + }; + + window_state.mouse_button_counter.set(mouse_button_counter); + + window_state.handler.borrow_mut().on_event(&mut window, Event::Mouse(event)); + } + } + WM_TIMER => { + let mut window = window_state.create_window(hwnd); + let mut window = crate::Window::new(&mut window); + + if wparam == WIN_FRAME_TIMER { + window_state.handler.borrow_mut().on_frame(&mut window); + } + return Some(0); + } + WM_CLOSE => { + // Make sure to release the borrow before the DefWindowProc call + { + let mut window = window_state.create_window(hwnd); + let mut window = crate::Window::new(&mut window); + + window_state + .handler + .borrow_mut() + .on_event(&mut window, Event::Window(WindowEvent::WillClose)); + } + + // DestroyWindow(hwnd); + // return Some(0); + return Some(DefWindowProcW(hwnd, msg, wparam, lparam)); + } + WM_CHAR | WM_SYSCHAR | WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP + | WM_INPUTLANGCHANGE => { + let mut window = window_state.create_window(hwnd); + let mut window = crate::Window::new(&mut 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().on_event(&mut window, Event::Keyboard(event)); + } + + if msg != WM_SYSKEYDOWN { + return Some(0); + } + } + WM_SIZE => { + let mut window = window_state.create_window(hwnd); + let mut window = crate::Window::new(&mut window); + + let width = (lparam & 0xFFFF) as u16 as u32; + let height = ((lparam >> 16) & 0xFFFF) as u16 as u32; + + let window_info = { + let mut window_info = window_state.window_info.borrow_mut(); + *window_info = + WindowInfo::from_physical_size(PhySize { width, height }, window_info.scale()); + + *window_info + }; + + window_state + .handler + .borrow_mut() + .on_event(&mut window, Event::Window(WindowEvent::Resized(window_info))); + } + WM_DPICHANGED => { + // To avoid weirdness with the realtime borrow checker. + let new_rect = { + if let WindowScalePolicy::SystemScaleFactor = window_state.scale_policy { + let dpi = (wparam & 0xFFFF) as u16 as u32; + let scale_factor = dpi as f64 / 96.0; + + let mut window_info = window_state.window_info.borrow_mut(); + *window_info = + WindowInfo::from_logical_size(window_info.logical_size(), scale_factor); + + Some(( + RECT { + left: 0, + top: 0, + // todo: check if usize fits into i32 + right: window_info.physical_size().width as i32, + bottom: window_info.physical_size().height as i32, + }, + window_state.dw_style, + )) + } else { + None + } + }; + if let Some((mut new_rect, dw_style)) = 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, dw_style, 0, 0); + + // Windows makes us resize the window manually. This will trigger another `WM_SIZE` event, + // which we can then send the user the new scale factor. + SetWindowPos( + hwnd, + hwnd, + new_rect.left as i32, + new_rect.top as i32, + new_rect.right - new_rect.left, + new_rect.bottom - new_rect.top, + SWP_NOZORDER | SWP_NOMOVE, + ); + } + } + WM_NCDESTROY => { + unregister_wnd_class(window_state.window_class); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); + } + _ => { + if msg == BV_WINDOW_MUST_CLOSE { + DestroyWindow(hwnd); + return Some(0); + } + } + } + + None +} + unsafe fn register_wnd_class() -> ATOM { // We generate a unique name for the new window class to prevent name collisions let class_name_str = format!("Baseview-{}", generate_guid()); @@ -390,6 +418,7 @@ unsafe fn unregister_wnd_class(wnd_class: ATOM) { /// `handler` from indirectly triggering other events that would also need to be handled using /// `handler`. struct WindowState { + hwnd: HWND, window_class: ATOM, window_info: RefCell, _parent_handle: Option, @@ -399,6 +428,13 @@ struct WindowState { scale_policy: WindowScalePolicy, dw_style: u32, + /// 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 + /// borrowed in `wnd_proc`. So the `resize()` function below cannot also mutably borrow that + /// window state at the same time. + deferred_tasks: Rc>>, + #[cfg(feature = "opengl")] gl_context: Rc>, } @@ -406,18 +442,64 @@ struct WindowState { impl WindowState { #[cfg(not(feature = "opengl"))] fn create_window(&self, hwnd: HWND) -> Window { - Window { hwnd } + Window { hwnd, deferred_tasks: self.deferred_tasks.clone() } } #[cfg(feature = "opengl")] fn create_window(&self, hwnd: HWND) -> Window { - Window { hwnd, gl_context: self.gl_context.clone() } + Window { + hwnd, + deferred_tasks: self.deferred_tasks.clone(), + gl_context: self.gl_context.clone(), + } + } + + /// Handle a deferred task as described in [`Self::deferred_tasks + pub(self) fn handle_deferred_task(&self, task: WindowTask) { + match task { + WindowTask::Resize(size) => { + let window_info = { + let mut window_info = self.window_info.borrow_mut(); + let scaling = window_info.scale(); + *window_info = WindowInfo::from_logical_size(size, scaling); + + *window_info + }; + + unsafe { + SetWindowPos( + self.hwnd, + self.hwnd, + 0, + 0, + window_info.physical_size().width as i32, + window_info.physical_size().height as i32, + SWP_NOZORDER | SWP_NOMOVE, + ) + }; + } + } } } +/// Tasks that must be deferred until the end of [`wnd_proc()`] to avoid reentrant `WindowState` +/// borrows. See the docstring on [`WindowState::deferred_tasks`] for more information. +#[derive(Debug, Clone)] +enum WindowTask { + /// Resize the window to the given size. The size is in logical pixels. DPI scaling is applied + /// automatically. + Resize(Size), +} + pub struct Window { + /// The HWND belonging to this window. The window's actual state is stored in the `WindowState` + /// struct associated with this HWND through `unsafe { GetWindowLongPtrW(self.hwnd, + /// GWLP_USERDATA) } as *const WindowState`. hwnd: HWND, + /// See [`WindowState::deferred_tasks`]. + deferred_tasks: Rc>>, + #[cfg(feature = "opengl")] gl_context: Rc>, } @@ -546,11 +628,20 @@ impl Window { GlContext::create(&handle, gl_config).expect("Could not create OpenGL context") })); + // The build closure shouldn't be enqueueing deferred tasks yet, but we'll try handling + // them just in case to avoid losing them + let deferred_tasks: Rc>> = + Rc::new(RefCell::new(VecDeque::new())); + #[cfg(not(feature = "opengl"))] - let handler = build(&mut crate::Window::new(&mut Window { hwnd })); + let handler = build(&mut crate::Window::new(&mut Window { + hwnd, + deferred_tasks: deferred_tasks.clone(), + })); #[cfg(feature = "opengl")] let handler = build(&mut crate::Window::new(&mut Window { hwnd, + deferred_tasks: deferred_tasks.clone(), gl_context: gl_context.clone(), })); let handler = RefCell::new(Box::new(handler)); @@ -559,6 +650,7 @@ impl Window { let parent_handle = if parented { Some(parent_handle) } else { None }; let window_state = Box::new(WindowState { + hwnd, window_class, window_info: RefCell::new(window_info), _parent_handle: parent_handle, @@ -568,10 +660,18 @@ impl Window { scale_policy: options.scale, dw_style: flags, + deferred_tasks: Rc::new(RefCell::new(VecDeque::with_capacity(4))), + #[cfg(feature = "opengl")] gl_context, }); + // If the plugin did queue up tasks as part of the build handler, then we'll process + // them now + for task in deferred_tasks.borrow_mut().drain(..) { + window_state.handle_deferred_task(task); + } + // Only works on Windows 10 unfortunately. SetProcessDpiAwarenessContext( winapi::shared::windef::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, @@ -633,6 +733,13 @@ impl Window { } } + pub fn resize(&mut self, size: Size) { + // To avoid reentrant event handler calls we'll defer the actual resizing until after the + // event has been handled + let task = WindowTask::Resize(size); + self.deferred_tasks.borrow_mut().push_back(task); + } + #[cfg(feature = "opengl")] pub fn gl_context(&self) -> Option<&GlContext> { self.gl_context.as_ref().as_ref() diff --git a/src/window.rs b/src/window.rs index 237e8b6..e137502 100644 --- a/src/window.rs +++ b/src/window.rs @@ -99,12 +99,9 @@ impl<'a> Window<'a> { self.window.close(); } - /// Resize the window to the given size. - /// - /// # TODO - /// - /// This is currently only supported on Linux. - #[cfg(target_os = "linux")] + /// Resize the window to the given size. The size is always in logical pixels. DPI scaling will + /// automatically be accounted for. + #[cfg(any(target_os = "linux", target_os = "windows"))] pub fn resize(&mut self, size: Size) { self.window.resize(size); }