diff --git a/winit-appkit/Cargo.toml b/winit-appkit/Cargo.toml index 2d98f6731..5aa1802ad 100644 --- a/winit-appkit/Cargo.toml +++ b/winit-appkit/Cargo.toml @@ -111,7 +111,12 @@ objc2-foundation = { workspace = true, features = [ "NSThread", "NSValue", ] } -winit-common = { workspace = true, features = ["core-foundation", "event-handler", "foundation"] } +winit-common = { workspace = true, features = [ + "core-foundation", + "event-handler", + "foundation", + "positioner", +] } [dev-dependencies] winit.workspace = true diff --git a/winit-appkit/src/monitor.rs b/winit-appkit/src/monitor.rs index d40b854e7..cc4a15e52 100644 --- a/winit-appkit/src/monitor.rs +++ b/winit-appkit/src/monitor.rs @@ -6,7 +6,7 @@ use std::ptr::NonNull; use std::{fmt, ptr}; use dispatch2::run_on_main; -use dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; +use dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; use objc2::MainThreadMarker; use objc2::rc::Retained; use objc2_app_kit::NSScreen; @@ -171,6 +171,22 @@ impl MonitorHandle { }) } + /// The monitor's work area, i.e. its bounds minus space reserved by the system for the menu + /// bar and the Dock (see `NSScreen.visibleFrame`). `None` if the underlying + /// `NSScreen` can no longer be found. + pub(crate) fn work_area(&self) -> Option<(PhysicalPosition, PhysicalSize)> { + let scale_factor = self.scale_factor(); + run_on_main(|mtm| { + let visible_frame = self.ns_screen(mtm)?.visibleFrame(); + let origin = flip_window_screen_coordinates(visible_frame); + let position = + LogicalPosition::new(origin.x, origin.y).to_physical::(scale_factor); + let size = LogicalSize::new(visible_frame.size.width, visible_frame.size.height) + .to_physical::(scale_factor); + Some((position, size)) + }) + } + pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option> { let uuid = self.uuid(); NSScreen::screens(mtm).into_iter().find(|screen| { diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index 8a72a02d9..810b9fe02 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -15,7 +15,7 @@ use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::window::{ ImeCapabilities, ImeRequest, ImeRequestError, Theme, UserAttentionType, Window as CoreWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType, + WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowPositioner, WindowType, }; use super::event_loop::ActiveEventLoop; @@ -26,7 +26,6 @@ pub(crate) struct Window { window: MainThreadBound>, /// The window only keeps a weak reference to this, so we must keep it around here. delegate: MainThreadBound>, - window_type: WindowType, } impl Window { @@ -35,15 +34,22 @@ impl Window { attributes: WindowAttributes, ) -> Result { let mtm = window_target.mtm; - let window_type = attributes.window_type; let delegate = autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?; window_target.app_state.register_window(&delegate, mtm); - Ok(Window { + let window = Window { window: MainThreadBound::new(delegate.window().retain(), mtm), delegate: MainThreadBound::new(delegate, mtm), - window_type, - }) + }; + window.reposition(); + Ok(window) + } + + /// Recomputes this window's position (and, if constrained, its size) from its positioner + /// state, using [`winit_common::positioner::place_window`], and applies the result. No-op if + /// this window isn't anchored, or if it has no parent. + fn reposition(&self) { + self.maybe_wait_on_main(|delegate| delegate.reposition()); } pub(crate) fn maybe_wait_on_main( @@ -99,7 +105,15 @@ impl rwh_06::HasWindowHandle for Window { impl CoreWindow for Window { fn window_type(&self) -> WindowType { - self.window_type + self.maybe_wait_on_main(|delegate| delegate.window_type()) + } + + fn positioner(&self) -> WindowPositioner { + self.maybe_wait_on_main(|delegate| delegate.popup_positioner()) + } + + fn set_positioner(&self, positioner: WindowPositioner) { + self.maybe_wait_on_main(|delegate| delegate.set_popup_positioner(positioner)); } fn id(&self) -> winit_core::window::WindowId { diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index a2077a58a..68609548e 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -44,6 +44,7 @@ use objc2_foundation::{ }; use tracing::{debug_span, trace, warn}; use winit_common::core_foundation::MainRunLoop; +use winit_common::positioner::place_window; use winit_core::cursor::Cursor; use winit_core::data_transfer::DataTransferId; use winit_core::error::{NotSupportedError, RequestError}; @@ -52,7 +53,8 @@ use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider}; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, - UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType, + UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowPositioner, + WindowType, }; use super::app_state::AppState; @@ -112,7 +114,16 @@ pub(crate) struct State { is_simple_fullscreen: Cell, saved_style: Cell>, is_borderless_game: Cell, - is_popup: Cell, + /// The window's role, used for creation/role behavior (decorations, activation/keyboard-grab + /// default, initial placement). Does *not* determine whether the window is anchor-positioned + /// -- see `anchored`. + window_type: WindowType, + /// Whether this window is positioned relative to its parent using the anchor/gravity/ + /// positioner system, either because it's a [`WindowType::Popup`] or because anchor + /// attributes were set on a [`WindowType::Window`]. Gates the parent-relative coordinate + /// frame used when applying anchor/gravity/positioner-offset changes. + anchored: bool, + positioner: RefCell, } define_class!( @@ -149,6 +160,9 @@ define_class!( let _entered = debug_span!("windowDidResize:").entered(); // NOTE: WindowEvent::SurfaceResized is reported using NSViewFrameDidChangeNotification. self.emit_move_event(); + // `windowDidMove:` isn't triggered when a move is part of a resize (e.g. dragging the + // top or left edge), so anchored children need repositioning here too. + self.reposition_child_windows(); } #[unsafe(method(windowWillStartLiveResize:))] @@ -170,6 +184,7 @@ define_class!( fn window_did_move(&self, _: Option<&AnyObject>) { let _entered = debug_span!("windowDidMove:").entered(); self.emit_move_event(); + self.reposition_child_windows(); } #[unsafe(method(windowDidChangeBackingProperties:))] @@ -657,7 +672,7 @@ fn new_window( app_state: &Rc, attrs: &WindowAttributes, macos_attrs: &WindowAttributesMacOS, - is_popup: bool, + anchored: bool, mtm: MainThreadMarker, ) -> Option> { autoreleasepool(|_| { @@ -685,10 +700,12 @@ fn new_window( None => NSSize::new(800.0, 600.0), }; let position = match attrs.position { - // A popup's position is parent-relative; it's applied in `WindowDelegate::new` - // (after the delegate exists) via the shared translation in - // `set_outer_position`. - _ if is_popup => NSPoint::new(0.0, 0.0), + // An anchored window's position is parent-relative; it's applied in + // `WindowDelegate::new` (after the delegate exists) via the shared + // translation in `set_outer_position`. A parentless anchored window can't be + // positioned that way, so it falls through to the same handling as a + // non-anchored window below. + _ if anchored && attrs.parent_window().is_some() => NSPoint::new(0.0, 0.0), Some(position) => { let position = position.to_logical(scale_factor); flip_window_screen_coordinates(NSRect::new( @@ -839,8 +856,8 @@ fn new_window( window.collectionBehavior() | NSWindowCollectionBehavior::FullScreenAuxiliary, ); } - // Popups are positioned relative to their parent in `WindowDelegate::new`. - if attrs.position.is_none() && !is_popup { + // Center window if the position is not set or it doesn't have any parent + if attrs.position.is_none() && !(anchored && attrs.parent_window().is_some()) { window.center(); } @@ -904,7 +921,9 @@ impl WindowDelegate { .and_then(|attrs| attrs.cast::().ok()) .unwrap_or_default(); - let is_popup = matches!(attrs.window_type(), WindowType::Popup); + let window_type = attrs.window_type(); + let is_popup = matches!(window_type, WindowType::Popup); + let anchored = is_popup || attrs.positioner.is_some(); if is_popup { // A popup is an undecorated, non-activating panel with no titlebar buttons. Model it // as such so it flows through the existing borderless + panel paths in `new_window` @@ -918,7 +937,7 @@ impl WindowDelegate { } } - let window = new_window(app_state, &attrs, &macos_attrs, is_popup, mtm) + let window = new_window(app_state, &attrs, &macos_attrs, anchored, mtm) .ok_or_else(|| os_error!("couldn't create `NSWindow`"))?; match attrs.parent_window() { @@ -979,7 +998,9 @@ impl WindowDelegate { is_simple_fullscreen: Cell::new(false), saved_style: Cell::new(None), is_borderless_game: Cell::new(macos_attrs.borderless_game), - is_popup: Cell::new(is_popup), + window_type, + anchored, + positioner: RefCell::new(attrs.positioner.unwrap_or_default()), }); let delegate: Retained = unsafe { msg_send![super(delegate), init] }; @@ -1026,10 +1047,10 @@ impl WindowDelegate { delegate.set_window_level(attrs.window_level); - // The popup position is relative to the parent window, and the parent is only - // attached above, so apply the (translated) position now. Default to the parent's + // An anchored window's position is relative to the parent window, and the parent is + // only attached above, so apply the (translated) position now. Default to the parent's // content top-left when no position was given. - if is_popup { + if anchored { let position = attrs.position.unwrap_or_else(|| LogicalPosition::new(0.0, 0.0).into()); delegate.set_outer_position(position); } @@ -1189,8 +1210,8 @@ impl WindowDelegate { pub fn outer_position(&self) -> Result, RequestError> { let position = flip_window_screen_coordinates(self.window().frame()); - let position = - self.translate_popup_position_to_parent(LogicalPosition::new(position.x, position.y)); + let position = self + .translate_anchored_position_to_parent(LogicalPosition::new(position.x, position.y)); Ok(position.to_physical(self.scale_factor())) } @@ -1215,7 +1236,7 @@ impl WindowDelegate { pub fn set_outer_position(&self, position: Position) { let position = position.to_logical(self.scale_factor()); - let position = self.translate_popup_position(position); + let position = self.translate_anchored_position(position); let point = flip_window_screen_coordinates(NSRect::new( NSPoint::new(position.x, position.y), self.window().frame().size, @@ -1223,11 +1244,11 @@ impl WindowDelegate { self.window().setFrameOrigin(point); } - /// Popups receive their position relative to the top-left of the parent window's + /// Anchored windows receive their position relative to the top-left of the parent window's /// content area (matching the Win32 and Wayland backends). macOS positions windows /// in global screen coordinates, so add the parent content area's origin. - fn translate_popup_position(&self, position: LogicalPosition) -> LogicalPosition { - if !self.ivars().is_popup.get() { + fn translate_anchored_position(&self, position: LogicalPosition) -> LogicalPosition { + if !self.ivars().anchored { return position; } let Some(parent) = self.window().parentWindow() else { @@ -1238,15 +1259,15 @@ impl WindowDelegate { LogicalPosition::new(parent_origin.x + position.x, parent_origin.y + position.y) } - /// Inverse of [`Self::translate_popup_position`]. Popups report their position + /// Inverse of [`Self::translate_anchored_position`]. Anchored windows report their position /// relative to the top-left of the parent window's content area (matching the /// Win32 and Wayland backends), so subtract the parent content area's origin from - /// the global screen coordinates. Non-popup windows are returned unchanged. - fn translate_popup_position_to_parent( + /// the global screen coordinates. Non-anchored windows are returned unchanged. + fn translate_anchored_position_to_parent( &self, position: LogicalPosition, ) -> LogicalPosition { - if !self.ivars().is_popup.get() { + if !self.ivars().anchored { return position; } let Some(parent) = self.window().parentWindow() else { @@ -1257,6 +1278,95 @@ impl WindowDelegate { LogicalPosition::new(position.x - parent_origin.x, position.y - parent_origin.y) } + /// The parent window's content area origin, in global (Winit, top-left/y-down) screen + /// coordinates. `None` if this window isn't anchored, or it has no parent. + pub fn parent_content_origin(&self) -> Option> { + if !self.ivars().anchored { + return None; + } + let parent = self.window().parentWindow()?; + let origin = flip_window_screen_coordinates(parent.contentRectForFrameRect(parent.frame())); + Some(LogicalPosition::new(origin.x, origin.y)) + } + + pub fn window_type(&self) -> WindowType { + self.ivars().window_type + } + + pub fn popup_positioner(&self) -> WindowPositioner { + *self.ivars().positioner.borrow() + } + + pub fn set_popup_positioner(&self, positioner: WindowPositioner) { + *self.ivars().positioner.borrow_mut() = positioner; + self.reposition(); + } + + /// Recomputes this window's position (and, if constrained, its size) from its positioner + /// state, using [`winit_common::positioner::place_window`], and applies the result. No-op if + /// this window isn't anchored. If it has no parent, the positioner is resolved relative to + /// the screen instead of the parent's content area. + pub(crate) fn reposition(&self) { + if !self.ivars().anchored { + return; + } + + let positioner = *self.ivars().positioner.borrow(); + + let parent_origin = self.parent_content_origin().unwrap_or_default(); + + let Some(monitor) = self.current_monitor() else { return }; + // Clip to the monitor's work area rather than its full bounds, so anchored popups don't + // get placed underneath the menu bar or the Dock. + let Some((work_area_position, work_area_size)) = monitor.work_area() else { return }; + + let scale_factor = self.scale_factor(); + + // The anchor rect's position is relative to the parent's content area, and + // `set_outer_position` re-adds the parent's screen origin for anchored windows (see + // `translate_anchored_position`). To stay in the same coordinate space, the clip region + // also needs to be expressed relative to the parent's content area. + let work_area_position = work_area_position.to_logical::(scale_factor); + let clip_position = LogicalPosition::new( + work_area_position.x - parent_origin.x, + work_area_position.y - parent_origin.y, + ); + let clip_size = work_area_size.to_logical::(scale_factor); + + let current_outer_size = self.outer_size().to_logical::(scale_factor); + + let (origin, new_outer_size) = + place_window(&positioner, scale_factor, current_outer_size, (clip_position, clip_size)); + + self.set_outer_position(Position::Logical(origin)); + if new_outer_size != current_outer_size { + // `place_window` positions the window by its outer/frame corner, so it operates on + // the outer size; convert back to the surface (content) size that + // `request_surface_size` expects, via AppKit's frame/content rect conversion. + let frame = NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(new_outer_size.width, new_outer_size.height), + ); + let content_size = self.window().contentRectForFrameRect(frame).size; + let content_size = LogicalSize::new(content_size.width, content_size.height); + let _ = self.request_surface_size(Size::Logical(content_size)); + } + } + + /// Repositions any anchored children owned by this window. AppKit's `addChildWindow:ordered:` + /// only manages ordering and visibility for child windows -- it doesn't keep their position + /// in sync with the parent -- so this has to be done manually whenever this window moves. + fn reposition_child_windows(&self) { + let Some(children) = self.window().childWindows() else { return }; + for child in children.iter() { + let Some(child_delegate) = child.delegate() else { continue }; + let Ok(child_delegate) = child_delegate.downcast::() else { continue }; + if child_delegate.ivars().anchored { + child_delegate.reposition(); + } + } + } + #[inline] pub fn surface_size(&self) -> PhysicalSize { self.view().surface_size() diff --git a/winit-common/Cargo.toml b/winit-common/Cargo.toml index a1b666697..4c5ec33c6 100644 --- a/winit-common/Cargo.toml +++ b/winit-common/Cargo.toml @@ -12,6 +12,9 @@ version.workspace = true # Event Handler event-handler = [] +# Positioner +positioner = ["dep:dpi"] + # XKB wayland = ["dep:memmap2"] x11 = ["xkbcommon-dl?/x11", "dep:x11-dl"] @@ -24,6 +27,7 @@ core-foundation = ["dep:block2", "dep:objc2", "dep:objc2-core-foundation"] foundation = ["dep:block2", "dep:objc2", "dep:objc2-foundation"] [dependencies] +dpi = { workspace = true, optional = true } smol_str = { workspace = true, optional = true } tracing.workspace = true winit-core.workspace = true diff --git a/winit-common/src/lib.rs b/winit-common/src/lib.rs index 33db2d67c..251f3ff39 100644 --- a/winit-common/src/lib.rs +++ b/winit-common/src/lib.rs @@ -8,5 +8,7 @@ pub mod core_foundation; pub mod event_handler; #[cfg(feature = "foundation")] pub mod foundation; +#[cfg(feature = "positioner")] +pub mod positioner; #[cfg(feature = "xkb")] pub mod xkb; diff --git a/winit-common/src/positioner.rs b/winit-common/src/positioner.rs new file mode 100644 index 000000000..7422234ca --- /dev/null +++ b/winit-common/src/positioner.rs @@ -0,0 +1,523 @@ +//! The `xdg_positioner`-style algorithm ([`place_window`]) that resolves a [`WindowPositioner`] +//! into a concrete position and size. +//! +//! This is only needed by backends (such as Win32 and AppKit) that have no native equivalent of +//! Wayland's `xdg_positioner` and therefore need to compute the popup placement themselves; it's +//! not part of winit's public API. + +use dpi::{LogicalPosition, LogicalSize}; +use winit_core::window::{ + WindowAnchor, WindowConstraintAdjustment, WindowGravity, WindowPositioner, +}; + +/// Returns, as fractions of the anchor rectangle's width/height, the point within that rectangle +/// that the popup is anchored to (0.0 = left/top edge, 0.5 = center, 1.0 = right/bottom edge). +/// Mirrors the Wayland `xdg_positioner` anchor semantics. +fn anchor_fraction(anchor: WindowAnchor) -> (f64, f64) { + match anchor { + WindowAnchor::Center => (0.5, 0.5), + WindowAnchor::Top => (0.5, 0.0), + WindowAnchor::Bottom => (0.5, 1.0), + WindowAnchor::Left => (0.0, 0.5), + WindowAnchor::Right => (1.0, 0.5), + WindowAnchor::TopLeft => (0.0, 0.0), + WindowAnchor::BottomLeft => (0.0, 1.0), + WindowAnchor::TopRight => (1.0, 0.0), + WindowAnchor::BottomRight => (1.0, 1.0), + // `WindowAnchor` is `#[non_exhaustive]`; fall back to `Center` for variants added after + // this crate was built against an older `winit-core`. + _ => (0.5, 0.5), + } +} + +/// Returns, as fractions of the popup's own width/height, the offset from the anchor point to +/// the popup's origin (top-left corner). For example a gravity of `BottomRight` places the +/// popup's top-left corner at the anchor point, so the popup grows down and to the right. +fn gravity_fraction(gravity: WindowGravity) -> (f64, f64) { + match gravity { + WindowGravity::Center => (-0.5, -0.5), + WindowGravity::Top => (-0.5, -1.0), + WindowGravity::Bottom => (-0.5, 0.0), + WindowGravity::Left => (-1.0, -0.5), + WindowGravity::Right => (0.0, -0.5), + WindowGravity::TopLeft => (-1.0, -1.0), + WindowGravity::BottomLeft => (-1.0, 0.0), + WindowGravity::TopRight => (0.0, -1.0), + WindowGravity::BottomRight => (0.0, 0.0), + // `WindowGravity` is `#[non_exhaustive]`; fall back to `Center` for variants added after + // this crate was built against an older `winit-core`. + _ => (-0.5, -0.5), + } +} + +/// Adjusts a single axis of the popup's placement to stay within `[clip_min, clip_max]`, applying +/// `flip`/`slide`/`resize` in the order the `xdg_positioner` protocol suggests: flip, then slide, +/// then resize. `flipped_origin` is the alternate origin obtained by mirroring both the anchor +/// edge and the gravity on this axis; it is only used when `flip` is set and it actually results +/// in a better fit than the original origin. +fn constrain_axis( + origin: f64, + extent: f64, + (clip_min, clip_max): (f64, f64), + flipped_origin: f64, + (flip, slide, resize): (bool, bool, bool), +) -> (f64, f64) { + let fits = |o: f64| o >= clip_min && o + extent <= clip_max; + + let mut origin = origin; + if !fits(origin) && flip && fits(flipped_origin) { + origin = flipped_origin; + } + + if !fits(origin) && slide { + // `extent` may exceed the clip size, in which case `clip_max - extent` would be less + // than `clip_min`; `.max(clip_min)` keeps the clamp range valid in that case. + let slide_max = (clip_max - extent).max(clip_min); + origin = origin.clamp(clip_min, slide_max); + } + + let mut extent = extent; + if resize && !fits(origin) { + let clamped_origin = origin.max(clip_min); + // Intersect with the clip rectangle on both ends, not just `clip_max`: if only the + // leading edge overflows (`origin < clip_min`) while the trailing edge already fits, + // this must shrink down to the original trailing edge rather than growing all the way + // out to `clip_max`. + extent = (clip_max.min(origin + extent) - clamped_origin).max(0.0); + origin = clamped_origin; + } + + (origin, extent) +} + +/// Finds a placement for a window of `window_size`, anchored per `positioner` (whose +/// [`anchor_rect`](WindowPositioner::anchor_rect) and +/// [`offset`](WindowPositioner::offset) are converted to logical +/// coordinates using `scale_factor`, then interpreted in the same coordinate space as `clip`), and +/// constrained to stay within the `clip` rectangle according to +/// [`constraint_adjustment`](WindowPositioner::constraint_adjustment). +/// +/// This mirrors the Wayland `xdg_positioner` placement algorithm used natively on Wayland, for +/// backends (such as Win32 and AppKit) that have no equivalent native concept and therefore need +/// to compute the popup position themselves: `anchor` selects a point on the anchor rectangle, +/// `gravity` decides which corner/edge of the popup is placed at that point, and if the resulting +/// rectangle doesn't fit inside the clip rectangle, `constraint_adjustment`'s flags decide whether +/// (and how) the popup is moved (slide), mirrored to the other side of the anchor point (flip), +/// or shrunk (resize) to fit. Axes are adjusted independently. If none of the flags are set for an +/// axis, that axis is left as computed even if it doesn't fit, matching the protocol's "none" +/// behavior. +pub fn place_window( + positioner: &WindowPositioner, + scale_factor: f64, + window_size: LogicalSize, + (clip_position, clip_size): (LogicalPosition, LogicalSize), +) -> (LogicalPosition, LogicalSize) { + let anchor = positioner.anchor; + let gravity = positioner.gravity; + let constraint_adjustment = positioner.constraint_adjustment; + let (anchor_position, anchor_size) = positioner.anchor_rect; + let anchor_position = anchor_position.to_logical::(scale_factor); + let anchor_size = anchor_size.to_logical::(scale_factor); + let offset = positioner.offset.to_logical::(scale_factor); + + let (anchor_fx, anchor_fy) = anchor_fraction(anchor); + let (gravity_fx, gravity_fy) = gravity_fraction(gravity); + + let anchor_point_x = anchor_position.x + anchor_size.width * anchor_fx; + let anchor_point_y = anchor_position.y + anchor_size.height * anchor_fy; + + let origin_x = anchor_point_x + window_size.width * gravity_fx + offset.x; + let origin_y = anchor_point_y + window_size.height * gravity_fy + offset.y; + + // Flipping mirrors both the anchor edge and the gravity on that axis, effectively placing the + // popup on the opposite side of the anchor rectangle. + let flipped_anchor_point_x = anchor_position.x + anchor_size.width * (1.0 - anchor_fx); + let flipped_anchor_point_y = anchor_position.y + anchor_size.height * (1.0 - anchor_fy); + let flipped_x = flipped_anchor_point_x + window_size.width * (-1.0 - gravity_fx) + offset.x; + let flipped_y = flipped_anchor_point_y + window_size.height * (-1.0 - gravity_fy) + offset.y; + + let clip_min_x = clip_position.x; + let clip_max_x = clip_position.x + clip_size.width; + let clip_min_y = clip_position.y; + let clip_max_y = clip_position.y + clip_size.height; + + let (x, width) = constrain_axis( + origin_x, + window_size.width, + (clip_min_x, clip_max_x), + flipped_x, + ( + constraint_adjustment.contains(WindowConstraintAdjustment::FLIP_X), + constraint_adjustment.contains(WindowConstraintAdjustment::SLIDE_X), + constraint_adjustment.contains(WindowConstraintAdjustment::RESIZE_X), + ), + ); + let (y, height) = constrain_axis( + origin_y, + window_size.height, + (clip_min_y, clip_max_y), + flipped_y, + ( + constraint_adjustment.contains(WindowConstraintAdjustment::FLIP_Y), + constraint_adjustment.contains(WindowConstraintAdjustment::SLIDE_Y), + constraint_adjustment.contains(WindowConstraintAdjustment::RESIZE_Y), + ), + ); + + (LogicalPosition::new(x, y), LogicalSize::new(width, height)) +} + +#[cfg(test)] +mod tests { + use dpi::{Position, Size}; + + use super::*; + + const GRID_COLS: usize = 60; + const GRID_ROWS: usize = 24; + const DRAW: bool = false; + + /// Prints an ASCII rendering of the clip region (`.`), the anchor rect (`A`), and the + /// resulting popup rect (`P`, `X` where it overlaps the anchor) for a quick visual sanity + /// check. Run with `cargo test -p winit-common positioner -- --nocapture` to see it. + // `println!` (rather than `tracing`) is intentional: this is only meant to be read directly + // via `--nocapture`, which doesn't require a tracing subscriber to be installed. + #[allow(clippy::disallowed_macros)] + fn draw( + label: &str, + clip: (LogicalPosition, LogicalSize), + anchor: (LogicalPosition, LogicalSize), + popup: (LogicalPosition, LogicalSize), + ) { + if !DRAW { + return; + } + + let (clip_position, clip_size) = clip; + let (anchor_position, anchor_size) = anchor; + let (popup_position, popup_size) = popup; + let has_clip = clip_size.width > 0.0 && clip_size.height > 0.0; + + // Bounding box covering everything we're about to draw, plus a small margin. + let mut min_x = anchor_position.x.min(popup_position.x); + let mut min_y = anchor_position.y.min(popup_position.y); + let mut max_x = + (anchor_position.x + anchor_size.width).max(popup_position.x + popup_size.width); + let mut max_y = + (anchor_position.y + anchor_size.height).max(popup_position.y + popup_size.height); + if has_clip { + min_x = min_x.min(clip_position.x); + min_y = min_y.min(clip_position.y); + max_x = max_x.max(clip_position.x + clip_size.width); + max_y = max_y.max(clip_position.y + clip_size.height); + } + let margin_x = ((max_x - min_x) * 0.1).max(1.0); + let margin_y = ((max_y - min_y) * 0.1).max(1.0); + min_x -= margin_x; + min_y -= margin_y; + max_x += margin_x; + max_y += margin_y; + + // Terminal characters are roughly twice as tall as they are wide, so use half the + // vertical scale to keep the rendered rectangles' proportions roughly correct. + let scale = + (GRID_COLS as f64 / (max_x - min_x)).min(2.0 * GRID_ROWS as f64 / (max_y - min_y)); + + let mut grid = vec![vec![' '; GRID_COLS]; GRID_ROWS]; + let mut plot = |position: LogicalPosition, size: LogicalSize, ch: char| { + let x0 = ((position.x - min_x) * scale).round() as isize; + let y0 = ((position.y - min_y) * scale / 2.0).round() as isize; + let x1 = ((position.x + size.width - min_x) * scale).round() as isize; + let y1 = ((position.y + size.height - min_y) * scale / 2.0).round() as isize; + for y in y0.max(0)..y1.min(GRID_ROWS as isize) { + for x in x0.max(0)..x1.min(GRID_COLS as isize) { + let cell = &mut grid[y as usize][x as usize]; + *cell = if *cell == ' ' || *cell == ch { ch } else { 'X' }; + } + } + }; + + if has_clip { + plot(clip_position, clip_size, '.'); + } + plot(anchor_position, anchor_size, 'A'); + plot(popup_position, popup_size, 'P'); + + println!("--- {label} (A = anchor, P = popup, X = overlap, . = clip region) ---"); + for row in grid { + println!("{}", row.into_iter().collect::()); + } + } + + /// A 20x10 anchor rect at (100, 100), popup size 40x30, no constraints applied + /// (unclipped). + #[test] + fn test_place_popup_gravity() { + let anchor_position = LogicalPosition::new(100., 100.); + let anchor_size = LogicalSize::new(20., 10.); + let popup_size = LogicalSize::new(40., 30.); + let clip_position = LogicalPosition::new(0., 0.); + let clip_size = LogicalSize::new(0., 0.); + let offset = Position::Logical(LogicalPosition::new(0., 0.)); + + let place = |anchor: WindowAnchor, gravity: WindowGravity| { + let positioner = WindowPositioner::new( + anchor, + (Position::Logical(anchor_position), Size::Logical(anchor_size)), + offset, + gravity, + WindowConstraintAdjustment::empty(), + ); + place_window(&positioner, 1.0, popup_size, (clip_position, clip_size)) + }; + + // BottomRight gravity anchored to the anchor's bottom-right corner: the popup's top-left + // corner sits exactly at the anchor rect's bottom-right corner. + let (origin, size) = place(WindowAnchor::BottomRight, WindowGravity::BottomRight); + draw( + "gravity: BottomRight anchor + BottomRight gravity", + (clip_position, clip_size), + (anchor_position, anchor_size), + (origin, size), + ); + assert_eq!(origin, LogicalPosition::new(120., 110.)); + assert_eq!(size, popup_size); + + // TopLeft gravity anchored to the anchor's top-left corner: the popup's bottom-right + // corner sits exactly at the anchor rect's top-left corner, so the popup extends + // up-left. + let (origin, size) = place(WindowAnchor::TopLeft, WindowGravity::TopLeft); + draw( + "gravity: TopLeft anchor + TopLeft gravity", + (clip_position, clip_size), + (anchor_position, anchor_size), + (origin, size), + ); + assert_eq!(origin, LogicalPosition::new(100. - 40., 100. - 30.)); + + // Bottom anchor + Bottom gravity: horizontally centered on the anchor, growing downward + // from its bottom edge. + let (origin, size) = place(WindowAnchor::Bottom, WindowGravity::Bottom); + draw( + "gravity: Bottom anchor + Bottom gravity", + (clip_position, clip_size), + (anchor_position, anchor_size), + (origin, size), + ); + assert_eq!( + origin, + LogicalPosition::new(100. + anchor_size.width / 2. - popup_size.width / 2., 110.) + ); + + // Center anchor + Center gravity centers the popup exactly on the anchor rect's center. + let (origin, size) = place(WindowAnchor::Center, WindowGravity::Center); + draw( + "gravity: Center anchor + Center gravity", + (clip_position, clip_size), + (anchor_position, anchor_size), + (origin, size), + ); + let anchor_center = + LogicalPosition::new(100. + anchor_size.width / 2., 100. + anchor_size.height / 2.); + assert_eq!( + origin, + LogicalPosition::new( + anchor_center.x - popup_size.width / 2., + anchor_center.y - popup_size.height / 2. + ) + ); + } + + /// Place the Anchor so that the popup will go outside of the right side of the clip rectangle + /// Because of FLIP_X the popup will be flipped on the left side of the anchor rectangle + #[test] + fn test_place_popup_flip() { + // Anchor rect hugging the right edge of a 300x300 clip region; with BottomRight gravity + // the popup would overflow past the right edge, so flipping should place it to the left + // instead. + let clip_position = LogicalPosition::new(0., 0.); + let clip_size = LogicalSize::new(300., 300.); + let anchor_position = LogicalPosition::new(280., 100.); + let anchor_size = LogicalSize::new(10., 10.); + let popup_size = LogicalSize::new(50., 50.); + + let flip_only = WindowConstraintAdjustment::FLIP_X | WindowConstraintAdjustment::FLIP_Y; + let offset = Position::Logical(LogicalPosition::new(0., 0.)); + + // Without flipping the popup would start at x=290 and end at x=340, past the clip's + // right edge (300); flipping mirrors both anchor edge and gravity, so it should end up + // entirely to the left of the anchor rect instead, fully inside the clip region. + let positioner = WindowPositioner::new( + WindowAnchor::TopRight, + (Position::Logical(anchor_position), Size::Logical(anchor_size)), + offset, + WindowGravity::BottomRight, + flip_only, + ); + let (origin, size) = place_window(&positioner, 1.0, popup_size, (clip_position, clip_size)); + draw("flip", (clip_position, clip_size), (anchor_position, anchor_size), (origin, size)); + assert!(origin.x >= 0. && origin.x + size.width <= 300.); + assert_eq!(size, popup_size); + // Flipped horizontally: popup's right edge lands on the anchor rect's left edge + // (x=280). + assert_eq!(origin.x, 280. - popup_size.width); + // Not flipped vertically: still grows down from the anchor's top edge. + assert_eq!(origin.y, 100.); + } + + /// Anchor near the bottom-right corner of the clip region; sliding (without flipping) + /// should shift the popup back into view while keeping its size and general placement + /// direction. + #[test] + fn test_place_popup_slide() { + let clip_position = LogicalPosition::new(0., 0.); + let clip_size = LogicalSize::new(300., 300.); + let anchor_position = LogicalPosition::new(280., 280.); + let popup_size = LogicalSize::new(50., 50.); + + let slide_only = WindowConstraintAdjustment::SLIDE_X | WindowConstraintAdjustment::SLIDE_Y; + let offset = Position::Logical(LogicalPosition::new(0., 0.)); + + let positioner = WindowPositioner::new( + WindowAnchor::BottomRight, + (Position::Logical(anchor_position), Size::Logical(LogicalSize::new(0., 0.))), + offset, + WindowGravity::BottomRight, + slide_only, + ); + let (origin, size) = place_window(&positioner, 1.0, popup_size, (clip_position, clip_size)); + draw( + "slide", + (clip_position, clip_size), + (anchor_position, LogicalSize::new(0., 0.)), + (origin, size), + ); + assert_eq!(size, popup_size); + assert_eq!(origin, LogicalPosition::new(250., 250.)); + } + + /// Popup larger than the clip region on both axes; with only `resize` enabled it should + /// be shrunk (and clamped) to exactly fill the clip region rather than sliding or + /// flipping. + #[test] + fn test_place_popup_resize() { + let clip_position = LogicalPosition::new(0., 0.); + let clip_size = LogicalSize::new(300., 300.); + let anchor_position = LogicalPosition::new(0., 0.); + let popup_size = LogicalSize::new(500., 500.); + + let resize_only = + WindowConstraintAdjustment::RESIZE_X | WindowConstraintAdjustment::RESIZE_Y; + let offset = Position::Logical(LogicalPosition::new(0., 0.)); + + let positioner = WindowPositioner::new( + WindowAnchor::TopLeft, + (Position::Logical(anchor_position), Size::Logical(LogicalSize::new(0., 0.))), + offset, + WindowGravity::BottomRight, + resize_only, + ); + let (origin, size) = place_window(&positioner, 1.0, popup_size, (clip_position, clip_size)); + draw( + "resize", + (clip_position, clip_size), + (anchor_position, LogicalSize::new(0., 0.)), + (origin, size), + ); + assert_eq!(origin, clip_position); + assert_eq!(size, clip_size); + } + + /// Popup that only overflows the clip region on its leading edge (its trailing edge + /// already fits comfortably). With only `resize` enabled, it should shrink down to the + /// intersection with the clip region -- i.e. stay anchored to its original trailing edge -- + /// rather than growing all the way out to the far side of the clip region. + #[test] + fn test_place_popup_resize_leading_edge_only() { + let clip_position = LogicalPosition::new(0., 0.); + let clip_size = LogicalSize::new(300., 300.); + // TopLeft anchor + BottomRight gravity places the popup's origin exactly at the anchor + // position, so this popup starts at x=-20 (off the left edge) and ends at x=30 (well + // within the clip region). + let anchor_position = LogicalPosition::new(-20., 0.); + let popup_size = LogicalSize::new(50., 50.); + + let resize_only = WindowConstraintAdjustment::RESIZE_X; + let offset = Position::Logical(LogicalPosition::new(0., 0.)); + + let positioner = WindowPositioner::new( + WindowAnchor::TopLeft, + (Position::Logical(anchor_position), Size::Logical(LogicalSize::new(0., 0.))), + offset, + WindowGravity::BottomRight, + resize_only, + ); + let (origin, size) = place_window(&positioner, 1.0, popup_size, (clip_position, clip_size)); + draw( + "resize (leading edge only)", + (clip_position, clip_size), + (anchor_position, LogicalSize::new(0., 0.)), + (origin, size), + ); + assert_eq!(origin, LogicalPosition::new(0., 0.)); + // Trailing edge (originally at x=30) must stay put -- the popup should shrink to width + // 30, not grow out to the clip region's full width of 300. + assert_eq!(size, LogicalSize::new(30., 50.)); + } + + /// With no constraint-adjustment flags set, an overflowing popup is left exactly where + /// the anchor/gravity math puts it, matching the `xdg_positioner` "none" behavior. + #[test] + fn test_place_popup_no_adjustment() { + let clip_position = LogicalPosition::new(0., 0.); + let clip_size = LogicalSize::new(100., 100.); + let anchor_position = LogicalPosition::new(90., 90.); + let popup_size = LogicalSize::new(50., 50.); + let offset = Position::Logical(LogicalPosition::new(0., 0.)); + + let positioner = WindowPositioner::new( + WindowAnchor::TopLeft, + (Position::Logical(anchor_position), Size::Logical(LogicalSize::new(0., 0.))), + offset, + WindowGravity::BottomRight, + WindowConstraintAdjustment::empty(), + ); + let (origin, size) = place_window(&positioner, 1.0, popup_size, (clip_position, clip_size)); + draw( + "no adjustment", + (clip_position, clip_size), + (anchor_position, LogicalSize::new(0., 0.)), + (origin, size), + ); + assert_eq!(origin, anchor_position); + assert_eq!(size, popup_size); + } + + /// Sanity check: when the popup already fits, none of the adjustment flags should move + /// or resize it, regardless of which are enabled. + #[test] + fn test_place_popup_all_adjustment_no_op_when_fits() { + let clip_position = LogicalPosition::new(0., 0.); + let clip_size = LogicalSize::new(300., 300.); + let anchor_position = LogicalPosition::new(100., 100.); + let popup_size = LogicalSize::new(50., 50.); + let offset = Position::Logical(LogicalPosition::new(0., 0.)); + + let positioner = WindowPositioner::new( + WindowAnchor::TopLeft, + (Position::Logical(anchor_position), Size::Logical(LogicalSize::new(0., 0.))), + offset, + WindowGravity::BottomRight, + WindowConstraintAdjustment::all(), + ); + let (origin, size) = place_window(&positioner, 1.0, popup_size, (clip_position, clip_size)); + draw( + "all adjustment, already fits", + (clip_position, clip_size), + (anchor_position, LogicalSize::new(0., 0.)), + (origin, size), + ); + assert_eq!(origin, anchor_position); + assert_eq!(size, popup_size); + } +} diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 2e0247d87..455d8f793 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -1,4 +1,6 @@ //! The [`Window`] trait and associated types. +mod positioner; + use std::any::Any; use std::fmt; @@ -7,6 +9,7 @@ use cursor_icon::CursorIcon; use dpi::{ LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size, }; +pub use positioner::{WindowAnchor, WindowConstraintAdjustment, WindowGravity}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -57,6 +60,9 @@ pub enum WindowType { /// tooltip. Requires a parent set via [`WindowAttributes::with_parent_window`], and its /// position is interpreted relative to that parent. /// + /// The anchor/gravity/positioning system described on [`WindowAttributes::with_positioner`] + /// can be used to position the popup or window in a more advanced way + /// /// ## Platform-specific /// /// - **macOS:** A borderless, non-activating child window. The system does *not* draw rounded @@ -66,6 +72,71 @@ pub enum WindowType { Popup, } +/// The positioner state backing a window's anchor-based placement. +/// +/// Set at window creation via [`WindowAttributes::with_positioner`], and read/mutated at runtime +/// through [`Window::positioner`]/[`Window::set_positioner`]. See those methods for +/// platform-specific behavior, and [`WindowPositioner::default`] for the values used when +/// [`WindowAttributes::with_positioner`] is never called. +/// +/// The structure is based on the wayland structure. For more information see the wayland +/// documentation [XDG Positioner](https://wayland.app/protocols/xdg-shell#xdg_positioner) +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct WindowPositioner { + /// The edge or corner of the anchor rect used to position the window relative to it. + /// + /// Combined with [`gravity`](Self::gravity), this controls which corner/edge of the anchor + /// rectangle the window is pinned to. Defaults to [`WindowAnchor::Center`]. + pub anchor: WindowAnchor, + /// The anchor rectangle the window is positioned relative to. + /// + /// The [`Position`] is the top-left corner of the rectangle relative to the parent window's + /// content area, and the [`Size`] its dimensions. Defaults to a `1x1` rectangle at the + /// content origin. Passing a [`WindowPositioner`] to [`WindowAttributes::with_positioner`] + /// overrides the position value set with [`WindowAttributes::with_position`]. + pub anchor_rect: (Position, Size), + /// The window's position relative to the anchor rect. Defaults to no offset. + pub offset: Position, + /// The direction the window surface extends away from the anchor point. + /// + /// Combined with [`anchor`](Self::anchor), this determines the final position of the window + /// relative to its anchor rectangle. Defaults to [`WindowGravity::Center`]. + pub gravity: WindowGravity, + /// How the window should be repositioned when it would be constrained. + /// + /// The flags in [`WindowConstraintAdjustment`] can be combined to allow sliding, flipping, + /// and/or resizing the window independently on each axis. Defaults to no adjustment. + pub constraint_adjustment: WindowConstraintAdjustment, +} + +impl WindowPositioner { + pub fn new( + anchor: WindowAnchor, + anchor_rect: (Position, Size), + offset: Position, + gravity: WindowGravity, + constraint_adjustment: WindowConstraintAdjustment, + ) -> Self { + WindowPositioner { anchor, anchor_rect, offset, gravity, constraint_adjustment } + } +} + +impl Default for WindowPositioner { + fn default() -> Self { + Self { + anchor: WindowAnchor::default(), + anchor_rect: ( + Position::Logical(LogicalPosition::new(0.0, 0.0)), + Size::Logical(LogicalSize::new(1.0, 1.0)), + ), + offset: Position::Logical(LogicalPosition::new(0.0, 0.0)), + gravity: WindowGravity::default(), + constraint_adjustment: WindowConstraintAdjustment::empty(), + } + } +} + /// Attributes used when creating a window. #[derive(Debug)] #[non_exhaustive] @@ -105,6 +176,8 @@ pub struct WindowAttributes { pub fullscreen: Option, pub platform: Option>, pub window_type: WindowType, + /// See [`WindowAttributes::with_positioner`]. + pub positioner: Option, } impl WindowAttributes { @@ -438,6 +511,27 @@ impl WindowAttributes { pub fn window_type(&self) -> WindowType { self.window_type } + + /// Sets the positioner used to place the window relative to its anchor rect. + /// + /// See [`WindowPositioner`] and its fields for what each part of the positioner controls and + /// its default when left as `None`. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Only takes effect when the window is a [`WindowType::Popup`], since the + /// Wayland positioner is part of the `xdg_popup` protocol role. + /// - **macOS, Windows:** Works for both [`WindowType::Window`] and [`WindowType::Popup`]. A + /// [`WindowType::Popup`] always requires a parent window to be set via + /// [`with_parent_window`](Self::with_parent_window). A [`WindowType::Window`] without a + /// parent is positioned relative to the screen's available space instead of the parent's + /// content area. + /// - **X11, Web, Android, iOS, Orbital:** No effect. + #[inline] + pub fn with_positioner(mut self, positioner: WindowPositioner) -> Self { + self.positioner = Some(positioner); + self + } } impl Clone for WindowAttributes { @@ -466,6 +560,7 @@ impl Clone for WindowAttributes { fullscreen: self.fullscreen.clone(), platform: self.platform.as_ref().map(|platform| platform.box_clone()), window_type: self.window_type, + positioner: self.positioner, } } } @@ -497,6 +592,7 @@ impl Default for WindowAttributes { cursor: Cursor::default(), blur: Default::default(), window_type: Default::default(), + positioner: Default::default(), } } } @@ -541,6 +637,31 @@ pub trait Window: Any + Send + Sync + fmt::Debug { /// Returns the window type of this window fn window_type(&self) -> WindowType; + /// Returns the positioner used to place this window relative to its anchor rect. + /// + /// Returns [`WindowPositioner::default`] if this window doesn't use anchor positioning, see + /// [`WindowAttributes::with_positioner`]. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Always [`WindowPositioner::default`] unless the window is a + /// [`WindowType::Popup`], since the Wayland positioner is part of the `xdg_popup` protocol + /// role. + fn positioner(&self) -> WindowPositioner { + WindowPositioner::default() + } + + /// Sets the positioner used to place this window relative to its anchor rect. + /// + /// No-op if this window doesn't use anchor positioning, see + /// [`WindowAttributes::with_positioner`]. + /// + /// ## Platform-specific + /// + /// - **Wayland:** No-op unless the window is a [`WindowType::Popup`], since the Wayland + /// positioner is part of the `xdg_popup` protocol role. + fn set_positioner(&self, _positioner: WindowPositioner) {} + /// Returns an identifier unique to the window. fn id(&self) -> WindowId; diff --git a/winit-core/src/window/positioner.rs b/winit-core/src/window/positioner.rs new file mode 100644 index 000000000..297cf44fa --- /dev/null +++ b/winit-core/src/window/positioner.rs @@ -0,0 +1,54 @@ +//! Anchor-based window placement: the types describing a placement request. +//! +//! The `xdg_positioner`-style algorithm that resolves these into a concrete position and size +//! lives in `winit-common`, since it's only needed by backends without a native equivalent of +//! Wayland's `xdg_positioner` (such as Win32 and AppKit) and isn't part of winit's public API. + +/// Anchor rect within the parent surface +/// See: https://wayland.app/protocols/xdg-shell#xdg_positioner:request:set_anchor_rect +#[derive(Debug, Clone, Copy, Default, PartialEq)] +#[non_exhaustive] +pub enum WindowAnchor { + #[default] + Center, + Top, + Bottom, + Left, + Right, + TopLeft, + BottomLeft, + TopRight, + BottomRight, +} + +/// Defines in what direction a surface should be positioned +/// See: https://wayland.app/protocols/xdg-shell#xdg_positioner:request:set_gravity +#[derive(Debug, Clone, Copy, Default, PartialEq)] +#[non_exhaustive] +pub enum WindowGravity { + #[default] + Center, + Top, + Bottom, + Left, + Right, + TopLeft, + BottomLeft, + TopRight, + BottomRight, +} + +bitflags::bitflags! { + /// Specify how the window should be positioned if the originally intended position caused the + /// surface to be constrained See: https://wayland.app/protocols/xdg-shell#xdg_positioner:request:set_constraint_adjustment + /// For all other platforms than wayland the behaviour is simulated on the winit side + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] + pub struct WindowConstraintAdjustment: u32 { + const SLIDE_X = 1 << 0; + const SLIDE_Y = 1 << 1; + const FLIP_X = 1 << 2; + const FLIP_Y = 1 << 3; + const RESIZE_X = 1 << 4; + const RESIZE_Y = 1 << 5; + } +} diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 27d8c24dc..113e2b074 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -22,7 +22,7 @@ use std::ffi::c_void; use std::hash::BuildHasher; use std::ptr::NonNull; -use dpi::{LogicalSize, PhysicalSize, Position, Size}; +use dpi::{LogicalSize, PhysicalSize}; use sctk::reexports::client::Proxy; use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::protocol::wl_surface::WlSurface; @@ -98,120 +98,6 @@ impl WindowExtWayland for dyn CoreWindow + '_ { } } -/// Additional methods on [`Popup`] that are specific to Wayland. -pub trait PopupExtWayland { - fn anchor_rect(&self) -> Option<(impl Into, impl Into)>; - - /// Sets the anchor edge of the parent surface the popup is positioned relative to. - /// - /// See [`PopupAnchor`] for the available edges and corners. - fn set_anchor(&self, anchor: PopupAnchor); - - /// Sets the anchor rectangle within the parent surface the popup is positioned relative to. - /// - /// `position` is the top-left corner of the rectangle relative to the parent window's content - /// area, and `size` its dimensions. - fn set_anchor_rect(&self, position: impl Into, size: impl Into); - - /// Sets how the compositor should reposition the popup when it would be constrained by screen - /// edges. - /// - /// See [`PopupConstraintAdjustment`] for the available adjustment flags. - fn set_constraint_adjustment(&self, constraint_adjustment: PopupConstraintAdjustment); - - /// Sets the direction the popup surface extends from the anchor point. - /// - /// See [`PopupGravity`] for the available directions. - fn set_gravity(&self, gravity: PopupGravity); - - /// Set the popup position relative to the anchor rect - fn set_positioner_offset(&self, position: impl Into); -} - -impl PopupExtWayland for dyn CoreWindow + '_ { - fn set_anchor(&self, anchor: PopupAnchor) { - if let Some(popup) = self.cast_ref::() { - popup.set_anchor(anchor); - } - } - - fn anchor_rect(&self) -> Option<(impl Into, impl Into)> { - if let Some(popup) = self.cast_ref::() { popup.anchor_rect() } else { None } - } - - fn set_anchor_rect(&self, position: impl Into, size: impl Into) { - if let Some(popup) = self.cast_ref::() { - popup.set_anchor_rect(position, size); - } - } - - fn set_constraint_adjustment(&self, constraint_adjustment: PopupConstraintAdjustment) { - if let Some(popup) = self.cast_ref::() { - popup.set_constraint_adjustment(constraint_adjustment); - } - } - - fn set_gravity(&self, gravity: PopupGravity) { - if let Some(popup) = self.cast_ref::() { - popup.set_gravity(gravity); - } - } - - fn set_positioner_offset(&self, position: impl Into) { - if let Some(popup) = self.cast_ref::() { - popup.set_positioner_offset(position); - } - } -} - -/// Anchor rect within the parent surface -/// See: https://wayland.app/protocols/xdg-shell#xdg_positioner:request:set_anchor_rect -#[derive(Debug, Clone, Copy, Default, PartialEq)] -#[non_exhaustive] -pub enum PopupAnchor { - #[default] - None, - Top, - Bottom, - Left, - Right, - TopLeft, - BottomLeft, - TopRight, - BottomRight, -} - -/// Defines in what direction a surface should be positioned -/// See: https://wayland.app/protocols/xdg-shell#xdg_positioner:request:set_gravity -#[derive(Debug, Clone, Copy, Default, PartialEq)] -#[non_exhaustive] -pub enum PopupGravity { - #[default] - None, - Top, - Bottom, - Left, - Right, - TopLeft, - BottomLeft, - TopRight, - BottomRight, -} - -bitflags::bitflags! { - /// Specify how the window should be positioned if the originally intended position caused the - /// surface to be constrained See: https://wayland.app/protocols/xdg-shell#xdg_positioner:request:set_constraint_adjustment - #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] - pub struct PopupConstraintAdjustment: u32 { - const SLIDE_X = 1 << 0; - const SLIDE_Y = 1 << 1; - const FLIP_X = 1 << 2; - const FLIP_Y = 1 << 3; - const RESIZE_X = 1 << 4; - const RESIZE_Y = 1 << 5; - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct ApplicationName { pub(crate) general: String, @@ -224,13 +110,6 @@ pub struct WindowAttributesWayland { pub(crate) name: Option, pub(crate) activation_token: Option, pub(crate) prefer_csd: bool, - pub(crate) anchor: Option, - pub(crate) anchor_rect: Option<(Position, Size)>, - /// Only for Popup: The offset of the popup to the - /// anchor rect - pub(crate) positioner_offset: Option, - pub(crate) gravity: Option, - pub(crate) constraint_adjustment: Option, } impl WindowAttributesWayland { @@ -269,63 +148,6 @@ impl WindowAttributesWayland { self.prefer_csd = prefer_csd; self } - - /// Sets the edge or corner of the anchor rectangle the popup is attached to. - /// - /// Combined with [`with_gravity`](Self::with_gravity), this controls which corner/edge of the - /// anchor rectangle the popup is pinned to. Has no effect unless the window is created as a - /// [`Popup`](winit_core::window::WindowType::Popup). - #[inline] - pub fn with_anchor(mut self, anchor: PopupAnchor) -> Self { - self.anchor = Some(anchor); - self - } - - /// Set the anchor rectangle the popup is positioned relative to. - /// - /// `position` is the top-left corner of the rectangle relative to the parent window's content - /// area, and `size` its dimensions. Defaults to a `1x1` rectangle at the content origin. - /// This value overwrites the position value set with `with_position` in the window attributes - #[inline] - pub fn with_anchor_rect( - mut self, - position: impl Into, - size: impl Into, - ) -> Self { - self.anchor_rect = Some((position.into(), size.into())); - self - } - - /// Set the popup position relative to the anchor rect - pub fn with_positioner_offset(mut self, position: impl Into) -> Self { - self.positioner_offset = Some(position.into()); - self - } - - /// Sets how the compositor should reposition the popup when it would be constrained. - /// - /// The flags in [`PopupConstraintAdjustment`] can be combined to allow sliding, flipping, - /// and/or resizing the popup independently on each axis. Has no effect unless the window is - /// created as a [`Popup`](winit_core::window::WindowType::Popup). - #[inline] - pub fn with_constraint_adjustment( - mut self, - constraint_adjustment: PopupConstraintAdjustment, - ) -> Self { - self.constraint_adjustment = Some(constraint_adjustment); - self - } - - /// Sets the direction the popup surface extends away from the anchor point. - /// - /// Combined with [`with_anchor`](Self::with_anchor), this determines the final position of the - /// popup relative to its anchor rectangle. Has no effect unless the window is created as a - /// [`Popup`](winit_core::window::WindowType::Popup). - #[inline] - pub fn with_gravity(mut self, gravity: PopupGravity) -> Self { - self.gravity = Some(gravity); - self - } } impl PlatformWindowAttributes for WindowAttributesWayland { @@ -381,64 +203,3 @@ fn image_to_buffer( Ok(buffer) } - -impl From for wayland_protocols::xdg::shell::client::xdg_positioner::Gravity { - fn from(value: PopupGravity) -> Self { - use wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; - match value { - PopupGravity::None => Gravity::None, - PopupGravity::Top => Gravity::Top, - PopupGravity::Bottom => Gravity::Bottom, - PopupGravity::Left => Gravity::Left, - PopupGravity::Right => Gravity::Right, - PopupGravity::TopLeft => Gravity::TopLeft, - PopupGravity::BottomLeft => Gravity::BottomLeft, - PopupGravity::TopRight => Gravity::TopRight, - PopupGravity::BottomRight => Gravity::BottomRight, - } - } -} - -impl From for wayland_protocols::xdg::shell::client::xdg_positioner::Anchor { - fn from(value: PopupAnchor) -> Self { - use wayland_protocols::xdg::shell::client::xdg_positioner::Anchor; - match value { - PopupAnchor::None => Anchor::None, - PopupAnchor::Top => Anchor::Top, - PopupAnchor::Bottom => Anchor::Bottom, - PopupAnchor::Left => Anchor::Left, - PopupAnchor::Right => Anchor::Right, - PopupAnchor::TopLeft => Anchor::TopLeft, - PopupAnchor::BottomLeft => Anchor::BottomLeft, - PopupAnchor::TopRight => Anchor::TopRight, - PopupAnchor::BottomRight => Anchor::BottomRight, - } - } -} - -impl From - for wayland_protocols::xdg::shell::client::xdg_positioner::ConstraintAdjustment -{ - fn from(value: PopupConstraintAdjustment) -> Self { - use wayland_protocols::xdg::shell::client::xdg_positioner::ConstraintAdjustment; - - const _: () = { - assert!( - PopupConstraintAdjustment::SLIDE_X.bits() == ConstraintAdjustment::SlideX.bits() - ); - assert!( - PopupConstraintAdjustment::SLIDE_Y.bits() == ConstraintAdjustment::SlideY.bits() - ); - assert!(PopupConstraintAdjustment::FLIP_X.bits() == ConstraintAdjustment::FlipX.bits()); - assert!(PopupConstraintAdjustment::FLIP_Y.bits() == ConstraintAdjustment::FlipY.bits()); - assert!( - PopupConstraintAdjustment::RESIZE_X.bits() == ConstraintAdjustment::ResizeX.bits() - ); - assert!( - PopupConstraintAdjustment::RESIZE_Y.bits() == ConstraintAdjustment::ResizeY.bits() - ); - }; - - ConstraintAdjustment::from_bits_retain(value.bits()) - } -} diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs index cdfd20909..4cd092404 100644 --- a/winit-wayland/src/popup.rs +++ b/winit-wayland/src/popup.rs @@ -18,16 +18,16 @@ use winit_core::event::{Ime, WindowEvent}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, - UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, - WindowLevel, + UserAttentionType, Window as CoreWindow, WindowAnchor, WindowAttributes, WindowButtons, + WindowConstraintAdjustment, WindowGravity, WindowId, WindowLevel, WindowPositioner, }; use super::ActiveEventLoop; use super::output::MonitorHandle; +use crate::WindowAttributesWayland; use crate::window::Handles; use crate::window::handles::WindowRequests; use crate::window::state::{WindowState, WindowType}; -use crate::{PopupExtWayland, WindowAttributesWayland}; #[derive(Debug)] pub struct Popup { @@ -66,27 +66,21 @@ impl Popup { .xdg_activation .as_ref() .map(|activation_state| activation_state.global().clone()); - let positioner = XdgPositioner::new(&state.xdg_shell) + let xdg_positioner = XdgPositioner::new(&state.xdg_shell) .map_err(|_| error("Failed to create positioner"))?; let parent_window_id = WindowId::from_raw(parent_window_handle.surface.as_ptr() as usize); let (popup, popup_state) = if let Some(parent_window_state) = state.windows.borrow().get(&parent_window_id) { - let wayland_attributes = attributes - .platform - .as_ref() - .and_then(|p| p.cast_ref::()) - .cloned() - .unwrap_or_default(); - let WindowAttributesWayland { - gravity, + let WindowPositioner { anchor, anchor_rect, + offset: positioner_offset, + gravity, constraint_adjustment, - positioner_offset, .. - } = wayland_attributes; + } = attributes.positioner.unwrap_or_default(); let grab_keyboard = attributes.active; let mut parent_window_state = parent_window_state.lock().unwrap(); @@ -107,48 +101,51 @@ impl Popup { // This is important for client side decorations let geometry_origin = parent_window_state.content_surface_origin(); let anchor_position = LogicalPosition::new(-geometry_origin.x, -geometry_origin.y); - positioner.set_anchor(anchor.unwrap_or(crate::PopupAnchor::TopLeft).into()); - positioner.set_gravity(gravity.unwrap_or(crate::PopupGravity::BottomRight).into()); - constraint_adjustment - .inspect(|c| positioner.set_constraint_adjustment((*c).into())); - let (anchor_rect_position, anchor_rect_size) = match anchor_rect { - Some((position, size)) => ( - position.to_logical::(scale_factor), - size.to_logical::(scale_factor), - ), - None => { - // anchor rect was not specified use attributes.position - let pos: LogicalPosition = attributes - .position - .map(|position| position.to_logical(scale_factor)) - .unwrap_or_default(); - (pos, LogicalSize::new(1, 1)) - }, + if xdg_positioner.version() >= 3 { + xdg_positioner.set_reactive(); + } + xdg_positioner.set_anchor(from_anchor(anchor)); + xdg_positioner.set_gravity(from_gravity(gravity)); + xdg_positioner + .set_constraint_adjustment(from_constraint_adjustment(constraint_adjustment)); + + let (anchor_rect_position, anchor_rect_size) = if attributes.positioner.is_some() { + let size = anchor_rect.1.to_logical::(scale_factor); + ( + anchor_rect.0.to_logical::(scale_factor), + LogicalSize::new(size.width.max(1), size.height.max(1)), + ) + } else { + // anchor rect was not specified use attributes.position + let pos: LogicalPosition = attributes + .position + .map(|position| position.to_logical(scale_factor)) + .unwrap_or_default(); + (pos, LogicalSize::new(1, 1)) }; + let anchor_rect = ( LogicalPosition::new( anchor_rect_position.x + anchor_position.x, anchor_rect_position.y + anchor_position.y, ), - LogicalSize::new(anchor_rect_size.width.max(1), anchor_rect_size.height.max(1)), + anchor_rect_size, ); - positioner.set_anchor_rect( + xdg_positioner.set_anchor_rect( anchor_rect.0.x, anchor_rect.0.y, anchor_rect.1.width, anchor_rect.1.height, ); - positioner_offset.inspect(|o| { - let o = o.to_logical(scale_factor); - positioner.set_offset(o.x, o.y); - }); - positioner.set_size(size.width, size.height); + let offset: LogicalPosition = positioner_offset.to_logical(scale_factor); + xdg_positioner.set_offset(offset.x, offset.y); + xdg_positioner.set_size(size.width, size.height); let parent_surface = parent_window_state.window.xdg_surface(); let surface = state.compositor_state.create_surface(&queue_handle); let popup = SctkPopup::from_surface( Some(parent_surface), - &positioner, + &xdg_positioner, &queue_handle, surface.clone(), &state.xdg_shell, @@ -163,10 +160,16 @@ impl Popup { size.into(), WindowType::Popup { popup: popup.clone(), - positioner, + xdg_positioner, last_configure: None, - anchor_rect, parent_origin: geometry_origin, + positioner: WindowPositioner::new( + anchor, + (anchor_rect_position.into(), anchor_rect_size.into()), + positioner_offset, + gravity, + constraint_adjustment, + ), }, attributes.preferred_theme, false, @@ -288,6 +291,53 @@ impl CoreWindow for Popup { winit_core::window::WindowType::Popup } + fn positioner(&self) -> WindowPositioner { + let Some(state) = self.popup_state.upgrade() else { return WindowPositioner::default() }; + if let WindowType::Popup { positioner, .. } = &state.lock().unwrap().window { + *positioner + } else { + WindowPositioner::default() + } + } + + fn set_positioner(&self, new_positioner: WindowPositioner) { + let Some(state) = self.popup_state.upgrade() else { + return; + }; + + let mut state = state.lock().unwrap(); + let scale_factor = state.scale_factor(); + + if let WindowType::Popup { popup, xdg_positioner, parent_origin, positioner, .. } = + &mut state.window + { + *positioner = new_positioner; + + xdg_positioner.set_anchor(from_anchor(new_positioner.anchor)); + xdg_positioner.set_gravity(from_gravity(new_positioner.gravity)); + xdg_positioner.set_constraint_adjustment(from_constraint_adjustment( + new_positioner.constraint_adjustment, + )); + + let (position, size) = new_positioner.anchor_rect; + let size: LogicalSize = size.to_logical(scale_factor); + let position: LogicalPosition = position.to_logical(scale_factor); + xdg_positioner.set_anchor_rect( + position.x - parent_origin.x, + position.y - parent_origin.y, + size.width.max(1), + size.height.max(1), + ); + + let offset: LogicalPosition = new_positioner.offset.to_logical(scale_factor); + xdg_positioner.set_offset(offset.x, offset.y); + + if popup.xdg_popup().version() >= 3 { + popup.reposition(xdg_positioner, 0); + } + } + } + fn id(&self) -> WindowId { self.window_id } @@ -332,19 +382,22 @@ impl CoreWindow for Popup { let Some(s) = self.popup_state.upgrade() else { return }; let mut state = s.lock().unwrap(); let scale_factor = state.scale_factor(); - if let WindowType::Popup { popup, positioner, anchor_rect, parent_origin, .. } = + if let WindowType::Popup { popup, xdg_positioner, positioner, parent_origin, .. } = &mut state.window { - let position = position.to_logical(scale_factor); - anchor_rect.0 = position; - positioner.set_anchor_rect( - anchor_rect.0.x - parent_origin.x, - anchor_rect.0.y - parent_origin.y, - anchor_rect.1.width, - anchor_rect.1.height, + let size = positioner.anchor_rect.1; + positioner.anchor_rect = (position, size); + + let logical_position: LogicalPosition = position.to_logical(scale_factor); + let logical_size: LogicalSize = size.to_logical(scale_factor); + xdg_positioner.set_anchor_rect( + logical_position.x - parent_origin.x, + logical_position.y - parent_origin.y, + logical_size.width, + logical_size.height, ); if popup.xdg_popup().version() >= 3 { - popup.reposition(positioner, 0); + popup.reposition(xdg_positioner, 0); } } } @@ -668,80 +721,59 @@ impl rwh_06::HasDisplayHandle for Popup { } } -impl PopupExtWayland for Popup { - fn set_anchor(&self, anchor: crate::PopupAnchor) { - let Some(state) = self.popup_state.upgrade() else { - return; - }; - - if let WindowType::Popup { popup, positioner, .. } = &state.lock().unwrap().window { - positioner.set_anchor(anchor.into()); - popup.reposition(positioner, 0); - } - } - - fn anchor_rect(&self) -> Option<(impl Into, impl Into)> { - let state = self.popup_state.upgrade()?; - if let WindowType::Popup { anchor_rect, .. } = &state.lock().unwrap().window { - Some(*anchor_rect) - } else { - None - } - } - - fn set_anchor_rect(&self, position: impl Into, size: impl Into) { - let Some(state) = self.popup_state.upgrade() else { - return; - }; - - let state = state.lock().unwrap(); - let scale_factor = state.scale_factor(); - let size: LogicalSize = size.into().to_logical(scale_factor); - let position: LogicalPosition = position.into().to_logical(scale_factor); - - if let WindowType::Popup { popup, positioner, parent_origin, .. } = &state.window { - positioner.set_anchor_rect( - position.x - parent_origin.x, - position.y - parent_origin.y, - size.width.max(1), - size.height.max(1), - ); - popup.reposition(positioner, 0); - } - } - - fn set_positioner_offset(&self, position: impl Into) { - let Some(state) = self.popup_state.upgrade() else { - return; - }; - - let scale_factor = state.lock().unwrap().scale_factor(); - let position: LogicalPosition = position.into().to_logical(scale_factor); - if let WindowType::Popup { popup, positioner, .. } = &state.lock().unwrap().window { - positioner.set_offset(position.x, position.y); - popup.reposition(positioner, 0); - } - } - - fn set_constraint_adjustment(&self, constraint_adjustment: crate::PopupConstraintAdjustment) { - let Some(state) = self.popup_state.upgrade() else { - return; - }; - - if let WindowType::Popup { popup, positioner, .. } = &state.lock().unwrap().window { - positioner.set_constraint_adjustment(constraint_adjustment.into()); - popup.reposition(positioner, 0); - } - } - - fn set_gravity(&self, gravity: crate::PopupGravity) { - let Some(state) = self.popup_state.upgrade() else { - return; - }; - - if let WindowType::Popup { popup, positioner, .. } = &state.lock().unwrap().window { - positioner.set_gravity(gravity.into()); - popup.reposition(positioner, 0); - } +fn from_gravity( + gravity: WindowGravity, +) -> wayland_protocols::xdg::shell::client::xdg_positioner::Gravity { + use wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; + match gravity { + WindowGravity::Center => Gravity::None, + WindowGravity::Top => Gravity::Top, + WindowGravity::Bottom => Gravity::Bottom, + WindowGravity::Left => Gravity::Left, + WindowGravity::Right => Gravity::Right, + WindowGravity::TopLeft => Gravity::TopLeft, + WindowGravity::BottomLeft => Gravity::BottomLeft, + WindowGravity::TopRight => Gravity::TopRight, + WindowGravity::BottomRight => Gravity::BottomRight, + _ => Gravity::None, } } + +fn from_anchor( + value: WindowAnchor, +) -> wayland_protocols::xdg::shell::client::xdg_positioner::Anchor { + use wayland_protocols::xdg::shell::client::xdg_positioner::Anchor; + match value { + WindowAnchor::Center => Anchor::None, + WindowAnchor::Top => Anchor::Top, + WindowAnchor::Bottom => Anchor::Bottom, + WindowAnchor::Left => Anchor::Left, + WindowAnchor::Right => Anchor::Right, + WindowAnchor::TopLeft => Anchor::TopLeft, + WindowAnchor::BottomLeft => Anchor::BottomLeft, + WindowAnchor::TopRight => Anchor::TopRight, + WindowAnchor::BottomRight => Anchor::BottomRight, + _ => Anchor::None, + } +} + +fn from_constraint_adjustment( + value: WindowConstraintAdjustment, +) -> wayland_protocols::xdg::shell::client::xdg_positioner::ConstraintAdjustment { + use wayland_protocols::xdg::shell::client::xdg_positioner::ConstraintAdjustment; + + const _: () = { + assert!(WindowConstraintAdjustment::SLIDE_X.bits() == ConstraintAdjustment::SlideX.bits()); + assert!(WindowConstraintAdjustment::SLIDE_Y.bits() == ConstraintAdjustment::SlideY.bits()); + assert!(WindowConstraintAdjustment::FLIP_X.bits() == ConstraintAdjustment::FlipX.bits()); + assert!(WindowConstraintAdjustment::FLIP_Y.bits() == ConstraintAdjustment::FlipY.bits()); + assert!( + WindowConstraintAdjustment::RESIZE_X.bits() == ConstraintAdjustment::ResizeX.bits() + ); + assert!( + WindowConstraintAdjustment::RESIZE_Y.bits() == ConstraintAdjustment::ResizeY.bits() + ); + }; + + ConstraintAdjustment::from_bits_retain(value.bits()) +} diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index de90a591a..45ce43c8a 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -34,6 +34,7 @@ use winit_core::cursor::{CursorIcon, CustomCursor as CoreCustomCursor}; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, WindowId, + WindowPositioner, }; use crate::event_loop::OwnedDisplayHandle; @@ -64,10 +65,11 @@ pub enum WindowType { }, Popup { popup: Popup, - positioner: XdgPositioner, + xdg_positioner: XdgPositioner, last_configure: Option, parent_origin: LogicalPosition, - anchor_rect: (LogicalPosition, LogicalSize), + + positioner: WindowPositioner, }, } @@ -844,11 +846,11 @@ impl WindowState { self.resize(surface_size.to_logical(self.scale_factor())) } }, - WindowType::Popup { popup, positioner, .. } => { + WindowType::Popup { popup, xdg_positioner, .. } => { let size = surface_size.to_logical(self.scale_factor()); - positioner.set_size(size.width, size.height); + xdg_positioner.set_size(size.width, size.height); if popup.xdg_popup().version() >= 3 { - popup.reposition(positioner, 0); + popup.reposition(xdg_positioner, 0); } }, } diff --git a/winit-win32/Cargo.toml b/winit-win32/Cargo.toml index 3835afccc..195437d82 100644 --- a/winit-win32/Cargo.toml +++ b/winit-win32/Cargo.toml @@ -20,6 +20,7 @@ serde = { workspace = true, optional = true } smol_str.workspace = true tracing.workspace = true url.workspace = true +winit-common = { workspace = true, features = ["positioner"] } winit-core.workspace = true # Platform-specific diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index 2408a04de..ca29be1bd 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -40,27 +40,28 @@ use windows_sys::Win32::UI::Input::{ MOUSE_MOVE_RELATIVE, RAWINPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, }; use windows_sys::Win32::UI::WindowsAndMessaging::{ - CREATESTRUCTW, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GWL_STYLE, - GWL_USERDATA, GetClientRect, GetCursorPos, GetMenu, HTCAPTION, HTCLIENT, LoadCursorW, - MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, MsgWaitForMultipleObjectsEx, - NCCALCSIZE_PARAMS, PEN_FLAG_BARREL, PEN_FLAG_ERASER, PEN_MASK_PRESSURE, PEN_MASK_ROTATION, - PEN_MASK_TILT_X, PEN_MASK_TILT_Y, PM_REMOVE, PT_PEN, PT_TOUCH, PeekMessageW, PostMessageW, - QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, RegisterClassExW, RegisterWindowMessageA, - SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, - SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SetCursor, SetWindowPos, - SystemParametersInfoW, TranslateMessage, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE, - WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, - WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, - WM_INPUT, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, - WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, - WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, - WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, - WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, - WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WMSZ_BOTTOM, - WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, - WMSZ_TOPRIGHT, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, - WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, + CREATESTRUCTW, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, + EnumThreadWindows, GW_OWNER, GWL_STYLE, GWL_USERDATA, GetClientRect, GetCursorPos, GetMenu, + GetWindow, HTCAPTION, HTCLIENT, LoadCursorW, MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, + MsgWaitForMultipleObjectsEx, NCCALCSIZE_PARAMS, PEN_FLAG_BARREL, PEN_FLAG_ERASER, + PEN_MASK_PRESSURE, PEN_MASK_ROTATION, PEN_MASK_TILT_X, PEN_MASK_TILT_Y, PM_REMOVE, PT_PEN, + PT_TOUCH, PeekMessageW, PostMessageW, QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, + RegisterClassExW, RegisterWindowMessageA, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, + SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, + SWP_NOZORDER, SetCursor, SetWindowPos, SystemParametersInfoW, TranslateMessage, WHEEL_DELTA, + WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, + WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, + WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, + WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, + WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, + WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, + WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, + WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, + WM_XBUTTONDOWN, WM_XBUTTONUP, WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, + WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT, WNDCLASSEXW, WS_EX_LAYERED, + WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, }; +use windows_sys::core::BOOL; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor, CustomCursorSource}; use winit_core::data_transfer::{ @@ -94,7 +95,7 @@ use crate::keyboard::KeyEventBuilder; use crate::keyboard_layout::LAYOUT_CACHE; use crate::monitor::{self, MonitorHandle}; use crate::util::{WIN10_BUILD_VERSION, wrap_device_id}; -use crate::window::{InitData, Window}; +use crate::window::{self, InitData, Window}; use crate::window_state::{CursorFlags, ImeState, WindowFlags, WindowState}; use crate::{raw_input, util}; @@ -1112,6 +1113,31 @@ unsafe fn lose_active_focus(window: HWND, userdata: &WindowData) { userdata.send_window_event(window, Focused(false)); } +/// Repositions any anchored windows owned by `parent`. Win32 does not reposition owned windows +/// when their owner moves (unlike Wayland subsurfaces or X11's override-redirect popups), so this +/// has to be done manually whenever `parent` receives a `WM_WINDOWPOSCHANGED` that moved it. +unsafe fn reposition_owned_windows(parent: HWND) { + unsafe extern "system" fn enum_proc(hwnd: HWND, lparam: LPARAM) -> BOOL { + if unsafe { GetWindow(hwnd, GW_OWNER) } != lparam as HWND { + return true.into(); // continue enumeration + } + + let userdata_ptr = unsafe { util::get_window_long(hwnd, GWL_USERDATA) } as *mut WindowData; + if !userdata_ptr.is_null() { + let userdata = unsafe { &*userdata_ptr }; + if userdata.window_state_lock().window_flags.contains(WindowFlags::ANCHORED) { + window::reposition_owned_popup(hwnd, &userdata.window_state); + } + } + + true.into() // continue enumeration + } + + unsafe { + EnumThreadWindows(GetCurrentThreadId(), Some(enum_proc), parent as LPARAM); + } +} + /// Any window whose callback is configured to this function will have its events propagated /// through the events loop of the thread the window was created in. // This is the callback that is called by `DispatchMessage` in the events loop. @@ -1449,6 +1475,8 @@ unsafe fn public_window_callback_inner( let physical_position = unsafe { PhysicalPosition::new((*windowpos).x, (*windowpos).y) }; userdata.send_window_event(window, Moved(physical_position)); + + unsafe { reposition_owned_windows(window) }; } // This is necessary for us to still get sent WM_SIZE. diff --git a/winit-win32/src/monitor.rs b/winit-win32/src/monitor.rs index 0e252418a..9d031bc2d 100644 --- a/winit-win32/src/monitor.rs +++ b/winit-win32/src/monitor.rs @@ -126,6 +126,16 @@ impl MonitorHandle { } } + /// The monitor's work area, i.e. its bounds minus space reserved by the system for things + /// like the taskbar (see `rcWork` in [`MONITORINFO`]). + pub(crate) fn work_area(&self) -> Option<(PhysicalPosition, PhysicalSize)> { + let rc_work = get_monitor_info(self.0).ok()?.monitorInfo.rcWork; + Some((PhysicalPosition { x: rc_work.left, y: rc_work.top }, PhysicalSize { + width: (rc_work.right - rc_work.left) as u32, + height: (rc_work.bottom - rc_work.top) as u32, + })) + } + pub(crate) fn video_mode_handles(&self) -> Box> { // EnumDisplaySettingsExW can return duplicate values (or some of the // fields are probably changing, but we aren't looking at those fields diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index bf78bb369..5c0fdb5b5 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -7,7 +7,9 @@ use std::sync::mpsc::channel; use std::sync::{Arc, Mutex, MutexGuard}; use std::{io, panic, ptr}; -use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; +use dpi::{ + LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size, +}; use tracing::warn; use windows_sys::Win32::Foundation::{ HWND, LPARAM, OLE_E_WRONGCOMPOBJ, POINT, POINTS, RECT, RPC_E_CHANGED_MODE, S_OK, WPARAM, @@ -47,6 +49,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ TPM_RETURNCMD, TrackPopupMenu, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, WM_NCLBUTTONDOWN, WM_SETICON, WM_SYSCOMMAND, WNDCLASSEXW, }; +use winit_common::positioner::place_window; use winit_core::cursor::Cursor; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::icon::{Icon, RgbaIcon}; @@ -54,7 +57,7 @@ use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, Monito use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, - WindowLevel, WindowType, + WindowLevel, WindowPositioner, WindowType, }; use crate::dark_mode::try_theme; @@ -100,9 +103,6 @@ pub struct Window { // The events loop proxy. thread_executor: event_loop::EventLoopThreadExecutor, - - /// The type of window of this - window_type: WindowType, } impl Window { @@ -114,37 +114,26 @@ impl Window { // First person to remove the need for cloning here gets a cookie! // // done. you owe me -- ossi - unsafe { init(w_attr, &event_loop.0) } + let window = unsafe { init(w_attr, &event_loop.0) }?; + window.reposition(); + Ok(window) } fn window_state_lock(&self) -> MutexGuard<'_, WindowState> { self.window_state.lock().unwrap() } - // If we have a popup the position is relative to the parent window and not + // If the window is anchored the position is relative to the parent window and not // relative to the screen. Therefore we have to translate it from the parent // coordinate system to the display coordinate system fn translate_outer_position(&self, position: Position) -> PhysicalPosition { - let position = position.to_physical::(self.scale_factor()); - let mut point = POINT { x: position.x, y: position.y }; - - let window_flags = self.window_state_lock().window_flags; - if window_flags.contains(WindowFlags::POPUP) && !window_flags.contains(WindowFlags::CHILD) { - let parent = unsafe { GetParent(self.hwnd()) }; - if !parent.is_null() { - unsafe { - ClientToScreen(parent, &mut point); - } - } - } - - PhysicalPosition::new(point.x, point.y) + translate_outer_position(self.hwnd(), &self.window_state, position, self.scale_factor()) } - // Inverse of `translate_outer_position`: if we have a popup the position is + // Inverse of `translate_outer_position`: if the window is anchored the position is // reported relative to the parent window instead of the screen. Therefore we // translate it from the display coordinate system back to the parent - // coordinate system. Non-popup windows are left in screen coordinates. + // coordinate system. Non-anchored windows are left in screen coordinates. fn translate_outer_position_to_parent( &self, position: PhysicalPosition, @@ -152,7 +141,9 @@ impl Window { let mut point = POINT { x: position.x, y: position.y }; let window_flags = self.window_state_lock().window_flags; - if window_flags.contains(WindowFlags::POPUP) && !window_flags.contains(WindowFlags::CHILD) { + if window_flags.contains(WindowFlags::ANCHORED) + && !window_flags.contains(WindowFlags::CHILD) + { let parent = unsafe { GetParent(self.hwnd()) }; if !parent.is_null() { unsafe { @@ -164,6 +155,28 @@ impl Window { PhysicalPosition::new(point.x, point.y) } + /// Recomputes this window's position (and, if constrained, its size) from its positioner + /// state, using [`winit_common::positioner::place_window`], and applies the result. No-op if + /// this window isn't anchored. If it has no parent (which the Win32 backend never allows for + /// a popup, see `init`), the positioner is resolved relative to the screen's work area + /// instead of the parent's content area. + fn reposition(&self) { + let (anchored, positioner) = { + let window_state = self.window_state_lock(); + (window_state.anchored, window_state.positioner) + }; + let Some((origin, size, current_size)) = + compute_anchored_placement(self.hwnd(), anchored, &positioner, self.scale_factor()) + else { + return; + }; + + self.set_outer_position(Position::Logical(origin)); + if size != current_size { + let _ = self.request_surface_size(Size::Logical(size)); + } + } + /// Returns the `hwnd` of this window. pub fn hwnd(&self) -> HWND { self.window.hwnd() @@ -463,7 +476,16 @@ impl rwh_06::HasWindowHandle for Window { impl CoreWindow for Window { fn window_type(&self) -> WindowType { - self.window_type + self.window_state_lock().window_type + } + + fn positioner(&self) -> WindowPositioner { + self.window_state_lock().positioner + } + + fn set_positioner(&self, positioner: WindowPositioner) { + self.window_state_lock().positioner = positioner; + self.reposition(); } fn set_title(&self, text: &str) { @@ -566,12 +588,7 @@ impl CoreWindow for Window { } fn outer_size(&self) -> PhysicalSize { - util::WindowArea::Outer - .get_rect(self.hwnd()) - .map(|rect| { - PhysicalSize::new((rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32) - }) - .unwrap() + outer_size_of(self.hwnd()) } fn request_surface_size(&self, size: Size) -> Option> { @@ -1215,6 +1232,159 @@ impl CoreWindow for Window { } } +fn outer_size_of(hwnd: HWND) -> PhysicalSize { + util::WindowArea::Outer + .get_rect(hwnd) + .map(|rect| { + PhysicalSize::new((rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32) + }) + .unwrap() +} + +fn surface_size_of(hwnd: HWND) -> PhysicalSize { + util::WindowArea::Inner + .get_rect(hwnd) + .map(|rect| { + PhysicalSize::new((rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32) + }) + .unwrap() +} + +// If the window is anchored the position is relative to the parent window and not relative to +// the screen. Therefore we have to translate it from the parent coordinate system to the display +// coordinate system. Free function so it can be used both from `Window` and from an owned +// popup's `hwnd`/`WindowState` alone (see `reposition_owned_popup`). +fn translate_outer_position( + hwnd: HWND, + window_state: &Mutex, + position: Position, + scale_factor: f64, +) -> PhysicalPosition { + let position = position.to_physical::(scale_factor); + let mut point = POINT { x: position.x, y: position.y }; + + let window_flags = window_state.lock().unwrap().window_flags; + if window_flags.contains(WindowFlags::ANCHORED) && !window_flags.contains(WindowFlags::CHILD) { + let parent = unsafe { GetParent(hwnd) }; + if !parent.is_null() { + unsafe { + ClientToScreen(parent, &mut point); + } + } + } + + PhysicalPosition::new(point.x, point.y) +} + +/// Computes an anchored window's new outer position and surface size from its positioner state, +/// using [`winit_common::positioner::place_window`]. Returns `None` if the window isn't anchored, +/// or its monitor's work area can't be determined. +/// +/// On success, returns `(origin, size, current_size)`: +/// - `origin`: the window's new outer position, in logical coordinates relative to the parent's +/// content area, or to the screen if this window has no parent +/// - `size`: the window's new surface size, as constrained by [`place_window`] and converted back +/// from the outer size it operates on (see below) +/// - `current_size`: the window's surface size *before* this placement, i.e. `hwnd`'s current +/// surface size +fn compute_anchored_placement( + hwnd: HWND, + anchored: bool, + positioner: &WindowPositioner, + scale_factor: f64, +) -> Option<(LogicalPosition, LogicalSize, LogicalSize)> { + if !anchored { + return None; + } + + let monitor = monitor::current_monitor(hwnd); + // Clip to the monitor's work area rather than its full bounds, so anchored popups don't get + // placed underneath the taskbar. + let (work_area_position, work_area_size) = monitor.work_area()?; + + // The anchor rect's position is relative to the parent's content area (see + // `Popup::set_anchor_rect`), and `set_outer_position` re-adds the parent's screen origin for + // anchored windows (see `translate_outer_position`). To stay in the same coordinate space, + // the clip region also needs to be expressed relative to the parent's content area. Without a + // parent, both the anchor rect and `set_outer_position` operate directly in screen + // coordinates instead, so the origin is left at zero and the clip region stays in screen + // space. + let parent = unsafe { GetParent(hwnd) }; + let mut parent_origin = POINT { x: 0, y: 0 }; + if !parent.is_null() { + unsafe { ClientToScreen(parent, &mut parent_origin) }; + } + + let clip_position = PhysicalPosition::new( + work_area_position.x - parent_origin.x, + work_area_position.y - parent_origin.y, + ) + .to_logical::(scale_factor); + let clip_size = work_area_size.to_logical::(scale_factor); + + let current_outer_size = outer_size_of(hwnd).to_logical::(scale_factor); + let current_size = surface_size_of(hwnd).to_logical::(scale_factor); + + let (origin, outer_size) = + place_window(positioner, scale_factor, current_outer_size, (clip_position, clip_size)); + + // The window's non-client insets (title bar, borders, ...), assumed constant regardless of + // the window's size, to translate `outer_size` back into a surface size. + let insets_width = current_outer_size.width - current_size.width; + let insets_height = current_outer_size.height - current_size.height; + let size = LogicalSize::new( + (outer_size.width - insets_width).max(0.0), + (outer_size.height - insets_height).max(0.0), + ); + + Some((origin, size, current_size)) +} + +/// Repositions (and, if constrained, resizes) the popup with the given `hwnd`, using its +/// positioner state stored in `window_state` +/// +/// This is used to reposition a window when its parent moves, since Win32 does not do so +/// automatically for owned windows, this is only ever called from the event loop +/// thread (from a sibling window's message procedure while handling the parent's +/// `WM_WINDOWPOSCHANGED`), so it's safe to update the OS window and `WindowState` directly +/// instead of going through `EventLoopThreadExecutor` +pub(crate) fn reposition_owned_popup(hwnd: HWND, window_state: &Mutex) { + let (anchored, positioner, scale_factor) = { + let state = window_state.lock().unwrap(); + (state.anchored, state.positioner, state.scale_factor) + }; + + let Some((origin, size, current_size)) = + compute_anchored_placement(hwnd, anchored, &positioner, scale_factor) + else { + return; + }; + + let position = + translate_outer_position(hwnd, window_state, Position::Logical(origin), scale_factor); + WindowState::set_window_flags(window_state.lock().unwrap(), hwnd, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + unsafe { + SetWindowPos( + hwnd, + ptr::null_mut(), + position.x, + position.y, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE, + ); + InvalidateRgn(hwnd, ptr::null_mut(), false.into()); + } + + if size != current_size { + let physical_size = Size::Logical(size).to_physical::(scale_factor); + let window_flags = window_state.lock().unwrap().window_flags; + window_flags.set_size(hwnd, physical_size); + } +} + pub(super) struct InitData<'a> { // inputs pub runner: &'a Rc, @@ -1266,7 +1436,6 @@ impl InitData<'_> { window: SyncWindowHandle(window), window_state, thread_executor: self.runner.create_thread_executor(), - window_type: self.attributes.window_type, } } @@ -1426,9 +1595,14 @@ unsafe fn init( unsafe { register_window_class(&class_name) }; let is_popup = matches!(attributes.window_type, WindowType::Popup); + // Whether this window is positioned relative to its parent via the anchor/gravity/ + // positioner system -- either because it's a `WindowType::Popup`, or because anchor + // attributes were explicitly set on a `WindowType::Window` (Windows supports both). + let anchored = is_popup || attributes.positioner.is_some(); let mut window_flags = WindowFlags::empty(); window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations); window_flags.set(WindowFlags::POPUP, is_popup); + window_flags.set(WindowFlags::ANCHORED, anchored); window_flags.set(WindowFlags::MARKER_UNDECORATED_SHADOW, win_attributes.decoration_shadow); window_flags .set(WindowFlags::ALWAYS_ON_TOP, attributes.window_level == WindowLevel::AlwaysOnTop); diff --git a/winit-win32/src/window_state.rs b/winit-win32/src/window_state.rs index c0b24f9b9..01461a89d 100644 --- a/winit-win32/src/window_state.rs +++ b/winit-win32/src/window_state.rs @@ -20,7 +20,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ use winit_core::icon::Icon; use winit_core::keyboard::ModifiersState; use winit_core::monitor::Fullscreen; -use winit_core::window::{ImeCapabilities, Theme, WindowAttributes}; +use winit_core::window::{ImeCapabilities, Theme, WindowAttributes, WindowPositioner, WindowType}; use crate::{SelectedCursor, WindowAttributesWindows, event_loop, util}; @@ -51,6 +51,21 @@ pub(crate) struct WindowState { pub window_flags: WindowFlags, + /// The role of this window. Governs creation/role behavior (OS window style, decorations) + /// only -- see `anchored` for whether this window is positioned via the anchor system. + pub window_type: WindowType, + + /// Whether this window is positioned relative to its parent using the anchor/gravity/ + /// positioner system, either because it's a [`WindowType::Popup`] or because anchor + /// attributes were set on a [`WindowType::Window`]. Stored here (rather than only on the + /// `Window` struct) so it stays reachable from just an `hwnd` via `GWL_USERDATA` -- e.g. to + /// reposition an anchored window when its parent moves. + pub anchored: bool, + + /// The positioner state backing `anchored` placement, meaningful only when `anchored` is + /// `true`. + pub positioner: WindowPositioner, + pub ime_state: ImeState, pub ime_capabilities: Option, @@ -140,6 +155,12 @@ bitflags! { const CLIP_CHILDREN = 1 << 22; + /// Whether this window is positioned relative to its parent via the anchor/gravity/ + /// positioner system. Independent of `POPUP`, which only selects the OS window style -- + /// a `WindowType::Window` can be `ANCHORED` too. Used to pick the coordinate frame in + /// `translate_outer_position`/`translate_outer_position_to_parent`. + const ANCHORED = 1 << 23; + const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits(); } } @@ -186,6 +207,11 @@ impl WindowState { preferred_theme, window_flags: WindowFlags::empty(), + window_type: attributes.window_type, + anchored: matches!(attributes.window_type, WindowType::Popup) + || attributes.positioner.is_some(), + positioner: attributes.positioner.unwrap_or_default(), + ime_state: ImeState::Disabled, ime_capabilities: None, @@ -287,23 +313,25 @@ impl WindowFlags { } } else { style |= WS_CAPTION | WS_SYSMENU | WS_BORDER; + + if self.contains(WindowFlags::RESIZABLE) { + style |= WS_SIZEBOX; + } + if self.contains(WindowFlags::MAXIMIZABLE) { + style |= WS_MAXIMIZEBOX; + } + if self.contains(WindowFlags::MINIMIZABLE) { + style |= WS_MINIMIZEBOX; + } + + if self.contains(WindowFlags::ON_TASKBAR) { + style_ex |= WS_EX_APPWINDOW; + } }; - if self.contains(WindowFlags::RESIZABLE) { - style |= WS_SIZEBOX; - } - if self.contains(WindowFlags::MAXIMIZABLE) { - style |= WS_MAXIMIZEBOX; - } - if self.contains(WindowFlags::MINIMIZABLE) { - style |= WS_MINIMIZEBOX; - } if self.contains(WindowFlags::VISIBLE) { style |= WS_VISIBLE; } - if self.contains(WindowFlags::ON_TASKBAR) { - style_ex |= WS_EX_APPWINDOW; - } if self.contains(WindowFlags::ALWAYS_ON_TOP) { style_ex |= WS_EX_TOPMOST; } diff --git a/winit/examples/popup.rs b/winit/examples/popup.rs new file mode 100644 index 000000000..934592e47 --- /dev/null +++ b/winit/examples/popup.rs @@ -0,0 +1,214 @@ +//! This example shows the capabilities of popups +//! Move the mouse on the window and press 'A' to create a new Popup and 'D' to delete it again. +//! Move the Window to the border so see how the constraint adjustments behave like flipping or +//! sliding. See `spawn_popup` and play with the various properties + +#[cfg(any(x11_platform, macos_platform, windows_platform, wayland_platform))] +#[allow(deprecated)] +fn main() -> Result<(), impl std::error::Error> { + use std::collections::HashMap; + + use softbuffer::{Context, Surface}; + use tracing::info; + use winit::application::ApplicationHandler; + use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, Position}; + use winit::event::{ElementState, KeyEvent, WindowEvent}; + use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle}; + use winit::raw_window_handle::HasRawWindowHandle; + use winit::window::{Window, WindowAttributes, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + #[derive(Debug)] + struct WindowData { + surface: Surface>, + color: u32, + } + + impl WindowData { + fn new(context: &Context, window: Box, color: u32) -> Self { + let surface = Surface::new(context, window).unwrap(); + Self { surface, color } + } + } + + #[derive(Debug)] + struct Application { + parent_window_id: Option, + main_window: Option, + windows: HashMap, + popups: Vec, + position: Option>, + context: Context, + } + + impl ApplicationHandler for Application { + fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { + let attributes = WindowAttributes::default() + .with_title("parent window") + .with_position(Position::Logical(LogicalPosition::new(0.0, 0.0))) + .with_surface_size(LogicalSize::new(600.0f32, 600.0f32)) + .with_decorations(true); + let window = event_loop.create_window(attributes).unwrap(); + self.parent_window_id = Some(window.id()); + + if self.main_window.is_none() { + self.main_window = Some(window.id()); + } + self.windows.insert(window.id(), WindowData::new(&self.context, window, 0xffbbbbbb)); + } + + fn window_event( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: winit::window::WindowId, + event: WindowEvent, + ) { + use winit::keyboard::{KeyCode, PhysicalKey}; + + match event { + WindowEvent::CloseRequested => { + self.windows.remove(&window_id); + self.popups.retain_mut(|id| id != &window_id); + if self.windows.is_empty() { + event_loop.exit(); + } + }, + WindowEvent::PointerEntered { .. } => { + // On x11, println when the cursor entered in a window even if the child window + // is created by some key inputs. + // the child windows are always placed at (0, 0) with size (200, 200) in the + // parent window, so we also can see this log when we move + // the cursor around (200, 200) in parent window. + info!("cursor entered in the window {window_id:?}"); + }, + WindowEvent::PointerMoved { position, .. } => { + let tracked = self.popups.last().copied().or(self.main_window); + self.position = (tracked == Some(window_id)).then(|| { + info!("Physical position: {position:?}"); + position + }); + }, + WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Released, + physical_key: PhysicalKey::Code(code), + .. + }, + .. + } => { + match code { + KeyCode::KeyA => { + let window_id = if let Some(popup_id) = self.popups.last() { + popup_id + } else { + // The mainwindow must exist, otherwise the event_loop was ended + &self.main_window.unwrap() + }; + + // Add a new Popup + let child_index = self.windows.len() - 1; + let child_color = + 0xff000000 + 3_u32.pow((child_index + 2).rem_euclid(16) as u32); + + let parent_window = self.windows.get(window_id).unwrap(); + let child_window = spawn_popup( + parent_window.surface.window().as_ref(), + event_loop, + child_index, + self.position, + ); + let child_id = child_window.id(); + self.popups.push(child_id); + self.windows.insert( + child_id, + WindowData::new(&self.context, child_window, child_color), + ); + }, + KeyCode::KeyD => { + // Delete + if let Some(l) = self.popups.pop() { + self.windows.remove(&l); + } + + // // When deleting the first, it should not lead to a wayland protocol + // // error + // if let Some(l) = self.popups.first() { + // self.windows.remove(&l); + // } + }, + _ => (), + } + }, + WindowEvent::RedrawRequested => { + if let Some(window) = self.windows.get_mut(&window_id) { + if window_id == self.parent_window_id.unwrap() { + fill::fill(&mut window.surface); + } else { + fill::fill_with_color(&mut window.surface, window.color); + } + } + }, + _ => (), + } + } + } + + fn spawn_popup( + parent: &dyn Window, + event_loop: &dyn ActiveEventLoop, + _child_count: usize, + position: Option>, + ) -> Box { + use winit::dpi::Size; + use winit::window::{ + WindowAnchor, WindowConstraintAdjustment, WindowGravity, WindowPositioner, WindowType, + }; + + let parent = parent.raw_window_handle().unwrap(); + + let mut window_attributes = WindowAttributes::default() + .with_title("child window") + .with_surface_size(LogicalSize::new(300.0f32, 300.0)) + .with_decorations(false) + .with_visible(true) + .with_active(true) // Grab keyboard + .with_window_type(WindowType::Popup) + .with_positioner(WindowPositioner::new( + WindowAnchor::TopLeft, + ( + Position::Physical(position.unwrap_or_default().cast()), + Size::Logical(LogicalSize { width: 1., height: 1. }), + ), + Position::Logical(LogicalPosition { x: 0., y: 0. }), + WindowGravity::BottomRight, + WindowConstraintAdjustment::all(), + )); + + // `with_parent_window` is unsafe. Parent window must be a valid window. + window_attributes = unsafe { window_attributes.with_parent_window(Some(parent)) }; + + event_loop.create_window(window_attributes).unwrap() + } + + let event_loop = EventLoop::new().unwrap(); + let context = Context::new(event_loop.owned_display_handle()).unwrap(); + event_loop.run_app(Application { + context, + parent_window_id: None, + windows: HashMap::new(), + popups: Vec::default(), + position: Default::default(), + main_window: None, + }) +} + +#[cfg(not(any(x11_platform, macos_platform, windows_platform, wayland_platform)))] +fn main() { + panic!( + "This example is supported only on wayland, x11, macOS, and Windows, with the `rwh_06` \ + feature enabled." + ); +} diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index fcbf77888..4300342e7 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -51,7 +51,19 @@ changelog entry. - On Android, added scancode conversions for more obscure key codes. - On Wayland, added `HoldGesture` event for multi-finger hold gestures - On Wayland, added ext-background-effect-v1 support. -- On Wayland, Windows and macOS, added native popups (`WindowType::Popup`). +- On Wayland, Windows and macOS, added native popups (`WindowType::Popup`), with + `WindowAttributes::with_positioner` for configuring their placement via a + `WindowPositioner` (grouping the anchor edge/corner, the anchor rect, the + gravity direction, the positioner offset and the constraint adjustment, using + the new `WindowAnchor`, `WindowGravity` and `WindowConstraintAdjustment` + types), and matching `Window::positioner`/`set_positioner` methods for + reading/controlling it at runtime. These work on every `Window`, popup or not; + use `Window::window_type` to tell whether a given window is a genuine + `WindowType::Popup`. On Windows and macOS, which have no native positioner + concept, the placement is computed by winit itself, mirroring Wayland's + `xdg_positioner` behavior, and it also works for a plain `WindowType::Window` + that has a parent; on Wayland the positioner is part of the `xdg_popup` + protocol, so it only applies to `WindowType::Popup`. - On macOS, add `WindowAttributesMacOS::with_fullscreen_auxiliary` and `WindowExtMacOS::set_fullscreen_auxiliary` / `WindowExtMacOS::fullscreen_auxiliary`, allowing a window to be shown on the same Space as a fullscreen window