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/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 _, + )) + } } } 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/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/settings.rs b/src/settings.rs index 491f5ae3..1bfc8d7e 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,32 @@ 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 } + } + + #[inline] + pub fn ratio(&self) -> f64 { + (self.numerator as f64) / (self.denominator as f64) + } +} + +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..41af0554 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,26 +1,34 @@ use crate::dpi::Size; -use crate::{WindowSettings, WindowSize}; -use dpi::{LogicalSize, PhysicalSize}; +use crate::{AspectRatio, WindowSettings, WindowSize}; +use dpi::{LogicalSize, PhysicalSize, Pixel}; #[derive(Copy, Clone)] pub(crate) enum SizingStrategy { - Fixed, - Resizable { min_size: Option, max_size: Option }, + 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 }; } } - Self::Resizable { min_size: settings.min_size, 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] @@ -31,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, } } @@ -39,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, } } @@ -86,10 +94,124 @@ 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 } + } +} + +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()) + ); } } diff --git a/src/window.rs b/src/window.rs index 7da06306..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())?; @@ -176,6 +183,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).