mirror of
https://github.com/rust-windowing/winit.git
synced 2026-09-02 23:00:05 -04:00
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:
@@ -111,7 +111,12 @@ objc2-foundation = { workspace = true, features = [
|
||||
"NSThread",
|
||||
"NSValue",
|
||||
] }
|
||||
winit-common = { workspace = true, features = ["core-foundation", "event-handler", "foundation"] }
|
||||
winit-common = { workspace = true, features = [
|
||||
"core-foundation",
|
||||
"event-handler",
|
||||
"foundation",
|
||||
"positioner",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
winit.workspace = true
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::ptr::NonNull;
|
||||
use std::{fmt, ptr};
|
||||
|
||||
use dispatch2::run_on_main;
|
||||
use dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
|
||||
use dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
|
||||
use objc2::MainThreadMarker;
|
||||
use objc2::rc::Retained;
|
||||
use objc2_app_kit::NSScreen;
|
||||
@@ -171,6 +171,22 @@ impl MonitorHandle {
|
||||
})
|
||||
}
|
||||
|
||||
/// The monitor's work area, i.e. its bounds minus space reserved by the system for the menu
|
||||
/// bar and the Dock (see `NSScreen.visibleFrame`). `None` if the underlying
|
||||
/// `NSScreen` can no longer be found.
|
||||
pub(crate) fn work_area(&self) -> Option<(PhysicalPosition<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>> {
|
||||
let uuid = self.uuid();
|
||||
NSScreen::screens(mtm).into_iter().find(|screen| {
|
||||
|
||||
@@ -15,7 +15,7 @@ use winit_core::icon::Icon;
|
||||
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle};
|
||||
use winit_core::window::{
|
||||
ImeCapabilities, ImeRequest, ImeRequestError, Theme, UserAttentionType, Window as CoreWindow,
|
||||
WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType,
|
||||
WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowPositioner, WindowType,
|
||||
};
|
||||
|
||||
use super::event_loop::ActiveEventLoop;
|
||||
@@ -26,7 +26,6 @@ pub(crate) struct Window {
|
||||
window: MainThreadBound<Retained<NSWindow>>,
|
||||
/// The window only keeps a weak reference to this, so we must keep it around here.
|
||||
delegate: MainThreadBound<Retained<WindowDelegate>>,
|
||||
window_type: WindowType,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
@@ -35,15 +34,22 @@ impl Window {
|
||||
attributes: WindowAttributes,
|
||||
) -> Result<Self, RequestError> {
|
||||
let mtm = window_target.mtm;
|
||||
let window_type = attributes.window_type;
|
||||
let delegate =
|
||||
autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?;
|
||||
window_target.app_state.register_window(&delegate, mtm);
|
||||
Ok(Window {
|
||||
let window = Window {
|
||||
window: MainThreadBound::new(delegate.window().retain(), mtm),
|
||||
delegate: MainThreadBound::new(delegate, mtm),
|
||||
window_type,
|
||||
})
|
||||
};
|
||||
window.reposition();
|
||||
Ok(window)
|
||||
}
|
||||
|
||||
/// Recomputes this window's position (and, if constrained, its size) from its positioner
|
||||
/// state, using [`winit_common::positioner::place_window`], and applies the result. No-op if
|
||||
/// this window isn't anchored, or if it has no parent.
|
||||
fn reposition(&self) {
|
||||
self.maybe_wait_on_main(|delegate| delegate.reposition());
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_wait_on_main<R: Send>(
|
||||
@@ -99,7 +105,15 @@ impl rwh_06::HasWindowHandle for Window {
|
||||
|
||||
impl CoreWindow for Window {
|
||||
fn window_type(&self) -> WindowType {
|
||||
self.window_type
|
||||
self.maybe_wait_on_main(|delegate| delegate.window_type())
|
||||
}
|
||||
|
||||
fn positioner(&self) -> WindowPositioner {
|
||||
self.maybe_wait_on_main(|delegate| delegate.popup_positioner())
|
||||
}
|
||||
|
||||
fn set_positioner(&self, positioner: WindowPositioner) {
|
||||
self.maybe_wait_on_main(|delegate| delegate.set_popup_positioner(positioner));
|
||||
}
|
||||
|
||||
fn id(&self) -> winit_core::window::WindowId {
|
||||
|
||||
@@ -44,6 +44,7 @@ use objc2_foundation::{
|
||||
};
|
||||
use tracing::{debug_span, trace, warn};
|
||||
use winit_common::core_foundation::MainRunLoop;
|
||||
use winit_common::positioner::place_window;
|
||||
use winit_core::cursor::Cursor;
|
||||
use winit_core::data_transfer::DataTransferId;
|
||||
use winit_core::error::{NotSupportedError, RequestError};
|
||||
@@ -52,7 +53,8 @@ use winit_core::icon::Icon;
|
||||
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider};
|
||||
use winit_core::window::{
|
||||
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme,
|
||||
UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType,
|
||||
UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowPositioner,
|
||||
WindowType,
|
||||
};
|
||||
|
||||
use super::app_state::AppState;
|
||||
@@ -112,7 +114,16 @@ pub(crate) struct State {
|
||||
is_simple_fullscreen: Cell<bool>,
|
||||
saved_style: Cell<Option<NSWindowStyleMask>>,
|
||||
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!(
|
||||
@@ -149,6 +160,9 @@ define_class!(
|
||||
let _entered = debug_span!("windowDidResize:").entered();
|
||||
// NOTE: WindowEvent::SurfaceResized is reported using NSViewFrameDidChangeNotification.
|
||||
self.emit_move_event();
|
||||
// `windowDidMove:` isn't triggered when a move is part of a resize (e.g. dragging the
|
||||
// top or left edge), so anchored children need repositioning here too.
|
||||
self.reposition_child_windows();
|
||||
}
|
||||
|
||||
#[unsafe(method(windowWillStartLiveResize:))]
|
||||
@@ -170,6 +184,7 @@ define_class!(
|
||||
fn window_did_move(&self, _: Option<&AnyObject>) {
|
||||
let _entered = debug_span!("windowDidMove:").entered();
|
||||
self.emit_move_event();
|
||||
self.reposition_child_windows();
|
||||
}
|
||||
|
||||
#[unsafe(method(windowDidChangeBackingProperties:))]
|
||||
@@ -657,7 +672,7 @@ fn new_window(
|
||||
app_state: &Rc<AppState>,
|
||||
attrs: &WindowAttributes,
|
||||
macos_attrs: &WindowAttributesMacOS,
|
||||
is_popup: bool,
|
||||
anchored: bool,
|
||||
mtm: MainThreadMarker,
|
||||
) -> Option<Retained<NSWindow>> {
|
||||
autoreleasepool(|_| {
|
||||
@@ -685,10 +700,12 @@ fn new_window(
|
||||
None => NSSize::new(800.0, 600.0),
|
||||
};
|
||||
let position = match attrs.position {
|
||||
// A popup's position is parent-relative; it's applied in `WindowDelegate::new`
|
||||
// (after the delegate exists) via the shared translation in
|
||||
// `set_outer_position`.
|
||||
_ if is_popup => NSPoint::new(0.0, 0.0),
|
||||
// An anchored window's position is parent-relative; it's applied in
|
||||
// `WindowDelegate::new` (after the delegate exists) via the shared
|
||||
// translation in `set_outer_position`. A parentless anchored window can't be
|
||||
// positioned that way, so it falls through to the same handling as a
|
||||
// non-anchored window below.
|
||||
_ if anchored && attrs.parent_window().is_some() => NSPoint::new(0.0, 0.0),
|
||||
Some(position) => {
|
||||
let position = position.to_logical(scale_factor);
|
||||
flip_window_screen_coordinates(NSRect::new(
|
||||
@@ -839,8 +856,8 @@ fn new_window(
|
||||
window.collectionBehavior() | NSWindowCollectionBehavior::FullScreenAuxiliary,
|
||||
);
|
||||
}
|
||||
// Popups are positioned relative to their parent in `WindowDelegate::new`.
|
||||
if attrs.position.is_none() && !is_popup {
|
||||
// Center window if the position is not set or it doesn't have any parent
|
||||
if attrs.position.is_none() && !(anchored && attrs.parent_window().is_some()) {
|
||||
window.center();
|
||||
}
|
||||
|
||||
@@ -904,7 +921,9 @@ impl WindowDelegate {
|
||||
.and_then(|attrs| attrs.cast::<WindowAttributesMacOS>().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let is_popup = matches!(attrs.window_type(), WindowType::Popup);
|
||||
let window_type = attrs.window_type();
|
||||
let is_popup = matches!(window_type, WindowType::Popup);
|
||||
let anchored = is_popup || attrs.positioner.is_some();
|
||||
if is_popup {
|
||||
// A popup is an undecorated, non-activating panel with no titlebar buttons. Model it
|
||||
// as such so it flows through the existing borderless + panel paths in `new_window`
|
||||
@@ -918,7 +937,7 @@ impl WindowDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let window = new_window(app_state, &attrs, &macos_attrs, is_popup, mtm)
|
||||
let window = new_window(app_state, &attrs, &macos_attrs, anchored, mtm)
|
||||
.ok_or_else(|| os_error!("couldn't create `NSWindow`"))?;
|
||||
|
||||
match attrs.parent_window() {
|
||||
@@ -979,7 +998,9 @@ impl WindowDelegate {
|
||||
is_simple_fullscreen: Cell::new(false),
|
||||
saved_style: Cell::new(None),
|
||||
is_borderless_game: Cell::new(macos_attrs.borderless_game),
|
||||
is_popup: Cell::new(is_popup),
|
||||
window_type,
|
||||
anchored,
|
||||
positioner: RefCell::new(attrs.positioner.unwrap_or_default()),
|
||||
});
|
||||
let delegate: Retained<WindowDelegate> = unsafe { msg_send![super(delegate), init] };
|
||||
|
||||
@@ -1026,10 +1047,10 @@ impl WindowDelegate {
|
||||
|
||||
delegate.set_window_level(attrs.window_level);
|
||||
|
||||
// The popup position is relative to the parent window, and the parent is only
|
||||
// attached above, so apply the (translated) position now. Default to the parent's
|
||||
// An anchored window's position is relative to the parent window, and the parent is
|
||||
// only attached above, so apply the (translated) position now. Default to the parent's
|
||||
// content top-left when no position was given.
|
||||
if is_popup {
|
||||
if anchored {
|
||||
let position = attrs.position.unwrap_or_else(|| LogicalPosition::new(0.0, 0.0).into());
|
||||
delegate.set_outer_position(position);
|
||||
}
|
||||
@@ -1189,8 +1210,8 @@ impl WindowDelegate {
|
||||
|
||||
pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, RequestError> {
|
||||
let position = flip_window_screen_coordinates(self.window().frame());
|
||||
let position =
|
||||
self.translate_popup_position_to_parent(LogicalPosition::new(position.x, position.y));
|
||||
let position = self
|
||||
.translate_anchored_position_to_parent(LogicalPosition::new(position.x, position.y));
|
||||
Ok(position.to_physical(self.scale_factor()))
|
||||
}
|
||||
|
||||
@@ -1215,7 +1236,7 @@ impl WindowDelegate {
|
||||
|
||||
pub fn set_outer_position(&self, position: Position) {
|
||||
let position = position.to_logical(self.scale_factor());
|
||||
let position = self.translate_popup_position(position);
|
||||
let position = self.translate_anchored_position(position);
|
||||
let point = flip_window_screen_coordinates(NSRect::new(
|
||||
NSPoint::new(position.x, position.y),
|
||||
self.window().frame().size,
|
||||
@@ -1223,11 +1244,11 @@ impl WindowDelegate {
|
||||
self.window().setFrameOrigin(point);
|
||||
}
|
||||
|
||||
/// Popups receive their position relative to the top-left of the parent window's
|
||||
/// Anchored windows receive their position relative to the top-left of the parent window's
|
||||
/// content area (matching the Win32 and Wayland backends). macOS positions windows
|
||||
/// in global screen coordinates, so add the parent content area's origin.
|
||||
fn translate_popup_position(&self, position: LogicalPosition<f64>) -> LogicalPosition<f64> {
|
||||
if !self.ivars().is_popup.get() {
|
||||
fn translate_anchored_position(&self, position: LogicalPosition<f64>) -> LogicalPosition<f64> {
|
||||
if !self.ivars().anchored {
|
||||
return position;
|
||||
}
|
||||
let Some(parent) = self.window().parentWindow() else {
|
||||
@@ -1238,15 +1259,15 @@ impl WindowDelegate {
|
||||
LogicalPosition::new(parent_origin.x + position.x, parent_origin.y + position.y)
|
||||
}
|
||||
|
||||
/// Inverse of [`Self::translate_popup_position`]. Popups report their position
|
||||
/// Inverse of [`Self::translate_anchored_position`]. Anchored windows report their position
|
||||
/// relative to the top-left of the parent window's content area (matching the
|
||||
/// Win32 and Wayland backends), so subtract the parent content area's origin from
|
||||
/// the global screen coordinates. Non-popup windows are returned unchanged.
|
||||
fn translate_popup_position_to_parent(
|
||||
/// the global screen coordinates. Non-anchored windows are returned unchanged.
|
||||
fn translate_anchored_position_to_parent(
|
||||
&self,
|
||||
position: LogicalPosition<f64>,
|
||||
) -> LogicalPosition<f64> {
|
||||
if !self.ivars().is_popup.get() {
|
||||
if !self.ivars().anchored {
|
||||
return position;
|
||||
}
|
||||
let Some(parent) = self.window().parentWindow() else {
|
||||
@@ -1257,6 +1278,95 @@ impl WindowDelegate {
|
||||
LogicalPosition::new(position.x - parent_origin.x, position.y - parent_origin.y)
|
||||
}
|
||||
|
||||
/// The parent window's content area origin, in global (Winit, top-left/y-down) screen
|
||||
/// coordinates. `None` if this window isn't anchored, or it has no parent.
|
||||
pub fn parent_content_origin(&self) -> Option<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]
|
||||
pub fn surface_size(&self) -> PhysicalSize<u32> {
|
||||
self.view().surface_size()
|
||||
|
||||
@@ -12,6 +12,9 @@ version.workspace = true
|
||||
# Event Handler
|
||||
event-handler = []
|
||||
|
||||
# Positioner
|
||||
positioner = ["dep:dpi"]
|
||||
|
||||
# XKB
|
||||
wayland = ["dep:memmap2"]
|
||||
x11 = ["xkbcommon-dl?/x11", "dep:x11-dl"]
|
||||
@@ -24,6 +27,7 @@ core-foundation = ["dep:block2", "dep:objc2", "dep:objc2-core-foundation"]
|
||||
foundation = ["dep:block2", "dep:objc2", "dep:objc2-foundation"]
|
||||
|
||||
[dependencies]
|
||||
dpi = { workspace = true, optional = true }
|
||||
smol_str = { workspace = true, optional = true }
|
||||
tracing.workspace = true
|
||||
winit-core.workspace = true
|
||||
|
||||
@@ -8,5 +8,7 @@ pub mod core_foundation;
|
||||
pub mod event_handler;
|
||||
#[cfg(feature = "foundation")]
|
||||
pub mod foundation;
|
||||
#[cfg(feature = "positioner")]
|
||||
pub mod positioner;
|
||||
#[cfg(feature = "xkb")]
|
||||
pub mod xkb;
|
||||
|
||||
523
winit-common/src/positioner.rs
Normal file
523
winit-common/src/positioner.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
//! The [`Window`] trait and associated types.
|
||||
mod positioner;
|
||||
|
||||
use std::any::Any;
|
||||
use std::fmt;
|
||||
|
||||
@@ -7,6 +9,7 @@ use cursor_icon::CursorIcon;
|
||||
use dpi::{
|
||||
LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size,
|
||||
};
|
||||
pub use positioner::{WindowAnchor, WindowConstraintAdjustment, WindowGravity};
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -57,6 +60,9 @@ pub enum WindowType {
|
||||
/// tooltip. Requires a parent set via [`WindowAttributes::with_parent_window`], and its
|
||||
/// position is interpreted relative to that parent.
|
||||
///
|
||||
/// The anchor/gravity/positioning system described on [`WindowAttributes::with_positioner`]
|
||||
/// can be used to position the popup or window in a more advanced way
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **macOS:** A borderless, non-activating child window. The system does *not* draw rounded
|
||||
@@ -66,6 +72,71 @@ pub enum WindowType {
|
||||
Popup,
|
||||
}
|
||||
|
||||
/// The positioner state backing a window's anchor-based placement.
|
||||
///
|
||||
/// Set at window creation via [`WindowAttributes::with_positioner`], and read/mutated at runtime
|
||||
/// through [`Window::positioner`]/[`Window::set_positioner`]. See those methods for
|
||||
/// platform-specific behavior, and [`WindowPositioner::default`] for the values used when
|
||||
/// [`WindowAttributes::with_positioner`] is never called.
|
||||
///
|
||||
/// The structure is based on the wayland structure. For more information see the wayland
|
||||
/// documentation [XDG Positioner](https://wayland.app/protocols/xdg-shell#xdg_positioner)
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct WindowPositioner {
|
||||
/// The edge or corner of the anchor rect used to position the window relative to it.
|
||||
///
|
||||
/// Combined with [`gravity`](Self::gravity), this controls which corner/edge of the anchor
|
||||
/// rectangle the window is pinned to. Defaults to [`WindowAnchor::Center`].
|
||||
pub anchor: WindowAnchor,
|
||||
/// The anchor rectangle the window is positioned relative to.
|
||||
///
|
||||
/// The [`Position`] is the top-left corner of the rectangle relative to the parent window's
|
||||
/// content area, and the [`Size`] its dimensions. Defaults to a `1x1` rectangle at the
|
||||
/// content origin. Passing a [`WindowPositioner`] to [`WindowAttributes::with_positioner`]
|
||||
/// overrides the position value set with [`WindowAttributes::with_position`].
|
||||
pub anchor_rect: (Position, Size),
|
||||
/// The window's position relative to the anchor rect. Defaults to no offset.
|
||||
pub offset: Position,
|
||||
/// The direction the window surface extends away from the anchor point.
|
||||
///
|
||||
/// Combined with [`anchor`](Self::anchor), this determines the final position of the window
|
||||
/// relative to its anchor rectangle. Defaults to [`WindowGravity::Center`].
|
||||
pub gravity: WindowGravity,
|
||||
/// How the window should be repositioned when it would be constrained.
|
||||
///
|
||||
/// The flags in [`WindowConstraintAdjustment`] can be combined to allow sliding, flipping,
|
||||
/// and/or resizing the window independently on each axis. Defaults to no adjustment.
|
||||
pub constraint_adjustment: WindowConstraintAdjustment,
|
||||
}
|
||||
|
||||
impl WindowPositioner {
|
||||
pub fn new(
|
||||
anchor: WindowAnchor,
|
||||
anchor_rect: (Position, Size),
|
||||
offset: Position,
|
||||
gravity: WindowGravity,
|
||||
constraint_adjustment: WindowConstraintAdjustment,
|
||||
) -> Self {
|
||||
WindowPositioner { anchor, anchor_rect, offset, gravity, constraint_adjustment }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WindowPositioner {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
anchor: WindowAnchor::default(),
|
||||
anchor_rect: (
|
||||
Position::Logical(LogicalPosition::new(0.0, 0.0)),
|
||||
Size::Logical(LogicalSize::new(1.0, 1.0)),
|
||||
),
|
||||
offset: Position::Logical(LogicalPosition::new(0.0, 0.0)),
|
||||
gravity: WindowGravity::default(),
|
||||
constraint_adjustment: WindowConstraintAdjustment::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attributes used when creating a window.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
@@ -105,6 +176,8 @@ pub struct WindowAttributes {
|
||||
pub fullscreen: Option<Fullscreen>,
|
||||
pub platform: Option<Box<dyn PlatformWindowAttributes>>,
|
||||
pub window_type: WindowType,
|
||||
/// See [`WindowAttributes::with_positioner`].
|
||||
pub positioner: Option<WindowPositioner>,
|
||||
}
|
||||
|
||||
impl WindowAttributes {
|
||||
@@ -438,6 +511,27 @@ impl WindowAttributes {
|
||||
pub fn window_type(&self) -> WindowType {
|
||||
self.window_type
|
||||
}
|
||||
|
||||
/// Sets the positioner used to place the window relative to its anchor rect.
|
||||
///
|
||||
/// See [`WindowPositioner`] and its fields for what each part of the positioner controls and
|
||||
/// its default when left as `None`.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Wayland:** Only takes effect when the window is a [`WindowType::Popup`], since the
|
||||
/// Wayland positioner is part of the `xdg_popup` protocol role.
|
||||
/// - **macOS, Windows:** Works for both [`WindowType::Window`] and [`WindowType::Popup`]. A
|
||||
/// [`WindowType::Popup`] always requires a parent window to be set via
|
||||
/// [`with_parent_window`](Self::with_parent_window). A [`WindowType::Window`] without a
|
||||
/// parent is positioned relative to the screen's available space instead of the parent's
|
||||
/// content area.
|
||||
/// - **X11, Web, Android, iOS, Orbital:** No effect.
|
||||
#[inline]
|
||||
pub fn with_positioner(mut self, positioner: WindowPositioner) -> Self {
|
||||
self.positioner = Some(positioner);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for WindowAttributes {
|
||||
@@ -466,6 +560,7 @@ impl Clone for WindowAttributes {
|
||||
fullscreen: self.fullscreen.clone(),
|
||||
platform: self.platform.as_ref().map(|platform| platform.box_clone()),
|
||||
window_type: self.window_type,
|
||||
positioner: self.positioner,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,6 +592,7 @@ impl Default for WindowAttributes {
|
||||
cursor: Cursor::default(),
|
||||
blur: Default::default(),
|
||||
window_type: Default::default(),
|
||||
positioner: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,6 +637,31 @@ pub trait Window: Any + Send + Sync + fmt::Debug {
|
||||
/// Returns the window type of this window
|
||||
fn window_type(&self) -> WindowType;
|
||||
|
||||
/// Returns the positioner used to place this window relative to its anchor rect.
|
||||
///
|
||||
/// Returns [`WindowPositioner::default`] if this window doesn't use anchor positioning, see
|
||||
/// [`WindowAttributes::with_positioner`].
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Wayland:** Always [`WindowPositioner::default`] unless the window is a
|
||||
/// [`WindowType::Popup`], since the Wayland positioner is part of the `xdg_popup` protocol
|
||||
/// role.
|
||||
fn positioner(&self) -> WindowPositioner {
|
||||
WindowPositioner::default()
|
||||
}
|
||||
|
||||
/// Sets the positioner used to place this window relative to its anchor rect.
|
||||
///
|
||||
/// No-op if this window doesn't use anchor positioning, see
|
||||
/// [`WindowAttributes::with_positioner`].
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Wayland:** No-op unless the window is a [`WindowType::Popup`], since the Wayland
|
||||
/// positioner is part of the `xdg_popup` protocol role.
|
||||
fn set_positioner(&self, _positioner: WindowPositioner) {}
|
||||
|
||||
/// Returns an identifier unique to the window.
|
||||
fn id(&self) -> WindowId;
|
||||
|
||||
|
||||
54
winit-core/src/window/positioner.rs
Normal file
54
winit-core/src/window/positioner.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ use std::ffi::c_void;
|
||||
use std::hash::BuildHasher;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use dpi::{LogicalSize, PhysicalSize, Position, Size};
|
||||
use dpi::{LogicalSize, PhysicalSize};
|
||||
use sctk::reexports::client::Proxy;
|
||||
use sctk::reexports::client::backend::ObjectId;
|
||||
use sctk::reexports::client::protocol::wl_surface::WlSurface;
|
||||
@@ -98,120 +98,6 @@ impl WindowExtWayland for dyn CoreWindow + '_ {
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional methods on [`Popup`] that are specific to Wayland.
|
||||
pub trait PopupExtWayland {
|
||||
fn anchor_rect(&self) -> Option<(impl Into<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)]
|
||||
pub(crate) struct ApplicationName {
|
||||
pub(crate) general: String,
|
||||
@@ -224,13 +110,6 @@ pub struct WindowAttributesWayland {
|
||||
pub(crate) name: Option<ApplicationName>,
|
||||
pub(crate) activation_token: Option<ActivationToken>,
|
||||
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 {
|
||||
@@ -269,63 +148,6 @@ impl WindowAttributesWayland {
|
||||
self.prefer_csd = prefer_csd;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the edge or corner of the anchor rectangle the popup is attached to.
|
||||
///
|
||||
/// Combined with [`with_gravity`](Self::with_gravity), this controls which corner/edge of the
|
||||
/// anchor rectangle the popup is pinned to. Has no effect unless the window is created as a
|
||||
/// [`Popup`](winit_core::window::WindowType::Popup).
|
||||
#[inline]
|
||||
pub fn with_anchor(mut self, anchor: PopupAnchor) -> Self {
|
||||
self.anchor = Some(anchor);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the anchor rectangle the popup is positioned relative to.
|
||||
///
|
||||
/// `position` is the top-left corner of the rectangle relative to the parent window's content
|
||||
/// area, and `size` its dimensions. Defaults to a `1x1` rectangle at the content origin.
|
||||
/// This value overwrites the position value set with `with_position` in the window attributes
|
||||
#[inline]
|
||||
pub fn with_anchor_rect(
|
||||
mut self,
|
||||
position: impl Into<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 {
|
||||
@@ -381,64 +203,3 @@ fn image_to_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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,16 +18,16 @@ use winit_core::event::{Ime, WindowEvent};
|
||||
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle};
|
||||
use winit_core::window::{
|
||||
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme,
|
||||
UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId,
|
||||
WindowLevel,
|
||||
UserAttentionType, Window as CoreWindow, WindowAnchor, WindowAttributes, WindowButtons,
|
||||
WindowConstraintAdjustment, WindowGravity, WindowId, WindowLevel, WindowPositioner,
|
||||
};
|
||||
|
||||
use super::ActiveEventLoop;
|
||||
use super::output::MonitorHandle;
|
||||
use crate::WindowAttributesWayland;
|
||||
use crate::window::Handles;
|
||||
use crate::window::handles::WindowRequests;
|
||||
use crate::window::state::{WindowState, WindowType};
|
||||
use crate::{PopupExtWayland, WindowAttributesWayland};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Popup {
|
||||
@@ -66,27 +66,21 @@ impl Popup {
|
||||
.xdg_activation
|
||||
.as_ref()
|
||||
.map(|activation_state| activation_state.global().clone());
|
||||
let positioner = XdgPositioner::new(&state.xdg_shell)
|
||||
let xdg_positioner = XdgPositioner::new(&state.xdg_shell)
|
||||
.map_err(|_| error("Failed to create positioner"))?;
|
||||
let parent_window_id =
|
||||
WindowId::from_raw(parent_window_handle.surface.as_ptr() as usize);
|
||||
let (popup, popup_state) = if let Some(parent_window_state) =
|
||||
state.windows.borrow().get(&parent_window_id)
|
||||
{
|
||||
let wayland_attributes = attributes
|
||||
.platform
|
||||
.as_ref()
|
||||
.and_then(|p| p.cast_ref::<WindowAttributesWayland>())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let WindowAttributesWayland {
|
||||
gravity,
|
||||
let WindowPositioner {
|
||||
anchor,
|
||||
anchor_rect,
|
||||
offset: positioner_offset,
|
||||
gravity,
|
||||
constraint_adjustment,
|
||||
positioner_offset,
|
||||
..
|
||||
} = wayland_attributes;
|
||||
} = attributes.positioner.unwrap_or_default();
|
||||
let grab_keyboard = attributes.active;
|
||||
|
||||
let mut parent_window_state = parent_window_state.lock().unwrap();
|
||||
@@ -107,48 +101,51 @@ impl Popup {
|
||||
// This is important for client side decorations
|
||||
let geometry_origin = parent_window_state.content_surface_origin();
|
||||
let anchor_position = LogicalPosition::new(-geometry_origin.x, -geometry_origin.y);
|
||||
positioner.set_anchor(anchor.unwrap_or(crate::PopupAnchor::TopLeft).into());
|
||||
positioner.set_gravity(gravity.unwrap_or(crate::PopupGravity::BottomRight).into());
|
||||
constraint_adjustment
|
||||
.inspect(|c| positioner.set_constraint_adjustment((*c).into()));
|
||||
let (anchor_rect_position, anchor_rect_size) = match anchor_rect {
|
||||
Some((position, size)) => (
|
||||
position.to_logical::<i32>(scale_factor),
|
||||
size.to_logical::<i32>(scale_factor),
|
||||
),
|
||||
None => {
|
||||
// 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))
|
||||
},
|
||||
if xdg_positioner.version() >= 3 {
|
||||
xdg_positioner.set_reactive();
|
||||
}
|
||||
xdg_positioner.set_anchor(from_anchor(anchor));
|
||||
xdg_positioner.set_gravity(from_gravity(gravity));
|
||||
xdg_positioner
|
||||
.set_constraint_adjustment(from_constraint_adjustment(constraint_adjustment));
|
||||
|
||||
let (anchor_rect_position, anchor_rect_size) = if attributes.positioner.is_some() {
|
||||
let size = anchor_rect.1.to_logical::<i32>(scale_factor);
|
||||
(
|
||||
anchor_rect.0.to_logical::<i32>(scale_factor),
|
||||
LogicalSize::new(size.width.max(1), size.height.max(1)),
|
||||
)
|
||||
} else {
|
||||
// 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 = (
|
||||
LogicalPosition::new(
|
||||
anchor_rect_position.x + anchor_position.x,
|
||||
anchor_rect_position.y + anchor_position.y,
|
||||
),
|
||||
LogicalSize::new(anchor_rect_size.width.max(1), anchor_rect_size.height.max(1)),
|
||||
anchor_rect_size,
|
||||
);
|
||||
positioner.set_anchor_rect(
|
||||
xdg_positioner.set_anchor_rect(
|
||||
anchor_rect.0.x,
|
||||
anchor_rect.0.y,
|
||||
anchor_rect.1.width,
|
||||
anchor_rect.1.height,
|
||||
);
|
||||
positioner_offset.inspect(|o| {
|
||||
let o = o.to_logical(scale_factor);
|
||||
positioner.set_offset(o.x, o.y);
|
||||
});
|
||||
positioner.set_size(size.width, size.height);
|
||||
let offset: LogicalPosition<i32> = positioner_offset.to_logical(scale_factor);
|
||||
xdg_positioner.set_offset(offset.x, offset.y);
|
||||
xdg_positioner.set_size(size.width, size.height);
|
||||
|
||||
let parent_surface = parent_window_state.window.xdg_surface();
|
||||
let surface = state.compositor_state.create_surface(&queue_handle);
|
||||
let popup = SctkPopup::from_surface(
|
||||
Some(parent_surface),
|
||||
&positioner,
|
||||
&xdg_positioner,
|
||||
&queue_handle,
|
||||
surface.clone(),
|
||||
&state.xdg_shell,
|
||||
@@ -163,10 +160,16 @@ impl Popup {
|
||||
size.into(),
|
||||
WindowType::Popup {
|
||||
popup: popup.clone(),
|
||||
positioner,
|
||||
xdg_positioner,
|
||||
last_configure: None,
|
||||
anchor_rect,
|
||||
parent_origin: geometry_origin,
|
||||
positioner: WindowPositioner::new(
|
||||
anchor,
|
||||
(anchor_rect_position.into(), anchor_rect_size.into()),
|
||||
positioner_offset,
|
||||
gravity,
|
||||
constraint_adjustment,
|
||||
),
|
||||
},
|
||||
attributes.preferred_theme,
|
||||
false,
|
||||
@@ -288,6 +291,53 @@ impl CoreWindow for Popup {
|
||||
winit_core::window::WindowType::Popup
|
||||
}
|
||||
|
||||
fn positioner(&self) -> WindowPositioner {
|
||||
let Some(state) = self.popup_state.upgrade() else { return WindowPositioner::default() };
|
||||
if let WindowType::Popup { positioner, .. } = &state.lock().unwrap().window {
|
||||
*positioner
|
||||
} else {
|
||||
WindowPositioner::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn set_positioner(&self, new_positioner: WindowPositioner) {
|
||||
let Some(state) = self.popup_state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut state = state.lock().unwrap();
|
||||
let scale_factor = state.scale_factor();
|
||||
|
||||
if let WindowType::Popup { popup, xdg_positioner, parent_origin, positioner, .. } =
|
||||
&mut state.window
|
||||
{
|
||||
*positioner = new_positioner;
|
||||
|
||||
xdg_positioner.set_anchor(from_anchor(new_positioner.anchor));
|
||||
xdg_positioner.set_gravity(from_gravity(new_positioner.gravity));
|
||||
xdg_positioner.set_constraint_adjustment(from_constraint_adjustment(
|
||||
new_positioner.constraint_adjustment,
|
||||
));
|
||||
|
||||
let (position, size) = new_positioner.anchor_rect;
|
||||
let size: LogicalSize<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 {
|
||||
self.window_id
|
||||
}
|
||||
@@ -332,19 +382,22 @@ impl CoreWindow for Popup {
|
||||
let Some(s) = self.popup_state.upgrade() else { return };
|
||||
let mut state = s.lock().unwrap();
|
||||
let scale_factor = state.scale_factor();
|
||||
if let WindowType::Popup { popup, positioner, anchor_rect, parent_origin, .. } =
|
||||
if let WindowType::Popup { popup, xdg_positioner, positioner, parent_origin, .. } =
|
||||
&mut state.window
|
||||
{
|
||||
let position = position.to_logical(scale_factor);
|
||||
anchor_rect.0 = position;
|
||||
positioner.set_anchor_rect(
|
||||
anchor_rect.0.x - parent_origin.x,
|
||||
anchor_rect.0.y - parent_origin.y,
|
||||
anchor_rect.1.width,
|
||||
anchor_rect.1.height,
|
||||
let size = positioner.anchor_rect.1;
|
||||
positioner.anchor_rect = (position, size);
|
||||
|
||||
let logical_position: LogicalPosition<i32> = position.to_logical(scale_factor);
|
||||
let logical_size: LogicalSize<i32> = size.to_logical(scale_factor);
|
||||
xdg_positioner.set_anchor_rect(
|
||||
logical_position.x - parent_origin.x,
|
||||
logical_position.y - parent_origin.y,
|
||||
logical_size.width,
|
||||
logical_size.height,
|
||||
);
|
||||
if popup.xdg_popup().version() >= 3 {
|
||||
popup.reposition(positioner, 0);
|
||||
popup.reposition(xdg_positioner, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -668,80 +721,59 @@ impl rwh_06::HasDisplayHandle for Popup {
|
||||
}
|
||||
}
|
||||
|
||||
impl PopupExtWayland for Popup {
|
||||
fn set_anchor(&self, anchor: crate::PopupAnchor) {
|
||||
let Some(state) = self.popup_state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let WindowType::Popup { popup, positioner, .. } = &state.lock().unwrap().window {
|
||||
positioner.set_anchor(anchor.into());
|
||||
popup.reposition(positioner, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn anchor_rect(&self) -> Option<(impl Into<Position>, impl Into<Size>)> {
|
||||
let state = self.popup_state.upgrade()?;
|
||||
if let WindowType::Popup { anchor_rect, .. } = &state.lock().unwrap().window {
|
||||
Some(*anchor_rect)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn set_anchor_rect(&self, position: impl Into<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_gravity(
|
||||
gravity: WindowGravity,
|
||||
) -> wayland_protocols::xdg::shell::client::xdg_positioner::Gravity {
|
||||
use wayland_protocols::xdg::shell::client::xdg_positioner::Gravity;
|
||||
match gravity {
|
||||
WindowGravity::Center => Gravity::None,
|
||||
WindowGravity::Top => Gravity::Top,
|
||||
WindowGravity::Bottom => Gravity::Bottom,
|
||||
WindowGravity::Left => Gravity::Left,
|
||||
WindowGravity::Right => Gravity::Right,
|
||||
WindowGravity::TopLeft => Gravity::TopLeft,
|
||||
WindowGravity::BottomLeft => Gravity::BottomLeft,
|
||||
WindowGravity::TopRight => Gravity::TopRight,
|
||||
WindowGravity::BottomRight => Gravity::BottomRight,
|
||||
_ => Gravity::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_anchor(
|
||||
value: WindowAnchor,
|
||||
) -> wayland_protocols::xdg::shell::client::xdg_positioner::Anchor {
|
||||
use wayland_protocols::xdg::shell::client::xdg_positioner::Anchor;
|
||||
match value {
|
||||
WindowAnchor::Center => Anchor::None,
|
||||
WindowAnchor::Top => Anchor::Top,
|
||||
WindowAnchor::Bottom => Anchor::Bottom,
|
||||
WindowAnchor::Left => Anchor::Left,
|
||||
WindowAnchor::Right => Anchor::Right,
|
||||
WindowAnchor::TopLeft => Anchor::TopLeft,
|
||||
WindowAnchor::BottomLeft => Anchor::BottomLeft,
|
||||
WindowAnchor::TopRight => Anchor::TopRight,
|
||||
WindowAnchor::BottomRight => Anchor::BottomRight,
|
||||
_ => Anchor::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_constraint_adjustment(
|
||||
value: WindowConstraintAdjustment,
|
||||
) -> wayland_protocols::xdg::shell::client::xdg_positioner::ConstraintAdjustment {
|
||||
use wayland_protocols::xdg::shell::client::xdg_positioner::ConstraintAdjustment;
|
||||
|
||||
const _: () = {
|
||||
assert!(WindowConstraintAdjustment::SLIDE_X.bits() == ConstraintAdjustment::SlideX.bits());
|
||||
assert!(WindowConstraintAdjustment::SLIDE_Y.bits() == ConstraintAdjustment::SlideY.bits());
|
||||
assert!(WindowConstraintAdjustment::FLIP_X.bits() == ConstraintAdjustment::FlipX.bits());
|
||||
assert!(WindowConstraintAdjustment::FLIP_Y.bits() == ConstraintAdjustment::FlipY.bits());
|
||||
assert!(
|
||||
WindowConstraintAdjustment::RESIZE_X.bits() == ConstraintAdjustment::ResizeX.bits()
|
||||
);
|
||||
assert!(
|
||||
WindowConstraintAdjustment::RESIZE_Y.bits() == ConstraintAdjustment::ResizeY.bits()
|
||||
);
|
||||
};
|
||||
|
||||
ConstraintAdjustment::from_bits_retain(value.bits())
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ use winit_core::cursor::{CursorIcon, CustomCursor as CoreCustomCursor};
|
||||
use winit_core::error::{NotSupportedError, RequestError};
|
||||
use winit_core::window::{
|
||||
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, WindowId,
|
||||
WindowPositioner,
|
||||
};
|
||||
|
||||
use crate::event_loop::OwnedDisplayHandle;
|
||||
@@ -64,10 +65,11 @@ pub enum WindowType {
|
||||
},
|
||||
Popup {
|
||||
popup: Popup,
|
||||
positioner: XdgPositioner,
|
||||
xdg_positioner: XdgPositioner,
|
||||
last_configure: Option<PopupConfigure>,
|
||||
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()))
|
||||
}
|
||||
},
|
||||
WindowType::Popup { popup, positioner, .. } => {
|
||||
WindowType::Popup { popup, xdg_positioner, .. } => {
|
||||
let size = surface_size.to_logical(self.scale_factor());
|
||||
positioner.set_size(size.width, size.height);
|
||||
xdg_positioner.set_size(size.width, size.height);
|
||||
if popup.xdg_popup().version() >= 3 {
|
||||
popup.reposition(positioner, 0);
|
||||
popup.reposition(xdg_positioner, 0);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ serde = { workspace = true, optional = true }
|
||||
smol_str.workspace = true
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
winit-common = { workspace = true, features = ["positioner"] }
|
||||
winit-core.workspace = true
|
||||
|
||||
# Platform-specific
|
||||
|
||||
@@ -40,27 +40,28 @@ use windows_sys::Win32::UI::Input::{
|
||||
MOUSE_MOVE_RELATIVE, RAWINPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE,
|
||||
};
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
CREATESTRUCTW, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GWL_STYLE,
|
||||
GWL_USERDATA, GetClientRect, GetCursorPos, GetMenu, HTCAPTION, HTCLIENT, LoadCursorW,
|
||||
MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, MsgWaitForMultipleObjectsEx,
|
||||
NCCALCSIZE_PARAMS, PEN_FLAG_BARREL, PEN_FLAG_ERASER, PEN_MASK_PRESSURE, PEN_MASK_ROTATION,
|
||||
PEN_MASK_TILT_X, PEN_MASK_TILT_Y, PM_REMOVE, PT_PEN, PT_TOUCH, PeekMessageW, PostMessageW,
|
||||
QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, RegisterClassExW, RegisterWindowMessageA,
|
||||
SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES,
|
||||
SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SetCursor, SetWindowPos,
|
||||
SystemParametersInfoW, TranslateMessage, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE,
|
||||
WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO,
|
||||
WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION,
|
||||
WM_INPUT, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP,
|
||||
WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL,
|
||||
WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT,
|
||||
WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR,
|
||||
WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP,
|
||||
WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WMSZ_BOTTOM,
|
||||
WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT,
|
||||
WMSZ_TOPRIGHT, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
|
||||
WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE,
|
||||
CREATESTRUCTW, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW,
|
||||
EnumThreadWindows, GW_OWNER, GWL_STYLE, GWL_USERDATA, GetClientRect, GetCursorPos, GetMenu,
|
||||
GetWindow, HTCAPTION, HTCLIENT, LoadCursorW, MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE,
|
||||
MsgWaitForMultipleObjectsEx, NCCALCSIZE_PARAMS, PEN_FLAG_BARREL, PEN_FLAG_ERASER,
|
||||
PEN_MASK_PRESSURE, PEN_MASK_ROTATION, PEN_MASK_TILT_X, PEN_MASK_TILT_Y, PM_REMOVE, PT_PEN,
|
||||
PT_TOUCH, PeekMessageW, PostMessageW, QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL,
|
||||
RegisterClassExW, RegisterWindowMessageA, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED,
|
||||
SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE,
|
||||
SWP_NOZORDER, SetCursor, SetWindowPos, SystemParametersInfoW, TranslateMessage, WHEEL_DELTA,
|
||||
WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE,
|
||||
WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION,
|
||||
WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP,
|
||||
WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR,
|
||||
WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE,
|
||||
WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE,
|
||||
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING,
|
||||
WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING,
|
||||
WM_XBUTTONDOWN, WM_XBUTTONUP, WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT,
|
||||
WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT, WNDCLASSEXW, WS_EX_LAYERED,
|
||||
WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE,
|
||||
};
|
||||
use windows_sys::core::BOOL;
|
||||
use winit_core::application::ApplicationHandler;
|
||||
use winit_core::cursor::{CustomCursor, CustomCursorSource};
|
||||
use winit_core::data_transfer::{
|
||||
@@ -94,7 +95,7 @@ use crate::keyboard::KeyEventBuilder;
|
||||
use crate::keyboard_layout::LAYOUT_CACHE;
|
||||
use crate::monitor::{self, MonitorHandle};
|
||||
use crate::util::{WIN10_BUILD_VERSION, wrap_device_id};
|
||||
use crate::window::{InitData, Window};
|
||||
use crate::window::{self, InitData, Window};
|
||||
use crate::window_state::{CursorFlags, ImeState, WindowFlags, WindowState};
|
||||
use crate::{raw_input, util};
|
||||
|
||||
@@ -1112,6 +1113,31 @@ unsafe fn lose_active_focus(window: HWND, userdata: &WindowData) {
|
||||
userdata.send_window_event(window, Focused(false));
|
||||
}
|
||||
|
||||
/// Repositions any anchored windows owned by `parent`. Win32 does not reposition owned windows
|
||||
/// when their owner moves (unlike Wayland subsurfaces or X11's override-redirect popups), so this
|
||||
/// has to be done manually whenever `parent` receives a `WM_WINDOWPOSCHANGED` that moved it.
|
||||
unsafe fn reposition_owned_windows(parent: HWND) {
|
||||
unsafe extern "system" fn enum_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
if unsafe { GetWindow(hwnd, GW_OWNER) } != lparam as HWND {
|
||||
return true.into(); // continue enumeration
|
||||
}
|
||||
|
||||
let userdata_ptr = unsafe { util::get_window_long(hwnd, GWL_USERDATA) } as *mut WindowData;
|
||||
if !userdata_ptr.is_null() {
|
||||
let userdata = unsafe { &*userdata_ptr };
|
||||
if userdata.window_state_lock().window_flags.contains(WindowFlags::ANCHORED) {
|
||||
window::reposition_owned_popup(hwnd, &userdata.window_state);
|
||||
}
|
||||
}
|
||||
|
||||
true.into() // continue enumeration
|
||||
}
|
||||
|
||||
unsafe {
|
||||
EnumThreadWindows(GetCurrentThreadId(), Some(enum_proc), parent as LPARAM);
|
||||
}
|
||||
}
|
||||
|
||||
/// Any window whose callback is configured to this function will have its events propagated
|
||||
/// through the events loop of the thread the window was created in.
|
||||
// This is the callback that is called by `DispatchMessage` in the events loop.
|
||||
@@ -1449,6 +1475,8 @@ unsafe fn public_window_callback_inner(
|
||||
let physical_position =
|
||||
unsafe { PhysicalPosition::new((*windowpos).x, (*windowpos).y) };
|
||||
userdata.send_window_event(window, Moved(physical_position));
|
||||
|
||||
unsafe { reposition_owned_windows(window) };
|
||||
}
|
||||
|
||||
// This is necessary for us to still get sent WM_SIZE.
|
||||
|
||||
@@ -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>> {
|
||||
// EnumDisplaySettingsExW can return duplicate values (or some of the
|
||||
// fields are probably changing, but we aren't looking at those fields
|
||||
|
||||
@@ -7,7 +7,9 @@ use std::sync::mpsc::channel;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::{io, panic, ptr};
|
||||
|
||||
use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size};
|
||||
use dpi::{
|
||||
LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size,
|
||||
};
|
||||
use tracing::warn;
|
||||
use windows_sys::Win32::Foundation::{
|
||||
HWND, LPARAM, OLE_E_WRONGCOMPOBJ, POINT, POINTS, RECT, RPC_E_CHANGED_MODE, S_OK, WPARAM,
|
||||
@@ -47,6 +49,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
TPM_RETURNCMD, TrackPopupMenu, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, WM_NCLBUTTONDOWN, WM_SETICON,
|
||||
WM_SYSCOMMAND, WNDCLASSEXW,
|
||||
};
|
||||
use winit_common::positioner::place_window;
|
||||
use winit_core::cursor::Cursor;
|
||||
use winit_core::error::{NotSupportedError, RequestError};
|
||||
use winit_core::icon::{Icon, RgbaIcon};
|
||||
@@ -54,7 +57,7 @@ use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, Monito
|
||||
use winit_core::window::{
|
||||
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme,
|
||||
UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId,
|
||||
WindowLevel, WindowType,
|
||||
WindowLevel, WindowPositioner, WindowType,
|
||||
};
|
||||
|
||||
use crate::dark_mode::try_theme;
|
||||
@@ -100,9 +103,6 @@ pub struct Window {
|
||||
|
||||
// The events loop proxy.
|
||||
thread_executor: event_loop::EventLoopThreadExecutor,
|
||||
|
||||
/// The type of window of this
|
||||
window_type: WindowType,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
@@ -114,37 +114,26 @@ impl Window {
|
||||
// First person to remove the need for cloning here gets a cookie!
|
||||
//
|
||||
// done. you owe me -- ossi
|
||||
unsafe { init(w_attr, &event_loop.0) }
|
||||
let window = unsafe { init(w_attr, &event_loop.0) }?;
|
||||
window.reposition();
|
||||
Ok(window)
|
||||
}
|
||||
|
||||
fn window_state_lock(&self) -> MutexGuard<'_, WindowState> {
|
||||
self.window_state.lock().unwrap()
|
||||
}
|
||||
|
||||
// If we have a popup the position is relative to the parent window and not
|
||||
// If the window is anchored the position is relative to the parent window and not
|
||||
// relative to the screen. Therefore we have to translate it from the parent
|
||||
// coordinate system to the display coordinate system
|
||||
fn translate_outer_position(&self, position: Position) -> PhysicalPosition<i32> {
|
||||
let position = position.to_physical::<i32>(self.scale_factor());
|
||||
let mut point = POINT { x: position.x, y: position.y };
|
||||
|
||||
let window_flags = self.window_state_lock().window_flags;
|
||||
if window_flags.contains(WindowFlags::POPUP) && !window_flags.contains(WindowFlags::CHILD) {
|
||||
let parent = unsafe { GetParent(self.hwnd()) };
|
||||
if !parent.is_null() {
|
||||
unsafe {
|
||||
ClientToScreen(parent, &mut point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PhysicalPosition::new(point.x, point.y)
|
||||
translate_outer_position(self.hwnd(), &self.window_state, position, self.scale_factor())
|
||||
}
|
||||
|
||||
// Inverse of `translate_outer_position`: if we have a popup the position is
|
||||
// Inverse of `translate_outer_position`: if the window is anchored the position is
|
||||
// reported relative to the parent window instead of the screen. Therefore we
|
||||
// translate it from the display coordinate system back to the parent
|
||||
// coordinate system. Non-popup windows are left in screen coordinates.
|
||||
// coordinate system. Non-anchored windows are left in screen coordinates.
|
||||
fn translate_outer_position_to_parent(
|
||||
&self,
|
||||
position: PhysicalPosition<i32>,
|
||||
@@ -152,7 +141,9 @@ impl Window {
|
||||
let mut point = POINT { x: position.x, y: position.y };
|
||||
|
||||
let window_flags = self.window_state_lock().window_flags;
|
||||
if window_flags.contains(WindowFlags::POPUP) && !window_flags.contains(WindowFlags::CHILD) {
|
||||
if window_flags.contains(WindowFlags::ANCHORED)
|
||||
&& !window_flags.contains(WindowFlags::CHILD)
|
||||
{
|
||||
let parent = unsafe { GetParent(self.hwnd()) };
|
||||
if !parent.is_null() {
|
||||
unsafe {
|
||||
@@ -164,6 +155,28 @@ impl Window {
|
||||
PhysicalPosition::new(point.x, point.y)
|
||||
}
|
||||
|
||||
/// Recomputes this window's position (and, if constrained, its size) from its positioner
|
||||
/// state, using [`winit_common::positioner::place_window`], and applies the result. No-op if
|
||||
/// this window isn't anchored. If it has no parent (which the Win32 backend never allows for
|
||||
/// a popup, see `init`), the positioner is resolved relative to the screen's work area
|
||||
/// instead of the parent's content area.
|
||||
fn reposition(&self) {
|
||||
let (anchored, positioner) = {
|
||||
let window_state = self.window_state_lock();
|
||||
(window_state.anchored, window_state.positioner)
|
||||
};
|
||||
let Some((origin, size, current_size)) =
|
||||
compute_anchored_placement(self.hwnd(), anchored, &positioner, self.scale_factor())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.set_outer_position(Position::Logical(origin));
|
||||
if size != current_size {
|
||||
let _ = self.request_surface_size(Size::Logical(size));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `hwnd` of this window.
|
||||
pub fn hwnd(&self) -> HWND {
|
||||
self.window.hwnd()
|
||||
@@ -463,7 +476,16 @@ impl rwh_06::HasWindowHandle for Window {
|
||||
|
||||
impl CoreWindow for Window {
|
||||
fn window_type(&self) -> WindowType {
|
||||
self.window_type
|
||||
self.window_state_lock().window_type
|
||||
}
|
||||
|
||||
fn positioner(&self) -> WindowPositioner {
|
||||
self.window_state_lock().positioner
|
||||
}
|
||||
|
||||
fn set_positioner(&self, positioner: WindowPositioner) {
|
||||
self.window_state_lock().positioner = positioner;
|
||||
self.reposition();
|
||||
}
|
||||
|
||||
fn set_title(&self, text: &str) {
|
||||
@@ -566,12 +588,7 @@ impl CoreWindow for Window {
|
||||
}
|
||||
|
||||
fn outer_size(&self) -> PhysicalSize<u32> {
|
||||
util::WindowArea::Outer
|
||||
.get_rect(self.hwnd())
|
||||
.map(|rect| {
|
||||
PhysicalSize::new((rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32)
|
||||
})
|
||||
.unwrap()
|
||||
outer_size_of(self.hwnd())
|
||||
}
|
||||
|
||||
fn request_surface_size(&self, size: Size) -> Option<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> {
|
||||
// inputs
|
||||
pub runner: &'a Rc<EventLoopRunner>,
|
||||
@@ -1266,7 +1436,6 @@ impl InitData<'_> {
|
||||
window: SyncWindowHandle(window),
|
||||
window_state,
|
||||
thread_executor: self.runner.create_thread_executor(),
|
||||
window_type: self.attributes.window_type,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1426,9 +1595,14 @@ unsafe fn init(
|
||||
unsafe { register_window_class(&class_name) };
|
||||
|
||||
let is_popup = matches!(attributes.window_type, WindowType::Popup);
|
||||
// Whether this window is positioned relative to its parent via the anchor/gravity/
|
||||
// positioner system -- either because it's a `WindowType::Popup`, or because anchor
|
||||
// attributes were explicitly set on a `WindowType::Window` (Windows supports both).
|
||||
let anchored = is_popup || attributes.positioner.is_some();
|
||||
let mut window_flags = WindowFlags::empty();
|
||||
window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations);
|
||||
window_flags.set(WindowFlags::POPUP, is_popup);
|
||||
window_flags.set(WindowFlags::ANCHORED, anchored);
|
||||
window_flags.set(WindowFlags::MARKER_UNDECORATED_SHADOW, win_attributes.decoration_shadow);
|
||||
window_flags
|
||||
.set(WindowFlags::ALWAYS_ON_TOP, attributes.window_level == WindowLevel::AlwaysOnTop);
|
||||
|
||||
@@ -20,7 +20,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
use winit_core::icon::Icon;
|
||||
use winit_core::keyboard::ModifiersState;
|
||||
use winit_core::monitor::Fullscreen;
|
||||
use winit_core::window::{ImeCapabilities, Theme, WindowAttributes};
|
||||
use winit_core::window::{ImeCapabilities, Theme, WindowAttributes, WindowPositioner, WindowType};
|
||||
|
||||
use crate::{SelectedCursor, WindowAttributesWindows, event_loop, util};
|
||||
|
||||
@@ -51,6 +51,21 @@ pub(crate) struct WindowState {
|
||||
|
||||
pub window_flags: WindowFlags,
|
||||
|
||||
/// The role of this window. Governs creation/role behavior (OS window style, decorations)
|
||||
/// only -- see `anchored` for whether this window is positioned via the anchor system.
|
||||
pub window_type: WindowType,
|
||||
|
||||
/// Whether this window is positioned relative to its parent using the anchor/gravity/
|
||||
/// positioner system, either because it's a [`WindowType::Popup`] or because anchor
|
||||
/// attributes were set on a [`WindowType::Window`]. Stored here (rather than only on the
|
||||
/// `Window` struct) so it stays reachable from just an `hwnd` via `GWL_USERDATA` -- e.g. to
|
||||
/// reposition an anchored window when its parent moves.
|
||||
pub anchored: bool,
|
||||
|
||||
/// The positioner state backing `anchored` placement, meaningful only when `anchored` is
|
||||
/// `true`.
|
||||
pub positioner: WindowPositioner,
|
||||
|
||||
pub ime_state: ImeState,
|
||||
pub ime_capabilities: Option<ImeCapabilities>,
|
||||
|
||||
@@ -140,6 +155,12 @@ bitflags! {
|
||||
|
||||
const CLIP_CHILDREN = 1 << 22;
|
||||
|
||||
/// Whether this window is positioned relative to its parent via the anchor/gravity/
|
||||
/// positioner system. Independent of `POPUP`, which only selects the OS window style --
|
||||
/// a `WindowType::Window` can be `ANCHORED` too. Used to pick the coordinate frame in
|
||||
/// `translate_outer_position`/`translate_outer_position_to_parent`.
|
||||
const ANCHORED = 1 << 23;
|
||||
|
||||
const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits();
|
||||
}
|
||||
}
|
||||
@@ -186,6 +207,11 @@ impl WindowState {
|
||||
preferred_theme,
|
||||
window_flags: WindowFlags::empty(),
|
||||
|
||||
window_type: attributes.window_type,
|
||||
anchored: matches!(attributes.window_type, WindowType::Popup)
|
||||
|| attributes.positioner.is_some(),
|
||||
positioner: attributes.positioner.unwrap_or_default(),
|
||||
|
||||
ime_state: ImeState::Disabled,
|
||||
ime_capabilities: None,
|
||||
|
||||
@@ -287,23 +313,25 @@ impl WindowFlags {
|
||||
}
|
||||
} else {
|
||||
style |= WS_CAPTION | WS_SYSMENU | WS_BORDER;
|
||||
|
||||
if self.contains(WindowFlags::RESIZABLE) {
|
||||
style |= WS_SIZEBOX;
|
||||
}
|
||||
if self.contains(WindowFlags::MAXIMIZABLE) {
|
||||
style |= WS_MAXIMIZEBOX;
|
||||
}
|
||||
if self.contains(WindowFlags::MINIMIZABLE) {
|
||||
style |= WS_MINIMIZEBOX;
|
||||
}
|
||||
|
||||
if self.contains(WindowFlags::ON_TASKBAR) {
|
||||
style_ex |= WS_EX_APPWINDOW;
|
||||
}
|
||||
};
|
||||
|
||||
if self.contains(WindowFlags::RESIZABLE) {
|
||||
style |= WS_SIZEBOX;
|
||||
}
|
||||
if self.contains(WindowFlags::MAXIMIZABLE) {
|
||||
style |= WS_MAXIMIZEBOX;
|
||||
}
|
||||
if self.contains(WindowFlags::MINIMIZABLE) {
|
||||
style |= WS_MINIMIZEBOX;
|
||||
}
|
||||
if self.contains(WindowFlags::VISIBLE) {
|
||||
style |= WS_VISIBLE;
|
||||
}
|
||||
if self.contains(WindowFlags::ON_TASKBAR) {
|
||||
style_ex |= WS_EX_APPWINDOW;
|
||||
}
|
||||
if self.contains(WindowFlags::ALWAYS_ON_TOP) {
|
||||
style_ex |= WS_EX_TOPMOST;
|
||||
}
|
||||
|
||||
214
winit/examples/popup.rs
Normal file
214
winit/examples/popup.rs
Normal 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."
|
||||
);
|
||||
}
|
||||
@@ -51,7 +51,19 @@ changelog entry.
|
||||
- On Android, added scancode conversions for more obscure key codes.
|
||||
- On Wayland, added `HoldGesture` event for multi-finger hold gestures
|
||||
- On Wayland, added ext-background-effect-v1 support.
|
||||
- On Wayland, Windows and macOS, added native popups (`WindowType::Popup`).
|
||||
- On Wayland, Windows and macOS, added native popups (`WindowType::Popup`), with
|
||||
`WindowAttributes::with_positioner` for configuring their placement via a
|
||||
`WindowPositioner` (grouping the anchor edge/corner, the anchor rect, the
|
||||
gravity direction, the positioner offset and the constraint adjustment, using
|
||||
the new `WindowAnchor`, `WindowGravity` and `WindowConstraintAdjustment`
|
||||
types), and matching `Window::positioner`/`set_positioner` methods for
|
||||
reading/controlling it at runtime. These work on every `Window`, popup or not;
|
||||
use `Window::window_type` to tell whether a given window is a genuine
|
||||
`WindowType::Popup`. On Windows and macOS, which have no native positioner
|
||||
concept, the placement is computed by winit itself, mirroring Wayland's
|
||||
`xdg_positioner` behavior, and it also works for a plain `WindowType::Window`
|
||||
that has a parent; on Wayland the positioner is part of the `xdg_popup`
|
||||
protocol, so it only applies to `WindowType::Popup`.
|
||||
- On macOS, add `WindowAttributesMacOS::with_fullscreen_auxiliary` and
|
||||
`WindowExtMacOS::set_fullscreen_auxiliary` / `WindowExtMacOS::fullscreen_auxiliary`, allowing a
|
||||
window to be shown on the same Space as a fullscreen window
|
||||
|
||||
Reference in New Issue
Block a user