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/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/win/sizing.rs b/src/platform/win/sizing.rs index 07af03c1..54ebbc0d 100644 --- a/src/platform/win/sizing.rs +++ b/src/platform/win/sizing.rs @@ -1,9 +1,13 @@ use crate::platform::{BaseviewWindow, PlatformError, PlatformResult}; +use crate::utils::SizingStrategy; use crate::warn; -use crate::wrappers::win32::window::{HWnd, WmSizing}; +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}; -use dpi::PhysicalSize; +use dpi::{PhysicalSize, Pixel}; impl BaseviewWindow { fn adapt_host_window_to_size( @@ -89,6 +93,69 @@ 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 keep_width_size = PhysicalSize { + width: requested_size.width, + height: (requested_size.width as f64 / aspect_ratio).cast(), + }; + + let keep_height_size = PhysicalSize { + width: (requested_size.height as f64 * aspect_ratio).cast(), + height: requested_size.height, + }; + + 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 }; + + 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), + + 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), + } + } +} + +#[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 { @@ -220,4 +287,32 @@ impl WmSizing for BaseviewWindow { (min_size, max_size) } + + 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 ctx = DpiAwarenessGuard::new( + &self.shared_state.user32, + self.shared_state.dpi_scaling_strategy.get(), + ) + .unwrap_or_else(|_| DpiAwarenessGuard::dummy()); + + let style = window.get_style().unwrap_or(WindowStyle::dummy()); + let dpi = self.shared_state.current_dpi.get(); + + 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/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..c88ec3f0 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 const fn new(numerator: u32, denominator: u32) -> Self { + Self { numerator, denominator } + } + + #[inline] + pub const 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..a7230704 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,12 +47,14 @@ impl SizingStrategy { #[inline] pub fn max_size(&self) -> Option { match self { - Self::Fixed => None, + Self::Fixed { .. } => None, Self::Resizable { max_size, .. } => *max_size, } } - 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); @@ -86,10 +96,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..dd45f779 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.sizing_strategy().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). diff --git a/src/wrappers/win32/rect.rs b/src/wrappers/win32/rect.rs index cf09c35f..90616a69 100644 --- a/src/wrappers/win32/rect.rs +++ b/src/wrappers/win32/rect.rs @@ -25,6 +25,60 @@ impl Rect { } } + #[inline] + 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 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_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_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 + } + #[inline] pub fn is_empty(&self) -> bool { let size = self.size(); @@ -66,3 +120,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 f931cada..1f81fb8d 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, _: HWnd, _: 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..18fdaafc 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, window: HWnd, 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(window, dragging_rect, edge)?; + rect.write(changed_rect.0); + + Some(1) + } // Draw WM_PAINT => { @@ -252,3 +263,36 @@ fn hi_word(wparam: WPARAM) -> u16 { fn lo_word(lparam: LPARAM) -> u16 { (lparam & 0xffff) as u16 } + +#[repr(u8)] +#[derive(Copy, Clone)] +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 + } +} 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..c55d734e --- /dev/null +++ b/tests/aspect-ratio/src/main.rs @@ -0,0 +1,89 @@ +use baseview::dpi::{LogicalSize, Pixel}; +use baseview::gl::{GlConfig, GlContext}; +use baseview::{ + 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(2, 1); + +struct FemtovgExample { + gl_context: GlContext, + canvas: RefCell>, +} + +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() }) + } +} + +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 aspect_ratio = ASPECT_RATIO.ratio(); + + let canvas_height = canvas.height(); + let canvas_width = canvas.width(); + + // 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(); + + 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(); + context.swap_buffers()?; + unsafe { context.make_not_current()? }; + + Ok(()) + } + + 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_min_size(LogicalSize::new(100, 100)) + .with_aspect_ratio(Some(ASPECT_RATIO)) + .with_gl_config(GlConfig { alpha_bits: 8, ..GlConfig::default() }); + + let window = Window::create(window_open_options, FemtovgExample::new)?; + + window.run_until_closed()?; + Ok(()) +}