diff --git a/Cargo.toml b/Cargo.toml index 5ad265be..6795df2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ tracing = { version = "0.1", optional = true } x11rb = { version = "0.13.2", features = ["cursor", "dri3", "present", "resource_manager", "allow-unsafe-code", "dl-libxcb"], default-features = false } xkbcommon-dl = { version = "0.4.2", features = ["x11"] } x11-dl = { version = "2.21.0" } -calloop = "0.14.4" +calloop = "=0.14.4" # 0.14.5 has MSRV of 1.86 percent-encoding = "2.3.2" bytemuck = { version = "1.25.2", features = ["extern_crate_alloc"] } libloading = "0.8.9" # Libloading 0.9 is out but its MSRV is 1.88 diff --git a/src/platform/win/dpi.rs b/src/platform/win/dpi.rs index 082214a2..36af7bad 100644 --- a/src/platform/win/dpi.rs +++ b/src/platform/win/dpi.rs @@ -9,6 +9,8 @@ use std::ops::Deref; #[derive(Copy, Clone, Default)] pub(crate) struct DpiScalingStrategy { pub assume_96_dpi: bool, + pub should_use_host_suggested_scale_factor: bool, + pub should_enable_nc_dpi_scaling_manually: bool, pub thread_dpi_awareness_context: Option, } @@ -47,6 +49,11 @@ impl DpiScalingStrategy { return Self::get_from_process(user32, &shcore); }; + crate::debug!( + "Parent DPI Awareness Context detected: {:?}", + parent_dpi_ctx.get_type(user32_lib) + ); + if parent.supports_mixed_dpi_hosting_behavior(user32_lib) { Self::get_best_matching_with_dpi_parent_awareness_context( parent_dpi_ctx, @@ -91,9 +98,11 @@ impl DpiScalingStrategy { // These are documented to not be compatible with per-monitor awareness types, so we'll fall back to System-aware // See: https://learn.microsoft.com/en-us/windows/win32/api/windef/ne-windef-dpi_hosting_behavior#remarks if matches!(dpi_awareness_type, Some(Unaware | UnawareGDIScaled | SystemDpiAware)) { - crate::debug!("Parent has DPI Awareness Context with Per-Monitor DPI awareness, falling back to System DPI Awareness."); + crate::debug!("Parent has DPI Awareness Context without Per-Monitor DPI awareness, falling back to System DPI Awareness."); return Self { assume_96_dpi: false, + should_use_host_suggested_scale_factor: false, + should_enable_nc_dpi_scaling_manually: false, thread_dpi_awareness_context: Some(SystemDpiAware.into()), }; } @@ -113,7 +122,19 @@ impl DpiScalingStrategy { // If type is unknown, assume it's better than System-Aware, and we can at least fetch the actual DPI. let assume_96_dpi = matches!(dpi_awareness_type, Some(Unaware | UnawareGDIScaled)); - Self { assume_96_dpi, thread_dpi_awareness_context: Some(dpi_awareness_context) } + // If type is unknown, assume it's better than Per-Monitor-Aware-V2, and we'll get WM_DPICHANGED messages from the OS + let should_use_host_suggested_scale_factor = + matches!(dpi_awareness_type, Some(PerMonitorDpiAware)); + + let should_enable_nc_dpi_scaling_manually = + matches!(dpi_awareness_type, Some(PerMonitorDpiAware)); + + Self { + assume_96_dpi, + should_use_host_suggested_scale_factor, + should_enable_nc_dpi_scaling_manually, + thread_dpi_awareness_context: Some(dpi_awareness_context), + } } fn get_from_process_legacy(shcore: &LazyLibraryModule) -> Self { @@ -126,12 +147,24 @@ impl DpiScalingStrategy { crate::debug!("Using legacy Process DPI Awareness: {:?}", awareness); let assume_96_dpi = matches!(awareness, None | Some(Unaware)); - - Self { assume_96_dpi, thread_dpi_awareness_context: None } + let should_use_host_suggested_scale_factor = matches!(awareness, Some(PerMonitorDpiAware)); + let should_enable_nc_dpi_scaling_manually = matches!(awareness, Some(PerMonitorDpiAware)); + + Self { + assume_96_dpi, + should_use_host_suggested_scale_factor, + should_enable_nc_dpi_scaling_manually, + thread_dpi_awareness_context: None, + } } fn completely_unaware() -> Self { - Self { assume_96_dpi: true, thread_dpi_awareness_context: None } + Self { + assume_96_dpi: true, + should_use_host_suggested_scale_factor: false, + should_enable_nc_dpi_scaling_manually: false, + thread_dpi_awareness_context: None, + } } fn get_best_supported( diff --git a/src/platform/win/error.rs b/src/platform/win/error.rs index adbf71f6..91742dd2 100644 --- a/src/platform/win/error.rs +++ b/src/platform/win/error.rs @@ -1,4 +1,5 @@ use crate::HandlerError; +use dpi::PhysicalSize; use std::fmt::Display; pub type Result = std::result::Result; @@ -6,7 +7,8 @@ pub type Result = std::result::Result; #[derive(Debug)] pub enum PlatformError { Win32(windows_core::Error), - ResizeFailed, + UnexpectedResizeResult { new_actual_size: PhysicalSize }, + InvalidScaleFactor, Handler(HandlerError), } @@ -27,7 +29,14 @@ impl Display for PlatformError { match self { PlatformError::Win32(e) => Display::fmt(e, f), PlatformError::Handler(e) => Display::fmt(e, f), - PlatformError::ResizeFailed => f.write_str("Window resize request failed."), + PlatformError::UnexpectedResizeResult { new_actual_size } => { + write!( + f, + "Resize operation did not result in expected size, got {:?} instead.", + new_actual_size + ) + } + PlatformError::InvalidScaleFactor => f.write_str("Invalid window scale factor."), } } } diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index ca5aab62..48861282 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -98,7 +98,9 @@ impl WindowHandle { if self.state.current_size.get() == new_size { Ok(()) } else { - Err(PlatformError::ResizeFailed) + Err(PlatformError::UnexpectedResizeResult { + new_actual_size: self.state.current_size.get(), + }) } } @@ -106,14 +108,21 @@ impl WindowHandle { self.state.sizing_strategy } - pub fn suggest_scale_factor(&self, scale_factor: f64) -> Result<()> { + pub fn suggest_scale_factor(&self, new_host_scale_factor: f64) -> Result<()> { + let new_dpi = Dpi::from_scale_factor(new_host_scale_factor) + .ok_or(PlatformError::InvalidScaleFactor)?; + let current_scale_factor = self.state.scale_factor(); - self.state.fallback_scale_factor.set(Some(scale_factor)); + self.state.fallback_scale_factor.set(Some(new_host_scale_factor)); + + let dpi_scaling_strategy = self.state.dpi_scaling_strategy.get(); - if self.state.current_dpi.get().is_some() { + if !dpi_scaling_strategy.should_use_host_suggested_scale_factor { return Ok(()); } + self.state.current_dpi.set(new_dpi); + let Some(hwnd) = self.hwnd.get() else { return Ok(()) }; let current_size = self.state.current_size.get(); @@ -122,7 +131,7 @@ impl WindowHandle { .current_size .get() .to_logical::(current_scale_factor) - .to_physical(self.state.scale_factor()); + .to_physical(new_host_scale_factor); // This call doesn't meaningfully change the scaling factor, ignore the result if current_size == new_size { @@ -130,15 +139,16 @@ impl WindowHandle { } let _guard = self.state.originate_host_resize(); - let dpi_ctx = - DpiAwarenessGuard::new(&self.state.user32, self.state.dpi_scaling_strategy.get())?; + let dpi_ctx = DpiAwarenessGuard::new(&self.state.user32, dpi_scaling_strategy)?; - hwnd.resize_and_activate(new_size, None, &dpi_ctx)?; + hwnd.resize_and_activate(new_size, new_dpi, &dpi_ctx)?; if self.state.current_size.get() == new_size { Ok(()) } else { - Err(PlatformError::ResizeFailed) + Err(PlatformError::UnexpectedResizeResult { + new_actual_size: self.state.current_size.get(), + }) } } @@ -289,9 +299,7 @@ impl BaseviewWindow { self.host.notify_destroyed() } - fn request_resize_from_host( - &self, new_size: WindowSize, - ) -> core::result::Result<(), HandlerError> { + fn request_host_resize(&self, new_size: WindowSize) -> core::result::Result<(), HandlerError> { if self.shared_state.resize_host_originated.get() { return Ok(()); }; @@ -299,6 +307,29 @@ impl BaseviewWindow { self.host.request_resize(new_size) } + fn adapt_host_window_to_size( + &self, previous_size: PhysicalSize, new_size: WindowSize, + ) -> core::result::Result<(), ()> { + if let Err(e) = self.request_host_resize(new_size) { + warn!("Resize request from Host failed: {}. Reverting to previous size.", e); + + if let Some(handler) = self.handler.get() { + if let Err(e) = handler.resized(new_size) { + warn!("Window Handler failed to resize to previous window size: {}", e); + } + }; + + self.window_state.shared.current_size.set(previous_size); + if let Err(e) = self.window_state.resize(previous_size.into()) { + warn!("Failed to resize back to previous window size: {}", e); + } + + return Err(()); + } + + Ok(()) + } + pub(crate) fn handle_on_frame(&self) { let Some(handler) = self.handler.get() else { return }; @@ -326,7 +357,7 @@ impl Drop for BaseviewWindow { impl WindowImpl for BaseviewWindow { fn non_client_create(&self, window: HWnd) -> std::result::Result<(), PlatformError> { - if self.shared_state.dpi_scaling_strategy.get().assume_96_dpi { + if self.shared_state.dpi_scaling_strategy.get().should_enable_nc_dpi_scaling_manually { window.enable_non_client_dpi_scaling(&self.shared_state.user32); } @@ -346,23 +377,37 @@ impl WindowImpl for BaseviewWindow { .get_dpi_for_window(window, &self.shared_state.user32); if let Some(dpi) = dpi { - if Some(dpi) != window_state.shared.current_dpi.get() { - window_state.shared.current_dpi.set(Some(dpi)); + if dpi != window_state.shared.current_dpi.get() { + window_state.shared.current_dpi.set(dpi); // 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 + // So, at window creation, we assume a DPI=96 (or parent DPI if available), and if it ends up wrong, we resize the window // to the actual logical size the user desired. let new_size = self.initial_size.to_physical(dpi.scale_factor()); // Preemptively update so a synchronous WM_SIZE from SetWindowPos below // doesn't also emit Resized. - window_state.shared.current_size.set(new_size); + let previous_size = window_state.shared.current_size.replace(new_size); let guard = DpiAwarenessGuard::new( &window_state.shared.user32, self.shared_state.dpi_scaling_strategy.get(), )?; - window.resize_and_activate(new_size, Some(dpi), &guard)?; + window.resize_and_activate(new_size, dpi, &guard)?; + + let _ = self.adapt_host_window_to_size( + previous_size, + WindowSize::from_physical(new_size, dpi.scale_factor()), + ); + } else { + // If the host queried size before it gave us a parent, this is the one it's been using. + let previous_host_size = self.initial_size.to_physical(1.0); + let current_size = self.initial_size.to_physical(dpi.scale_factor()); + + let _ = self.adapt_host_window_to_size( + previous_host_size, + WindowSize::from_physical(current_size, dpi.scale_factor()), + ); } } @@ -598,7 +643,7 @@ unsafe fn wnd_proc_inner( return Some(-1); } - if let Err(e) = window_bv.request_resize_from_host(new_size) { + if let Err(e) = window_bv.request_host_resize(new_size) { warn!("Resize request from Host failed: {}. Reverting to previous size.", e); if let Err(e) = handler.resized(new_size) { @@ -634,9 +679,9 @@ unsafe fn wnd_proc_inner( let new_size = suggested_rect.size(); let changed = window_state.shared.current_size.get() != new_size - || window_state.shared.current_dpi.get() != Some(dpi); + || window_state.shared.current_dpi.get() != dpi; - window_state.shared.current_dpi.set(Some(dpi)); + window_state.shared.current_dpi.set(dpi); let previous_size = window_state.shared.current_size.replace(new_size); // Windows makes us resize the window manually. This however will not send a WM_SIZE event, @@ -654,20 +699,11 @@ unsafe fn wnd_proc_inner( if let Err(e) = window_state.resize(previous_size.into()) { warn!("Failed to resize back to previous window size: {}", e); } - } - if let Err(e) = window_bv.request_resize_from_host(new_size) { - warn!("Resize request from Host failed: {}. Reverting to previous size.", e); - - if let Err(e) = handler.resized(new_size) { - warn!("Window Handler failed to resize to previous window size: {}", e); - } - - window_state.shared.current_size.set(previous_size); - if let Err(e) = window_state.resize(previous_size.into()) { - warn!("Failed to resize back to previous window size: {}", e); - } + return Some(-1); + } + if let Err(()) = window_bv.adapt_host_window_to_size(previous_size, new_size) { return Some(-1); } } @@ -710,16 +746,22 @@ unsafe fn wnd_proc_inner( if let Some(size) = sizing.min_size() { let size = size.to_physical(window_state.shared.scale_factor()); - let size = - ctx.client_area_to_nc_area(size.into(), style, dpi).unwrap().size().cast(); + let size = ctx + .client_area_to_nc_area(size.into(), style, Some(dpi)) + .unwrap() + .size() + .cast(); let pt = POINT { x: size.width, y: size.height }; (&raw mut (*info).ptMinTrackSize).write(pt); } if let Some(size) = sizing.max_size() { let size = size.to_physical(window_state.shared.scale_factor()); - let size = - ctx.client_area_to_nc_area(size.into(), style, dpi).unwrap().size().cast(); + let size = ctx + .client_area_to_nc_area(size.into(), style, Some(dpi)) + .unwrap() + .size() + .cast(); let pt = POINT { x: size.width, y: size.height }; (&raw mut (*info).ptMaxTrackSize).write(pt); } @@ -740,7 +782,7 @@ impl WindowHandle { pub fn create_window(init: WindowInitializer) -> Result { let extended_user_32 = LibraryModule::load()?; - let shared_state = WindowSharedState::new(extended_user_32, &init.settings); + let shared_state = WindowSharedState::new(extended_user_32, &init); if init.settings.wait_for_parent && init.settings.parent.is_none() { return Ok(WindowHandle { diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index 58bd85d2..48d02045 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -8,7 +8,6 @@ use crate::wrappers::win32::cursor::SystemCursor; use crate::wrappers::win32::h_instance::HInstance; use crate::wrappers::win32::window::HWnd; use crate::wrappers::win32::{Dpi, DpiAwarenessGuard, ExtendedUser32, LibraryModule}; -use crate::WindowSettings; use crate::{MouseCursor, WindowSize}; use raw_window_handle::{DisplayHandle, Win32WindowHandle}; use std::cell::{Cell, Ref, RefCell}; @@ -132,7 +131,7 @@ pub struct WindowSharedState { pub parented: Cell, pub is_alive: Cell, pub current_size: Cell>, - pub current_dpi: Cell>, // None if Win32 HiDPI isn't supported + pub current_dpi: Cell, pub fallback_scale_factor: Cell>, pub resize_host_originated: Cell, pub destroy_host_originated: Cell, @@ -143,20 +142,27 @@ pub struct WindowSharedState { } impl WindowSharedState { - pub fn new(user32: LibraryModule, settings: &WindowSettings) -> Rc { - Self { - parented: (settings.parent.is_some() || settings.wait_for_parent).into(), + pub fn new(user32: LibraryModule, init: &WindowInitializer) -> Rc { + let state = Self { + sizing_strategy: SizingStrategy::from_settings(&init.settings), + user32, + + // Internal state is_alive: true.into(), - current_dpi: None.into(), - current_size: settings.size.to_physical(1.0).into(), - fallback_scale_factor: settings.fallback_scale_factor.into(), resize_host_originated: false.into(), destroy_host_originated: false.into(), - sizing_strategy: SizingStrategy::from_settings(settings), - user32, + + // Overriden by init() below + parented: false.into(), + current_dpi: Dpi::default().into(), + current_size: init.settings.size.to_physical(1.0).into(), + fallback_scale_factor: init.settings.fallback_scale_factor.into(), dpi_scaling_strategy: DpiScalingStrategy::default().into(), - } - .into() + }; + + state.init(init); + + state.into() } pub fn init(&self, init: &WindowInitializer) { @@ -169,9 +175,22 @@ impl WindowSharedState { ); if strategy.assume_96_dpi { - self.current_dpi.set(Some(Dpi::default())); + self.current_dpi.set(Dpi::default()); + self.current_size.set(init.settings.size.to_physical(1.0)); + self.fallback_scale_factor.set(Some(1.0)); + } else { + let parent_dpi = parent.and_then(|p| p.get_dpi(&self.user32)); + let scale_factor = parent_dpi + .map(|dpi| dpi.scale_factor()) + .or(init.settings.fallback_scale_factor) + .unwrap_or(1.0); + + self.current_dpi.set(parent_dpi.unwrap_or_default()); + self.current_size.set(init.settings.size.to_physical(scale_factor)); + self.fallback_scale_factor.set(init.settings.fallback_scale_factor); } + self.parented.set(init.settings.parent.is_some() || init.settings.wait_for_parent); self.dpi_scaling_strategy.set(strategy); } @@ -180,11 +199,16 @@ impl WindowSharedState { } pub fn scale_factor(&self) -> f64 { - if let Some(dpi) = self.current_dpi.get() { - dpi.scale_factor() - } else { - self.fallback_scale_factor.get().unwrap_or(1.0) + let strategy = self.dpi_scaling_strategy.get(); + if strategy.assume_96_dpi { + return 1.0; } + + if strategy.should_use_host_suggested_scale_factor { + return self.fallback_scale_factor.get().unwrap_or(1.0); + } + + self.current_dpi.get().scale_factor() } pub fn originate_host_resize(&self) -> impl Drop + use<'_> { diff --git a/src/wrappers/win32/dpi.rs b/src/wrappers/win32/dpi.rs index ab529977..079974d3 100644 --- a/src/wrappers/win32/dpi.rs +++ b/src/wrappers/win32/dpi.rs @@ -3,6 +3,7 @@ use crate::platform::DpiScalingStrategy; use crate::wrappers::win32::user32::ExtendedUser32; use crate::wrappers::win32::DpiAwarenessContextType::*; use std::ffi::c_void; +use std::fmt::{Debug, Formatter}; use std::num::NonZeroU32; use std::ptr::NonNull; use windows_core::{Error, Result}; @@ -19,6 +20,10 @@ impl Dpi { self.0.get() as f64 / USER_DEFAULT_SCREEN_DPI as f64 } + pub fn from_scale_factor(scale_factor: f64) -> Option { + Some(Dpi(NonZeroU32::new((scale_factor * USER_DEFAULT_SCREEN_DPI as f64) as u32)?)) + } + /// Windows 10, version 1607. pub fn get_system(user32: &ExtendedUser32) -> Option { if let Some(get_dpi_for_system) = user32.get_dpi_for_system { @@ -68,6 +73,13 @@ impl Default for Dpi { } } +impl Debug for Dpi { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.get().fmt(f) + } +} + /// Win8 Legacy (replaced by DpiAwarenessContext in Win10), process-wide. #[repr(i32)] #[derive(Copy, Clone, Eq, PartialEq, Debug)] @@ -159,9 +171,7 @@ impl DpiAwarenessContext { /// Windows 10, version 1607. pub fn set_thread(&self, user32: &ExtendedUser32) -> Option> { - let previous = unsafe { - user32.set_thread_dpi_awareness_context?(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) - }; + let previous = unsafe { user32.set_thread_dpi_awareness_context?(self.inner.as_ptr()) }; let Some(inner) = NonNull::new(previous) else { return Some(Err(Error::from_thread())) }; @@ -169,9 +179,7 @@ impl DpiAwarenessContext { } pub fn set_process(&self, user32: &ExtendedUser32) -> Option> { - let result = unsafe { - user32.set_process_dpi_awareness_context?(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) - }; + let result = unsafe { user32.set_process_dpi_awareness_context?(self.inner.as_ptr()) }; if result == FALSE { return Some(Err(Error::from_thread())); @@ -325,6 +333,14 @@ impl<'a> DpiAwarenessGuard<'a> { } } +impl Debug for DpiAwarenessGuard<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DpiAwarenessGuard") + .field("context_type", &self.inner.map(|(c, u)| c.get_type(u))) + .finish() + } +} + impl Drop for DpiAwarenessGuard<'_> { fn drop(&mut self) { if let Some(inner) = self.inner { diff --git a/src/wrappers/win32/rect.rs b/src/wrappers/win32/rect.rs index c4bd73e6..77e95341 100644 --- a/src/wrappers/win32/rect.rs +++ b/src/wrappers/win32/rect.rs @@ -1,4 +1,5 @@ use crate::dpi::PhysicalSize; +use std::fmt::Debug; use windows_sys::Win32::Foundation::RECT; #[derive(Copy, Clone)] @@ -25,3 +26,14 @@ impl From> for Rect { }) } } + +impl Debug for Rect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Rect") + .field("left", &self.0.left) + .field("top", &self.0.top) + .field("right", &self.0.right) + .field("bottom", &self.0.bottom) + .finish() + } +} diff --git a/src/wrappers/win32/window/handle.rs b/src/wrappers/win32/window/handle.rs index 1f463d83..cdf17dd8 100644 --- a/src/wrappers/win32/window/handle.rs +++ b/src/wrappers/win32/window/handle.rs @@ -173,12 +173,12 @@ impl HWnd { } pub fn resize_and_activate( - &self, client_size: PhysicalSize, window_dpi: Option, dpi_ctx: &DpiAwarenessGuard, + &self, client_size: PhysicalSize, window_dpi: Dpi, dpi_ctx: &DpiAwarenessGuard, ) -> Result<()> { let style = self.get_style()?; let rect = Rect::from(client_size); - let rect = dpi_ctx.client_area_to_nc_area(rect, style, window_dpi)?; + let rect = dpi_ctx.client_area_to_nc_area(rect, style, Some(window_dpi))?; self.resize_nc_and_activate(rect.size()) }