diff --git a/src/win/window.rs b/src/win/window.rs index 6788ae20..cdad8794 100644 --- a/src/win/window.rs +++ b/src/win/window.rs @@ -9,8 +9,6 @@ use windows_sys::Win32::{ WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, 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, }, }, }; @@ -42,7 +40,7 @@ use crate::wrappers::win32::cursor::SystemCursor; use crate::gl::GlContext; use crate::wrappers::win32::window::*; use crate::wrappers::win32::{ - ole_initialize, run_thread_message_loop_until, set_process_per_monitor_dpi_aware, Rect, + ole_initialize, run_thread_message_loop_until, Dpi, DpiAwarenessContext, Rect, WindowStyle, }; #[allow(non_snake_case)] @@ -126,34 +124,29 @@ impl WindowImpl for BaseviewWindow { self._keyboard_hook.set(Some(hook::init_keyboard_hook(hwnd))); - // Only works on Windows 10 unfortunately. - set_process_per_monitor_dpi_aware(); - // Now we can get the actual dpi of the window. - let new_size = if let WindowScalePolicy::SystemScaleFactor = self.window_state.scale_policy - { - // Only works on Windows 10 unfortunately. - let dpi = window.get_dpi()?; - let scale_factor = dpi as f64 / 96.0; - - let current_scale_factor = window_state.current_scale_factor.get(); + let dpi = window.get_dpi()?; + let mut dpi_changed = false; + + if dpi != window_state.current_dpi.get() { + window_state.current_dpi.set(dpi); + dpi_changed = true; + + // If the user's requested initial size was in system-scaled logical pixels + if let WindowScalePolicy::SystemScaleFactor = self.window_state.scale_policy { + // We cannot create a window in "logical" pixels, and we can't DPI-scale to physical pixels because we + // have no way to know where the window will end up. + // So, at window creation, we assume a DPI=96, and if it ends up wrong, we resize the window + // to the actual logical size the user desired. + let new_size = WindowInfo::from_logical_size(self.initial_size, dpi.scale_factor()) + .physical_size(); - 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(new_size) - } else { - None + window.resize_and_activate(new_size, dpi)?; } - } else { - None - }; + } let drop_target = ComObject::new(DropTarget::new(Rc::downgrade(window_state))); self._drop_target.set(Some(drop_target.clone())); @@ -161,15 +154,6 @@ impl WindowImpl for BaseviewWindow { ole_initialize()?; window.register_drag_drop(drop_target.as_interface())?; - if let Some(new_size) = new_size { - // 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. - window.resize_and_activate(new_size, window_state.dw_style)?; - - // 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(); @@ -191,6 +175,11 @@ impl WindowImpl for BaseviewWindow { }; *window_state.handler.borrow_mut() = Some(handler); + if dpi_changed { + // Send an initial Resized event so users get the correct scale factor and physical size. + self.window_state.send_resized(); + } + Ok(()) } @@ -404,7 +393,7 @@ unsafe fn wnd_proc_inner( window_state.current_size.set(new_size); - WindowInfo::from_physical_size(new_size, window_state.current_scale_factor.get()) + WindowInfo::from_physical_size(new_size, window_state.current_scale_factor()) }; window_state.handle_event(Event::Window(WindowEvent::Resized(new_window_info))); @@ -412,35 +401,28 @@ unsafe fn wnd_proc_inner( None } WM_DPICHANGED => { - let new_rect = Rect((lparam as *const RECT).read()); + let suggested_nc_rect = Rect((lparam as *const RECT).read()); + let dpi = Dpi((wparam & 0xFFFF) as u16 as u32); - let current_size = window_state.current_size.get(); - let new_size = new_rect.size(); + let dpi_ctx = DpiAwarenessContext::new().unwrap(); + let style = window.get_style().unwrap(); + let suggested_rect = + dpi_ctx.nc_area_to_client_area(suggested_nc_rect, style, dpi).unwrap(); - let mut changed = current_size != new_size; + let new_size = suggested_rect.size(); - 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 changed = window_state.current_size.get() != new_size + || window_state.current_dpi.get() != dpi; - changed |= window_state.current_scale_factor.get() != scale_factor; - - window_state.current_scale_factor.set(scale_factor); - } + window_state.current_dpi.set(dpi); + window_state.current_size.set(new_size); // Windows makes us resize the window manually. This however will not send a WM_SIZE event, // hence why we are notifying the window handler manually below. - let _ = window.set_nc_rect(new_rect); + let _ = window.set_nc_rect(suggested_nc_rect); if changed { - // FIXME: this confuses NC size and client size! - window_state.current_size.set(new_size); - - let new_window_info = WindowInfo::from_physical_size( - new_size, - window_state.current_scale_factor.get(), - ); - window_state.handle_event(Event::Window(WindowEvent::Resized(new_window_info))); + window_state.send_resized(); } None @@ -482,7 +464,7 @@ pub(super) struct WindowState { /// GWLP_USERDATA) } as *const WindowState`. pub hwnd: HWND, current_size: Cell, - current_scale_factor: Cell, + current_dpi: Cell, // None if in non-system scale policy keyboard_state: RefCell, mouse_button_counter: Cell, mouse_was_outside_window: Cell, @@ -490,7 +472,6 @@ pub(super) struct WindowState { // Initialized late so the `Window` can hold a reference to this `WindowState` handler: RefCell>>, 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 @@ -504,13 +485,10 @@ pub(super) struct WindowState { } impl WindowState { - pub fn new( - hwnd: HWND, current_size: PhySize, scaling: f64, scale_policy: WindowScalePolicy, - style_flags: u32, - ) -> Self { + pub fn new(hwnd: HWND, current_size: PhySize, scale_policy: WindowScalePolicy) -> Self { Self { hwnd, - current_scale_factor: scaling.into(), + current_dpi: Dpi::default().into(), current_size: current_size.into(), keyboard_state: RefCell::new(KeyboardState::new()), mouse_button_counter: Cell::new(0), @@ -518,7 +496,6 @@ impl WindowState { cursor_icon: Cell::new(MouseCursor::Default), handler: RefCell::new(None), scale_policy, - dw_style: style_flags, deferred_tasks: RefCell::new(VecDeque::with_capacity(4)), @@ -547,19 +524,22 @@ impl WindowState { } pub(super) fn window_info(&self) -> WindowInfo { - WindowInfo::from_physical_size(self.current_size.get(), self.current_scale_factor.get()) + WindowInfo::from_physical_size(self.current_size.get(), self.current_scale_factor()) + } + + fn current_scale_factor(&self) -> f64 { + match self.scale_policy { + WindowScalePolicy::ScaleFactor(scale) => scale, + WindowScalePolicy::SystemScaleFactor => self.current_dpi.get().scale_factor(), + } } pub(super) fn keyboard_state(&self) -> Ref<'_, KeyboardState> { self.keyboard_state.borrow() } - 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()); - - self.handle_event(Event::Window(WindowEvent::Resized(window_info))); + fn send_resized(&self) { + self.handle_event(Event::Window(WindowEvent::Resized(self.window_info()))); } /// Handle a deferred task as described in [`Self::deferred_tasks`]. @@ -568,11 +548,11 @@ impl WindowState { WindowTask::Resize(size) => { // `self.window_info` will be modified in response to the `WM_SIZE` event that // follows the `SetWindowPos()` call - let scaling = self.current_scale_factor.get(); - let window_info = WindowInfo::from_logical_size(size, scaling); + let dpi = self.current_dpi.get(); + let window_info = WindowInfo::from_logical_size(size, dpi.scale_factor()); let new_size = window_info.physical_size(); - window.resize_and_activate(new_size, self.dw_style).unwrap(); + window.resize_and_activate(new_size, dpi).unwrap(); } WindowTask::Focus => window.set_focus().unwrap(), } @@ -631,42 +611,26 @@ impl Window<'_> { { let title = HSTRING::from(options.title); - let scaling = match options.scale { + let scaling_factor = match options.scale { WindowScalePolicy::SystemScaleFactor => 1.0, WindowScalePolicy::ScaleFactor(scale) => scale, }; - let current_size = WindowInfo::from_logical_size(options.size, scaling).physical_size(); - let mut rect = Rect::from(current_size); + let window_size = + WindowInfo::from_logical_size(options.size, scaling_factor).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 style = if parented { WindowStyle::parented() } else { WindowStyle::embedded() }; + let dpi_ctx = DpiAwarenessContext::new().unwrap(); - if !parented { - rect = rect.client_area_to_nc_area(flags).unwrap(); - } + let rect = + dpi_ctx.client_area_to_nc_area(window_size.into(), style, Dpi::default()).unwrap(); let is_open = Rc::new(Cell::new(true)); let parent_handle = ParentHandle { is_open: is_open.clone() }; let initializer = move |hwnd: HWnd| { - let window_state = Rc::new(WindowState::new( - hwnd.as_raw(), - current_size, - scaling, - options.scale, - flags, - )); + let window_state = Rc::new(WindowState::new(hwnd.as_raw(), window_size, options.scale)); BaseviewWindow { window_state, @@ -683,7 +647,7 @@ impl Window<'_> { }; let hwnd = - create_window(&title, flags, rect.size(), parent as *mut _, initializer).unwrap(); + create_window(&title, style, rect.size(), parent as *mut _, initializer).unwrap(); // SAFETY: this handle should be safe to use let window = unsafe { HWnd::from_raw(hwnd) }; diff --git a/src/wrappers/win32.rs b/src/wrappers/win32.rs index f41021e2..6818f6e0 100644 --- a/src/wrappers/win32.rs +++ b/src/wrappers/win32.rs @@ -1,18 +1,19 @@ pub mod cursor; +mod dpi; pub mod h_instance; mod rect; +mod style; pub mod uuid; pub mod window; +pub use dpi::*; pub use rect::Rect; +pub use style::*; use std::ptr::null_mut; use windows_core::{Error, Result, HRESULT}; use windows_sys::Win32::Foundation::{S_FALSE, S_OK}; use windows_sys::Win32::System::Ole::OleInitialize; -use windows_sys::Win32::UI::HiDpi::{ - SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, -}; use windows_sys::Win32::UI::WindowsAndMessaging::{ DispatchMessageW, GetMessageW, TranslateMessage, MSG, }; @@ -51,8 +52,3 @@ pub fn run_thread_message_loop_until(until: impl Fn() -> bool) -> Result<()> { } } } - -// Only works on Windows 10 unfortunately. -pub fn set_process_per_monitor_dpi_aware() { - unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE) }; -} diff --git a/src/wrappers/win32/dpi.rs b/src/wrappers/win32/dpi.rs new file mode 100644 index 00000000..e594e495 --- /dev/null +++ b/src/wrappers/win32/dpi.rs @@ -0,0 +1,74 @@ +use super::*; +use windows_core::{Error, Result}; +use windows_sys::Win32::Foundation::RECT; +use windows_sys::Win32::UI::HiDpi::*; +use windows_sys::Win32::UI::WindowsAndMessaging::USER_DEFAULT_SCREEN_DPI; + +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct Dpi(pub u32); + +impl Dpi { + pub fn scale_factor(&self) -> f64 { + self.0 as f64 / USER_DEFAULT_SCREEN_DPI as f64 + } +} + +impl Default for Dpi { + fn default() -> Self { + Self(USER_DEFAULT_SCREEN_DPI) + } +} + +pub struct DpiAwarenessContext { + previous: DPI_AWARENESS_CONTEXT, +} + +impl DpiAwarenessContext { + pub fn new() -> Result { + let mut previous = + unsafe { SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }; + + if previous.is_null() { + previous = + unsafe { SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE) }; + } + + if previous.is_null() { + return Err(Error::from_win32()); + } + + Ok(DpiAwarenessContext { previous }) + } + + pub fn client_area_to_nc_area( + &self, mut rect: Rect, style: WindowStyle, dpi: Dpi, + ) -> Result { + // AdjustWindowRectExForDpi takes the current DPI awareness context in consideration. + // Therefore, this method taking &self enforces that the DPI aware context is correct. + let result = + unsafe { AdjustWindowRectExForDpi(&mut rect.0, style.style, 0, style.style_ex, dpi.0) }; + + if result == 0 { + return Err(Error::from_win32()); + } + + Ok(rect) + } + + pub fn nc_area_to_client_area(&self, rect: Rect, style: WindowStyle, dpi: Dpi) -> Result { + let result = self.client_area_to_nc_area(Rect::EMPTY, style, dpi)?; + + Ok(Rect(RECT { + left: rect.0.left - result.0.left, + top: rect.0.top - result.0.top, + bottom: rect.0.bottom - result.0.bottom, + right: rect.0.right - result.0.right, + })) + } +} + +impl Drop for DpiAwarenessContext { + fn drop(&mut self) { + let _ = unsafe { SetThreadDpiAwarenessContext(self.previous) }; + } +} diff --git a/src/wrappers/win32/rect.rs b/src/wrappers/win32/rect.rs index 1ad74eab..8f7c29be 100644 --- a/src/wrappers/win32/rect.rs +++ b/src/wrappers/win32/rect.rs @@ -1,20 +1,11 @@ use crate::PhySize; -use windows_core::{Error, Result}; use windows_sys::Win32::Foundation::RECT; -use windows_sys::Win32::UI::WindowsAndMessaging::AdjustWindowRectEx; #[derive(Copy, Clone)] pub struct Rect(pub RECT); impl Rect { - pub fn client_area_to_nc_area(mut self, style: u32) -> Result { - let result = unsafe { AdjustWindowRectEx(&mut self.0, style, 0, 0) }; - if result == 0 { - return Err(Error::from_win32()); - } - - Ok(self) - } + pub const EMPTY: Self = Self(RECT { left: 0, top: 0, right: 0, bottom: 0 }); pub fn size(&self) -> PhySize { PhySize { diff --git a/src/wrappers/win32/style.rs b/src/wrappers/win32/style.rs new file mode 100644 index 00000000..1437cb84 --- /dev/null +++ b/src/wrappers/win32/style.rs @@ -0,0 +1,26 @@ +use windows_sys::Win32::UI::WindowsAndMessaging::*; + +#[derive(Copy, Clone)] +pub struct WindowStyle { + pub style: WINDOW_STYLE, + pub style_ex: WINDOW_EX_STYLE, +} + +impl WindowStyle { + pub const fn parented() -> Self { + Self { style: WS_CHILD | WS_VISIBLE, style_ex: 0 } + } + + pub const fn embedded() -> Self { + Self { + style: WS_POPUPWINDOW + | WS_CAPTION + | WS_VISIBLE + | WS_SIZEBOX + | WS_MINIMIZEBOX + | WS_MAXIMIZEBOX + | WS_CLIPSIBLINGS, + style_ex: 0, + } + } +} diff --git a/src/wrappers/win32/window.rs b/src/wrappers/win32/window.rs index 038b129a..ad62e3c2 100644 --- a/src/wrappers/win32/window.rs +++ b/src/wrappers/win32/window.rs @@ -12,9 +12,10 @@ use window_class::RegisteredClass; use windows_core::{Error, Result, HSTRING}; use crate::wrappers::win32::h_instance::HInstance; +use crate::wrappers::win32::style::WindowStyle; use crate::PhySize; use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; -use windows_sys::Win32::UI::WindowsAndMessaging::{CreateWindowExW, WINDOW_STYLE}; +use windows_sys::Win32::UI::WindowsAndMessaging::CreateWindowExW; pub trait WindowImpl: 'static { /// Called during the processing of the WM_CREATE message, but after this type was properly @@ -49,7 +50,7 @@ pub trait WindowImpl: 'static { /// 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_size: PhySize, parent: HWND, + title: &HSTRING, style: WindowStyle, nc_size: PhySize, parent: HWND, initializer: impl FnOnce(HWnd) -> W + 'static, ) -> Result { let instance = HInstance::get(); @@ -59,10 +60,10 @@ pub fn create_window( let hwnd = unsafe { CreateWindowExW( - 0, + style.style_ex, window_class.as_atom_ptr(), title.as_ptr(), - flags, + style.style, 0, 0, nc_size.width.try_into().unwrap_or(i32::MAX), diff --git a/src/wrappers/win32/window/handle.rs b/src/wrappers/win32/window/handle.rs index 587ae11d..f356af5a 100644 --- a/src/wrappers/win32/window/handle.rs +++ b/src/wrappers/win32/window/handle.rs @@ -1,4 +1,5 @@ -use crate::wrappers::win32::Rect; +use crate::wrappers::win32::style::WindowStyle; +use crate::wrappers::win32::{Dpi, DpiAwarenessContext, Rect}; use crate::PhySize; use std::marker::PhantomData; use std::num::NonZeroUsize; @@ -12,8 +13,9 @@ use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ GetFocus, ReleaseCapture, SetCapture, SetFocus, TrackMouseEvent, TME_LEAVE, TRACKMOUSEEVENT, }; use windows_sys::Win32::UI::WindowsAndMessaging::{ - DestroyWindow, GetWindowLongPtrW, SetTimer, SetWindowLongPtrW, SetWindowPos, ShowWindow, - GWLP_USERDATA, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOZORDER, SW_SHOW, + DestroyWindow, GetWindowLongPtrW, GetWindowLongW, SetTimer, SetWindowLongPtrW, SetWindowPos, + ShowWindow, GWLP_USERDATA, GWL_EXSTYLE, GWL_STYLE, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOZORDER, + SW_SHOW, WINDOW_LONG_PTR_INDEX, }; /// A simple wrapper around a HWND. @@ -58,11 +60,37 @@ impl HWnd<'_> { Err(error) } - pub fn get_dpi(&self) -> Result { + pub fn get_long(&self, index: WINDOW_LONG_PTR_INDEX) -> Result { + // SAFETY: This function is always safe to call + unsafe { SetLastError(0) }; + // SAFETY: This type guarantees the HWND is still valid. + let result = unsafe { GetWindowLongW(self.0, index) }; + if result != 0 { + return Ok(result); + } + + // We can't know if a return value of 0 is indicative of an error, or if it's just because the + // value was actually 0. So we check GetLastError instead (called by Error::from_win32). + let error = Error::from_win32(); + if error.code() == HRESULT(0) { + return Ok(result); + } + + Err(error) + } + + pub fn get_style(&self) -> Result { + Ok(WindowStyle { + style: self.get_long(GWL_STYLE)? as _, + style_ex: self.get_long(GWL_EXSTYLE)? as _, + }) + } + + pub fn get_dpi(&self) -> Result { // SAFETY: This type guarantees the HWND is safe to use. match unsafe { GetDpiForWindow(self.0) } { 0 => Err(Error::from_win32()), - dpi => Ok(dpi), + dpi => Ok(Dpi(dpi)), } } @@ -106,8 +134,12 @@ impl HWnd<'_> { Ok(()) } - pub fn resize_and_activate(&self, client_size: PhySize, style: u32) -> Result<()> { - let rect = Rect::from(client_size).client_area_to_nc_area(style)?; + pub fn resize_and_activate(&self, client_size: PhySize, window_dpi: Dpi) -> Result<()> { + let dpi_ctx = DpiAwarenessContext::new()?; + let style = self.get_style()?; + + let rect = Rect::from(client_size); + let rect = dpi_ctx.client_area_to_nc_area(rect, style, window_dpi)?; self.resize_nc_and_activate(rect.size()) }