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