diff --git a/examples/plugin_clack/src/gui.rs b/examples/plugin_clack/src/gui.rs index 613ae6a6..2f98ebb3 100644 --- a/examples/plugin_clack/src/gui.rs +++ b/examples/plugin_clack/src/gui.rs @@ -93,8 +93,23 @@ impl PluginGuiImpl for ExamplePluginMainThread<'_> { }) } - fn adjust_size(&mut self, size: GuiSize) -> Option { - Some(size) // Not supported yet + fn adjust_size(&mut self, mut size: GuiSize) -> Option { + let Some(gui) = &self.gui else { return None }; + let scale_factor = gui.handle.size().scale_factor; + + if let Some(max_size) = gui.handle.max_size() { + let max_size = size_to_gui_size(max_size, scale_factor); + size.width = size.width.min(max_size.width); + size.height = size.height.min(max_size.height); + } + + if let Some(min_size) = gui.handle.min_size() { + let min_size = size_to_gui_size(min_size, scale_factor); + size.width = size.width.max(min_size.width); + size.height = size.height.max(min_size.height); + } + + Some(size) } fn set_size(&mut self, size: GuiSize) -> Result<(), PluginError> { @@ -150,6 +165,20 @@ impl PluginGuiImpl for ExamplePluginMainThread<'_> { } } +fn size_to_gui_size(size: Size, scale_factor: f64) -> GuiSize { + #[cfg(target_os = "macos")] + { + let size = size.to_logical(scale_factor); + GuiSize { width: size.width, height: size.height } + } + + #[cfg(not(target_os = "macos"))] + { + let size = size.to_physical(scale_factor); + GuiSize { width: size.width, height: size.height } + } +} + fn window_size_to_gui_size(size: WindowSize) -> GuiSize { #[cfg(target_os = "macos")] { diff --git a/examples/render_wgpu/src/main.rs b/examples/render_wgpu/src/main.rs index f72944d5..7d254377 100644 --- a/examples/render_wgpu/src/main.rs +++ b/examples/render_wgpu/src/main.rs @@ -211,8 +211,10 @@ impl WindowHandler for WgpuExample { fn main() -> Result<(), baseview::Error> { env_logger::builder().filter_level(LevelFilter::Debug).init(); - let window_open_options = - WindowSettings::new().with_title("WGPU on Baseview").with_size(LogicalSize::new(512, 512)); + let window_open_options = WindowSettings::new() + .with_title("WGPU on Baseview") + .with_size(LogicalSize::new(512, 512)) + .with_max_size(LogicalSize::new(512, 512)); Window::create(window_open_options, |c| pollster::block_on(WgpuExample::new(c)))? .run_until_closed()?; diff --git a/src/lib.rs b/src/lib.rs index a7f6a6e6..8872fe2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,4 +28,5 @@ pub use window::*; #[allow(unused)] pub(crate) use tracing::*; +mod utils; pub(crate) mod wrappers; diff --git a/src/platform/macos/view.rs b/src/platform/macos/view.rs index 9c143270..3b484889 100644 --- a/src/platform/macos/view.rs +++ b/src/platform/macos/view.rs @@ -5,6 +5,7 @@ use super::window::WindowSharedState; use crate::host::Host; use crate::platform::*; use crate::tracing::warn; +use crate::utils::SizingStrategy; use crate::window::WindowInitializer; use crate::wrappers::appkit::*; use crate::MouseEvent::{ButtonPressed, ButtonReleased}; @@ -85,7 +86,11 @@ impl BaseviewView { let view_rect = NSRect::new(NSPoint::ZERO, NSSize::new(final_size.width, final_size.height)); - let state = Rc::new(WindowSharedState::new(final_size, 1.0, init.settings.resizable)); + let state = Rc::new(WindowSharedState::new( + final_size, + 1.0, + SizingStrategy::from_settings(&init.settings), + )); let inner = BaseviewView { mtm, @@ -111,6 +116,8 @@ impl BaseviewView { view.state.scale_factor.set(view.view.backing_scale_factor()); view.state.size.set(view.view.size()); + Self::apply_size_constraints(view); + #[cfg(feature = "opengl")] if let Some(gl_config) = init.settings.gl_config { let gl_context = super::gl::GlContext::create(view.view, gl_config, view.mtm)?; @@ -244,6 +251,24 @@ impl BaseviewView { Self::close(this, false); } } + + fn apply_size_constraints(this: ViewRef) { + let ViewParentingType::Windowed { owned_window } = &*this.parenting.borrow() else { + return; + }; + let Some(window) = owned_window.load() else { return }; + let scale_factor = window.backingScaleFactor(); + + if let Some(min_size) = this.state.sizing_strategy.min_size() { + let min_size = min_size.to_logical(scale_factor); + window.setContentMinSize(NSSize::new(min_size.width, min_size.height)); + } + + if let Some(max_size) = this.state.sizing_strategy.max_size() { + let max_size = max_size.to_logical(scale_factor); + window.setContentMaxSize(NSSize::new(max_size.width, max_size.height)); + } + } } impl Drop for BaseviewView { @@ -290,6 +315,10 @@ impl ViewImpl for BaseviewView { let current_size = this.view.size(); let current_scale_factor = this.view.backing_scale_factor(); + if this.state.scale_factor.get() != current_scale_factor { + Self::apply_size_constraints(this); + } + // Only send the event when the window's size has actually changed to be in line with the // other platform implementations if this.state.scale_factor.get() != current_scale_factor diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index 808c8b43..954d7847 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -9,6 +9,7 @@ use std::rc::Rc; use crate::platform::macos::view::{BaseviewView, ViewParentingType}; use crate::platform::ParentWindowHandle; use crate::platform::Result; +use crate::utils::SizingStrategy; use crate::wrappers::appkit::{create_window, View}; use crate::*; @@ -96,7 +97,15 @@ impl WindowHandle { } pub fn is_resizable(&self) -> bool { - self.state.resizable + self.state.sizing_strategy.is_resizable() + } + + pub fn min_size(&self) -> Option { + self.state.sizing_strategy.min_size() + } + + pub fn max_size(&self) -> Option { + self.state.sizing_strategy.max_size() } #[inline] @@ -170,16 +179,16 @@ pub(crate) struct WindowSharedState { pub closed: Cell, pub size: Cell>, pub scale_factor: Cell, - pub resizable: bool, + pub sizing_strategy: SizingStrategy, } impl WindowSharedState { - pub fn new(size: LogicalSize, scale_factor: f64, resizable: bool) -> Self { + pub fn new(size: LogicalSize, scale_factor: f64, sizing_strategy: SizingStrategy) -> Self { Self { closed: false.into(), size: size.into(), scale_factor: scale_factor.into(), - resizable, + sizing_strategy, } } } diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index 1867139e..afe0519b 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -1,22 +1,14 @@ use windows_core::{ComObject, HSTRING}; use windows_sys::Win32::{ Foundation::{LPARAM, LRESULT, RECT, WPARAM}, - UI::{ - Controls::WM_MOUSELEAVE, - WindowsAndMessaging::{ - HTCLIENT, WHEEL_DELTA, WM_CHAR, WM_CLOSE, WM_DPICHANGED, WM_INPUTLANGCHANGE, - WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, - WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN, - WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SIZE, WM_SYSCHAR, WM_SYSKEYDOWN, - WM_SYSKEYUP, WM_TIMER, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, - }, - }, + UI::{Controls::WM_MOUSELEAVE, WindowsAndMessaging::*}, }; use crate::{warn, HandlerError}; use dpi::{PhysicalPosition, PhysicalSize, Size}; use std::cell::Cell; use std::num::NonZeroUsize; +use windows_sys::Win32::Foundation::POINT; pub(crate) const BV_WINDOW_MUST_CLOSE: u32 = WM_USER + 1; @@ -69,7 +61,15 @@ impl WindowHandle { } pub fn is_resizable(&self) -> bool { - self.state.resizable + self.state.sizing_strategy.is_resizable() + } + + pub fn min_size(&self) -> Option { + self.state.sizing_strategy.min_size() + } + + pub fn max_size(&self) -> Option { + self.state.sizing_strategy.max_size() } pub fn size(&self) -> WindowSize { @@ -627,6 +627,38 @@ unsafe fn wnd_proc_inner( None } } + WM_GETMINMAXINFO => { + let sizing = window_state.shared.sizing_strategy; + + // Only implement this message if we actually need to specify a min/max size + if let (None, None) = (sizing.min_size(), sizing.max_size()) { + return None; + } + + let info = lparam as *mut MINMAXINFO; + + let ctx = DpiAwarenessContext::new(&window_state.user32).unwrap(); + let style = window.get_style().unwrap(); + let dpi = window_state.shared.current_dpi.get(); + + if let Some(size) = sizing.min_size() { + let size = size.to_physical(window_state.shared.scale_factor()); + let size = + ctx.client_area_to_nc_area(size.into(), style, dpi).unwrap().size().cast(); + let pt = POINT { x: size.width, y: size.height }; + (&raw mut (*info).ptMinTrackSize).write(pt); + } + + if let Some(size) = sizing.max_size() { + let size = size.to_physical(window_state.shared.scale_factor()); + let size = + ctx.client_area_to_nc_area(size.into(), style, dpi).unwrap().size().cast(); + let pt = POINT { x: size.width, y: size.height }; + (&raw mut (*info).ptMaxTrackSize).write(pt); + } + + Some(0) + } // NOTE: `WM_NCDESTROY` is handled in the outer function because this deallocates the window // state BV_WINDOW_MUST_CLOSE => { diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index 9207c962..75e00247 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -1,5 +1,6 @@ use crate::platform::win::keyboard::KeyboardState; use crate::platform::PlatformHandle; +use crate::utils::SizingStrategy; use crate::wrappers::win32::cursor::SystemCursor; use crate::wrappers::win32::h_instance::HInstance; use crate::wrappers::win32::window::HWnd; @@ -151,7 +152,7 @@ pub struct WindowSharedState { pub destroy_host_originated: Cell, pub user32: ExtendedUser32, - pub resizable: bool, + pub sizing_strategy: SizingStrategy, } impl WindowSharedState { @@ -164,7 +165,7 @@ impl WindowSharedState { fallback_scale_factor: settings.fallback_scale_factor.into(), resize_host_originated: false.into(), destroy_host_originated: false.into(), - resizable: settings.resizable, + sizing_strategy: SizingStrategy::from_settings(settings), user32, } .into() diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index 119427fd..81582180 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -1,9 +1,10 @@ use crate::platform::x11::event_loop::EventLoop; use crate::platform::x11::visual_info::WindowVisualConfig; use crate::platform::x11::window_thread::WindowThreadShared; -use crate::platform::x11::xcb_connection::{get_size_hints, WmSizeHintsExt}; +use crate::platform::x11::xcb_connection::get_size_hints; use crate::platform::x11::xcb_window::XcbWindow; use crate::platform::*; +use crate::utils::SizingStrategy; use crate::{warn, MouseCursor, WindowHandler, WindowSettings, WindowSize}; use calloop::LoopSignal; use dpi::{PhysicalSize, Size}; @@ -11,7 +12,6 @@ use raw_window_handle::{DisplayHandle, XlibWindowHandle}; use std::cell::Cell; use std::rc::Rc; use std::sync::Arc; -use x11rb::properties::WmSizeHints; use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt, InputFocus, Visualid}; use x11rb::CURRENT_TIME; @@ -51,7 +51,7 @@ pub(crate) struct WindowInner { pub(crate) scaling_factor: ScalingFactor, window_size: Cell>, - pub(crate) is_resizable: bool, + pub(crate) sizing_strategy: SizingStrategy, mouse_cursor: Cell, pub(crate) visual_id: Visualid, @@ -76,7 +76,9 @@ impl WindowInner { let physical_size = options.size.to_physical(initial_scale_factor); - let size_hints = get_size_hints(&options, initial_scale_factor); + let sizing_strategy = SizingStrategy::from_settings(&options); + + let size_hints = get_size_hints(&sizing_strategy, physical_size, initial_scale_factor); #[cfg(feature = "opengl")] let visual_info = @@ -127,7 +129,7 @@ impl WindowInner { system: scaling.into(), suggested: options.fallback_scale_factor.into(), }, - is_resizable: options.resizable, + sizing_strategy, mouse_cursor: MouseCursor::default().into(), loop_signal: ev_loop.get_signal(), @@ -193,8 +195,9 @@ impl WindowInner { let new_physical_size = size.to_physical(self.scaling_factor.get()); self.xcb_window.resize(new_physical_size)?.check()?; - if !self.is_resizable { - let size_hints = WmSizeHints::new().with_fixed_size(new_physical_size.cast()); + if !self.sizing_strategy.is_resizable() { + let size_hints = + get_size_hints(&self.sizing_strategy, new_physical_size, self.scale_factor()); self.xcb_window.set_size_hints(size_hints)?.check()?; } @@ -222,8 +225,8 @@ impl WindowInner { } self.xcb_window.resize(new_size.cast())?.check()?; // Will not call handler, as size is the same as above. - if !self.is_resizable { - let size_hints = WmSizeHints::new().with_fixed_size(new_size.cast()); + if !self.sizing_strategy.is_resizable() { + let size_hints = get_size_hints(&self.sizing_strategy, new_size, self.scale_factor()); self.xcb_window.set_size_hints(size_hints)?.check()?; } diff --git a/src/platform/x11/window_thread.rs b/src/platform/x11/window_thread.rs index 9e10e728..1e545851 100644 --- a/src/platform/x11/window_thread.rs +++ b/src/platform/x11/window_thread.rs @@ -3,6 +3,7 @@ use crate::handler::WindowHandlerBuilder; use crate::host::HostCallbacks; use crate::platform::x11::event_loop::{EventLoop, MainThreadCaller}; use crate::platform::x11::window_shared::WindowInner; +use crate::utils::SizingStrategy; use crate::warn; use crate::window::WindowInitializer; use crate::{WindowContext, WindowSettings, WindowSize}; @@ -12,7 +13,7 @@ use std::cell::{Cell, RefCell}; use std::panic::resume_unwind; use std::rc::Rc; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; -use std::sync::{mpsc, Mutex}; +use std::sync::{mpsc, Mutex, OnceLock}; use std::thread; use std::thread::JoinHandle; @@ -22,7 +23,7 @@ pub(crate) struct WindowThreadShared { size: AtomicU32, final_error: Mutex>, stopped_requested_from_host: AtomicBool, - is_resizable: AtomicBool, + sizing_strategy: OnceLock, } impl WindowThreadShared { @@ -33,14 +34,14 @@ impl WindowThreadShared { size: 0.into(), scaling_factor: 0.into(), stopped_requested_from_host: false.into(), - is_resizable: true.into(), + sizing_strategy: OnceLock::new(), } } fn init(&self, window: &WindowInner) { self.set_size(window.get_size()); self.set_scaling_factor(window.scale_factor()); - self.set_resizable(window.is_resizable); + let Ok(()) = self.sizing_strategy.set(window.sizing_strategy) else { unreachable!() }; } pub fn get_size(&self) -> PhysicalSize { @@ -56,12 +57,8 @@ impl WindowThreadShared { self.size.store(bytes, Ordering::Relaxed); } - pub fn set_resizable(&self, resizable: bool) { - self.is_resizable.store(resizable, Ordering::Relaxed); - } - - pub fn is_resizable(&self) -> bool { - self.is_resizable.load(Ordering::Relaxed) + pub fn sizing_strategy(&self) -> SizingStrategy { + self.sizing_strategy.get().copied().unwrap_or_default() } pub fn get_scaling_factor(&self) -> f64 { @@ -213,7 +210,15 @@ impl WindowThreadHandle { } pub fn is_resizable(&self) -> bool { - self.shared.is_resizable() + self.shared.sizing_strategy().is_resizable() + } + + pub fn min_size(&self) -> Option { + self.shared.sizing_strategy().min_size() + } + + pub fn max_size(&self) -> Option { + self.shared.sizing_strategy().max_size() } pub fn handle_main_thread_callback(&self) { diff --git a/src/platform/x11/xcb_connection.rs b/src/platform/x11/xcb_connection.rs index a82c55ff..4dd4d44c 100644 --- a/src/platform/x11/xcb_connection.rs +++ b/src/platform/x11/xcb_connection.rs @@ -14,7 +14,7 @@ use crate::MouseCursor; mod get_property; pub use get_property::GetPropertyError; mod size_hints; -pub use size_hints::{get_size_hints, WmSizeHintsExt}; +pub use size_hints::get_size_hints; x11rb::atom_manager! { pub Atoms: AtomsCookie { diff --git a/src/platform/x11/xcb_connection/size_hints.rs b/src/platform/x11/xcb_connection/size_hints.rs index 9d2157ba..eb11cde4 100644 --- a/src/platform/x11/xcb_connection/size_hints.rs +++ b/src/platform/x11/xcb_connection/size_hints.rs @@ -1,24 +1,29 @@ -use crate::WindowSettings; -use dpi::PhysicalSize; +use crate::utils::SizingStrategy; +use dpi::{PhysicalSize, Pixel}; use x11rb::properties::WmSizeHints; -pub fn get_size_hints(settings: &WindowSettings, scale_factor: f64) -> WmSizeHints { +pub fn get_size_hints( + strategy: &SizingStrategy, current_size: PhysicalSize, scale_factor: f64, +) -> WmSizeHints { let mut size_hints = WmSizeHints::default(); - if !settings.resizable { - size_hints = size_hints.with_fixed_size(settings.size.to_physical(scale_factor)); + match strategy { + SizingStrategy::Fixed => { + size_hints.min_size = Some(to_size_hint(current_size)); + size_hints.max_size = size_hints.min_size; + } + 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))); + } } size_hints } -pub trait WmSizeHintsExt: Sized { - fn with_fixed_size(self, size: PhysicalSize) -> Self; -} - -impl WmSizeHintsExt for WmSizeHints { - fn with_fixed_size(mut self, size: PhysicalSize) -> Self { - self.max_size = Some((size.width, size.height)); - self - } +fn to_size_hint(size: PhysicalSize) -> (i32, i32) { + let size = size.cast(); + (size.width, size.height) } diff --git a/src/settings.rs b/src/settings.rs index 22d51f3f..65ff592b 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -30,6 +30,9 @@ pub struct WindowSettings { /// Whether the window can be resized. pub resizable: bool, + pub min_size: Option, + pub max_size: 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. @@ -111,6 +114,18 @@ impl WindowSettings { self } + #[inline] + pub fn with_min_size>(mut self, min_size: impl Into>) -> Self { + self.min_size = min_size.into().map(S::into); + self + } + + #[inline] + pub fn with_max_size>(mut self, max_size: impl Into>) -> Self { + self.max_size = max_size.into().map(S::into); + self + } + /// Sets [`gl_config`](Self::gl_config) to the given value. #[cfg(feature = "opengl")] #[inline] @@ -129,6 +144,8 @@ impl Default for WindowSettings { wait_for_parent: false, fallback_scale_factor: None, resizable: true, + min_size: None, + max_size: None, #[cfg(feature = "opengl")] gl_config: None, } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 00000000..f4712021 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,48 @@ +use crate::WindowSettings; +use dpi::Size; + +#[derive(Copy, Clone)] +pub(crate) enum SizingStrategy { + Fixed, + Resizable { min_size: Option, max_size: Option }, +} + +impl SizingStrategy { + pub fn from_settings(settings: &WindowSettings) -> Self { + if !settings.resizable { + return Self::Fixed; + } + + if let (Some(min_size), Some(max_size)) = (settings.min_size, settings.max_size) { + if min_size == max_size { + return Self::Fixed; + } + } + + Self::Resizable { min_size: settings.min_size, max_size: settings.max_size } + } + + pub fn is_resizable(&self) -> bool { + matches!(self, Self::Resizable { .. }) + } + + pub fn min_size(&self) -> Option { + match self { + Self::Fixed => None, + Self::Resizable { min_size, .. } => *min_size, + } + } + + pub fn max_size(&self) -> Option { + match self { + Self::Fixed => None, + Self::Resizable { max_size, .. } => *max_size, + } + } +} + +impl Default for SizingStrategy { + fn default() -> Self { + Self::Resizable { min_size: None, max_size: None } + } +} diff --git a/src/window.rs b/src/window.rs index 4cc976b4..ebb51a9c 100644 --- a/src/window.rs +++ b/src/window.rs @@ -159,6 +159,22 @@ impl Window { self.inner.is_resizable() } + /// Returns the minimum size of the window, if it has one. + /// + /// This is set by the [`WindowSettings::min_size`] field. + #[inline] + pub fn min_size(&self) -> Option { + self.inner.min_size() + } + + /// Returns the minimum size of the window, if it has one. + /// + /// This is set by the [`WindowSettings::max_size`] field. + #[inline] + pub fn max_size(&self) -> Option { + self.inner.max_size() + } + /// 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).