From d2f93d0a90bb8f5794f3a3d013dd5d02ab3b65d9 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Sun, 16 Aug 2026 08:29:22 +0200 Subject: [PATCH 01/10] X11 impl --- examples/render_wgpu/src/main.rs | 2 +- src/platform/x11/window_thread.rs | 2 +- src/platform/x11/xcb_connection/size_hints.rs | 10 ++++++ src/settings.rs | 33 +++++++++++++++++++ src/utils.rs | 19 ++++++++--- src/window.rs | 8 +++++ 6 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 src/platform/x11/xcb_connection/size_hints.rs diff --git a/examples/render_wgpu/src/main.rs b/examples/render_wgpu/src/main.rs index c423c3c1..e351a86d 100644 --- a/examples/render_wgpu/src/main.rs +++ b/examples/render_wgpu/src/main.rs @@ -217,7 +217,7 @@ fn main() -> Result<(), baseview::Error> { let window_open_options = WindowSettings::new() .with_title("WGPU on Baseview") .with_size(LogicalSize::new(512, 512)) - .with_max_size(LogicalSize::new(512, 512)); + .with_aspect_ratio(Some((1, 1))); Window::create(window_open_options, |c| pollster::block_on(WgpuExample::new(c)))? .run_until_closed()?; diff --git a/src/platform/x11/window_thread.rs b/src/platform/x11/window_thread.rs index 7316c7d0..9beb6177 100644 --- a/src/platform/x11/window_thread.rs +++ b/src/platform/x11/window_thread.rs @@ -7,8 +7,8 @@ use crate::platform::x11::present::PresentThreadShared; use crate::platform::x11::sizing::SizingThreadShared; use crate::platform::x11::window_shared::WindowShared; use crate::utils::SizingStrategy; -use crate::warn; use crate::window::WindowInitializer; +use crate::{warn, AspectRatio}; use crate::{WindowContext, WindowSettings, WindowSize}; use calloop::LoopSignal; use std::cell::{Cell, RefCell}; diff --git a/src/platform/x11/xcb_connection/size_hints.rs b/src/platform/x11/xcb_connection/size_hints.rs new file mode 100644 index 00000000..43fc86bd --- /dev/null +++ b/src/platform/x11/xcb_connection/size_hints.rs @@ -0,0 +1,10 @@ + +fn to_x11_aspect_ratio(aspect_ratio: crate::AspectRatio) -> AspectRatio { + match (aspect_ratio.numerator.try_into(), aspect_ratio.denominator.try_into()) { + (Ok(numerator), Ok(denominator)) => AspectRatio { numerator, denominator }, + _ => AspectRatio { + numerator: (aspect_ratio.numerator / 2) as i32, + denominator: (aspect_ratio.denominator / 2) as i32, + }, + } +} diff --git a/src/settings.rs b/src/settings.rs index 491f5ae3..09adb230 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -33,6 +33,8 @@ pub struct WindowSettings { pub min_size: Option, pub max_size: Option, + pub aspect_ratio: Option, + /// A fallback scale factor, if Baseview couldn't get one from the platform. /// /// If the platform does already provide an accurate scaling factor, this doesn't do anything. @@ -126,6 +128,12 @@ impl WindowSettings { self } + #[inline] + pub fn with_aspect_ratio>(mut self, aspect_ratio: Option) -> Self { + self.aspect_ratio = aspect_ratio.map(A::into); + self + } + /// Sets [`gl_config`](Self::gl_config) to the given value. #[cfg(feature = "opengl")] #[inline] @@ -146,6 +154,7 @@ impl Default for WindowSettings { resizable: true, min_size: None, max_size: None, + aspect_ratio: None, #[cfg(feature = "opengl")] gl_config: None, } @@ -193,3 +202,27 @@ impl From for ParentWindowHandle { Self { inner } } } + +/// An aspect ratio `numerator` / `denominator`. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct AspectRatio { + /// The numerator of the aspect ratio. + pub numerator: u32, + /// The denominator of the aspect ratio. + pub denominator: u32, +} + +impl AspectRatio { + /// Create a new aspect ratio with the given `numerator` and `denominator`. + #[inline] + pub fn new(numerator: u32, denominator: u32) -> Self { + Self { numerator, denominator } + } +} + +impl From<(u32, u32)> for AspectRatio { + #[inline] + fn from((numerator, denominator): (u32, u32)) -> Self { + Self::new(numerator, denominator) + } +} diff --git a/src/utils.rs b/src/utils.rs index 446cebe4..bffb1830 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,11 @@ use crate::dpi::Size; -use crate::{WindowSettings, WindowSize}; +use crate::{AspectRatio, WindowSettings, WindowSize}; use dpi::{LogicalSize, PhysicalSize}; #[derive(Copy, Clone)] pub(crate) enum SizingStrategy { Fixed, - Resizable { min_size: Option, max_size: Option }, + Resizable { min_size: Option, max_size: Option, aspect_ratio: Option }, } impl SizingStrategy { @@ -20,7 +20,11 @@ impl SizingStrategy { } } - Self::Resizable { min_size: settings.min_size, max_size: settings.max_size } + Self::Resizable { + min_size: settings.min_size, + max_size: settings.max_size, + aspect_ratio: settings.aspect_ratio, + } } #[inline] @@ -86,10 +90,17 @@ impl SizingStrategy { Size::Logical(size) => self.adjust_size_logical(size, window_size.scale_factor), } } + + pub fn aspect_ratio(&self) -> Option { + match self { + Self::Fixed => None, + Self::Resizable { aspect_ratio, .. } => *aspect_ratio, + } + } } impl Default for SizingStrategy { fn default() -> Self { - Self::Resizable { min_size: None, max_size: None } + Self::Resizable { min_size: None, max_size: None, aspect_ratio: None } } } diff --git a/src/window.rs b/src/window.rs index 7da06306..adcc7c5f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -176,6 +176,14 @@ impl Window { self.inner.sizing_strategy().max_size() } + /// Returns the fixed aspect ratio of the window, if it has one. + /// + /// This is set by the [`WindowSettings::aspect_ratio`] field. + #[inline] + pub fn aspect_ratio(&self) -> Option { + self.inner.aspect_ratio() + } + /// Performs the work the window thread had scheduled for the main thread. /// /// This must be called back on the main thread, as a response to [`HostMainThreadCaller::call_main_thread`](host::HostMainThreadCaller::call_main_thread). From 288e0036183f903b9eafa3e2bdbac1933f0a5b3e Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Sun, 16 Aug 2026 08:34:53 +0200 Subject: [PATCH 02/10] macOS impl --- src/platform/macos/view.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/platform/macos/view.rs b/src/platform/macos/view.rs index 86426581..cb494dbd 100644 --- a/src/platform/macos/view.rs +++ b/src/platform/macos/view.rs @@ -325,6 +325,13 @@ impl BaseviewView { let max_size = max_size.to_logical(scale_factor); window.setContentMaxSize(NSSize::new(max_size.width, max_size.height)); } + + if let Some(aspect_ratio) = this.state.sizing_strategy.aspect_ratio() { + window.setContentAspectRatio(NSSize::new( + aspect_ratio.numerator as _, + aspect_ratio.denominator as _, + )) + } } } From c1b168e4859d724e6c1a035a38b0130b87d5b918 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:30:48 +0200 Subject: [PATCH 03/10] min/max size now forced to respect apsect ratio --- src/settings.rs | 5 ++ src/utils.rs | 119 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/settings.rs b/src/settings.rs index 09adb230..1bfc8d7e 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -218,6 +218,11 @@ impl AspectRatio { pub fn new(numerator: u32, denominator: u32) -> Self { Self { numerator, denominator } } + + #[inline] + pub fn ratio(&self) -> f64 { + (self.numerator as f64) / (self.denominator as f64) + } } impl From<(u32, u32)> for AspectRatio { diff --git a/src/utils.rs b/src/utils.rs index bffb1830..7145677a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -20,11 +20,15 @@ impl SizingStrategy { } } - Self::Resizable { - min_size: settings.min_size, - max_size: settings.max_size, - aspect_ratio: settings.aspect_ratio, + let mut min_size = settings.min_size; + let mut max_size = settings.max_size; + + if let Some(ratio) = settings.aspect_ratio { + min_size = min_size.map(|s| adjust_min_size(s, ratio)); + max_size = max_size.map(|s| adjust_max_size(s, ratio)); } + + Self::Resizable { min_size, max_size, aspect_ratio: settings.aspect_ratio } } #[inline] @@ -104,3 +108,110 @@ impl Default for SizingStrategy { Self::Resizable { min_size: None, max_size: None, aspect_ratio: None } } } + +fn adjust_min_size(size: Size, ratio: AspectRatio) -> Size { + fn inner(x: P, y: P, ratio: AspectRatio) -> (P, P) { + if ratio.numerator < ratio.denominator { + let raw_ratio = ratio.ratio(); + let y = y.into(); + let y = y.max(x.into() / raw_ratio); + (x, y.cast()) + } else if ratio.numerator == ratio.denominator { + if x.into() > y.into() { + (x, x) + } else { + (y, y) + } + } else { + let raw_ratio = ratio.ratio(); + let x = x.into(); + let x = x.max(y.into() * raw_ratio); + (x.cast(), y) + } + } + + match size { + Size::Physical(PhysicalSize { width, height }) => { + Size::Physical(PhysicalSize::from(inner(width, height, ratio))) + } + Size::Logical(LogicalSize { width, height }) => { + Size::Logical(LogicalSize::from(inner(width, height, ratio))) + } + } +} + +fn adjust_max_size(size: Size, ratio: AspectRatio) -> Size { + fn inner(x: P, y: P, ratio: AspectRatio) -> (P, P) { + if ratio.numerator < ratio.denominator { + let raw_ratio = ratio.ratio(); + let x = x.into(); + let x = x.min(y.into() * raw_ratio); + (x.cast(), y) + } else if ratio.numerator == ratio.denominator { + if x.into() > y.into() { + (x, x) + } else { + (y, y) + } + } else { + let raw_ratio = ratio.ratio(); + let y = y.into(); + let y = y.min(x.into() / raw_ratio); + (x, y.cast()) + } + } + + match size { + Size::Physical(PhysicalSize { width, height }) => { + Size::Physical(PhysicalSize::from(inner(width, height, ratio))) + } + Size::Logical(LogicalSize { width, height }) => { + Size::Logical(LogicalSize::from(inner(width, height, ratio))) + } + } +} + +#[cfg(test)] +mod tests { + use crate::utils::SizingStrategy; + use crate::{AspectRatio, WindowSettings}; + use dpi::LogicalSize; + + #[test] + fn aspect_ratio_set_to_min_size() { + let mut settings = WindowSettings::new() + .with_min_size(LogicalSize::new(200.0, 300.0)) + .with_aspect_ratio(Some((1, 2))); + + assert_eq!( + SizingStrategy::from_settings(&settings).min_size(), + Some(LogicalSize::new(200.0, 400.0).into()) + ); + + settings.aspect_ratio = Some(AspectRatio::from((2, 1))); + + assert_eq!( + SizingStrategy::from_settings(&settings).min_size(), + Some(LogicalSize::new(600.0, 300.0).into()) + ); + } + + #[test] + fn aspect_ratio_set_to_max_size() { + let mut settings = WindowSettings::new() + .with_max_size(LogicalSize::new(200.0, 300.0)) + .with_aspect_ratio(Some((1, 2))); + + assert_eq!( + SizingStrategy::from_settings(&settings).max_size(), + Some(LogicalSize::new(150.0, 300.0).into()) + ); + + settings.aspect_ratio = Some(AspectRatio::from((2, 1))); + + assert_eq!( + SizingStrategy::from_settings(&settings).max_size(), + Some(LogicalSize::new(200.0, 100.0).into()) + ); + } +} From 809ae3c7820b88b5406a25c548509c498bb835cd Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 17 Aug 2026 05:26:02 +0200 Subject: [PATCH 04/10] wip --- src/utils.rs | 81 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 7145677a..a8f0f715 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,25 +4,25 @@ use dpi::{LogicalSize, PhysicalSize}; #[derive(Copy, Clone)] pub(crate) enum SizingStrategy { - Fixed, + Fixed { size: Size }, Resizable { min_size: Option, max_size: Option, aspect_ratio: Option }, } impl SizingStrategy { pub fn from_settings(settings: &WindowSettings) -> Self { if !settings.resizable { - return Self::Fixed; + return Self::Fixed { size: settings.size }; } - if let (Some(min_size), Some(max_size)) = (settings.min_size, settings.max_size) { + let mut min_size = settings.min_size; + let mut max_size = settings.max_size; + + if let (Some(min_size), Some(max_size)) = (min_size, max_size) { if min_size == max_size { - return Self::Fixed; + return Self::Fixed { size: min_size }; } } - let mut min_size = settings.min_size; - let mut max_size = settings.max_size; - if let Some(ratio) = settings.aspect_ratio { min_size = min_size.map(|s| adjust_min_size(s, ratio)); max_size = max_size.map(|s| adjust_max_size(s, ratio)); @@ -39,7 +39,7 @@ impl SizingStrategy { #[inline] pub fn min_size(&self) -> Option { match self { - Self::Fixed => None, + Self::Fixed { .. } => None, Self::Resizable { min_size, .. } => *min_size, } } @@ -47,7 +47,7 @@ impl SizingStrategy { #[inline] pub fn max_size(&self) -> Option { match self { - Self::Fixed => None, + Self::Fixed { .. } => None, Self::Resizable { max_size, .. } => *max_size, } } @@ -97,10 +97,49 @@ impl SizingStrategy { pub fn aspect_ratio(&self) -> Option { match self { - Self::Fixed => None, + Self::Fixed { .. } => None, Self::Resizable { aspect_ratio, .. } => *aspect_ratio, } } + + pub fn adjust_size(&self, size: Size, scale_factor: f64) -> Size { + let (min, max, ratio) = match *self { + SizingStrategy::Resizable { min_size, max_size, aspect_ratio } => { + (min_size, max_size, aspect_ratio) + } + SizingStrategy::Fixed { size: fixed_size } => { + return match size { + Size::Physical(_) => Size::Physical(fixed_size.to_physical(scale_factor)), + Size::Logical(_) => Size::Logical(fixed_size.to_logical(scale_factor)), + } + } + }; + + match size { + Size::Physical(mut size) => { + if let Some(max_size) = max { + let max_size = max_size.to_physical(scale_factor); + size.width = size.width.min(max_size.width); + size.height = size.height.min(max_size.height); + } + + if let Some(min_size) = min { + let min_size = min_size.to_physical(scale_factor); + size.width = size.width.max(min_size.width); + size.height = size.height.max(min_size.height); + } + + if let Some(ratio) = ratio { + todo!() + } + + size.into() + } + Size::Logical(size) => { + todo!() + } + } + } } impl Default for SizingStrategy { @@ -109,6 +148,19 @@ impl Default for SizingStrategy { } } +macro_rules! map_size { + ($size:expr, $ratio:expr, $inner:expr) => { + match $size { + Size::Physical(PhysicalSize { width, height }) => { + Size::Physical(PhysicalSize::from($inner(width, height, $ratio))) + } + Size::Logical(LogicalSize { width, height }) => { + Size::Logical(LogicalSize::from($inner(width, height, $ratio))) + } + } + }; +} + fn adjust_min_size(size: Size, ratio: AspectRatio) -> Size { fn inner(x: P, y: P, ratio: AspectRatio) -> (P, P) { if ratio.numerator < ratio.denominator { @@ -161,14 +213,7 @@ fn adjust_max_size(size: Size, ratio: AspectRatio) -> Size { } } - match size { - Size::Physical(PhysicalSize { width, height }) => { - Size::Physical(PhysicalSize::from(inner(width, height, ratio))) - } - Size::Logical(LogicalSize { width, height }) => { - Size::Logical(LogicalSize::from(inner(width, height, ratio))) - } - } + map_size!(size, ratio, inner) } #[cfg(test)] From 60ba31b6263122844b27974e90309ec8f1c2795f Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Sun, 25 Oct 2026 04:07:51 +0100 Subject: [PATCH 05/10] fixes --- src/platform/x11/error.rs | 2 + src/platform/x11/sizing.rs | 50 ++++++++------- src/platform/x11/xcb_connection/size_hints.rs | 10 --- src/utils.rs | 63 +++---------------- src/window.rs | 7 +++ 5 files changed, 47 insertions(+), 85 deletions(-) delete mode 100644 src/platform/x11/xcb_connection/size_hints.rs diff --git a/src/platform/x11/error.rs b/src/platform/x11/error.rs index 83bba98e..4fc341fe 100644 --- a/src/platform/x11/error.rs +++ b/src/platform/x11/error.rs @@ -65,6 +65,7 @@ pub enum PlatformError { RequestFromMainThreadFailed(RequestFailed), SendMainThread, Redraw(String), + ResizeAttemptedOnFixedWindow, #[cfg(feature = "opengl")] XLib(crate::wrappers::xlib::XLibError), #[cfg(feature = "opengl")] @@ -96,6 +97,7 @@ impl Display for PlatformError { PlatformError::Calloop(e) => e.fmt(f), PlatformError::RequestFromMainThreadFailed(e) => e.fmt(f), PlatformError::SendMainThread => FatalError::SendMainThread.fmt(f), + PlatformError::ResizeAttemptedOnFixedWindow => f.write_str("Attempted to resize a window which does not support resizing."), #[cfg(feature = "opengl")] PlatformError::XLib(e) => e.fmt(f), #[cfg(feature = "opengl")] diff --git a/src/platform/x11/sizing.rs b/src/platform/x11/sizing.rs index a5f78b49..8ca5f952 100644 --- a/src/platform/x11/sizing.rs +++ b/src/platform/x11/sizing.rs @@ -176,7 +176,7 @@ impl SizingStateShared { } pub fn make_size_hints(&self) -> WmSizeHints { - get_size_hints(&self.sizing_strategy, self.window_size.get(), self.scale_factor()) + get_size_hints(&self.sizing_strategy, self.scale_factor()) } pub fn store_size( @@ -198,13 +198,12 @@ impl SizingStateShared { return Ok(()); } - window.resize(new_size)?.check()?; - if !self.sizing_strategy.is_resizable() { - let size_hints = get_size_hints(&self.sizing_strategy, new_size, self.scale_factor()); - window.set_size_hints(size_hints)?.check()?; + return Err(PlatformError::ResizeAttemptedOnFixedWindow); } + window.resize(new_size)?.check()?; + // This will trigger a `ConfigureNotify` event which will in turn change `self.window_info` // and notify the window handler about it @@ -220,6 +219,10 @@ impl SizingStateShared { return Ok(()); }; + if !self.sizing_strategy.is_resizable() { + return Err(PlatformError::ResizeAttemptedOnFixedWindow); + } + if let Err(()) = handler.resize(WindowSize::from_physical(new_size.cast(), self.scale_factor())) { @@ -228,10 +231,6 @@ impl SizingStateShared { } shared.xcb_window.resize(new_size.cast())?.check()?; // Will not call handler, as size is the same as above. - if !self.sizing_strategy.is_resizable() { - let size_hints = get_size_hints(&self.sizing_strategy, new_size, self.scale_factor()); - shared.xcb_window.set_size_hints(size_hints)?.check()?; - } // These come from the Host, no need to notify it about the new size @@ -290,28 +289,37 @@ impl SizingThreadShared { } } -pub fn get_size_hints( - strategy: &SizingStrategy, current_size: PhysicalSize, scale_factor: f64, -) -> WmSizeHints { +pub fn get_size_hints(strategy: &SizingStrategy, scale_factor: f64) -> WmSizeHints { + fn to_x11_aspect_ratio(aspect_ratio: crate::AspectRatio) -> x11rb::properties::AspectRatio { + match (aspect_ratio.numerator.try_into(), aspect_ratio.denominator.try_into()) { + (Ok(numerator), Ok(denominator)) => { + x11rb::properties::AspectRatio { numerator, denominator } + } + _ => x11rb::properties::AspectRatio { + numerator: (aspect_ratio.numerator as f64 / 2.0) as i32, + denominator: (aspect_ratio.denominator as f64 / 2.0) as i32, + }, + } + } + let mut size_hints = WmSizeHints::default(); match strategy { - SizingStrategy::Fixed => { - size_hints.min_size = Some(to_size_hint(current_size)); + SizingStrategy::Fixed { size } => { + size_hints.min_size = Some(to_size_hint(size.to_physical(scale_factor))); size_hints.max_size = size_hints.min_size; + size_hints.aspect = None; } - SizingStrategy::Resizable { min_size, max_size } => { - size_hints.min_size = - min_size.map(|s| to_size_hint(s.to_physical::(scale_factor))); - size_hints.max_size = - max_size.map(|s| to_size_hint(s.to_physical::(scale_factor))); + SizingStrategy::Resizable { min_size, max_size, aspect_ratio } => { + size_hints.min_size = min_size.map(|s| to_size_hint(s.to_physical(scale_factor))); + size_hints.max_size = max_size.map(|s| to_size_hint(s.to_physical(scale_factor))); + size_hints.aspect = aspect_ratio.map(to_x11_aspect_ratio).map(|a| (a, a)) } } size_hints } -fn to_size_hint(size: PhysicalSize) -> (i32, i32) { - let size = size.cast(); +fn to_size_hint(size: PhysicalSize) -> (i32, i32) { (size.width, size.height) } diff --git a/src/platform/x11/xcb_connection/size_hints.rs b/src/platform/x11/xcb_connection/size_hints.rs deleted file mode 100644 index 43fc86bd..00000000 --- a/src/platform/x11/xcb_connection/size_hints.rs +++ /dev/null @@ -1,10 +0,0 @@ - -fn to_x11_aspect_ratio(aspect_ratio: crate::AspectRatio) -> AspectRatio { - match (aspect_ratio.numerator.try_into(), aspect_ratio.denominator.try_into()) { - (Ok(numerator), Ok(denominator)) => AspectRatio { numerator, denominator }, - _ => AspectRatio { - numerator: (aspect_ratio.numerator / 2) as i32, - denominator: (aspect_ratio.denominator / 2) as i32, - }, - } -} diff --git a/src/utils.rs b/src/utils.rs index a8f0f715..41af0554 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ use crate::dpi::Size; use crate::{AspectRatio, WindowSettings, WindowSize}; -use dpi::{LogicalSize, PhysicalSize}; +use dpi::{LogicalSize, PhysicalSize, Pixel}; #[derive(Copy, Clone)] pub(crate) enum SizingStrategy { @@ -101,45 +101,6 @@ impl SizingStrategy { Self::Resizable { aspect_ratio, .. } => *aspect_ratio, } } - - pub fn adjust_size(&self, size: Size, scale_factor: f64) -> Size { - let (min, max, ratio) = match *self { - SizingStrategy::Resizable { min_size, max_size, aspect_ratio } => { - (min_size, max_size, aspect_ratio) - } - SizingStrategy::Fixed { size: fixed_size } => { - return match size { - Size::Physical(_) => Size::Physical(fixed_size.to_physical(scale_factor)), - Size::Logical(_) => Size::Logical(fixed_size.to_logical(scale_factor)), - } - } - }; - - match size { - Size::Physical(mut size) => { - if let Some(max_size) = max { - let max_size = max_size.to_physical(scale_factor); - size.width = size.width.min(max_size.width); - size.height = size.height.min(max_size.height); - } - - if let Some(min_size) = min { - let min_size = min_size.to_physical(scale_factor); - size.width = size.width.max(min_size.width); - size.height = size.height.max(min_size.height); - } - - if let Some(ratio) = ratio { - todo!() - } - - size.into() - } - Size::Logical(size) => { - todo!() - } - } - } } impl Default for SizingStrategy { @@ -148,19 +109,6 @@ impl Default for SizingStrategy { } } -macro_rules! map_size { - ($size:expr, $ratio:expr, $inner:expr) => { - match $size { - Size::Physical(PhysicalSize { width, height }) => { - Size::Physical(PhysicalSize::from($inner(width, height, $ratio))) - } - Size::Logical(LogicalSize { width, height }) => { - Size::Logical(LogicalSize::from($inner(width, height, $ratio))) - } - } - }; -} - fn adjust_min_size(size: Size, ratio: AspectRatio) -> Size { fn inner(x: P, y: P, ratio: AspectRatio) -> (P, P) { if ratio.numerator < ratio.denominator { @@ -213,7 +161,14 @@ fn adjust_max_size(size: Size, ratio: AspectRatio) -> Size { } } - map_size!(size, ratio, inner) + match size { + Size::Physical(PhysicalSize { width, height }) => { + Size::Physical(PhysicalSize::from(inner(width, height, ratio))) + } + Size::Logical(LogicalSize { width, height }) => { + Size::Logical(LogicalSize::from(inner(width, height, ratio))) + } + } } #[cfg(test)] diff --git a/src/window.rs b/src/window.rs index adcc7c5f..33626c68 100644 --- a/src/window.rs +++ b/src/window.rs @@ -106,6 +106,13 @@ impl Window { /// The `size` can be provided in either physical or logical pixels. /// /// Using this method does *not* trigger the [`HostCallbacks::request_resize`](host::HostCallbacks) callback. + /// + /// # Errors + /// + /// This function will fail if the window is not resizable. + /// + /// It can also fail if either the underlying window or the platform failed to resize the window + /// or its contents. #[inline] pub fn resize(&self, size: impl Into) -> Result<(), Error> { self.inner.resize(size.into())?; From bbd1c900d0aa63525f16d997695e91326aa27fb8 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:19:30 +0100 Subject: [PATCH 06/10] win32 wip --- src/platform/win/sizing.rs | 22 +++++++++++-- src/window.rs | 2 +- src/wrappers/win32/rect.rs | 12 +++++++ src/wrappers/win32/timer.rs | 6 +++- src/wrappers/win32/window/interfaces.rs | 43 +++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/platform/win/sizing.rs b/src/platform/win/sizing.rs index 07af03c1..25784e82 100644 --- a/src/platform/win/sizing.rs +++ b/src/platform/win/sizing.rs @@ -1,9 +1,9 @@ use crate::platform::{BaseviewWindow, PlatformError, PlatformResult}; use crate::warn; -use crate::wrappers::win32::window::{HWnd, WmSizing}; +use crate::wrappers::win32::window::{HWnd, SizingEdge, WmSizing}; use crate::wrappers::win32::{Dpi, DpiAwarenessGuard, Rect, WindowStyle}; use crate::{HandlerError, WindowSize}; -use dpi::PhysicalSize; +use dpi::{PhysicalSize, Pixel}; impl BaseviewWindow { fn adapt_host_window_to_size( @@ -220,4 +220,22 @@ impl WmSizing for BaseviewWindow { (min_size, max_size) } + + fn on_sizing(&self, drag_rectangle: Rect, edge: SizingEdge) -> Option { + use SizingEdge::*; + + let aspect_ratio = self.shared_state.sizing_strategy.aspect_ratio()?.ratio(); + let requested_size = drag_rectangle.size(); + + match edge { + Right => { + let adjusted_height = requested_size.width as f64 * aspect_ratio; + Some(drag_rectangle.with_size(PhysicalSize { + width: requested_size.width, + height: adjusted_height.cast(), + })) + } + _ => None, + } + } } diff --git a/src/window.rs b/src/window.rs index 33626c68..dd45f779 100644 --- a/src/window.rs +++ b/src/window.rs @@ -188,7 +188,7 @@ impl Window { /// This is set by the [`WindowSettings::aspect_ratio`] field. #[inline] pub fn aspect_ratio(&self) -> Option { - self.inner.aspect_ratio() + self.inner.sizing_strategy().aspect_ratio() } /// Performs the work the window thread had scheduled for the main thread. diff --git a/src/wrappers/win32/rect.rs b/src/wrappers/win32/rect.rs index cf09c35f..8206be0b 100644 --- a/src/wrappers/win32/rect.rs +++ b/src/wrappers/win32/rect.rs @@ -25,6 +25,18 @@ impl Rect { } } + #[inline] + pub fn set_size(&mut self, size: PhysicalSize) { + self.0.right = self.0.left.saturating_add(size.width.try_into().unwrap_or(i32::MAX)); + self.0.bottom = self.0.top.saturating_add(size.height.try_into().unwrap_or(i32::MAX)); + } + + #[inline] + pub fn with_size(mut self, size: PhysicalSize) -> Self { + self.set_size(size); + self + } + #[inline] pub fn is_empty(&self) -> bool { let size = self.size(); diff --git a/src/wrappers/win32/timer.rs b/src/wrappers/win32/timer.rs index f931cada..c678af95 100644 --- a/src/wrappers/win32/timer.rs +++ b/src/wrappers/win32/timer.rs @@ -1,6 +1,7 @@ use crate::platform::{BaseviewWindow, PlatformError, PlatformResult}; use crate::wrappers::win32::window::{ - create_window, use_window, HWnd, WindowImpl, WmCursor, WmDraw, WmKeyboard, WmMouse, WmSizing, + create_window, use_window, HWnd, SizingEdge, WindowImpl, WmCursor, WmDraw, WmKeyboard, WmMouse, + WmSizing, }; use crate::wrappers::win32::{Dpi, DpiAwarenessGuard, Rect, WindowStyle}; use crate::{MouseButton, ScrollDelta}; @@ -205,6 +206,9 @@ impl WmSizing for TimerWindow { fn on_size(&self, _: HWnd, _: PhysicalSize) -> Result<(), ()> { Ok(()) } + fn on_sizing(&self, _: Rect, _: SizingEdge) -> Option { + None + } fn on_dpi_changed(&self, _: HWnd, _: Dpi, _: Rect) -> PlatformResult<()> { Ok(()) } diff --git a/src/wrappers/win32/window/interfaces.rs b/src/wrappers/win32/window/interfaces.rs index e56d1a7f..df1b6b98 100644 --- a/src/wrappers/win32/window/interfaces.rs +++ b/src/wrappers/win32/window/interfaces.rs @@ -19,6 +19,7 @@ pub trait WmSizing { fn get_min_max_size( &self, window: HWnd, ) -> (Option>, Option>); + fn on_sizing(&self, drag_rectangle: Rect, edge: SizingEdge) -> Option; } pub trait WmMouse { @@ -111,6 +112,16 @@ pub unsafe fn handle_message( Some(0) } + WM_SIZING => { + let rect = lparam as *mut RECT; + let dragging_rect = Rect(rect.read()); + let edge = SizingEdge::from_raw(wparam.try_into().ok()?)?; + + let changed_rect = inner.on_sizing(dragging_rect, edge)?; + rect.write(changed_rect.0); + + Some(1) + } // Draw WM_PAINT => { @@ -252,3 +263,35 @@ fn hi_word(wparam: WPARAM) -> u16 { fn lo_word(lparam: LPARAM) -> u16 { (lparam & 0xffff) as u16 } + +#[repr(u8)] +pub enum SizingEdge { + Bottom = WMSZ_BOTTOM as _, + BottomLeft = WMSZ_BOTTOMLEFT as _, + BottomRight = WMSZ_BOTTOMRIGHT as _, + Left = WMSZ_LEFT as _, + Right = WMSZ_RIGHT as _, + Top = WMSZ_TOP as _, + TopLeft = WMSZ_TOPLEFT as _, + TopRight = WMSZ_TOPRIGHT as _, +} + +impl SizingEdge { + pub fn from_raw(raw: u32) -> Option { + match raw { + WMSZ_BOTTOM => Some(SizingEdge::Bottom), + WMSZ_BOTTOMLEFT => Some(SizingEdge::BottomLeft), + WMSZ_BOTTOMRIGHT => Some(SizingEdge::BottomRight), + WMSZ_LEFT => Some(SizingEdge::Left), + WMSZ_RIGHT => Some(SizingEdge::Right), + WMSZ_TOP => Some(SizingEdge::Top), + WMSZ_TOPLEFT => Some(SizingEdge::TopLeft), + WMSZ_TOPRIGHT => Some(SizingEdge::TopRight), + _ => None, + } + } + + pub fn into_raw(self) -> u32 { + self as u32 + } +} From 0cd80509c6bf7e3eeba2422a0d4cebdfa9110599 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:01:05 +0100 Subject: [PATCH 07/10] wip --- src/platform/win/sizing.rs | 17 +++- tests/aspect-ratio/Cargo.toml | 11 +++ tests/aspect-ratio/src/main.rs | 148 +++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 tests/aspect-ratio/Cargo.toml create mode 100644 tests/aspect-ratio/src/main.rs diff --git a/src/platform/win/sizing.rs b/src/platform/win/sizing.rs index 25784e82..d0da18b4 100644 --- a/src/platform/win/sizing.rs +++ b/src/platform/win/sizing.rs @@ -227,14 +227,29 @@ impl WmSizing for BaseviewWindow { let aspect_ratio = self.shared_state.sizing_strategy.aspect_ratio()?.ratio(); let requested_size = drag_rectangle.size(); + if requested_size.width == 0 || requested_size.height == 0 { + return None; + } + + if aspect_ratio == 0. { + return None; + } + match edge { - Right => { + Left | Right => { let adjusted_height = requested_size.width as f64 * aspect_ratio; Some(drag_rectangle.with_size(PhysicalSize { width: requested_size.width, height: adjusted_height.cast(), })) } + Top | Bottom => { + let adjusted_width = requested_size.height as f64 / aspect_ratio; + Some(drag_rectangle.with_size(PhysicalSize { + width: adjusted_width.cast(), + height: requested_size.height, + })) + } _ => None, } } diff --git a/tests/aspect-ratio/Cargo.toml b/tests/aspect-ratio/Cargo.toml new file mode 100644 index 00000000..bbda1398 --- /dev/null +++ b/tests/aspect-ratio/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "aspect-ratio" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +baseview = { path = "../..", features = ["opengl"] } +femtovg = "0.27.0" +rand = "0.10.3" +color = "0.3.3" diff --git a/tests/aspect-ratio/src/main.rs b/tests/aspect-ratio/src/main.rs new file mode 100644 index 00000000..b849711d --- /dev/null +++ b/tests/aspect-ratio/src/main.rs @@ -0,0 +1,148 @@ +use baseview::dpi::LogicalSize; +use baseview::gl::{GlConfig, GlContext}; +use baseview::{ + DamageArea, DamageRect, Event, EventStatus, HandlerError, Window, WindowContext, WindowHandler, + WindowSettings, WindowSize, +}; +use color::{Hsl, OpaqueColor}; +use femtovg::renderer::OpenGl; +use femtovg::{Canvas, Color}; +use std::cell::{Cell, RefCell}; + +struct FemtovgExample { + gl_context: GlContext, + canvas: RefCell>, + damage_handler: DamageHandler, +} + +impl FemtovgExample { + fn new(window_context: WindowContext) -> Result { + let Some(gl_context) = window_context.gl_context() else { unreachable!() }; + unsafe { gl_context.make_current()? }; + + let renderer = + unsafe { OpenGl::new_from_function_cstr(|s| gl_context.get_proc_address(s)) }?; + + let mut canvas = Canvas::new(renderer)?; + let size = window_context.size(); + + canvas.set_size(size.physical.width, size.physical.height, size.scale_factor as f32); + + unsafe { gl_context.make_not_current()? }; + Ok(Self { gl_context, canvas: canvas.into(), damage_handler: DamageHandler::new() }) + } +} + +impl WindowHandler for FemtovgExample { + fn draw(&self) -> Result<(), HandlerError> { + let context = &self.gl_context; + unsafe { context.make_current()? }; + + let mut canvas = self.canvas.borrow_mut(); + + let screen_height = canvas.height(); + let screen_width = canvas.width(); + + let color_of_the_day = new_random_color(); + + if self.damage_handler.is_full_screen_clear() { + canvas.clear_rect(0, 0, screen_width, screen_height, color_of_the_day); + } else { + while let Some(rect) = self.damage_handler.next_damage() { + canvas.clear_rect( + rect.position().x, + rect.position().y, + rect.size().width, + rect.size().height, + color_of_the_day, + ); + } + } + + self.damage_handler.clear(); + + // Tell renderer to execute all drawing commands + canvas.flush(); + context.swap_buffers()?; + unsafe { context.make_not_current()? }; + + Ok(()) + } + + fn damage(&self, area: DamageArea) { + eprintln!("Damaged: {area:?}"); + match area { + DamageArea::FullWindow => self.damage_handler.set_full_screen_clear(), + DamageArea::Rect(rect) => self.damage_handler.add_damaged_rect(rect), + DamageArea::Rects(rects) => { + rects.iter().for_each(|r| self.damage_handler.add_damaged_rect(*r)) + } + _ => {} + } + } + + fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> { + let size = new_size.physical; + self.canvas.borrow_mut().set_size(size.width, size.height, new_size.scale_factor as f32); + + Ok(()) + } + + fn on_event(&self, _event: Event) -> EventStatus { + EventStatus::Ignored + } +} + +fn main() -> Result<(), baseview::Error> { + unsafe { baseview::assume_standalone_in_process() }; + + let window_open_options = WindowSettings::new() + .with_title("Baseview Partial redraw example") + .with_size(LogicalSize::new(512, 512)) + .with_gl_config(GlConfig { alpha_bits: 8, ..GlConfig::default() }); + + let window = Window::create(window_open_options, FemtovgExample::new)?; + + window.run_until_closed()?; + Ok(()) +} + +fn new_random_color() -> Color { + let hue = rand::random_range(0.0..360.0); + let hsv_color = OpaqueColor::::new([hue, 100.0, 15.0]); + let color = hsv_color.to_rgba8(); + + Color::rgb(color.r, color.g, color.b) +} + +struct DamageHandler { + full_screen: Cell, + damaged_rects: RefCell>, +} + +impl DamageHandler { + fn new() -> Self { + Self { full_screen: false.into(), damaged_rects: RefCell::new(Vec::with_capacity(8)) } + } + + fn is_full_screen_clear(&self) -> bool { + self.full_screen.get() + } + + fn set_full_screen_clear(&self) { + self.full_screen.set(true) + } + + fn clear(&self) { + self.full_screen.set(false); + self.damaged_rects.borrow_mut().clear(); + } + + fn next_damage(&self) -> Option { + self.damaged_rects.borrow_mut().pop() + } + + fn add_damaged_rect(&self, rect: DamageRect) { + self.damaged_rects.borrow_mut().push(rect); + } +} From 94d286436029c85280753729c9bcd9c1f30c877c Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:23:03 +0100 Subject: [PATCH 08/10] wip --- Cargo.toml | 1 + src/settings.rs | 4 +- tests/aspect-ratio/src/main.rs | 94 ++++++---------------------------- 3 files changed, 20 insertions(+), 79 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1ca6c7b9..44d3637f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,7 @@ members = [ "examples/test-frame-pacing", "examples/timers", "examples/external-wakeup", + "tests/aspect-ratio", "tests/partial-rendering" ] diff --git a/src/settings.rs b/src/settings.rs index 1bfc8d7e..c88ec3f0 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -215,12 +215,12 @@ pub struct AspectRatio { impl AspectRatio { /// Create a new aspect ratio with the given `numerator` and `denominator`. #[inline] - pub fn new(numerator: u32, denominator: u32) -> Self { + pub const fn new(numerator: u32, denominator: u32) -> Self { Self { numerator, denominator } } #[inline] - pub fn ratio(&self) -> f64 { + pub const fn ratio(&self) -> f64 { (self.numerator as f64) / (self.denominator as f64) } } diff --git a/tests/aspect-ratio/src/main.rs b/tests/aspect-ratio/src/main.rs index b849711d..fdee2811 100644 --- a/tests/aspect-ratio/src/main.rs +++ b/tests/aspect-ratio/src/main.rs @@ -1,18 +1,18 @@ -use baseview::dpi::LogicalSize; +use baseview::dpi::{LogicalSize, PhysicalSize, Pixel}; use baseview::gl::{GlConfig, GlContext}; use baseview::{ - DamageArea, DamageRect, Event, EventStatus, HandlerError, Window, WindowContext, WindowHandler, - WindowSettings, WindowSize, + AspectRatio, DamageArea, Event, EventStatus, HandlerError, Window, WindowContext, + WindowHandler, WindowSettings, WindowSize, }; -use color::{Hsl, OpaqueColor}; use femtovg::renderer::OpenGl; use femtovg::{Canvas, Color}; -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; + +const ASPECT_RATIO: AspectRatio = AspectRatio::new(1, 1); struct FemtovgExample { gl_context: GlContext, canvas: RefCell>, - damage_handler: DamageHandler, } impl FemtovgExample { @@ -29,7 +29,7 @@ impl FemtovgExample { canvas.set_size(size.physical.width, size.physical.height, size.scale_factor as f32); unsafe { gl_context.make_not_current()? }; - Ok(Self { gl_context, canvas: canvas.into(), damage_handler: DamageHandler::new() }) + Ok(Self { gl_context, canvas: canvas.into() }) } } @@ -39,27 +39,18 @@ impl WindowHandler for FemtovgExample { unsafe { context.make_current()? }; let mut canvas = self.canvas.borrow_mut(); + let aspect_ratio = ASPECT_RATIO.ratio(); - let screen_height = canvas.height(); - let screen_width = canvas.width(); - - let color_of_the_day = new_random_color(); + let canvas_height = canvas.height(); + let canvas_width = canvas.width(); - if self.damage_handler.is_full_screen_clear() { - canvas.clear_rect(0, 0, screen_width, screen_height, color_of_the_day); - } else { - while let Some(rect) = self.damage_handler.next_damage() { - canvas.clear_rect( - rect.position().x, - rect.position().y, - rect.size().width, - rect.size().height, - color_of_the_day, - ); - } - } + // If aspect ratio works, these should be equal to the canvas size + let height_ratioed = (canvas_width as f64 / aspect_ratio).cast(); + let width_ratioed = (canvas_height as f64 * aspect_ratio).cast(); - self.damage_handler.clear(); + canvas.clear_rect(0, 0, canvas_width, canvas_height, Color::black()); + canvas.clear_rect(0, height_ratioed, canvas_width, canvas_height, Color::rgb(255, 0, 0)); + canvas.clear_rect(width_ratioed, 0, canvas_width, canvas_height, Color::rgb(255, 0, 0)); // Tell renderer to execute all drawing commands canvas.flush(); @@ -69,18 +60,6 @@ impl WindowHandler for FemtovgExample { Ok(()) } - fn damage(&self, area: DamageArea) { - eprintln!("Damaged: {area:?}"); - match area { - DamageArea::FullWindow => self.damage_handler.set_full_screen_clear(), - DamageArea::Rect(rect) => self.damage_handler.add_damaged_rect(rect), - DamageArea::Rects(rects) => { - rects.iter().for_each(|r| self.damage_handler.add_damaged_rect(*r)) - } - _ => {} - } - } - fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> { let size = new_size.physical; self.canvas.borrow_mut().set_size(size.width, size.height, new_size.scale_factor as f32); @@ -99,6 +78,7 @@ fn main() -> Result<(), baseview::Error> { let window_open_options = WindowSettings::new() .with_title("Baseview Partial redraw example") .with_size(LogicalSize::new(512, 512)) + .with_aspect_ratio(Some(ASPECT_RATIO)) .with_gl_config(GlConfig { alpha_bits: 8, ..GlConfig::default() }); let window = Window::create(window_open_options, FemtovgExample::new)?; @@ -106,43 +86,3 @@ fn main() -> Result<(), baseview::Error> { window.run_until_closed()?; Ok(()) } - -fn new_random_color() -> Color { - let hue = rand::random_range(0.0..360.0); - let hsv_color = OpaqueColor::::new([hue, 100.0, 15.0]); - let color = hsv_color.to_rgba8(); - - Color::rgb(color.r, color.g, color.b) -} - -struct DamageHandler { - full_screen: Cell, - damaged_rects: RefCell>, -} - -impl DamageHandler { - fn new() -> Self { - Self { full_screen: false.into(), damaged_rects: RefCell::new(Vec::with_capacity(8)) } - } - - fn is_full_screen_clear(&self) -> bool { - self.full_screen.get() - } - - fn set_full_screen_clear(&self) { - self.full_screen.set(true) - } - - fn clear(&self) { - self.full_screen.set(false); - self.damaged_rects.borrow_mut().clear(); - } - - fn next_damage(&self) -> Option { - self.damaged_rects.borrow_mut().pop() - } - - fn add_damaged_rect(&self, rect: DamageRect) { - self.damaged_rects.borrow_mut().push(rect); - } -} From 33ab6ef602d17b03e75e37521e1cc3579b756775 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:07:08 +0100 Subject: [PATCH 09/10] wip --- src/platform/win/sizing.rs | 153 +++++++++++++++++++----- src/utils.rs | 4 +- src/wrappers/win32/rect.rs | 29 +++++ src/wrappers/win32/timer.rs | 2 +- src/wrappers/win32/window/interfaces.rs | 5 +- tests/aspect-ratio/src/main.rs | 8 +- 6 files changed, 166 insertions(+), 35 deletions(-) diff --git a/src/platform/win/sizing.rs b/src/platform/win/sizing.rs index d0da18b4..c484b608 100644 --- a/src/platform/win/sizing.rs +++ b/src/platform/win/sizing.rs @@ -1,5 +1,9 @@ use crate::platform::{BaseviewWindow, PlatformError, PlatformResult}; +use crate::utils::SizingStrategy; use crate::warn; +use crate::wrappers::win32::window::SizingEdge::{ + Bottom, BottomLeft, BottomRight, Left, Right, Top, TopLeft, TopRight, +}; use crate::wrappers::win32::window::{HWnd, SizingEdge, WmSizing}; use crate::wrappers::win32::{Dpi, DpiAwarenessGuard, Rect, WindowStyle}; use crate::{HandlerError, WindowSize}; @@ -89,6 +93,106 @@ impl BaseviewWindow { Ok(()) } + + fn on_sizing_inner( + &self, drag_rectangle: Rect, edge: SizingEdge, aspect_ratio: f64, min_max_size: MinMaxSize, + ) -> Rect { + use SizingEdge::*; + //let requested_size = min_max_size.clamp(drag_rectangle.size()); + let requested_size = drag_rectangle.size(); + + match edge { + Left | Right => { + let adjusted_height = requested_size.width as f64 / aspect_ratio; + drag_rectangle.with_size(PhysicalSize { + width: requested_size.width, + height: adjusted_height.cast(), + }) + } + Top | Bottom => { + let adjusted_width = requested_size.height as f64 * aspect_ratio; + drag_rectangle.with_size(PhysicalSize { + width: adjusted_width.cast(), + height: requested_size.height, + }) + } + BottomRight | TopRight => { + let adjusted_height = requested_size.width as f64 / aspect_ratio; + let adjusted_width = requested_size.height as f64 * aspect_ratio; + + let height_diff = requested_size.height.abs_diff(adjusted_height.cast()); + let width_diff = requested_size.width.abs_diff(adjusted_width.cast()); + + if height_diff > width_diff { + drag_rectangle.with_size(PhysicalSize { + width: requested_size.width, + height: adjusted_height.cast(), + }) + } else { + drag_rectangle.with_size(PhysicalSize { + width: adjusted_width.cast(), + height: requested_size.height, + }) + } + } + TopLeft | BottomLeft => { + let adjusted_height = requested_size.width as f64 / aspect_ratio; + let adjusted_width = requested_size.height as f64 * aspect_ratio; + + let height_diff = requested_size.height.abs_diff(adjusted_height.cast()); + let width_diff = requested_size.width.abs_diff(adjusted_width.cast()); + + let mut use_width = height_diff > width_diff; + + if aspect_ratio < 1.0 { + use_width = !use_width + } + + if use_width { + drag_rectangle.with_size_shifting_origin(PhysicalSize { + width: requested_size.width, + height: adjusted_height.cast(), + }) + } else { + drag_rectangle.with_size_shifting_origin(PhysicalSize { + width: adjusted_width.cast(), + height: requested_size.height, + }) + } + } + } + } +} + +#[derive(Copy, Clone)] +struct MinMaxSize { + min_size: Option>, + max_size: Option>, +} + +impl MinMaxSize { + pub fn from_strategy(sizing_strategy: &SizingStrategy, dpi: Dpi) -> Self { + let scale_factor = dpi.scale_factor(); + + Self { + min_size: sizing_strategy.min_size().map(|s| s.to_physical(scale_factor)), + max_size: sizing_strategy.max_size().map(|s| s.to_physical(scale_factor)), + } + } + + pub fn clamp(&self, mut size: PhysicalSize) -> PhysicalSize { + if let Some(max_size) = self.max_size { + size.width = size.width.min(max_size.width); + size.height = size.height.min(max_size.height); + } + + if let Some(min_size) = self.min_size { + size.width = size.width.max(min_size.width); + size.height = size.height.max(min_size.height); + } + + size + } } impl WmSizing for BaseviewWindow { @@ -221,36 +325,31 @@ impl WmSizing for BaseviewWindow { (min_size, max_size) } - fn on_sizing(&self, drag_rectangle: Rect, edge: SizingEdge) -> Option { - use SizingEdge::*; - + fn on_sizing(&self, window: HWnd, nc_drag_rectangle: Rect, edge: SizingEdge) -> Option { let aspect_ratio = self.shared_state.sizing_strategy.aspect_ratio()?.ratio(); - let requested_size = drag_rectangle.size(); - if requested_size.width == 0 || requested_size.height == 0 { - return None; - } + let ctx = DpiAwarenessGuard::new( + &self.shared_state.user32, + self.shared_state.dpi_scaling_strategy.get(), + ) + .unwrap_or_else(|_| DpiAwarenessGuard::dummy()); - if aspect_ratio == 0. { - return None; - } + let style = window.get_style().unwrap_or(WindowStyle::dummy()); + let dpi = self.shared_state.current_dpi.get(); - match edge { - Left | Right => { - let adjusted_height = requested_size.width as f64 * aspect_ratio; - Some(drag_rectangle.with_size(PhysicalSize { - width: requested_size.width, - height: adjusted_height.cast(), - })) - } - Top | Bottom => { - let adjusted_width = requested_size.height as f64 / aspect_ratio; - Some(drag_rectangle.with_size(PhysicalSize { - width: adjusted_width.cast(), - height: requested_size.height, - })) - } - _ => None, - } + let min_max_size = MinMaxSize::from_strategy(&self.shared_state.sizing_strategy, dpi); + + let Ok(client_rect) = ctx.nc_area_to_client_area(nc_drag_rectangle, style, Some(dpi)) + else { + return Some(self.on_sizing_inner(nc_drag_rectangle, edge, aspect_ratio, min_max_size)); + }; + + let updated_rect = self.on_sizing_inner(client_rect, edge, aspect_ratio, min_max_size); + + let Ok(nc_rect) = ctx.client_area_to_nc_area(updated_rect, style, Some(dpi)) else { + return Some(self.on_sizing_inner(client_rect, edge, aspect_ratio, min_max_size)); + }; + + Some(nc_rect) } } diff --git a/src/utils.rs b/src/utils.rs index 41af0554..a7230704 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -52,7 +52,9 @@ impl SizingStrategy { } } - fn adjust_size_physical(&self, mut size: PhysicalSize, scale_factor: f64) -> WindowSize { + pub(crate) fn adjust_size_physical( + &self, mut size: PhysicalSize, scale_factor: f64, + ) -> WindowSize { if let Some(max_size) = self.max_size() { let max_size = max_size.to_physical::(scale_factor); size.width = max_size.width.min(size.width); diff --git a/src/wrappers/win32/rect.rs b/src/wrappers/win32/rect.rs index 8206be0b..3b2c4f24 100644 --- a/src/wrappers/win32/rect.rs +++ b/src/wrappers/win32/rect.rs @@ -31,12 +31,29 @@ impl Rect { self.0.bottom = self.0.top.saturating_add(size.height.try_into().unwrap_or(i32::MAX)); } + #[inline] + pub fn set_size_shifting_origin(&mut self, size: PhysicalSize) { + let current_size = self.size(); + if let Some(w_diff) = checked_signed_diff(size.width, current_size.width) { + self.0.left = self.0.left.saturating_sub(w_diff); + } + if let Some(h_diff) = checked_signed_diff(size.height, current_size.height) { + self.0.top = self.0.top.saturating_add(h_diff); + } + } + #[inline] pub fn with_size(mut self, size: PhysicalSize) -> Self { self.set_size(size); self } + #[inline] + pub fn with_size_shifting_origin(mut self, size: PhysicalSize) -> Self { + self.set_size_shifting_origin(size); + self + } + #[inline] pub fn is_empty(&self) -> bool { let size = self.size(); @@ -78,3 +95,15 @@ impl From> for Rect { }) } } + +// Replace this with u32::checked_signed_diff from stdlib when MSRV >= 1.91 +fn checked_signed_diff(lhs: u32, rhs: u32) -> Option { + let res = lhs.wrapping_sub(rhs) as i32; + let overflow = (lhs >= rhs) == (res < 0); + + if !overflow { + Some(res) + } else { + None + } +} diff --git a/src/wrappers/win32/timer.rs b/src/wrappers/win32/timer.rs index c678af95..1f81fb8d 100644 --- a/src/wrappers/win32/timer.rs +++ b/src/wrappers/win32/timer.rs @@ -206,7 +206,7 @@ impl WmSizing for TimerWindow { fn on_size(&self, _: HWnd, _: PhysicalSize) -> Result<(), ()> { Ok(()) } - fn on_sizing(&self, _: Rect, _: SizingEdge) -> Option { + fn on_sizing(&self, _: HWnd, _: Rect, _: SizingEdge) -> Option { None } fn on_dpi_changed(&self, _: HWnd, _: Dpi, _: Rect) -> PlatformResult<()> { diff --git a/src/wrappers/win32/window/interfaces.rs b/src/wrappers/win32/window/interfaces.rs index df1b6b98..18fdaafc 100644 --- a/src/wrappers/win32/window/interfaces.rs +++ b/src/wrappers/win32/window/interfaces.rs @@ -19,7 +19,7 @@ pub trait WmSizing { fn get_min_max_size( &self, window: HWnd, ) -> (Option>, Option>); - fn on_sizing(&self, drag_rectangle: Rect, edge: SizingEdge) -> Option; + fn on_sizing(&self, window: HWnd, drag_rectangle: Rect, edge: SizingEdge) -> Option; } pub trait WmMouse { @@ -117,7 +117,7 @@ pub unsafe fn handle_message( let dragging_rect = Rect(rect.read()); let edge = SizingEdge::from_raw(wparam.try_into().ok()?)?; - let changed_rect = inner.on_sizing(dragging_rect, edge)?; + let changed_rect = inner.on_sizing(window, dragging_rect, edge)?; rect.write(changed_rect.0); Some(1) @@ -265,6 +265,7 @@ fn lo_word(lparam: LPARAM) -> u16 { } #[repr(u8)] +#[derive(Copy, Clone)] pub enum SizingEdge { Bottom = WMSZ_BOTTOM as _, BottomLeft = WMSZ_BOTTOMLEFT as _, diff --git a/tests/aspect-ratio/src/main.rs b/tests/aspect-ratio/src/main.rs index fdee2811..9df32492 100644 --- a/tests/aspect-ratio/src/main.rs +++ b/tests/aspect-ratio/src/main.rs @@ -1,14 +1,14 @@ -use baseview::dpi::{LogicalSize, PhysicalSize, Pixel}; +use baseview::dpi::{LogicalSize, Pixel}; use baseview::gl::{GlConfig, GlContext}; use baseview::{ - AspectRatio, DamageArea, Event, EventStatus, HandlerError, Window, WindowContext, - WindowHandler, WindowSettings, WindowSize, + AspectRatio, Event, EventStatus, HandlerError, Window, WindowContext, WindowHandler, + WindowSettings, WindowSize, }; use femtovg::renderer::OpenGl; use femtovg::{Canvas, Color}; use std::cell::RefCell; -const ASPECT_RATIO: AspectRatio = AspectRatio::new(1, 1); +const ASPECT_RATIO: AspectRatio = AspectRatio::new(2, 1); struct FemtovgExample { gl_context: GlContext, From ea1ae46f49a57b6a0634e2b584e50ea847c87d66 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:36:54 +0100 Subject: [PATCH 10/10] it works! --- src/platform/win/sizing.rs | 77 +++++++++------------------------- src/wrappers/win32/rect.rs | 51 ++++++++++++++++------ tests/aspect-ratio/src/main.rs | 1 + 3 files changed, 59 insertions(+), 70 deletions(-) diff --git a/src/platform/win/sizing.rs b/src/platform/win/sizing.rs index c484b608..54ebbc0d 100644 --- a/src/platform/win/sizing.rs +++ b/src/platform/win/sizing.rs @@ -98,68 +98,31 @@ impl BaseviewWindow { &self, drag_rectangle: Rect, edge: SizingEdge, aspect_ratio: f64, min_max_size: MinMaxSize, ) -> Rect { use SizingEdge::*; - //let requested_size = min_max_size.clamp(drag_rectangle.size()); - let requested_size = drag_rectangle.size(); - match edge { - Left | Right => { - let adjusted_height = requested_size.width as f64 / aspect_ratio; - drag_rectangle.with_size(PhysicalSize { - width: requested_size.width, - height: adjusted_height.cast(), - }) - } - Top | Bottom => { - let adjusted_width = requested_size.height as f64 * aspect_ratio; - drag_rectangle.with_size(PhysicalSize { - width: adjusted_width.cast(), - height: requested_size.height, - }) - } - BottomRight | TopRight => { - let adjusted_height = requested_size.width as f64 / aspect_ratio; - let adjusted_width = requested_size.height as f64 * aspect_ratio; - - let height_diff = requested_size.height.abs_diff(adjusted_height.cast()); - let width_diff = requested_size.width.abs_diff(adjusted_width.cast()); - - if height_diff > width_diff { - drag_rectangle.with_size(PhysicalSize { - width: requested_size.width, - height: adjusted_height.cast(), - }) - } else { - drag_rectangle.with_size(PhysicalSize { - width: adjusted_width.cast(), - height: requested_size.height, - }) - } - } - TopLeft | BottomLeft => { - let adjusted_height = requested_size.width as f64 / aspect_ratio; - let adjusted_width = requested_size.height as f64 * aspect_ratio; + let requested_size = min_max_size.clamp(drag_rectangle.size()); + + let keep_width_size = PhysicalSize { + width: requested_size.width, + height: (requested_size.width as f64 / aspect_ratio).cast(), + }; - let height_diff = requested_size.height.abs_diff(adjusted_height.cast()); - let width_diff = requested_size.width.abs_diff(adjusted_width.cast()); + let keep_height_size = PhysicalSize { + width: (requested_size.height as f64 * aspect_ratio).cast(), + height: requested_size.height, + }; - let mut use_width = height_diff > width_diff; + let requested_ratio = requested_size.width as f64 / requested_size.height as f64; + let biggest_fitting_rect = + if requested_ratio > aspect_ratio { keep_height_size } else { keep_width_size }; - if aspect_ratio < 1.0 { - use_width = !use_width - } + match edge { + Left | Right => drag_rectangle.with_size_anchor_top_left(keep_width_size), + Top | Bottom => drag_rectangle.with_size_anchor_top_left(keep_height_size), - if use_width { - drag_rectangle.with_size_shifting_origin(PhysicalSize { - width: requested_size.width, - height: adjusted_height.cast(), - }) - } else { - drag_rectangle.with_size_shifting_origin(PhysicalSize { - width: adjusted_width.cast(), - height: requested_size.height, - }) - } - } + BottomRight => drag_rectangle.with_size_anchor_top_left(biggest_fitting_rect), + TopRight => drag_rectangle.with_size_anchor_bottom_left(biggest_fitting_rect), + BottomLeft => drag_rectangle.with_size_anchor_top_right(biggest_fitting_rect), + TopLeft => drag_rectangle.with_size_anchor_bottom_right(biggest_fitting_rect), } } } diff --git a/src/wrappers/win32/rect.rs b/src/wrappers/win32/rect.rs index 3b2c4f24..90616a69 100644 --- a/src/wrappers/win32/rect.rs +++ b/src/wrappers/win32/rect.rs @@ -26,31 +26,56 @@ impl Rect { } #[inline] - pub fn set_size(&mut self, size: PhysicalSize) { + pub fn with_size_anchor_top_left(mut self, size: PhysicalSize) -> Self { self.0.right = self.0.left.saturating_add(size.width.try_into().unwrap_or(i32::MAX)); self.0.bottom = self.0.top.saturating_add(size.height.try_into().unwrap_or(i32::MAX)); + self } #[inline] - pub fn set_size_shifting_origin(&mut self, size: PhysicalSize) { - let current_size = self.size(); - if let Some(w_diff) = checked_signed_diff(size.width, current_size.width) { - self.0.left = self.0.left.saturating_sub(w_diff); - } - if let Some(h_diff) = checked_signed_diff(size.height, current_size.height) { - self.0.top = self.0.top.saturating_add(h_diff); - } + pub fn with_size_anchor_top_right(mut self, size: PhysicalSize) -> Self { + let current_width = self.size().width.try_into().unwrap_or(i32::MAX); + + let size = PhysicalSize { + width: size.width.try_into().unwrap_or(i32::MAX), + height: size.height.try_into().unwrap_or(i32::MAX), + }; + + self.0.left = self.0.left.saturating_add(current_width.saturating_sub(size.width)); + self.0.bottom = self.0.top.saturating_add(size.height); + + self } #[inline] - pub fn with_size(mut self, size: PhysicalSize) -> Self { - self.set_size(size); + pub fn with_size_anchor_bottom_left(mut self, size: PhysicalSize) -> Self { + let current_height = self.size().height.try_into().unwrap_or(i32::MAX); + + let size = PhysicalSize { + width: size.width.try_into().unwrap_or(i32::MAX), + height: size.height.try_into().unwrap_or(i32::MAX), + }; + + self.0.right = self.0.left.saturating_add(size.width); + self.0.top = self.0.top.saturating_add(current_height.saturating_sub(size.height)); + self } #[inline] - pub fn with_size_shifting_origin(mut self, size: PhysicalSize) -> Self { - self.set_size_shifting_origin(size); + pub fn with_size_anchor_bottom_right(mut self, size: PhysicalSize) -> Self { + let current_size = self.size(); + let current_height = current_size.height.try_into().unwrap_or(i32::MAX); + let current_width = current_size.width.try_into().unwrap_or(i32::MAX); + + let size = PhysicalSize { + width: size.width.try_into().unwrap_or(i32::MAX), + height: size.height.try_into().unwrap_or(i32::MAX), + }; + + self.0.left = self.0.left.saturating_add(current_width.saturating_sub(size.width)); + self.0.top = self.0.top.saturating_add(current_height.saturating_sub(size.height)); + self } diff --git a/tests/aspect-ratio/src/main.rs b/tests/aspect-ratio/src/main.rs index 9df32492..c55d734e 100644 --- a/tests/aspect-ratio/src/main.rs +++ b/tests/aspect-ratio/src/main.rs @@ -78,6 +78,7 @@ fn main() -> Result<(), baseview::Error> { let window_open_options = WindowSettings::new() .with_title("Baseview Partial redraw example") .with_size(LogicalSize::new(512, 512)) + .with_min_size(LogicalSize::new(100, 100)) .with_aspect_ratio(Some(ASPECT_RATIO)) .with_gl_config(GlConfig { alpha_bits: 8, ..GlConfig::default() });