diff --git a/examples/plugin_clack/src/gui.rs b/examples/plugin_clack/src/gui.rs index 6cf17273..613ae6a6 100644 --- a/examples/plugin_clack/src/gui.rs +++ b/examples/plugin_clack/src/gui.rs @@ -77,16 +77,19 @@ impl PluginGuiImpl for ExamplePluginMainThread<'_> { } fn can_resize(&mut self) -> bool { - true // Non-resizeable windows not supported yet + let Some(gui) = &self.gui else { return false }; + + gui.handle.is_resizable() } fn get_resize_hints(&mut self) -> Option { + let can_resize = self.can_resize(); + Some(GuiResizeHints { strategy: AspectRatioStrategy::Disregard, // Not supported - // Non-resizeable windows not supported yet - can_resize_vertically: true, - can_resize_horizontally: true, + can_resize_vertically: can_resize, + can_resize_horizontally: can_resize, }) } diff --git a/src/platform/macos/context.rs b/src/platform/macos/context.rs index 6c78b956..20e498fd 100644 --- a/src/platform/macos/context.rs +++ b/src/platform/macos/context.rs @@ -67,7 +67,7 @@ impl WindowContext { return Ok(()); } - BaseviewView::resize(view, size, true); + BaseviewView::resize(view, size, true, false); Ok(()) } diff --git a/src/platform/macos/view.rs b/src/platform/macos/view.rs index 3d4fd51b..9c143270 100644 --- a/src/platform/macos/view.rs +++ b/src/platform/macos/view.rs @@ -85,7 +85,7 @@ 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)); + let state = Rc::new(WindowSharedState::new(final_size, 1.0, init.settings.resizable)); let inner = BaseviewView { mtm, @@ -205,7 +205,7 @@ impl BaseviewView { this.parenting.replace(parenting); } - pub fn resize(this: ViewRef, size: Size, notify_host: bool) { + pub fn resize(this: ViewRef, size: Size, notify_host: bool, from_window: bool) { let size = size.to_logical::(this.view.backing_scale_factor()); // NOTE: macOS gives you a personal rave if you pass in fractional pixels here. Even // though the size is in fractional pixels. @@ -221,10 +221,12 @@ impl BaseviewView { gl_context.resize(size); } - // If this is a standalone window then we'll also need to resize the window itself - if let ViewParentingType::Windowed { owned_window } = &*this.parenting.borrow() { - if let Some(owned_window) = owned_window.load() { - owned_window.setContentSize(size); + if !from_window { + // If this is a standalone window then we'll also need to resize the window itself + if let ViewParentingType::Windowed { owned_window } = &*this.parenting.borrow() { + if let Some(owned_window) = owned_window.load() { + owned_window.setContentSize(size); + } } } @@ -275,6 +277,15 @@ impl ViewImpl for BaseviewView { true } + fn window_did_resize(this: ViewRef) { + let Some(window) = this.view.window() else { return }; + + let size = window.contentRectForFrameRect(window.frame()).size; + let size = LogicalSize::new(size.width, size.height); + + BaseviewView::resize(this, size.into(), true, true); + } + fn view_did_change_backing_properties(this: ViewRef, notify_host: bool) { let current_size = this.view.size(); let current_scale_factor = this.view.backing_scale_factor(); @@ -294,7 +305,7 @@ impl ViewImpl for BaseviewView { warn!("Window Handler failed to resize: {}", e); this.state.size.set(previous); - Self::resize(this, previous.into(), false); + Self::resize(this, previous.into(), false, false); return; } @@ -302,7 +313,7 @@ impl ViewImpl for BaseviewView { if let Err(e) = this.host.request_resize(new_size) { warn!("Host failed to resize parent view: {}", e); - Self::resize(this, previous.into(), false); + Self::resize(this, previous.into(), false, false); } } } diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index f6ae43ac..895352bc 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -95,6 +95,10 @@ impl WindowHandle { self.state.closed.get() } + pub fn is_resizable(&self) -> bool { + self.state.resizable + } + #[inline] pub fn handle_main_thread_callback(&self) { // No-op @@ -108,7 +112,7 @@ impl WindowHandle { let Some(view) = self.view.load() else { return Ok(()) }; let Some(view) = view.inner_ref() else { return Ok(()) }; - BaseviewView::resize(view, size, false); + BaseviewView::resize(view, size, false, false); Ok(()) } @@ -145,18 +149,18 @@ impl WindowHandle { } fn create_window_with_options( - options: &WindowSettings, mtm: MainThreadMarker, + settings: &WindowSettings, mtm: MainThreadMarker, ) -> Retained { - let initial_size = options.size.to_logical(1.0); - let window = create_window(initial_size, mtm); + let initial_size = settings.size.to_logical(1.0); + let window = create_window(initial_size, settings, mtm); window.center(); - let final_size = options.size.to_logical(window.backingScaleFactor()); + let final_size = settings.size.to_logical(window.backingScaleFactor()); if final_size != initial_size { window.setContentSize(NSSize::new(final_size.width, final_size.height)); } - let title = NSString::from_str(&options.title); + let title = NSString::from_str(&settings.title); window.setTitle(&title); window @@ -166,11 +170,17 @@ pub(crate) struct WindowSharedState { pub closed: Cell, pub size: Cell>, pub scale_factor: Cell, + pub resizable: bool, } impl WindowSharedState { - pub fn new(size: LogicalSize, scale_factor: f64) -> Self { - Self { closed: false.into(), size: size.into(), scale_factor: scale_factor.into() } + pub fn new(size: LogicalSize, scale_factor: f64, resizable: bool) -> Self { + Self { + closed: false.into(), + size: size.into(), + scale_factor: scale_factor.into(), + resizable, + } } } diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index e655cad7..1867139e 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -68,6 +68,10 @@ impl WindowHandle { self.state.is_alive.get() } + pub fn is_resizable(&self) -> bool { + self.state.resizable + } + pub fn size(&self) -> WindowSize { self.state.size() } @@ -211,11 +215,7 @@ impl BaseviewWindow { pub fn create(shared_state: Rc, init: WindowInitializer) -> Result { let dpi_ctx = DpiAwarenessContext::new(&shared_state.user32)?; - let style = if init.settings.parent.is_some() { - WindowStyle::parented() - } else { - WindowStyle::embedded() - }; + let style = WindowStyle::from_settings(&init.settings); let window_size = shared_state.current_size.get(); diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index ae66eea3..9207c962 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -151,6 +151,7 @@ pub struct WindowSharedState { pub destroy_host_originated: Cell, pub user32: ExtendedUser32, + pub resizable: bool, } impl WindowSharedState { @@ -163,6 +164,7 @@ impl WindowSharedState { fallback_scale_factor: settings.fallback_scale_factor.into(), resize_host_originated: false.into(), destroy_host_originated: false.into(), + resizable: settings.resizable, user32, } .into() diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index 25272814..119427fd 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -1,6 +1,7 @@ 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_window::XcbWindow; use crate::platform::*; use crate::{warn, MouseCursor, WindowHandler, WindowSettings, WindowSize}; @@ -10,6 +11,7 @@ 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; @@ -49,6 +51,7 @@ pub(crate) struct WindowInner { pub(crate) scaling_factor: ScalingFactor, window_size: Cell>, + pub(crate) is_resizable: bool, mouse_cursor: Cell, pub(crate) visual_id: Visualid, @@ -73,6 +76,8 @@ impl WindowInner { let physical_size = options.size.to_physical(initial_scale_factor); + let size_hints = get_size_hints(&options, initial_scale_factor); + #[cfg(feature = "opengl")] let visual_info = WindowVisualConfig::find_best_visual_config_for_gl(&xcb_connection, options.gl_config)?; @@ -93,6 +98,7 @@ impl WindowInner { xcb_window.set_title(&options.title)?, xcb_window.enable_wm_protocols()?, xcb_window.enable_dnd_protocols()?, + xcb_window.set_size_hints(size_hints)?, ]; for cookie in cookies { @@ -121,6 +127,7 @@ impl WindowInner { system: scaling.into(), suggested: options.fallback_scale_factor.into(), }, + is_resizable: options.resizable, mouse_cursor: MouseCursor::default().into(), loop_signal: ev_loop.get_signal(), @@ -186,6 +193,11 @@ 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()); + self.xcb_window.set_size_hints(size_hints)?.check()?; + } + // This will trigger a `ConfigureNotify` event which will in turn change `self.window_info` // and notify the window handler about it @@ -210,6 +222,10 @@ 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()); + self.xcb_window.set_size_hints(size_hints)?.check()?; + } // These come from the Host, no need to notify it about the new size diff --git a/src/platform/x11/window_thread.rs b/src/platform/x11/window_thread.rs index e32ec04f..fec8a228 100644 --- a/src/platform/x11/window_thread.rs +++ b/src/platform/x11/window_thread.rs @@ -22,6 +22,7 @@ pub(crate) struct WindowThreadShared { size: AtomicU32, final_error: Mutex>, stopped_requested_from_host: AtomicBool, + is_resizable: AtomicBool, } impl WindowThreadShared { @@ -32,12 +33,14 @@ impl WindowThreadShared { size: 0.into(), scaling_factor: 0.into(), stopped_requested_from_host: false.into(), + is_resizable: true.into(), } } fn init(&self, window: &WindowInner) { self.set_size(window.get_size()); self.set_scaling_factor(window.scale_factor()); + self.set_resizable(window.is_resizable); } pub fn get_size(&self) -> PhysicalSize { @@ -53,6 +56,14 @@ 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 get_scaling_factor(&self) -> f64 { f64::from_be_bytes(self.scaling_factor.load(Ordering::Relaxed).to_ne_bytes()) } @@ -201,6 +212,10 @@ impl WindowThreadHandle { !self.shared.stopped.load(Ordering::Relaxed) } + pub fn is_resizable(&self) -> bool { + self.shared.is_resizable() + } + pub fn handle_main_thread_callback(&mut self) { loop { let Some(receiver) = self.callback_receiver.as_mut() else { return }; diff --git a/src/platform/x11/xcb_connection.rs b/src/platform/x11/xcb_connection.rs index e56ef37c..a82c55ff 100644 --- a/src/platform/x11/xcb_connection.rs +++ b/src/platform/x11/xcb_connection.rs @@ -13,6 +13,8 @@ use crate::MouseCursor; mod get_property; pub use get_property::GetPropertyError; +mod size_hints; +pub use size_hints::{get_size_hints, WmSizeHintsExt}; 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 new file mode 100644 index 00000000..9d2157ba --- /dev/null +++ b/src/platform/x11/xcb_connection/size_hints.rs @@ -0,0 +1,24 @@ +use crate::WindowSettings; +use dpi::PhysicalSize; +use x11rb::properties::WmSizeHints; + +pub fn get_size_hints(settings: &WindowSettings, 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)); + } + + 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 + } +} diff --git a/src/platform/x11/xcb_window.rs b/src/platform/x11/xcb_window.rs index 3c23f1f9..686eaa82 100644 --- a/src/platform/x11/xcb_window.rs +++ b/src/platform/x11/xcb_window.rs @@ -7,6 +7,7 @@ use std::rc::Rc; use x11rb::connection::Connection; use x11rb::cookie::VoidCookie; use x11rb::errors::{ConnectionError, ReplyOrIdError}; +use x11rb::properties::WmSizeHints; use x11rb::protocol::xproto::{ AtomEnum, ConfigureWindowAux, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, WindowClass, @@ -119,6 +120,13 @@ impl XcbWindow { )?) } + pub fn set_size_hints( + &self, size_hints: WmSizeHints, + ) -> Result, ReplyOrIdError> { + Ok(size_hints + .set_normal_hints(&self.connection.conn as &XCBConnection, self.window_id.get())?) + } + #[inline] pub fn id(&self) -> NonZeroU32 { self.window_id diff --git a/src/settings.rs b/src/settings.rs index 8a5988e1..dd5fb2f4 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -27,6 +27,9 @@ pub struct WindowSettings { /// If the `parent` field is already set, this does nothing and is ignored. pub wait_for_parent: bool, + /// Whether the window can be resized. + pub resizable: bool, + /// 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. @@ -101,6 +104,13 @@ impl WindowSettings { self } + /// Sets [`resizable`](Self::resizable) to the given value. + #[inline] + pub fn with_resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } + /// Sets [`gl_config`](Self::gl_config) to the given value. #[cfg(feature = "opengl")] #[inline] @@ -118,6 +128,7 @@ impl Default for WindowSettings { parent: None, wait_for_parent: false, fallback_scale_factor: None, + resizable: true, #[cfg(feature = "opengl")] gl_config: None, } diff --git a/src/window.rs b/src/window.rs index 4f34ec4c..0a6fb02e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -151,6 +151,14 @@ impl Window { self.inner.is_open() } + /// Returns `true` if the window can be resized by the user, `false` otherwise. + /// + /// This is set by the [`WindowSettings::resizable`] field. + #[inline] + pub fn is_resizable(&self) -> bool { + self.inner.is_resizable() + } + /// 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/appkit/view.rs b/src/wrappers/appkit/view.rs index d80f94b1..acf507c9 100644 --- a/src/wrappers/appkit/view.rs +++ b/src/wrappers/appkit/view.rs @@ -151,7 +151,10 @@ impl Deref for ViewRef<'_, V> { pub trait ViewImpl: Sized { fn become_first_responder(this: ViewRef) -> bool; fn resign_first_responder(this: ViewRef) -> bool; + fn window_should_close(this: ViewRef) -> bool; + fn window_did_resize(this: ViewRef); + fn view_did_change_backing_properties(this: ViewRef, from_host: bool); fn hit_test(this: ViewRef<'_, Self>, point: NSPoint) -> Option<&NSView>; fn view_will_move_to_window(this: ViewRef, new_window: Option<&NSWindow>); diff --git a/src/wrappers/appkit/view/implementation.rs b/src/wrappers/appkit/view/implementation.rs index 6b7217fb..dac23079 100644 --- a/src/wrappers/appkit/view/implementation.rs +++ b/src/wrappers/appkit/view/implementation.rs @@ -55,6 +55,10 @@ pub unsafe fn create_view_class() -> &'static AnyClass { sel!(windowShouldClose:), window_should_close:: as extern "C-unwind" fn(_, _, _) -> _, ); + class.add_method( + sel!(windowDidResize:), + window_did_resize:: as extern "C-unwind" fn(_, _, _) -> _, + ); class.add_method(sel!(dealloc), dealloc:: as extern "C-unwind" fn(_, _)); class.add_method( sel!(viewWillMoveToWindow:), @@ -314,3 +318,10 @@ extern "C-unwind" fn other_mouse_up(this: &View, _sel: Sel, even let Some(inner) = this.inner_ref() else { return }; V::other_mouse_up(inner, event); } + +extern "C-unwind" fn window_did_resize( + this: &View, _sel: Sel, _notification: &NSNotification, +) { + let Some(inner) = this.inner_ref() else { return }; + V::window_did_resize(inner); +} diff --git a/src/wrappers/appkit/window.rs b/src/wrappers/appkit/window.rs index a0b59c10..bbfd0d53 100644 --- a/src/wrappers/appkit/window.rs +++ b/src/wrappers/appkit/window.rs @@ -1,21 +1,29 @@ use crate::wrappers::appkit::{View, ViewImpl}; +use crate::WindowSettings; use dpi::LogicalSize; use objc2::rc::Retained; use objc2::{msg_send, MainThreadMarker, MainThreadOnly}; use objc2_app_kit::{NSBackingStoreType, NSWindow, NSWindowStyleMask}; use objc2_foundation::{NSPoint, NSRect, NSSize}; -pub fn create_window(size: LogicalSize, mtm: MainThreadMarker) -> Retained { +pub fn create_window( + size: LogicalSize, settings: &WindowSettings, mtm: MainThreadMarker, +) -> Retained { let rect = NSRect::new(NSPoint::ZERO, NSSize { width: size.width, height: size.height }); + let mut style_mask = + NSWindowStyleMask::Titled | NSWindowStyleMask::Closable | NSWindowStyleMask::Miniaturizable; + + if settings.resizable { + style_mask |= NSWindowStyleMask::Resizable; + } + // SAFETY: This is safe because of the setReleasedWhenClosed(false) below let ns_window = unsafe { NSWindow::initWithContentRect_styleMask_backing_defer( NSWindow::alloc(mtm), rect, - NSWindowStyleMask::Titled - | NSWindowStyleMask::Closable - | NSWindowStyleMask::Miniaturizable, + style_mask, NSBackingStoreType::Buffered, false, ) diff --git a/src/wrappers/win32/style.rs b/src/wrappers/win32/style.rs index 82b9b2bc..9abf4bb0 100644 --- a/src/wrappers/win32/style.rs +++ b/src/wrappers/win32/style.rs @@ -1,3 +1,4 @@ +use crate::WindowSettings; use windows_sys::Win32::UI::WindowsAndMessaging::*; #[derive(Copy, Clone)] @@ -7,19 +8,20 @@ pub struct WindowStyle { } impl WindowStyle { - pub const fn parented() -> Self { - Self { style: WS_CHILD, style_ex: 0 } - } + pub fn from_settings(settings: &WindowSettings) -> Self { + if settings.parent.is_some() || settings.wait_for_parent { + return Self { style: WS_CHILD, style_ex: 0 }; + } - pub const fn embedded() -> Self { - Self { - style: WS_POPUPWINDOW - | WS_CAPTION - | WS_SIZEBOX - | WS_MINIMIZEBOX - | WS_MAXIMIZEBOX - | WS_CLIPSIBLINGS, + let mut style = Self { + style: WS_POPUPWINDOW | WS_CAPTION | WS_MINIMIZEBOX | WS_CLIPSIBLINGS, style_ex: 0, + }; + + if settings.resizable { + style.style |= WS_SIZEBOX | WS_MAXIMIZEBOX; } + + style } }