Popup anchor system refinement (#4646)

Implement possibility to change the anchor properties of a popup on wayland and imitate the behaviour for other platforms which do not support such anchoring system
This commit is contained in:
Martin Marmsoler
2026-09-02 09:48:58 +02:00
committed by GitHub
parent 874a1e31f1
commit 4ddca89067
19 changed files with 1582 additions and 471 deletions

View File

@@ -111,7 +111,12 @@ objc2-foundation = { workspace = true, features = [
"NSThread", "NSThread",
"NSValue", "NSValue",
] } ] }
winit-common = { workspace = true, features = ["core-foundation", "event-handler", "foundation"] } winit-common = { workspace = true, features = [
"core-foundation",
"event-handler",
"foundation",
"positioner",
] }
[dev-dependencies] [dev-dependencies]
winit.workspace = true winit.workspace = true

View File

@@ -6,7 +6,7 @@ use std::ptr::NonNull;
use std::{fmt, ptr}; use std::{fmt, ptr};
use dispatch2::run_on_main; use dispatch2::run_on_main;
use dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
use objc2::MainThreadMarker; use objc2::MainThreadMarker;
use objc2::rc::Retained; use objc2::rc::Retained;
use objc2_app_kit::NSScreen; 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<i32>, PhysicalSize<u32>)> {
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::<i32>(scale_factor);
let size = LogicalSize::new(visible_frame.size.width, visible_frame.size.height)
.to_physical::<u32>(scale_factor);
Some((position, size))
})
}
pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option<Retained<NSScreen>> { pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option<Retained<NSScreen>> {
let uuid = self.uuid(); let uuid = self.uuid();
NSScreen::screens(mtm).into_iter().find(|screen| { NSScreen::screens(mtm).into_iter().find(|screen| {

View File

@@ -15,7 +15,7 @@ use winit_core::icon::Icon;
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle};
use winit_core::window::{ use winit_core::window::{
ImeCapabilities, ImeRequest, ImeRequestError, Theme, UserAttentionType, Window as CoreWindow, ImeCapabilities, ImeRequest, ImeRequestError, Theme, UserAttentionType, Window as CoreWindow,
WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowPositioner, WindowType,
}; };
use super::event_loop::ActiveEventLoop; use super::event_loop::ActiveEventLoop;
@@ -26,7 +26,6 @@ pub(crate) struct Window {
window: MainThreadBound<Retained<NSWindow>>, window: MainThreadBound<Retained<NSWindow>>,
/// The window only keeps a weak reference to this, so we must keep it around here. /// The window only keeps a weak reference to this, so we must keep it around here.
delegate: MainThreadBound<Retained<WindowDelegate>>, delegate: MainThreadBound<Retained<WindowDelegate>>,
window_type: WindowType,
} }
impl Window { impl Window {
@@ -35,15 +34,22 @@ impl Window {
attributes: WindowAttributes, attributes: WindowAttributes,
) -> Result<Self, RequestError> { ) -> Result<Self, RequestError> {
let mtm = window_target.mtm; let mtm = window_target.mtm;
let window_type = attributes.window_type;
let delegate = let delegate =
autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?; autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?;
window_target.app_state.register_window(&delegate, mtm); window_target.app_state.register_window(&delegate, mtm);
Ok(Window { let window = Window {
window: MainThreadBound::new(delegate.window().retain(), mtm), window: MainThreadBound::new(delegate.window().retain(), mtm),
delegate: MainThreadBound::new(delegate, 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<R: Send>( pub(crate) fn maybe_wait_on_main<R: Send>(
@@ -99,7 +105,15 @@ impl rwh_06::HasWindowHandle for Window {
impl CoreWindow for Window { impl CoreWindow for Window {
fn window_type(&self) -> WindowType { 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 { fn id(&self) -> winit_core::window::WindowId {

View File

@@ -44,6 +44,7 @@ use objc2_foundation::{
}; };
use tracing::{debug_span, trace, warn}; use tracing::{debug_span, trace, warn};
use winit_common::core_foundation::MainRunLoop; use winit_common::core_foundation::MainRunLoop;
use winit_common::positioner::place_window;
use winit_core::cursor::Cursor; use winit_core::cursor::Cursor;
use winit_core::data_transfer::DataTransferId; use winit_core::data_transfer::DataTransferId;
use winit_core::error::{NotSupportedError, RequestError}; 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::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider};
use winit_core::window::{ use winit_core::window::{
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme,
UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType, UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowPositioner,
WindowType,
}; };
use super::app_state::AppState; use super::app_state::AppState;
@@ -112,7 +114,16 @@ pub(crate) struct State {
is_simple_fullscreen: Cell<bool>, is_simple_fullscreen: Cell<bool>,
saved_style: Cell<Option<NSWindowStyleMask>>, saved_style: Cell<Option<NSWindowStyleMask>>,
is_borderless_game: Cell<bool>, is_borderless_game: Cell<bool>,
is_popup: Cell<bool>, /// 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<WindowPositioner>,
} }
define_class!( define_class!(
@@ -149,6 +160,9 @@ define_class!(
let _entered = debug_span!("windowDidResize:").entered(); let _entered = debug_span!("windowDidResize:").entered();
// NOTE: WindowEvent::SurfaceResized is reported using NSViewFrameDidChangeNotification. // NOTE: WindowEvent::SurfaceResized is reported using NSViewFrameDidChangeNotification.
self.emit_move_event(); 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:))] #[unsafe(method(windowWillStartLiveResize:))]
@@ -170,6 +184,7 @@ define_class!(
fn window_did_move(&self, _: Option<&AnyObject>) { fn window_did_move(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidMove:").entered(); let _entered = debug_span!("windowDidMove:").entered();
self.emit_move_event(); self.emit_move_event();
self.reposition_child_windows();
} }
#[unsafe(method(windowDidChangeBackingProperties:))] #[unsafe(method(windowDidChangeBackingProperties:))]
@@ -657,7 +672,7 @@ fn new_window(
app_state: &Rc<AppState>, app_state: &Rc<AppState>,
attrs: &WindowAttributes, attrs: &WindowAttributes,
macos_attrs: &WindowAttributesMacOS, macos_attrs: &WindowAttributesMacOS,
is_popup: bool, anchored: bool,
mtm: MainThreadMarker, mtm: MainThreadMarker,
) -> Option<Retained<NSWindow>> { ) -> Option<Retained<NSWindow>> {
autoreleasepool(|_| { autoreleasepool(|_| {
@@ -685,10 +700,12 @@ fn new_window(
None => NSSize::new(800.0, 600.0), None => NSSize::new(800.0, 600.0),
}; };
let position = match attrs.position { let position = match attrs.position {
// A popup's position is parent-relative; it's applied in `WindowDelegate::new` // An anchored window's position is parent-relative; it's applied in
// (after the delegate exists) via the shared translation in // `WindowDelegate::new` (after the delegate exists) via the shared
// `set_outer_position`. // translation in `set_outer_position`. A parentless anchored window can't be
_ if is_popup => NSPoint::new(0.0, 0.0), // 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) => { Some(position) => {
let position = position.to_logical(scale_factor); let position = position.to_logical(scale_factor);
flip_window_screen_coordinates(NSRect::new( flip_window_screen_coordinates(NSRect::new(
@@ -839,8 +856,8 @@ fn new_window(
window.collectionBehavior() | NSWindowCollectionBehavior::FullScreenAuxiliary, window.collectionBehavior() | NSWindowCollectionBehavior::FullScreenAuxiliary,
); );
} }
// Popups are positioned relative to their parent in `WindowDelegate::new`. // Center window if the position is not set or it doesn't have any parent
if attrs.position.is_none() && !is_popup { if attrs.position.is_none() && !(anchored && attrs.parent_window().is_some()) {
window.center(); window.center();
} }
@@ -904,7 +921,9 @@ impl WindowDelegate {
.and_then(|attrs| attrs.cast::<WindowAttributesMacOS>().ok()) .and_then(|attrs| attrs.cast::<WindowAttributesMacOS>().ok())
.unwrap_or_default(); .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 { if is_popup {
// A popup is an undecorated, non-activating panel with no titlebar buttons. Model it // 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` // 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`"))?; .ok_or_else(|| os_error!("couldn't create `NSWindow`"))?;
match attrs.parent_window() { match attrs.parent_window() {
@@ -979,7 +998,9 @@ impl WindowDelegate {
is_simple_fullscreen: Cell::new(false), is_simple_fullscreen: Cell::new(false),
saved_style: Cell::new(None), saved_style: Cell::new(None),
is_borderless_game: Cell::new(macos_attrs.borderless_game), 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<WindowDelegate> = unsafe { msg_send![super(delegate), init] }; let delegate: Retained<WindowDelegate> = unsafe { msg_send![super(delegate), init] };
@@ -1026,10 +1047,10 @@ impl WindowDelegate {
delegate.set_window_level(attrs.window_level); delegate.set_window_level(attrs.window_level);
// The popup position is relative to the parent window, and the parent is only // An anchored window's position is relative to the parent window, and the parent is
// attached above, so apply the (translated) position now. Default to the parent's // only attached above, so apply the (translated) position now. Default to the parent's
// content top-left when no position was given. // 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()); let position = attrs.position.unwrap_or_else(|| LogicalPosition::new(0.0, 0.0).into());
delegate.set_outer_position(position); delegate.set_outer_position(position);
} }
@@ -1189,8 +1210,8 @@ impl WindowDelegate {
pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, RequestError> { pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, RequestError> {
let position = flip_window_screen_coordinates(self.window().frame()); let position = flip_window_screen_coordinates(self.window().frame());
let position = let position = self
self.translate_popup_position_to_parent(LogicalPosition::new(position.x, position.y)); .translate_anchored_position_to_parent(LogicalPosition::new(position.x, position.y));
Ok(position.to_physical(self.scale_factor())) Ok(position.to_physical(self.scale_factor()))
} }
@@ -1215,7 +1236,7 @@ impl WindowDelegate {
pub fn set_outer_position(&self, position: Position) { pub fn set_outer_position(&self, position: Position) {
let position = position.to_logical(self.scale_factor()); 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( let point = flip_window_screen_coordinates(NSRect::new(
NSPoint::new(position.x, position.y), NSPoint::new(position.x, position.y),
self.window().frame().size, self.window().frame().size,
@@ -1223,11 +1244,11 @@ impl WindowDelegate {
self.window().setFrameOrigin(point); 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 /// content area (matching the Win32 and Wayland backends). macOS positions windows
/// in global screen coordinates, so add the parent content area's origin. /// in global screen coordinates, so add the parent content area's origin.
fn translate_popup_position(&self, position: LogicalPosition<f64>) -> LogicalPosition<f64> { fn translate_anchored_position(&self, position: LogicalPosition<f64>) -> LogicalPosition<f64> {
if !self.ivars().is_popup.get() { if !self.ivars().anchored {
return position; return position;
} }
let Some(parent) = self.window().parentWindow() else { 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) 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 /// 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 /// Win32 and Wayland backends), so subtract the parent content area's origin from
/// the global screen coordinates. Non-popup windows are returned unchanged. /// the global screen coordinates. Non-anchored windows are returned unchanged.
fn translate_popup_position_to_parent( fn translate_anchored_position_to_parent(
&self, &self,
position: LogicalPosition<f64>, position: LogicalPosition<f64>,
) -> LogicalPosition<f64> { ) -> LogicalPosition<f64> {
if !self.ivars().is_popup.get() { if !self.ivars().anchored {
return position; return position;
} }
let Some(parent) = self.window().parentWindow() else { 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) 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<LogicalPosition<f64>> {
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::<f64>(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::<f64>(scale_factor);
let current_outer_size = self.outer_size().to_logical::<f64>(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::<WindowDelegate>() else { continue };
if child_delegate.ivars().anchored {
child_delegate.reposition();
}
}
}
#[inline] #[inline]
pub fn surface_size(&self) -> PhysicalSize<u32> { pub fn surface_size(&self) -> PhysicalSize<u32> {
self.view().surface_size() self.view().surface_size()

View File

@@ -12,6 +12,9 @@ version.workspace = true
# Event Handler # Event Handler
event-handler = [] event-handler = []
# Positioner
positioner = ["dep:dpi"]
# XKB # XKB
wayland = ["dep:memmap2"] wayland = ["dep:memmap2"]
x11 = ["xkbcommon-dl?/x11", "dep:x11-dl"] 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"] foundation = ["dep:block2", "dep:objc2", "dep:objc2-foundation"]
[dependencies] [dependencies]
dpi = { workspace = true, optional = true }
smol_str = { workspace = true, optional = true } smol_str = { workspace = true, optional = true }
tracing.workspace = true tracing.workspace = true
winit-core.workspace = true winit-core.workspace = true

View File

@@ -8,5 +8,7 @@ pub mod core_foundation;
pub mod event_handler; pub mod event_handler;
#[cfg(feature = "foundation")] #[cfg(feature = "foundation")]
pub mod foundation; pub mod foundation;
#[cfg(feature = "positioner")]
pub mod positioner;
#[cfg(feature = "xkb")] #[cfg(feature = "xkb")]
pub mod xkb; pub mod xkb;

View File

@@ -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<f64>,
(clip_position, clip_size): (LogicalPosition<f64>, LogicalSize<f64>),
) -> (LogicalPosition<f64>, LogicalSize<f64>) {
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::<f64>(scale_factor);
let anchor_size = anchor_size.to_logical::<f64>(scale_factor);
let offset = positioner.offset.to_logical::<f64>(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<f64>, LogicalSize<f64>),
anchor: (LogicalPosition<f64>, LogicalSize<f64>),
popup: (LogicalPosition<f64>, LogicalSize<f64>),
) {
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<f64>, size: LogicalSize<f64>, 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::<String>());
}
}
/// 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);
}
}

View File

@@ -1,4 +1,6 @@
//! The [`Window`] trait and associated types. //! The [`Window`] trait and associated types.
mod positioner;
use std::any::Any; use std::any::Any;
use std::fmt; use std::fmt;
@@ -7,6 +9,7 @@ use cursor_icon::CursorIcon;
use dpi::{ use dpi::{
LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size,
}; };
pub use positioner::{WindowAnchor, WindowConstraintAdjustment, WindowGravity};
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -57,6 +60,9 @@ pub enum WindowType {
/// tooltip. Requires a parent set via [`WindowAttributes::with_parent_window`], and its /// tooltip. Requires a parent set via [`WindowAttributes::with_parent_window`], and its
/// position is interpreted relative to that parent. /// 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 /// ## Platform-specific
/// ///
/// - **macOS:** A borderless, non-activating child window. The system does *not* draw rounded /// - **macOS:** A borderless, non-activating child window. The system does *not* draw rounded
@@ -66,6 +72,71 @@ pub enum WindowType {
Popup, 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. /// Attributes used when creating a window.
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
@@ -105,6 +176,8 @@ pub struct WindowAttributes {
pub fullscreen: Option<Fullscreen>, pub fullscreen: Option<Fullscreen>,
pub platform: Option<Box<dyn PlatformWindowAttributes>>, pub platform: Option<Box<dyn PlatformWindowAttributes>>,
pub window_type: WindowType, pub window_type: WindowType,
/// See [`WindowAttributes::with_positioner`].
pub positioner: Option<WindowPositioner>,
} }
impl WindowAttributes { impl WindowAttributes {
@@ -438,6 +511,27 @@ impl WindowAttributes {
pub fn window_type(&self) -> WindowType { pub fn window_type(&self) -> WindowType {
self.window_type 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 { impl Clone for WindowAttributes {
@@ -466,6 +560,7 @@ impl Clone for WindowAttributes {
fullscreen: self.fullscreen.clone(), fullscreen: self.fullscreen.clone(),
platform: self.platform.as_ref().map(|platform| platform.box_clone()), platform: self.platform.as_ref().map(|platform| platform.box_clone()),
window_type: self.window_type, window_type: self.window_type,
positioner: self.positioner,
} }
} }
} }
@@ -497,6 +592,7 @@ impl Default for WindowAttributes {
cursor: Cursor::default(), cursor: Cursor::default(),
blur: Default::default(), blur: Default::default(),
window_type: 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 /// Returns the window type of this window
fn window_type(&self) -> WindowType; 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. /// Returns an identifier unique to the window.
fn id(&self) -> WindowId; fn id(&self) -> WindowId;

View File

@@ -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;
}
}

View File

@@ -22,7 +22,7 @@ use std::ffi::c_void;
use std::hash::BuildHasher; use std::hash::BuildHasher;
use std::ptr::NonNull; use std::ptr::NonNull;
use dpi::{LogicalSize, PhysicalSize, Position, Size}; use dpi::{LogicalSize, PhysicalSize};
use sctk::reexports::client::Proxy; use sctk::reexports::client::Proxy;
use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::backend::ObjectId;
use sctk::reexports::client::protocol::wl_surface::WlSurface; 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<Position>, impl Into<Size>)>;
/// 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<Position>, size: impl Into<Size>);
/// 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<Position>);
}
impl PopupExtWayland for dyn CoreWindow + '_ {
fn set_anchor(&self, anchor: PopupAnchor) {
if let Some(popup) = self.cast_ref::<Popup>() {
popup.set_anchor(anchor);
}
}
fn anchor_rect(&self) -> Option<(impl Into<Position>, impl Into<Size>)> {
if let Some(popup) = self.cast_ref::<Popup>() { popup.anchor_rect() } else { None }
}
fn set_anchor_rect(&self, position: impl Into<Position>, size: impl Into<Size>) {
if let Some(popup) = self.cast_ref::<Popup>() {
popup.set_anchor_rect(position, size);
}
}
fn set_constraint_adjustment(&self, constraint_adjustment: PopupConstraintAdjustment) {
if let Some(popup) = self.cast_ref::<Popup>() {
popup.set_constraint_adjustment(constraint_adjustment);
}
}
fn set_gravity(&self, gravity: PopupGravity) {
if let Some(popup) = self.cast_ref::<Popup>() {
popup.set_gravity(gravity);
}
}
fn set_positioner_offset(&self, position: impl Into<Position>) {
if let Some(popup) = self.cast_ref::<Popup>() {
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)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ApplicationName { pub(crate) struct ApplicationName {
pub(crate) general: String, pub(crate) general: String,
@@ -224,13 +110,6 @@ pub struct WindowAttributesWayland {
pub(crate) name: Option<ApplicationName>, pub(crate) name: Option<ApplicationName>,
pub(crate) activation_token: Option<ActivationToken>, pub(crate) activation_token: Option<ActivationToken>,
pub(crate) prefer_csd: bool, pub(crate) prefer_csd: bool,
pub(crate) anchor: Option<PopupAnchor>,
pub(crate) anchor_rect: Option<(Position, Size)>,
/// Only for Popup: The offset of the popup to the
/// anchor rect
pub(crate) positioner_offset: Option<Position>,
pub(crate) gravity: Option<PopupGravity>,
pub(crate) constraint_adjustment: Option<PopupConstraintAdjustment>,
} }
impl WindowAttributesWayland { impl WindowAttributesWayland {
@@ -269,63 +148,6 @@ impl WindowAttributesWayland {
self.prefer_csd = prefer_csd; self.prefer_csd = prefer_csd;
self 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<Position>,
size: impl Into<Size>,
) -> 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<Position>) -> 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 { impl PlatformWindowAttributes for WindowAttributesWayland {
@@ -381,64 +203,3 @@ fn image_to_buffer(
Ok(buffer) Ok(buffer)
} }
impl From<PopupGravity> 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<PopupAnchor> 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<PopupConstraintAdjustment>
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())
}
}

View File

@@ -18,16 +18,16 @@ use winit_core::event::{Ime, WindowEvent};
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle};
use winit_core::window::{ use winit_core::window::{
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme,
UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, UserAttentionType, Window as CoreWindow, WindowAnchor, WindowAttributes, WindowButtons,
WindowLevel, WindowConstraintAdjustment, WindowGravity, WindowId, WindowLevel, WindowPositioner,
}; };
use super::ActiveEventLoop; use super::ActiveEventLoop;
use super::output::MonitorHandle; use super::output::MonitorHandle;
use crate::WindowAttributesWayland;
use crate::window::Handles; use crate::window::Handles;
use crate::window::handles::WindowRequests; use crate::window::handles::WindowRequests;
use crate::window::state::{WindowState, WindowType}; use crate::window::state::{WindowState, WindowType};
use crate::{PopupExtWayland, WindowAttributesWayland};
#[derive(Debug)] #[derive(Debug)]
pub struct Popup { pub struct Popup {
@@ -66,27 +66,21 @@ impl Popup {
.xdg_activation .xdg_activation
.as_ref() .as_ref()
.map(|activation_state| activation_state.global().clone()); .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"))?; .map_err(|_| error("Failed to create positioner"))?;
let parent_window_id = let parent_window_id =
WindowId::from_raw(parent_window_handle.surface.as_ptr() as usize); WindowId::from_raw(parent_window_handle.surface.as_ptr() as usize);
let (popup, popup_state) = if let Some(parent_window_state) = let (popup, popup_state) = if let Some(parent_window_state) =
state.windows.borrow().get(&parent_window_id) state.windows.borrow().get(&parent_window_id)
{ {
let wayland_attributes = attributes let WindowPositioner {
.platform
.as_ref()
.and_then(|p| p.cast_ref::<WindowAttributesWayland>())
.cloned()
.unwrap_or_default();
let WindowAttributesWayland {
gravity,
anchor, anchor,
anchor_rect, anchor_rect,
offset: positioner_offset,
gravity,
constraint_adjustment, constraint_adjustment,
positioner_offset,
.. ..
} = wayland_attributes; } = attributes.positioner.unwrap_or_default();
let grab_keyboard = attributes.active; let grab_keyboard = attributes.active;
let mut parent_window_state = parent_window_state.lock().unwrap(); let mut parent_window_state = parent_window_state.lock().unwrap();
@@ -107,48 +101,51 @@ impl Popup {
// This is important for client side decorations // This is important for client side decorations
let geometry_origin = parent_window_state.content_surface_origin(); let geometry_origin = parent_window_state.content_surface_origin();
let anchor_position = LogicalPosition::new(-geometry_origin.x, -geometry_origin.y); let anchor_position = LogicalPosition::new(-geometry_origin.x, -geometry_origin.y);
positioner.set_anchor(anchor.unwrap_or(crate::PopupAnchor::TopLeft).into()); if xdg_positioner.version() >= 3 {
positioner.set_gravity(gravity.unwrap_or(crate::PopupGravity::BottomRight).into()); xdg_positioner.set_reactive();
constraint_adjustment }
.inspect(|c| positioner.set_constraint_adjustment((*c).into())); xdg_positioner.set_anchor(from_anchor(anchor));
let (anchor_rect_position, anchor_rect_size) = match anchor_rect { xdg_positioner.set_gravity(from_gravity(gravity));
Some((position, size)) => ( xdg_positioner
position.to_logical::<i32>(scale_factor), .set_constraint_adjustment(from_constraint_adjustment(constraint_adjustment));
size.to_logical::<i32>(scale_factor),
), let (anchor_rect_position, anchor_rect_size) = if attributes.positioner.is_some() {
None => { let size = anchor_rect.1.to_logical::<i32>(scale_factor);
// anchor rect was not specified use attributes.position (
let pos: LogicalPosition<i32> = attributes anchor_rect.0.to_logical::<i32>(scale_factor),
.position LogicalSize::new(size.width.max(1), size.height.max(1)),
.map(|position| position.to_logical(scale_factor)) )
.unwrap_or_default(); } else {
(pos, LogicalSize::new(1, 1)) // anchor rect was not specified use attributes.position
}, let pos: LogicalPosition<i32> = attributes
.position
.map(|position| position.to_logical(scale_factor))
.unwrap_or_default();
(pos, LogicalSize::new(1, 1))
}; };
let anchor_rect = ( let anchor_rect = (
LogicalPosition::new( LogicalPosition::new(
anchor_rect_position.x + anchor_position.x, anchor_rect_position.x + anchor_position.x,
anchor_rect_position.y + anchor_position.y, 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.x,
anchor_rect.0.y, anchor_rect.0.y,
anchor_rect.1.width, anchor_rect.1.width,
anchor_rect.1.height, anchor_rect.1.height,
); );
positioner_offset.inspect(|o| { let offset: LogicalPosition<i32> = positioner_offset.to_logical(scale_factor);
let o = o.to_logical(scale_factor); xdg_positioner.set_offset(offset.x, offset.y);
positioner.set_offset(o.x, o.y); xdg_positioner.set_size(size.width, size.height);
});
positioner.set_size(size.width, size.height);
let parent_surface = parent_window_state.window.xdg_surface(); let parent_surface = parent_window_state.window.xdg_surface();
let surface = state.compositor_state.create_surface(&queue_handle); let surface = state.compositor_state.create_surface(&queue_handle);
let popup = SctkPopup::from_surface( let popup = SctkPopup::from_surface(
Some(parent_surface), Some(parent_surface),
&positioner, &xdg_positioner,
&queue_handle, &queue_handle,
surface.clone(), surface.clone(),
&state.xdg_shell, &state.xdg_shell,
@@ -163,10 +160,16 @@ impl Popup {
size.into(), size.into(),
WindowType::Popup { WindowType::Popup {
popup: popup.clone(), popup: popup.clone(),
positioner, xdg_positioner,
last_configure: None, last_configure: None,
anchor_rect,
parent_origin: geometry_origin, parent_origin: geometry_origin,
positioner: WindowPositioner::new(
anchor,
(anchor_rect_position.into(), anchor_rect_size.into()),
positioner_offset,
gravity,
constraint_adjustment,
),
}, },
attributes.preferred_theme, attributes.preferred_theme,
false, false,
@@ -288,6 +291,53 @@ impl CoreWindow for Popup {
winit_core::window::WindowType::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<i32> = size.to_logical(scale_factor);
let position: LogicalPosition<i32> = 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<i32> = 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 { fn id(&self) -> WindowId {
self.window_id self.window_id
} }
@@ -332,19 +382,22 @@ impl CoreWindow for Popup {
let Some(s) = self.popup_state.upgrade() else { return }; let Some(s) = self.popup_state.upgrade() else { return };
let mut state = s.lock().unwrap(); let mut state = s.lock().unwrap();
let scale_factor = state.scale_factor(); 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 &mut state.window
{ {
let position = position.to_logical(scale_factor); let size = positioner.anchor_rect.1;
anchor_rect.0 = position; positioner.anchor_rect = (position, size);
positioner.set_anchor_rect(
anchor_rect.0.x - parent_origin.x, let logical_position: LogicalPosition<i32> = position.to_logical(scale_factor);
anchor_rect.0.y - parent_origin.y, let logical_size: LogicalSize<i32> = size.to_logical(scale_factor);
anchor_rect.1.width, xdg_positioner.set_anchor_rect(
anchor_rect.1.height, logical_position.x - parent_origin.x,
logical_position.y - parent_origin.y,
logical_size.width,
logical_size.height,
); );
if popup.xdg_popup().version() >= 3 { 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 from_gravity(
fn set_anchor(&self, anchor: crate::PopupAnchor) { gravity: WindowGravity,
let Some(state) = self.popup_state.upgrade() else { ) -> wayland_protocols::xdg::shell::client::xdg_positioner::Gravity {
return; use wayland_protocols::xdg::shell::client::xdg_positioner::Gravity;
}; match gravity {
WindowGravity::Center => Gravity::None,
if let WindowType::Popup { popup, positioner, .. } = &state.lock().unwrap().window { WindowGravity::Top => Gravity::Top,
positioner.set_anchor(anchor.into()); WindowGravity::Bottom => Gravity::Bottom,
popup.reposition(positioner, 0); WindowGravity::Left => Gravity::Left,
} WindowGravity::Right => Gravity::Right,
} WindowGravity::TopLeft => Gravity::TopLeft,
WindowGravity::BottomLeft => Gravity::BottomLeft,
fn anchor_rect(&self) -> Option<(impl Into<Position>, impl Into<Size>)> { WindowGravity::TopRight => Gravity::TopRight,
let state = self.popup_state.upgrade()?; WindowGravity::BottomRight => Gravity::BottomRight,
if let WindowType::Popup { anchor_rect, .. } = &state.lock().unwrap().window { _ => Gravity::None,
Some(*anchor_rect)
} else {
None
}
}
fn set_anchor_rect(&self, position: impl Into<Position>, size: impl Into<Size>) {
let Some(state) = self.popup_state.upgrade() else {
return;
};
let state = state.lock().unwrap();
let scale_factor = state.scale_factor();
let size: LogicalSize<i32> = size.into().to_logical(scale_factor);
let position: LogicalPosition<i32> = 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<Position>) {
let Some(state) = self.popup_state.upgrade() else {
return;
};
let scale_factor = state.lock().unwrap().scale_factor();
let position: LogicalPosition<i32> = 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_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())
}

View File

@@ -34,6 +34,7 @@ use winit_core::cursor::{CursorIcon, CustomCursor as CoreCustomCursor};
use winit_core::error::{NotSupportedError, RequestError}; use winit_core::error::{NotSupportedError, RequestError};
use winit_core::window::{ use winit_core::window::{
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, WindowId, CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, WindowId,
WindowPositioner,
}; };
use crate::event_loop::OwnedDisplayHandle; use crate::event_loop::OwnedDisplayHandle;
@@ -64,10 +65,11 @@ pub enum WindowType {
}, },
Popup { Popup {
popup: Popup, popup: Popup,
positioner: XdgPositioner, xdg_positioner: XdgPositioner,
last_configure: Option<PopupConfigure>, last_configure: Option<PopupConfigure>,
parent_origin: LogicalPosition<i32>, parent_origin: LogicalPosition<i32>,
anchor_rect: (LogicalPosition<i32>, LogicalSize<i32>),
positioner: WindowPositioner,
}, },
} }
@@ -844,11 +846,11 @@ impl WindowState {
self.resize(surface_size.to_logical(self.scale_factor())) 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()); 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 { if popup.xdg_popup().version() >= 3 {
popup.reposition(positioner, 0); popup.reposition(xdg_positioner, 0);
} }
}, },
} }

View File

@@ -20,6 +20,7 @@ serde = { workspace = true, optional = true }
smol_str.workspace = true smol_str.workspace = true
tracing.workspace = true tracing.workspace = true
url.workspace = true url.workspace = true
winit-common = { workspace = true, features = ["positioner"] }
winit-core.workspace = true winit-core.workspace = true
# Platform-specific # Platform-specific

View File

@@ -40,27 +40,28 @@ use windows_sys::Win32::UI::Input::{
MOUSE_MOVE_RELATIVE, RAWINPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, MOUSE_MOVE_RELATIVE, RAWINPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE,
}; };
use windows_sys::Win32::UI::WindowsAndMessaging::{ use windows_sys::Win32::UI::WindowsAndMessaging::{
CREATESTRUCTW, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GWL_STYLE, CREATESTRUCTW, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW,
GWL_USERDATA, GetClientRect, GetCursorPos, GetMenu, HTCAPTION, HTCLIENT, LoadCursorW, EnumThreadWindows, GW_OWNER, GWL_STYLE, GWL_USERDATA, GetClientRect, GetCursorPos, GetMenu,
MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, MsgWaitForMultipleObjectsEx, GetWindow, HTCAPTION, HTCLIENT, LoadCursorW, MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE,
NCCALCSIZE_PARAMS, PEN_FLAG_BARREL, PEN_FLAG_ERASER, PEN_MASK_PRESSURE, PEN_MASK_ROTATION, MsgWaitForMultipleObjectsEx, NCCALCSIZE_PARAMS, PEN_FLAG_BARREL, PEN_FLAG_ERASER,
PEN_MASK_TILT_X, PEN_MASK_TILT_Y, PM_REMOVE, PT_PEN, PT_TOUCH, PeekMessageW, PostMessageW, PEN_MASK_PRESSURE, PEN_MASK_ROTATION, PEN_MASK_TILT_X, PEN_MASK_TILT_Y, PM_REMOVE, PT_PEN,
QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, RegisterClassExW, RegisterWindowMessageA, PT_TOUCH, PeekMessageW, PostMessageW, QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL,
SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, RegisterClassExW, RegisterWindowMessageA, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED,
SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SetCursor, SetWindowPos, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE,
SystemParametersInfoW, TranslateMessage, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE, SWP_NOZORDER, SetCursor, SetWindowPos, SystemParametersInfoW, TranslateMessage, WHEEL_DELTA,
WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE,
WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION,
WM_INPUT, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP,
WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR,
WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE,
WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE,
WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING,
WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WMSZ_BOTTOM, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING,
WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WM_XBUTTONDOWN, WM_XBUTTONUP, WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT,
WMSZ_TOPRIGHT, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT, WNDCLASSEXW, WS_EX_LAYERED,
WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, 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::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor, CustomCursorSource}; use winit_core::cursor::{CustomCursor, CustomCursorSource};
use winit_core::data_transfer::{ use winit_core::data_transfer::{
@@ -94,7 +95,7 @@ use crate::keyboard::KeyEventBuilder;
use crate::keyboard_layout::LAYOUT_CACHE; use crate::keyboard_layout::LAYOUT_CACHE;
use crate::monitor::{self, MonitorHandle}; use crate::monitor::{self, MonitorHandle};
use crate::util::{WIN10_BUILD_VERSION, wrap_device_id}; 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::window_state::{CursorFlags, ImeState, WindowFlags, WindowState};
use crate::{raw_input, util}; 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)); 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 /// 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. /// 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. // 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 = let physical_position =
unsafe { PhysicalPosition::new((*windowpos).x, (*windowpos).y) }; unsafe { PhysicalPosition::new((*windowpos).x, (*windowpos).y) };
userdata.send_window_event(window, Moved(physical_position)); userdata.send_window_event(window, Moved(physical_position));
unsafe { reposition_owned_windows(window) };
} }
// This is necessary for us to still get sent WM_SIZE. // This is necessary for us to still get sent WM_SIZE.

View File

@@ -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<i32>, PhysicalSize<u32>)> {
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<dyn Iterator<Item = VideoModeHandle>> { pub(crate) fn video_mode_handles(&self) -> Box<dyn Iterator<Item = VideoModeHandle>> {
// EnumDisplaySettingsExW can return duplicate values (or some of the // EnumDisplaySettingsExW can return duplicate values (or some of the
// fields are probably changing, but we aren't looking at those fields // fields are probably changing, but we aren't looking at those fields

View File

@@ -7,7 +7,9 @@ use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
use std::{io, panic, ptr}; 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 tracing::warn;
use windows_sys::Win32::Foundation::{ use windows_sys::Win32::Foundation::{
HWND, LPARAM, OLE_E_WRONGCOMPOBJ, POINT, POINTS, RECT, RPC_E_CHANGED_MODE, S_OK, WPARAM, 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, TPM_RETURNCMD, TrackPopupMenu, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, WM_NCLBUTTONDOWN, WM_SETICON,
WM_SYSCOMMAND, WNDCLASSEXW, WM_SYSCOMMAND, WNDCLASSEXW,
}; };
use winit_common::positioner::place_window;
use winit_core::cursor::Cursor; use winit_core::cursor::Cursor;
use winit_core::error::{NotSupportedError, RequestError}; use winit_core::error::{NotSupportedError, RequestError};
use winit_core::icon::{Icon, RgbaIcon}; use winit_core::icon::{Icon, RgbaIcon};
@@ -54,7 +57,7 @@ use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, Monito
use winit_core::window::{ use winit_core::window::{
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme,
UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId,
WindowLevel, WindowType, WindowLevel, WindowPositioner, WindowType,
}; };
use crate::dark_mode::try_theme; use crate::dark_mode::try_theme;
@@ -100,9 +103,6 @@ pub struct Window {
// The events loop proxy. // The events loop proxy.
thread_executor: event_loop::EventLoopThreadExecutor, thread_executor: event_loop::EventLoopThreadExecutor,
/// The type of window of this
window_type: WindowType,
} }
impl Window { impl Window {
@@ -114,37 +114,26 @@ impl Window {
// First person to remove the need for cloning here gets a cookie! // First person to remove the need for cloning here gets a cookie!
// //
// done. you owe me -- ossi // 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> { fn window_state_lock(&self) -> MutexGuard<'_, WindowState> {
self.window_state.lock().unwrap() 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 // relative to the screen. Therefore we have to translate it from the parent
// coordinate system to the display coordinate system // coordinate system to the display coordinate system
fn translate_outer_position(&self, position: Position) -> PhysicalPosition<i32> { fn translate_outer_position(&self, position: Position) -> PhysicalPosition<i32> {
let position = position.to_physical::<i32>(self.scale_factor()); translate_outer_position(self.hwnd(), &self.window_state, position, 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)
} }
// 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 // reported relative to the parent window instead of the screen. Therefore we
// translate it from the display coordinate system back to the parent // 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( fn translate_outer_position_to_parent(
&self, &self,
position: PhysicalPosition<i32>, position: PhysicalPosition<i32>,
@@ -152,7 +141,9 @@ impl Window {
let mut point = POINT { x: position.x, y: position.y }; let mut point = POINT { x: position.x, y: position.y };
let window_flags = self.window_state_lock().window_flags; 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()) }; let parent = unsafe { GetParent(self.hwnd()) };
if !parent.is_null() { if !parent.is_null() {
unsafe { unsafe {
@@ -164,6 +155,28 @@ impl Window {
PhysicalPosition::new(point.x, point.y) 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. /// Returns the `hwnd` of this window.
pub fn hwnd(&self) -> HWND { pub fn hwnd(&self) -> HWND {
self.window.hwnd() self.window.hwnd()
@@ -463,7 +476,16 @@ impl rwh_06::HasWindowHandle for Window {
impl CoreWindow for Window { impl CoreWindow for Window {
fn window_type(&self) -> WindowType { 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) { fn set_title(&self, text: &str) {
@@ -566,12 +588,7 @@ impl CoreWindow for Window {
} }
fn outer_size(&self) -> PhysicalSize<u32> { fn outer_size(&self) -> PhysicalSize<u32> {
util::WindowArea::Outer outer_size_of(self.hwnd())
.get_rect(self.hwnd())
.map(|rect| {
PhysicalSize::new((rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32)
})
.unwrap()
} }
fn request_surface_size(&self, size: Size) -> Option<PhysicalSize<u32>> { fn request_surface_size(&self, size: Size) -> Option<PhysicalSize<u32>> {
@@ -1215,6 +1232,159 @@ impl CoreWindow for Window {
} }
} }
fn outer_size_of(hwnd: HWND) -> PhysicalSize<u32> {
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<u32> {
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<WindowState>,
position: Position,
scale_factor: f64,
) -> PhysicalPosition<i32> {
let position = position.to_physical::<i32>(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<f64>, LogicalSize<f64>, LogicalSize<f64>)> {
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::<f64>(scale_factor);
let clip_size = work_area_size.to_logical::<f64>(scale_factor);
let current_outer_size = outer_size_of(hwnd).to_logical::<f64>(scale_factor);
let current_size = surface_size_of(hwnd).to_logical::<f64>(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<WindowState>) {
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::<u32>(scale_factor);
let window_flags = window_state.lock().unwrap().window_flags;
window_flags.set_size(hwnd, physical_size);
}
}
pub(super) struct InitData<'a> { pub(super) struct InitData<'a> {
// inputs // inputs
pub runner: &'a Rc<EventLoopRunner>, pub runner: &'a Rc<EventLoopRunner>,
@@ -1266,7 +1436,6 @@ impl InitData<'_> {
window: SyncWindowHandle(window), window: SyncWindowHandle(window),
window_state, window_state,
thread_executor: self.runner.create_thread_executor(), 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) }; unsafe { register_window_class(&class_name) };
let is_popup = matches!(attributes.window_type, WindowType::Popup); 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(); let mut window_flags = WindowFlags::empty();
window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations); window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations);
window_flags.set(WindowFlags::POPUP, is_popup); 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::MARKER_UNDECORATED_SHADOW, win_attributes.decoration_shadow);
window_flags window_flags
.set(WindowFlags::ALWAYS_ON_TOP, attributes.window_level == WindowLevel::AlwaysOnTop); .set(WindowFlags::ALWAYS_ON_TOP, attributes.window_level == WindowLevel::AlwaysOnTop);

View File

@@ -20,7 +20,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
use winit_core::icon::Icon; use winit_core::icon::Icon;
use winit_core::keyboard::ModifiersState; use winit_core::keyboard::ModifiersState;
use winit_core::monitor::Fullscreen; 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}; use crate::{SelectedCursor, WindowAttributesWindows, event_loop, util};
@@ -51,6 +51,21 @@ pub(crate) struct WindowState {
pub window_flags: WindowFlags, 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_state: ImeState,
pub ime_capabilities: Option<ImeCapabilities>, pub ime_capabilities: Option<ImeCapabilities>,
@@ -140,6 +155,12 @@ bitflags! {
const CLIP_CHILDREN = 1 << 22; 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(); const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits();
} }
} }
@@ -186,6 +207,11 @@ impl WindowState {
preferred_theme, preferred_theme,
window_flags: WindowFlags::empty(), 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_state: ImeState::Disabled,
ime_capabilities: None, ime_capabilities: None,
@@ -287,23 +313,25 @@ impl WindowFlags {
} }
} else { } else {
style |= WS_CAPTION | WS_SYSMENU | WS_BORDER; 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) { if self.contains(WindowFlags::VISIBLE) {
style |= WS_VISIBLE; style |= WS_VISIBLE;
} }
if self.contains(WindowFlags::ON_TASKBAR) {
style_ex |= WS_EX_APPWINDOW;
}
if self.contains(WindowFlags::ALWAYS_ON_TOP) { if self.contains(WindowFlags::ALWAYS_ON_TOP) {
style_ex |= WS_EX_TOPMOST; style_ex |= WS_EX_TOPMOST;
} }

214
winit/examples/popup.rs Normal file
View File

@@ -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<OwnedDisplayHandle, Box<dyn Window>>,
color: u32,
}
impl WindowData {
fn new(context: &Context<OwnedDisplayHandle>, window: Box<dyn Window>, color: u32) -> Self {
let surface = Surface::new(context, window).unwrap();
Self { surface, color }
}
}
#[derive(Debug)]
struct Application {
parent_window_id: Option<WindowId>,
main_window: Option<WindowId>,
windows: HashMap<WindowId, WindowData>,
popups: Vec<WindowId>,
position: Option<PhysicalPosition<f64>>,
context: Context<OwnedDisplayHandle>,
}
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<PhysicalPosition<f64>>,
) -> Box<dyn Window> {
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."
);
}

View File

@@ -51,7 +51,19 @@ changelog entry.
- On Android, added scancode conversions for more obscure key codes. - On Android, added scancode conversions for more obscure key codes.
- On Wayland, added `HoldGesture` event for multi-finger hold gestures - On Wayland, added `HoldGesture` event for multi-finger hold gestures
- On Wayland, added ext-background-effect-v1 support. - 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 - On macOS, add `WindowAttributesMacOS::with_fullscreen_auxiliary` and
`WindowExtMacOS::set_fullscreen_auxiliary` / `WindowExtMacOS::fullscreen_auxiliary`, allowing a `WindowExtMacOS::set_fullscreen_auxiliary` / `WindowExtMacOS::fullscreen_auxiliary`, allowing a
window to be shown on the same Space as a fullscreen window window to be shown on the same Space as a fullscreen window