From 9674d8ceef6976326fe9583a81f2e684daac05d6 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Mon, 27 Jul 2026 15:43:51 +0200 Subject: [PATCH] Wayland, Windows, MacOS: Popup Implementation (#4543) Implement proper decorationless popups by specifying the type of the child window with with_type() With this commit, different kind of child windows can be created - Popups: Special windows without any decoration which can be positioned relative to the parent - Window: Normal window with a parent or not The type can be specified during creation of the Window using the window_attributes and the with_type() function. As default a normal Window is used. If Popup is choosen a parent must be specified, otherwise the Popup creation fails with an Error returned by the new() function. Related issues: #403 and #4256 --- winit-android/src/event_loop.rs | 14 +- winit-appkit/src/window.rs | 9 +- winit-appkit/src/window_delegate.rs | 84 ++- winit-core/src/window.rs | 77 ++- winit-orbital/src/window.rs | 10 + winit-uikit/src/window.rs | 12 +- winit-wayland/src/event_loop/mod.rs | 78 ++- winit-wayland/src/lib.rs | 248 +++++++- winit-wayland/src/popup.rs | 747 +++++++++++++++++++++++++ winit-wayland/src/seat/keyboard/mod.rs | 8 +- winit-wayland/src/seat/mod.rs | 12 + winit-wayland/src/seat/pointer/mod.rs | 6 + winit-wayland/src/state.rs | 63 ++- winit-wayland/src/window/handles.rs | 142 +++++ winit-wayland/src/window/mod.rs | 217 +++---- winit-wayland/src/window/state.rs | 519 +++++++++++------ winit-web/src/window.rs | 12 +- winit-win32/src/window.rs | 89 ++- winit-win32/src/window_state.rs | 25 +- winit-x11/src/window.rs | 19 +- winit/examples/child_window.rs | 11 +- winit/src/changelog/unreleased.md | 1 + 22 files changed, 2035 insertions(+), 368 deletions(-) create mode 100644 winit-wayland/src/popup.rs create mode 100644 winit-wayland/src/window/handles.rs diff --git a/winit-android/src/event_loop.rs b/winit-android/src/event_loop.rs index 62895a34d..a106ab97f 100644 --- a/winit-android/src/event_loop.rs +++ b/winit-android/src/event_loop.rs @@ -769,9 +769,15 @@ pub struct Window { impl Window { pub(crate) fn new( el: &ActiveEventLoop, - _window_attrs: window::WindowAttributes, + window_attrs: window::WindowAttributes, ) -> Result { - // FIXME this ignores requested window attributes + if window_attrs.window_type() == window::WindowType::Popup { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Popups are not implemented for Android", + ))); + } + + // FIXME this ignores the rest of the requested window attributes Ok(Self { app: el.app.clone(), @@ -825,6 +831,10 @@ impl rwh_06::HasWindowHandle for Window { } impl CoreWindow for Window { + fn window_type(&self) -> window::WindowType { + window::WindowType::Window + } + fn id(&self) -> WindowId { GLOBAL_WINDOW } diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index c454160c1..8a72a02d9 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -15,7 +15,7 @@ use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::window::{ ImeCapabilities, ImeRequest, ImeRequestError, Theme, UserAttentionType, Window as CoreWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, + WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType, }; use super::event_loop::ActiveEventLoop; @@ -26,6 +26,7 @@ pub(crate) struct Window { window: MainThreadBound>, /// The window only keeps a weak reference to this, so we must keep it around here. delegate: MainThreadBound>, + window_type: WindowType, } impl Window { @@ -34,12 +35,14 @@ impl Window { attributes: WindowAttributes, ) -> Result { let mtm = window_target.mtm; + let window_type = attributes.window_type; let delegate = autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?; window_target.app_state.register_window(&delegate, mtm); Ok(Window { window: MainThreadBound::new(delegate.window().retain(), mtm), delegate: MainThreadBound::new(delegate, mtm), + window_type, }) } @@ -95,6 +98,10 @@ impl rwh_06::HasWindowHandle for Window { } impl CoreWindow for Window { + fn window_type(&self) -> WindowType { + self.window_type + } + fn id(&self) -> winit_core::window::WindowId { self.maybe_wait_on_main(|delegate| delegate.id()) } diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 226d80940..9a6515016 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -52,7 +52,7 @@ use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider}; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, - UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, + UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType, }; use super::app_state::AppState; @@ -112,6 +112,7 @@ pub(crate) struct State { is_simple_fullscreen: Cell, saved_style: Cell>, is_borderless_game: Cell, + is_popup: Cell, } define_class!( @@ -655,6 +656,7 @@ fn new_window( app_state: &Rc, attrs: &WindowAttributes, macos_attrs: &WindowAttributesMacOS, + is_popup: bool, mtm: MainThreadMarker, ) -> Option> { autoreleasepool(|_| { @@ -681,6 +683,10 @@ fn new_window( None => NSSize::new(800.0, 600.0), }; let position = match attrs.position { + // A popup's position is parent-relative; it's applied in `WindowDelegate::new` + // (after the delegate exists) via the shared translation in + // `set_outer_position`. + _ if is_popup => NSPoint::new(0.0, 0.0), Some(position) => { let position = position.to_logical(scale_factor); flip_window_screen_coordinates(NSRect::new( @@ -738,6 +744,8 @@ fn new_window( // confusing issues with the window not being properly activated. // // Winit ensures this by not allowing access to `ActiveEventLoop` before handling events. + // Panels (including popups) are non-activating so they don't steal key focus + // from their parent (matching menu/combobox semantics). let window: Retained = if macos_attrs.panel { masks |= NSWindowStyleMask::NonactivatingPanel; @@ -821,7 +829,8 @@ fn new_window( if !macos_attrs.has_shadow { window.setHasShadow(false); } - if attrs.position.is_none() { + // Popups are positioned relative to their parent in `WindowDelegate::new`. + if attrs.position.is_none() && !is_popup { window.center(); } @@ -884,13 +893,27 @@ impl WindowDelegate { mut attrs: WindowAttributes, mtm: MainThreadMarker, ) -> Result, RequestError> { - let macos_attrs = attrs + let mut macos_attrs = attrs .platform .take() .and_then(|attrs| attrs.cast::().ok()) .unwrap_or_default(); - let window = new_window(app_state, &attrs, &macos_attrs, mtm) + let is_popup = matches!(attrs.window_type(), WindowType::Popup); + if is_popup { + // A popup is an undecorated, non-activating panel with no titlebar buttons. Model it + // as such so it flows through the existing borderless + panel paths in `new_window` + // instead of needing dedicated branches. + attrs.decorations = false; + attrs.enabled_buttons = WindowButtons::empty(); + // Unless grab_keyboard is requested, use a non-activating panel so the popup doesn't + // steal keyboard focus from the parent window. + if !attrs.active { + macos_attrs.panel = true; + } + } + + let window = new_window(app_state, &attrs, &macos_attrs, is_popup, mtm) .ok_or_else(|| os_error!("couldn't create `NSWindow`"))?; match attrs.parent_window() { @@ -909,6 +932,11 @@ impl WindowDelegate { unsafe { parent.addChildWindow_ordered(&window, NSWindowOrderingMode::Above) }; }, Some(raw) => panic!("invalid raw window handle {raw:?} on macOS"), + None if is_popup => { + return Err(RequestError::NotSupported(NotSupportedError::new( + "a popup window requires a parent window", + ))); + }, None => (), } @@ -946,6 +974,7 @@ impl WindowDelegate { is_simple_fullscreen: Cell::new(false), saved_style: Cell::new(None), is_borderless_game: Cell::new(macos_attrs.borderless_game), + is_popup: Cell::new(is_popup), }); let delegate: Retained = unsafe { msg_send![super(delegate), init] }; @@ -992,6 +1021,14 @@ impl WindowDelegate { delegate.set_window_level(attrs.window_level); + // The popup position is relative to the parent window, and the parent is only + // attached above, so apply the (translated) position now. Default to the parent's + // content top-left when no position was given. + if is_popup { + let position = attrs.position.unwrap_or_else(|| LogicalPosition::new(0.0, 0.0).into()); + delegate.set_outer_position(position); + } + delegate.set_cursor(attrs.cursor); // Set fullscreen mode after we setup everything @@ -1147,7 +1184,9 @@ impl WindowDelegate { pub fn outer_position(&self) -> Result, RequestError> { let position = flip_window_screen_coordinates(self.window().frame()); - Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) + let position = + self.translate_popup_position_to_parent(LogicalPosition::new(position.x, position.y)); + Ok(position.to_physical(self.scale_factor())) } pub fn surface_position(&self) -> PhysicalPosition { @@ -1171,6 +1210,7 @@ impl WindowDelegate { pub fn set_outer_position(&self, position: Position) { let position = position.to_logical(self.scale_factor()); + let position = self.translate_popup_position(position); let point = flip_window_screen_coordinates(NSRect::new( NSPoint::new(position.x, position.y), self.window().frame().size, @@ -1178,6 +1218,40 @@ impl WindowDelegate { self.window().setFrameOrigin(point); } + /// Popups receive their position relative to the top-left of the parent window's + /// content area (matching the Win32 and Wayland backends). macOS positions windows + /// in global screen coordinates, so add the parent content area's origin. + fn translate_popup_position(&self, position: LogicalPosition) -> LogicalPosition { + if !self.ivars().is_popup.get() { + return position; + } + let Some(parent) = self.window().parentWindow() else { + return position; + }; + let parent_origin = + flip_window_screen_coordinates(parent.contentRectForFrameRect(parent.frame())); + LogicalPosition::new(parent_origin.x + position.x, parent_origin.y + position.y) + } + + /// Inverse of [`Self::translate_popup_position`]. Popups report their position + /// relative to the top-left of the parent window's content area (matching the + /// Win32 and Wayland backends), so subtract the parent content area's origin from + /// the global screen coordinates. Non-popup windows are returned unchanged. + fn translate_popup_position_to_parent( + &self, + position: LogicalPosition, + ) -> LogicalPosition { + if !self.ivars().is_popup.get() { + return position; + } + let Some(parent) = self.window().parentWindow() else { + return position; + }; + let parent_origin = + flip_window_screen_coordinates(parent.contentRectForFrameRect(parent.frame())); + LogicalPosition::new(position.x - parent_origin.x, position.y - parent_origin.y) + } + #[inline] pub fn surface_size(&self) -> PhysicalSize { self.view().surface_size() diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 896c5ea16..44e3eddba 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -46,6 +46,26 @@ impl fmt::Debug for WindowId { } } +/// The role of a window, used to request platform-specific window behavior. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub enum WindowType { + /// A normal, top-level window. + #[default] + Window, + /// A short-lived window anchored to a parent, such as a menu, combo-box dropdown, or + /// tooltip. Requires a parent set via [`WindowAttributes::with_parent_window`], and its + /// position is interpreted relative to that parent. + /// + /// ## Platform-specific + /// + /// - **macOS:** A borderless, non-activating child window. The system does *not* draw rounded + /// corners for it. To get a rounded, native-looking popup, create it transparent (via + /// [`WindowAttributes::with_transparent`]) and render the round border yourself. + /// - **X11, Web, Android, iOS, Orbital:** An error is returned because it is not implemented. + Popup, +} + /// Attributes used when creating a window. #[derive(Debug)] #[non_exhaustive] @@ -54,6 +74,11 @@ pub struct WindowAttributes { pub min_surface_size: Option, pub max_surface_size: Option, pub surface_resize_increments: Option, + /// The initial position of the window in screen coordinates. + /// + /// For popups, this position is relative to the parent window. + /// + /// **Wayland:** See `WindowAttributesWayland` for more options to position a popup. pub position: Option, pub resizable: bool, pub enabled_buttons: WindowButtons, @@ -67,11 +92,19 @@ pub struct WindowAttributes { pub preferred_theme: Option, pub content_protected: bool, pub window_level: WindowLevel, + /// Whether the window should be activated (focused) when shown. + /// + /// For [`WindowType::Popup`] windows this also controls keyboard grabbing: + /// - `true` — the popup captures keyboard input (Win32: omits `WS_EX_NOACTIVATE`, macOS: uses + /// an activating `NSWindow`, Wayland: issues `xdg_popup.grab`). + /// - `false` — the popup is non-activating and the parent window keeps focus (Win32: + /// `WS_EX_NOACTIVATE`, macOS: `NSWindowStyleMask::NonactivatingPanel`, Wayland: no grab). pub active: bool, pub cursor: Cursor, pub(crate) parent_window: Option, pub fullscreen: Option, pub platform: Option>, + pub window_type: WindowType, } impl WindowAttributes { @@ -145,6 +178,8 @@ impl WindowAttributes { /// position. There may be a small gap between this position and the window due to the /// specifics of the Window Manager. /// - **X11:** The top left corner of the window, the window's "outer" position. + /// - **Wayland:** The top left corner of the window if the window type is `WindowType::Popup` + /// otherwise ignored /// - **Others:** Ignored. #[inline] pub fn with_position>(mut self, position: P) -> Self { @@ -324,9 +359,13 @@ impl WindowAttributes { /// The window should be assumed as not focused by default /// following by the [`WindowEvent::Focused`]. /// + /// For [`WindowType::Popup`] windows, also controls keyboard grabbing — see + /// [`WindowAttributes::active`] for details. + /// /// ## Platform-specific: /// - /// **Android / iOS / X11 / Wayland / Orbital:** Unsupported. + /// **Android / iOS / X11 / Orbital:** Unsupported. + /// **Wayland:** Only supported for [`WindowType::Popup`]. /// /// [`WindowEvent::Focused`]: crate::event::WindowEvent::Focused #[inline] @@ -378,6 +417,27 @@ impl WindowAttributes { self.platform = Some(platform); self } + + /// Sets the [`WindowType`] (window vs. popup). + /// + /// Used by the Windows, Wayland and macOS backends; on X11 [`WindowType::Popup`] is not + /// implemented and window creation returns an error. + /// If the type is [`WindowType::Popup`], the parent must also be set via + /// [`with_parent_window`](Self::with_parent_window), and the position is interpreted + /// relative to that parent. + /// + /// See [`WindowType::Popup`] for the per-platform behavior, including how to obtain a + /// rounded, native-looking popup on macOS. + pub fn with_window_type(mut self, window_type: WindowType) -> Self { + self.window_type = window_type; + self + } + + /// Returns if the window type is a popup or a normal window + #[inline] + pub fn window_type(&self) -> WindowType { + self.window_type + } } impl Clone for WindowAttributes { @@ -405,6 +465,7 @@ impl Clone for WindowAttributes { parent_window: self.parent_window.clone(), fullscreen: self.fullscreen.clone(), platform: self.platform.as_ref().map(|platform| platform.box_clone()), + window_type: self.window_type, } } } @@ -435,6 +496,7 @@ impl Default for WindowAttributes { platform: Default::default(), cursor: Cursor::default(), blur: Default::default(), + window_type: Default::default(), } } } @@ -476,6 +538,9 @@ impl_dyn_casting!(PlatformWindowAttributes); /// **Web:** The [`Window`], which is represented by a `HTMLElementCanvas`, can /// not be closed by dropping the [`Window`]. pub trait Window: AsAny + Send + Sync + fmt::Debug { + /// Returns the window type of this window + fn window_type(&self) -> WindowType; + /// Returns an identifier unique to the window. fn id(&self) -> WindowId; @@ -648,10 +713,18 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// The coordinates can be negative if the top-left hand corner of the window is outside /// of the visible screen region, or on another monitor than the primary. /// + /// For a [`WindowType::Popup`] with a parent, the position is instead reported relative to the + /// top-left hand corner of the parent window's content area, mirroring the coordinate system + /// used by [`Window::set_outer_position`]. + /// /// ## Platform-specific /// /// - **Web:** Returns the top-left coordinates relative to the viewport. - /// - **Android / Wayland:** Always returns [`RequestError::NotSupported`]. + /// - **Android:** Always returns [`RequestError::NotSupported`]. + /// - **Wayland:** For a top-level window this always returns [`RequestError::NotSupported`], + /// since the compositor does not report absolute positions. For a [`WindowType::Popup`] the + /// compositor-decided position relative to the parent is returned once the popup has been + /// configured (before that, [`RequestError::NotSupported`]). fn outer_position(&self) -> Result, RequestError>; /// Sets the position of the window on the desktop. diff --git a/winit-orbital/src/window.rs b/winit-orbital/src/window.rs index 728c262a7..cc4fe4e19 100644 --- a/winit-orbital/src/window.rs +++ b/winit-orbital/src/window.rs @@ -37,6 +37,12 @@ impl Window { el: &ActiveEventLoop, attrs: window::WindowAttributes, ) -> Result { + if attrs.window_type() == window::WindowType::Popup { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Popups are not implemented for Orbital", + ))); + } + let scale = 1.; let (x, y) = if let Some(pos) = attrs.position { @@ -154,6 +160,10 @@ impl Window { } impl CoreWindow for Window { + fn window_type(&self) -> window::WindowType { + window::WindowType::Window + } + fn id(&self) -> WindowId { WindowId::from_raw(self.window_socket.fd()) } diff --git a/winit-uikit/src/window.rs b/winit-uikit/src/window.rs index d0576f949..bd2e77657 100644 --- a/winit-uikit/src/window.rs +++ b/winit-uikit/src/window.rs @@ -25,7 +25,7 @@ use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, - WindowLevel, + WindowLevel, WindowType, }; use super::app_state::EventWrapper; @@ -490,6 +490,12 @@ impl Window { event_loop: &ActiveEventLoop, mut window_attributes: WindowAttributes, ) -> Result { + if window_attributes.window_type() == WindowType::Popup { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Popups are not implemented for iOS", + ))); + } + let mtm = event_loop.mtm; if window_attributes.min_surface_size.is_some() { @@ -590,6 +596,10 @@ impl rwh_06::HasWindowHandle for Window { } impl CoreWindow for Window { + fn window_type(&self) -> WindowType { + WindowType::Window + } + fn id(&self) -> winit_core::window::WindowId { self.maybe_wait_on_main(|delegate| delegate.id()) } diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 456bfad84..ed0f66925 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -36,7 +36,7 @@ use winit_core::event_loop::{ }; use winit_core::icon::RgbaIcon; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; -use winit_core::window::Theme; +use winit_core::window::{Theme, WindowType}; use crate::dnd::{MimeData, dnd_action_winit_to_wl}; use crate::types::cursor::WaylandCustomCursor; @@ -322,6 +322,35 @@ impl EventLoop { self.single_iteration(app, cause); } + /// Recursive closing all windows from the child to the parent + fn find_windows_to_close( + window_id: &WindowId, + state: &mut WinitState, + out: &mut Vec, + ) -> bool { + if !state.window_requests.get_mut().get(window_id).unwrap().closed.load(Ordering::Relaxed) { + return false; + } + + out.push(*window_id); + fn window_to_close(window_id: &WindowId, out: &mut Vec, state: &mut WinitState) { + // We don't need to check here if it should be closed, because if the parent should + // be closed all children must be closed as well + let Some(window_state) = state.windows.get_mut().get(window_id) else { + return; + }; + let children = window_state.lock().unwrap().children().clone(); + // First all children and then all subchildren + out.extend(&children); + for child in children.clone() { + window_to_close(&child, out, state); + } + } + window_to_close(window_id, out, state); + + true + } + fn single_iteration(&mut self, app: &mut A, cause: StartCause) { // NOTE currently just indented to simplify the diff @@ -457,14 +486,33 @@ impl EventLoop { }); for window_id in window_ids.iter() { - let event = self.with_state(|state| { - let window_requests = state.window_requests.get_mut(); - if window_requests.get(window_id).unwrap().take_closed() { - mem::drop(window_requests.remove(window_id)); - mem::drop(state.windows.get_mut().remove(window_id)); - return Some(WindowEvent::Destroyed); - } + if self.with_state(|state| state.window_requests.get_mut().get(window_id).is_none()) { + continue; // The element might not exist anymore so just ignore + } + let mut windows_to_close = Vec::new(); + if self.with_state(|state| { + Self::find_windows_to_close(window_id, state, &mut windows_to_close) + }) { + for w in windows_to_close.into_iter().rev() { + self.with_state(|state| { + let parent = + state.windows.get_mut().get_mut(&w).unwrap().lock().unwrap().parent(); + if let Some(p) = parent.and_then(|p| state.windows.get_mut().get_mut(&p)) { + p.lock().unwrap().remove_child(&w) + } + + let window_requests = state.window_requests.get_mut(); + window_requests.get(&w).unwrap().take_closed(); + mem::drop(window_requests.remove(&w)); + mem::drop(state.windows.get_mut().remove(&w)); + }); + app.window_event(&self.active_event_loop, w, WindowEvent::Destroyed); + } + continue; + } + + let event = self.with_state(|state| { let mut window = state.windows.get_mut().get_mut(window_id).unwrap().lock().unwrap(); @@ -474,6 +522,7 @@ impl EventLoop { // Reset the frame callbacks state. window.frame_callback_reset(); + let window_requests = state.window_requests.get_mut(); let mut redraw_requested = window_requests.get(window_id).unwrap().take_redraw_requested(); @@ -662,8 +711,17 @@ impl RootActiveEventLoop for ActiveEventLoop { &self, window_attributes: winit_core::window::WindowAttributes, ) -> Result, RequestError> { - let window = crate::Window::new(self, window_attributes)?; - Ok(Box::new(window)) + match window_attributes.window_type() { + WindowType::Window => { + let window = crate::Window::new(self, window_attributes)?; + Ok(Box::new(window)) + }, + WindowType::Popup => { + let popup = crate::Popup::new(self, window_attributes)?; + Ok(Box::new(popup)) + }, + _ => Err(RequestError::NotSupported(NotSupportedError::new("Unsupported window type"))), + } } fn available_monitors(&self) -> Box> { diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 52ff28885..de640aed9 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -21,7 +21,7 @@ use std::ffi::c_void; use std::hash::BuildHasher; use std::ptr::NonNull; -use dpi::{LogicalSize, PhysicalSize}; +use dpi::{LogicalSize, PhysicalSize, Position, Size}; use sctk::reexports::client::Proxy; use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::protocol::wl_surface::WlSurface; @@ -40,6 +40,7 @@ macro_rules! os_error { mod dnd; mod event_loop; mod output; +mod popup; mod seat; mod state; mod types; @@ -47,6 +48,7 @@ mod window; pub use self::dnd::{DataOffer, DragSource, MimeData, MimeType}; pub use self::event_loop::{ActiveEventLoop, EventLoop}; +pub use self::popup::Popup; pub use self::window::Window; /// Additional methods on [`ActiveEventLoop`] that are specific to Wayland. @@ -95,6 +97,120 @@ impl WindowExtWayland for dyn CoreWindow + '_ { } } +/// Additional methods on [`Popup`] that are specific to Wayland. +pub trait PopupExtWayland { + fn anchor_rect(&self) -> Option<(impl Into, impl Into)>; + + /// Sets the anchor edge of the parent surface the popup is positioned relative to. + /// + /// See [`PopupAnchor`] for the available edges and corners. + fn set_anchor(&self, anchor: PopupAnchor); + + /// Sets the anchor rectangle within the parent surface the popup is positioned relative to. + /// + /// `position` is the top-left corner of the rectangle relative to the parent window's content + /// area, and `size` its dimensions. + fn set_anchor_rect(&self, position: impl Into, size: impl Into); + + /// Sets how the compositor should reposition the popup when it would be constrained by screen + /// edges. + /// + /// See [`PopupConstraintAdjustment`] for the available adjustment flags. + fn set_constraint_adjustment(&self, constraint_adjustment: PopupConstraintAdjustment); + + /// Sets the direction the popup surface extends from the anchor point. + /// + /// See [`PopupGravity`] for the available directions. + fn set_gravity(&self, gravity: PopupGravity); + + /// Set the popup position relative to the anchor rect + fn set_positioner_offset(&self, position: impl Into); +} + +impl PopupExtWayland for dyn CoreWindow + '_ { + fn set_anchor(&self, anchor: PopupAnchor) { + if let Some(popup) = self.cast_ref::() { + popup.set_anchor(anchor); + } + } + + fn anchor_rect(&self) -> Option<(impl Into, impl Into)> { + if let Some(popup) = self.cast_ref::() { popup.anchor_rect() } else { None } + } + + fn set_anchor_rect(&self, position: impl Into, size: impl Into) { + if let Some(popup) = self.cast_ref::() { + popup.set_anchor_rect(position, size); + } + } + + fn set_constraint_adjustment(&self, constraint_adjustment: PopupConstraintAdjustment) { + if let Some(popup) = self.cast_ref::() { + popup.set_constraint_adjustment(constraint_adjustment); + } + } + + fn set_gravity(&self, gravity: PopupGravity) { + if let Some(popup) = self.cast_ref::() { + popup.set_gravity(gravity); + } + } + + fn set_positioner_offset(&self, position: impl Into) { + if let Some(popup) = self.cast_ref::() { + popup.set_positioner_offset(position); + } + } +} + +/// Anchor rect within the parent surface +/// See: https://wayland.app/protocols/xdg-shell#xdg_positioner:request:set_anchor_rect +#[derive(Debug, Clone, Copy, Default, PartialEq)] +#[non_exhaustive] +pub enum PopupAnchor { + #[default] + None, + Top, + Bottom, + Left, + Right, + TopLeft, + BottomLeft, + TopRight, + BottomRight, +} + +/// Defines in what direction a surface should be positioned +/// See: https://wayland.app/protocols/xdg-shell#xdg_positioner:request:set_gravity +#[derive(Debug, Clone, Copy, Default, PartialEq)] +#[non_exhaustive] +pub enum PopupGravity { + #[default] + None, + Top, + Bottom, + Left, + Right, + TopLeft, + BottomLeft, + TopRight, + BottomRight, +} + +bitflags::bitflags! { + /// Specify how the window should be positioned if the originally intended position caused the + /// surface to be constrained See: https://wayland.app/protocols/xdg-shell#xdg_positioner:request:set_constraint_adjustment + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] + pub struct PopupConstraintAdjustment: u32 { + const SLIDE_X = 1 << 0; + const SLIDE_Y = 1 << 1; + const FLIP_X = 1 << 2; + const FLIP_Y = 1 << 3; + const RESIZE_X = 1 << 4; + const RESIZE_Y = 1 << 5; + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct ApplicationName { pub(crate) general: String, @@ -107,6 +223,13 @@ pub struct WindowAttributesWayland { pub(crate) name: Option, pub(crate) activation_token: Option, pub(crate) prefer_csd: bool, + pub(crate) anchor: Option, + pub(crate) anchor_rect: Option<(Position, Size)>, + /// Only for Popup: The offset of the popup to the + /// anchor rect + pub(crate) positioner_offset: Option, + pub(crate) gravity: Option, + pub(crate) constraint_adjustment: Option, } impl WindowAttributesWayland { @@ -123,6 +246,11 @@ impl WindowAttributesWayland { self } + /// Sets an activation token to use when creating the window. + /// + /// The activation token allows the compositor to grant focus to the new window, + /// overriding focus-stealing prevention. Obtain a token via + /// [`ActiveEventLoop::request_activation_token`]. #[inline] pub fn with_activation_token(mut self, token: ActivationToken) -> Self { self.activation_token = Some(token); @@ -140,6 +268,63 @@ impl WindowAttributesWayland { self.prefer_csd = prefer_csd; self } + + /// Sets the edge or corner of the anchor rectangle the popup is attached to. + /// + /// Combined with [`with_gravity`](Self::with_gravity), this controls which corner/edge of the + /// anchor rectangle the popup is pinned to. Has no effect unless the window is created as a + /// [`Popup`](winit_core::window::WindowType::Popup). + #[inline] + pub fn with_anchor(mut self, anchor: PopupAnchor) -> Self { + self.anchor = Some(anchor); + self + } + + /// Set the anchor rectangle the popup is positioned relative to. + /// + /// `position` is the top-left corner of the rectangle relative to the parent window's content + /// area, and `size` its dimensions. Defaults to a `1x1` rectangle at the content origin. + /// This value overwrites the position value set with `with_position` in the window attributes + #[inline] + pub fn with_anchor_rect( + mut self, + position: impl Into, + size: impl Into, + ) -> Self { + self.anchor_rect = Some((position.into(), size.into())); + self + } + + /// Set the popup position relative to the anchor rect + pub fn with_positioner_offset(mut self, position: impl Into) -> Self { + self.positioner_offset = Some(position.into()); + self + } + + /// Sets how the compositor should reposition the popup when it would be constrained. + /// + /// The flags in [`PopupConstraintAdjustment`] can be combined to allow sliding, flipping, + /// and/or resizing the popup independently on each axis. Has no effect unless the window is + /// created as a [`Popup`](winit_core::window::WindowType::Popup). + #[inline] + pub fn with_constraint_adjustment( + mut self, + constraint_adjustment: PopupConstraintAdjustment, + ) -> Self { + self.constraint_adjustment = Some(constraint_adjustment); + self + } + + /// Sets the direction the popup surface extends away from the anchor point. + /// + /// Combined with [`with_anchor`](Self::with_anchor), this determines the final position of the + /// popup relative to its anchor rectangle. Has no effect unless the window is created as a + /// [`Popup`](winit_core::window::WindowType::Popup). + #[inline] + pub fn with_gravity(mut self, gravity: PopupGravity) -> Self { + self.gravity = Some(gravity); + self + } } impl PlatformWindowAttributes for WindowAttributesWayland { @@ -195,3 +380,64 @@ fn image_to_buffer( Ok(buffer) } + +impl From for wayland_protocols::xdg::shell::client::xdg_positioner::Gravity { + fn from(value: PopupGravity) -> Self { + use wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; + match value { + PopupGravity::None => Gravity::None, + PopupGravity::Top => Gravity::Top, + PopupGravity::Bottom => Gravity::Bottom, + PopupGravity::Left => Gravity::Left, + PopupGravity::Right => Gravity::Right, + PopupGravity::TopLeft => Gravity::TopLeft, + PopupGravity::BottomLeft => Gravity::BottomLeft, + PopupGravity::TopRight => Gravity::TopRight, + PopupGravity::BottomRight => Gravity::BottomRight, + } + } +} + +impl From for wayland_protocols::xdg::shell::client::xdg_positioner::Anchor { + fn from(value: PopupAnchor) -> Self { + use wayland_protocols::xdg::shell::client::xdg_positioner::Anchor; + match value { + PopupAnchor::None => Anchor::None, + PopupAnchor::Top => Anchor::Top, + PopupAnchor::Bottom => Anchor::Bottom, + PopupAnchor::Left => Anchor::Left, + PopupAnchor::Right => Anchor::Right, + PopupAnchor::TopLeft => Anchor::TopLeft, + PopupAnchor::BottomLeft => Anchor::BottomLeft, + PopupAnchor::TopRight => Anchor::TopRight, + PopupAnchor::BottomRight => Anchor::BottomRight, + } + } +} + +impl From + for wayland_protocols::xdg::shell::client::xdg_positioner::ConstraintAdjustment +{ + fn from(value: PopupConstraintAdjustment) -> Self { + use wayland_protocols::xdg::shell::client::xdg_positioner::ConstraintAdjustment; + + const _: () = { + assert!( + PopupConstraintAdjustment::SLIDE_X.bits() == ConstraintAdjustment::SlideX.bits() + ); + assert!( + PopupConstraintAdjustment::SLIDE_Y.bits() == ConstraintAdjustment::SlideY.bits() + ); + assert!(PopupConstraintAdjustment::FLIP_X.bits() == ConstraintAdjustment::FlipX.bits()); + assert!(PopupConstraintAdjustment::FLIP_Y.bits() == ConstraintAdjustment::FlipY.bits()); + assert!( + PopupConstraintAdjustment::RESIZE_X.bits() == ConstraintAdjustment::ResizeX.bits() + ); + assert!( + PopupConstraintAdjustment::RESIZE_Y.bits() == ConstraintAdjustment::ResizeY.bits() + ); + }; + + ConstraintAdjustment::from_bits_retain(value.bits()) + } +} diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs new file mode 100644 index 000000000..cdfd20909 --- /dev/null +++ b/winit-wayland/src/popup.rs @@ -0,0 +1,747 @@ +use core::sync::atomic::Ordering; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex, Weak}; + +use dpi::{ + LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size, +}; +use rwh_06::RawWindowHandle; +use sctk::compositor::SurfaceData; +use sctk::shell::WaylandSurface; +use sctk::shell::xdg::popup::Popup as SctkPopup; +use sctk::shell::xdg::{XdgPositioner, XdgSurface}; +use wayland_client::Proxy; +use wayland_client::protocol::wl_display::WlDisplay; +use winit_core::cursor::Cursor; +use winit_core::error::{NotSupportedError, RequestError}; +use winit_core::event::{Ime, WindowEvent}; +use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; +use winit_core::window::{ + CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, + UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, + WindowLevel, +}; + +use super::ActiveEventLoop; +use super::output::MonitorHandle; +use crate::window::Handles; +use crate::window::handles::WindowRequests; +use crate::window::state::{WindowState, WindowType}; +use crate::{PopupExtWayland, WindowAttributesWayland}; + +#[derive(Debug)] +pub struct Popup { + /// The state of the popup. + /// The only single truth of the state is stored + /// in the event loop state, because if the server decides to destroy the popup + /// we cannot use it anymore + popup_state: Weak>, + + /// Window id. + window_id: WindowId, + + /// The wayland display used solely for raw window handle. + #[allow(dead_code)] + display: WlDisplay, + + handles: Handles, +} + +impl Popup { + pub(crate) fn new( + event_loop_window_target: &ActiveEventLoop, + mut attributes: WindowAttributes, + ) -> Result { + fn error(message: &'static str) -> RequestError { + RequestError::NotSupported(NotSupportedError::new(message)) + } + + let parent_window_handle = + attributes.parent_window().ok_or(error("Popup without a parent is not supported!"))?; + if let RawWindowHandle::Wayland(parent_window_handle) = parent_window_handle { + let queue_handle = event_loop_window_target.queue_handle.clone(); + let mut state = event_loop_window_target.state.borrow_mut(); + let monitors = state.monitors.clone(); + let xdg_activation = state + .xdg_activation + .as_ref() + .map(|activation_state| activation_state.global().clone()); + let positioner = XdgPositioner::new(&state.xdg_shell) + .map_err(|_| error("Failed to create positioner"))?; + let parent_window_id = + WindowId::from_raw(parent_window_handle.surface.as_ptr() as usize); + let (popup, popup_state) = if let Some(parent_window_state) = + state.windows.borrow().get(&parent_window_id) + { + let wayland_attributes = attributes + .platform + .as_ref() + .and_then(|p| p.cast_ref::()) + .cloned() + .unwrap_or_default(); + let WindowAttributesWayland { + gravity, + anchor, + anchor_rect, + constraint_adjustment, + positioner_offset, + .. + } = wayland_attributes; + let grab_keyboard = attributes.active; + + let mut parent_window_state = parent_window_state.lock().unwrap(); + + // Use the scale factor and xdg geometry of the parent. + let scale_factor = parent_window_state.scale_factor(); + let size = attributes + .surface_size + .ok_or(error("Invalid size for popup"))? + .to_logical(scale_factor); + if size.width == 0_i32 || size.height == 0_i32 { + return Err(error("The popups size must not be zero")); + } + + // Anchoring + // The anchor rect is relative to the parent window geometry, so we need to subtract + // the geometry origin from the position to get the correct anchor rect. + // This is important for client side decorations + let geometry_origin = parent_window_state.content_surface_origin(); + let anchor_position = LogicalPosition::new(-geometry_origin.x, -geometry_origin.y); + positioner.set_anchor(anchor.unwrap_or(crate::PopupAnchor::TopLeft).into()); + positioner.set_gravity(gravity.unwrap_or(crate::PopupGravity::BottomRight).into()); + constraint_adjustment + .inspect(|c| positioner.set_constraint_adjustment((*c).into())); + let (anchor_rect_position, anchor_rect_size) = match anchor_rect { + Some((position, size)) => ( + position.to_logical::(scale_factor), + size.to_logical::(scale_factor), + ), + None => { + // anchor rect was not specified use attributes.position + let pos: LogicalPosition = attributes + .position + .map(|position| position.to_logical(scale_factor)) + .unwrap_or_default(); + (pos, LogicalSize::new(1, 1)) + }, + }; + let anchor_rect = ( + LogicalPosition::new( + anchor_rect_position.x + anchor_position.x, + anchor_rect_position.y + anchor_position.y, + ), + LogicalSize::new(anchor_rect_size.width.max(1), anchor_rect_size.height.max(1)), + ); + positioner.set_anchor_rect( + anchor_rect.0.x, + anchor_rect.0.y, + anchor_rect.1.width, + anchor_rect.1.height, + ); + positioner_offset.inspect(|o| { + let o = o.to_logical(scale_factor); + positioner.set_offset(o.x, o.y); + }); + positioner.set_size(size.width, size.height); + + let parent_surface = parent_window_state.window.xdg_surface(); + let surface = state.compositor_state.create_surface(&queue_handle); + let popup = SctkPopup::from_surface( + Some(parent_surface), + &positioner, + &queue_handle, + surface.clone(), + &state.xdg_shell, + ) + .map_err(|_| error("Failed to create popup"))?; + parent_window_state.add_child(super::make_wid(popup.wl_surface())); + drop(parent_window_state); + + let mut popup_state = WindowState::new( + event_loop_window_target, + &state, + size.into(), + WindowType::Popup { + popup: popup.clone(), + positioner, + last_configure: None, + anchor_rect, + parent_origin: geometry_origin, + }, + attributes.preferred_theme, + false, + scale_factor, + Some(parent_window_id), + ); + + // Set transparency hint. + popup_state.set_transparent(attributes.transparent); + + // Set blur. + let _ = popup_state.set_blur(attributes.blur); + + let WindowAttributesWayland { activation_token, .. } = *attributes + .platform + .take() + .and_then(|p| p.cast::().ok()) + .unwrap_or_default(); + + // Activate the window when the token is passed. + if let (Some(xdg_activation), Some(token)) = + (xdg_activation.as_ref(), activation_token) + { + xdg_activation.activate(token.into_raw(), &surface); + } + + // Request a keyboard grab so the compositor routes key events to + // this popup rather than the parent window. Must happen before the + // first commit that maps the surface. + if grab_keyboard { + // Use the seat with the most recent event + let grab = state + .seat_state + .seats() + .filter_map(|seat| { + let serial = state.seats.get(&seat.id())?.latest_serial()?; + Some((seat, serial)) + }) + .max_by_key(|(_, serial)| *serial); + + if let Some((seat, serial)) = grab { + popup.xdg_popup().grab(&seat, serial); + } + } + + popup.wl_surface().commit(); + // popup.commit(); Trait not implemented in Sctk + + let popup_state = Arc::new(Mutex::new(popup_state)); + + (popup, popup_state) + } else { + return Err(error("Parent window id unknown")); + }; + + let window_id = super::make_wid(popup.wl_surface()); + state.windows.get_mut().insert(window_id, popup_state.clone()); + + let window_requests = WindowRequests { + redraw_requested: AtomicBool::new(true), + closed: AtomicBool::new(false), + }; + let window_requests = Arc::new(window_requests); + state.window_requests.get_mut().insert(window_id, window_requests.clone()); + + // Setup the event sync to insert `WindowEvents` right from the window. + let window_events_sink = state.window_events_sink.clone(); + + let mut wayland_source = event_loop_window_target.wayland_dispatcher.as_source_mut(); + let event_queue = wayland_source.queue(); + // Do a roundtrip. + event_queue.roundtrip(&mut state).map_err(|err| os_error!(err))?; + + // XXX Wait for the initial configure to arrive. + while !popup_state.lock().unwrap().is_configured() { + event_queue.blocking_dispatch(&mut state).map_err(|err| os_error!(err))?; + // The compositor may dismiss a popup (e.g. invalid grab serial) by sending + // popup_done before configure. Detect that and bail out instead of looping forever. + if state + .window_compositor_updates + .iter() + .any(|u| u.window_id == window_id && u.close_window) + { + return Err(error("Popup was dismissed by the compositor before configure")); + } + } + + // Wake-up event loop, so it'll send initial redraw requested. + let event_loop_awakener = event_loop_window_target.event_loop_awakener.clone(); + event_loop_awakener.ping(); + + Ok(Self { + popup_state: Arc::downgrade(&popup_state), + window_id, + display: event_loop_window_target.handle.connection.display().clone(), + handles: Handles { + queue_handle, + window_requests, + monitors, + event_loop_awakener, + window_events_sink, + + xdg_activation, + attention_requested: Arc::new(AtomicBool::new(false)), + + compositor: state.compositor_state.clone(), + }, + }) + } else { + Err(RequestError::NotSupported(NotSupportedError::new( + "A Popup requires a parent wayland window handle", + ))) + } + } +} + +impl CoreWindow for Popup { + fn window_type(&self) -> winit_core::window::WindowType { + winit_core::window::WindowType::Popup + } + + fn id(&self) -> WindowId { + self.window_id + } + + fn request_redraw(&self) { + self.handles.request_redraw(); + } + + #[inline] + fn title(&self) -> String { + let Some(s) = self.popup_state.upgrade() else { return String::new() }; + s.lock().unwrap().title().to_owned() + } + + fn pre_present_notify(&self) { + let Some(s) = self.popup_state.upgrade() else { return }; + s.lock().unwrap().request_frame_callback(); + } + + fn reset_dead_keys(&self) { + winit_common::xkb::reset_dead_keys() + } + + fn surface_position(&self) -> PhysicalPosition { + (0, 0).into() + } + + fn outer_position(&self) -> Result, RequestError> { + let s = self + .popup_state + .upgrade() + .ok_or_else(|| NotSupportedError::new("the popup has been destroyed"))?; + let state = s.lock().unwrap(); + if let WindowType::Popup { last_configure: Some(configure), .. } = &state.window { + let (x, y) = configure.position; + return Ok(LogicalPosition::new(x, y).to_physical(state.scale_factor())); + } + Err(NotSupportedError::new("the popup has not been configured yet").into()) + } + + fn set_outer_position(&self, position: Position) { + let Some(s) = self.popup_state.upgrade() else { return }; + let mut state = s.lock().unwrap(); + let scale_factor = state.scale_factor(); + if let WindowType::Popup { popup, positioner, anchor_rect, parent_origin, .. } = + &mut state.window + { + let position = position.to_logical(scale_factor); + anchor_rect.0 = position; + positioner.set_anchor_rect( + anchor_rect.0.x - parent_origin.x, + anchor_rect.0.y - parent_origin.y, + anchor_rect.1.width, + anchor_rect.1.height, + ); + if popup.xdg_popup().version() >= 3 { + popup.reposition(positioner, 0); + } + } + } + + fn surface_size(&self) -> PhysicalSize { + let Some(s) = self.popup_state.upgrade() else { return PhysicalSize::default() }; + let popup_state = s.lock().unwrap(); + let scale_factor = popup_state.scale_factor(); + super::logical_to_physical_rounded(popup_state.surface_size(), scale_factor) + } + + fn request_surface_size(&self, size: Size) -> Option> { + let s = self.popup_state.upgrade()?; + let mut popup_state = s.lock().unwrap(); + let new_size = popup_state.request_surface_size(size); + self.request_redraw(); + Some(new_size) + } + + fn outer_size(&self) -> PhysicalSize { + let Some(s) = self.popup_state.upgrade() else { return PhysicalSize::default() }; + let popup_state = s.lock().unwrap(); + let scale_factor = popup_state.scale_factor(); + super::logical_to_physical_rounded(popup_state.outer_size(), scale_factor) + } + + fn safe_area(&self) -> PhysicalInsets { + PhysicalInsets::new(0, 0, 0, 0) + } + + fn set_min_surface_size(&self, min_size: Option) { + let scale_factor = self.scale_factor(); + let min_size = min_size.map(|size| size.to_logical(scale_factor)); + let Some(s) = self.popup_state.upgrade() else { return }; + s.lock().unwrap().set_min_surface_size(min_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + + /// Set the maximum surface size for the window. + #[inline] + fn set_max_surface_size(&self, max_size: Option) { + let scale_factor = self.scale_factor(); + let max_size = max_size.map(|size| size.to_logical(scale_factor)); + let Some(s) = self.popup_state.upgrade() else { return }; + s.lock().unwrap().set_max_surface_size(max_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + + fn surface_resize_increments(&self) -> Option> { + let s = self.popup_state.upgrade()?; + let popup_state = s.lock().unwrap(); + let scale_factor = popup_state.scale_factor(); + popup_state + .resize_increments() + .map(|size| super::logical_to_physical_rounded(size, scale_factor)) + } + + fn set_surface_resize_increments(&self, increments: Option) { + let Some(s) = self.popup_state.upgrade() else { return }; + let mut popup_state = s.lock().unwrap(); + let scale_factor = popup_state.scale_factor(); + let increments = increments.map(|size| size.to_logical(scale_factor)); + popup_state.set_resize_increments(increments); + } + + fn set_title(&self, title: &str) { + let Some(s) = self.popup_state.upgrade() else { return }; + s.lock().unwrap().set_title(title.to_owned()); + } + + #[inline] + fn set_transparent(&self, transparent: bool) { + let Some(s) = self.popup_state.upgrade() else { return }; + s.lock().unwrap().set_transparent(transparent); + } + + fn set_visible(&self, _visible: bool) { + // Not possible on Wayland. + } + + fn is_visible(&self) -> Option { + None + } + + fn set_resizable(&self, _resizable: bool) { + // A popup cannot be resized with the mouse + } + + fn is_resizable(&self) -> bool { + // A popup cannot be resized with the mouse + false + } + + fn set_enabled_buttons(&self, _buttons: WindowButtons) { + // TODO(kchibisov) v5 of the xdg_shell allows that. + } + + fn enabled_buttons(&self) -> WindowButtons { + // TODO(kchibisov) v5 of the xdg_shell allows that. + WindowButtons::all() + } + + fn set_minimized(&self, _minimized: bool) { + // Not possible for popups + } + + fn is_minimized(&self) -> Option { + // XXX clients don't know whether they are minimized or not. + None + } + + fn set_maximized(&self, _maximized: bool) { + // Not possible for popups + } + + fn is_maximized(&self) -> bool { + // Not possible for popups + false + } + + fn set_fullscreen(&self, _fullscreen: Option) { + // Not possible for popups + } + + fn fullscreen(&self) -> Option { + None + } + + #[inline] + fn scale_factor(&self) -> f64 { + let Some(s) = self.popup_state.upgrade() else { return 1.0 }; + s.lock().unwrap().scale_factor() + } + + #[inline] + fn set_blur(&self, blur: bool) { + let Some(s) = self.popup_state.upgrade() else { return }; + if s.lock().unwrap().set_blur(blur) { + self.request_redraw(); + } + } + + #[inline] + fn set_decorations(&self, _decorate: bool) { + // Popup does not support decorations + } + + #[inline] + fn is_decorated(&self) -> bool { + // Popup does not support decorations + false + } + + fn set_window_level(&self, _level: WindowLevel) { + // Popup does not have a window level + } + + fn set_window_icon(&self, _window_icon: Option) { + // Popup does not have a window icon + } + + #[inline] + fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> { + let Some(s) = self.popup_state.upgrade() else { return Ok(()) }; + let state_changed = s.lock().unwrap().request_ime_update(request)?; + + if let Some(allowed) = state_changed { + let event = WindowEvent::Ime(if allowed { Ime::Enabled } else { Ime::Disabled }); + self.handles.push_window_event(event, self.window_id); + } + + Ok(()) + } + + #[inline] + fn ime_capabilities(&self) -> Option { + let s = self.popup_state.upgrade()?; + s.lock().unwrap().ime_allowed() + } + + fn focus_window(&self) {} + + fn has_focus(&self) -> bool { + let Some(s) = self.popup_state.upgrade() else { return false }; + s.lock().unwrap().has_focus() + } + + fn request_user_attention(&self, request_type: Option) { + if let Some(state) = self.popup_state.upgrade() { + let state = state.lock().unwrap(); + let surface = state.window.wl_surface(); + self.handles.request_user_attention(surface, request_type); + } + } + + fn set_theme(&self, _theme: Option) { + // A popup does not have a frame + } + + fn theme(&self) -> Option { + // A popup does not have a frame + None + } + + fn set_content_protected(&self, _protected: bool) {} + + fn set_cursor(&self, cursor: Cursor) { + let Some(s) = self.popup_state.upgrade() else { return }; + let mut popup_state = s.lock().unwrap(); + match cursor { + Cursor::Icon(icon) => popup_state.set_cursor(icon), + Cursor::Custom(cursor) => popup_state.set_custom_cursor(cursor), + } + } + + fn set_cursor_position(&self, position: Position) -> Result<(), RequestError> { + let Some(s) = self.popup_state.upgrade() else { return Err(RequestError::Ignored) }; + let scale_factor = s.lock().unwrap().scale_factor(); + let position = position.to_logical(scale_factor); + s.lock() + .unwrap() + .set_cursor_position(position) + // Request redraw on success, since the state is double buffered. + .map(|_| self.request_redraw()) + } + + fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), RequestError> { + let Some(s) = self.popup_state.upgrade() else { return Err(RequestError::Ignored) }; + s.lock().unwrap().set_cursor_grab(mode) + } + + fn set_cursor_visible(&self, visible: bool) { + let Some(s) = self.popup_state.upgrade() else { return }; + s.lock().unwrap().set_cursor_visible(visible); + } + + fn drag_window(&self) -> Result<(), RequestError> { + // Popup does not support dragging + Err(RequestError::Ignored) + } + + fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), RequestError> { + // Popup does not support dragging + Err(RequestError::Ignored) + } + + fn show_window_menu(&self, _position: Position) { + // A popup does not have a menu + } + + fn set_cursor_hittest(&self, hittest: bool) -> Result<(), RequestError> { + let Some(state) = self.popup_state.upgrade() else { + return Err(RequestError::Ignored); + }; + + self.handles.set_cursor_hittest(state.lock().unwrap().window.wl_surface(), hittest) + } + + fn current_monitor(&self) -> Option { + let state = self.popup_state.upgrade()?; + let state = state.lock().unwrap(); + let data = state.window.wl_surface().data::>()?; + data.outputs() + .next() + .map(MonitorHandle::new) + .map(|monitor| CoreMonitorHandle(Arc::new(monitor))) + } + + fn available_monitors(&self) -> Box> { + self.handles.available_monitors() + } + + fn primary_monitor(&self) -> Option { + // NOTE: There's no such concept on Wayland. + None + } + + /// Get the raw-window-handle v0.6 display handle. + fn rwh_06_display_handle(&self) -> &dyn rwh_06::HasDisplayHandle { + self + } + + /// Get the raw-window-handle v0.6 window handle. + fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle { + self + } +} + +impl Drop for Popup { + fn drop(&mut self) { + self.handles.window_requests.closed.store(true, Ordering::Relaxed); + self.handles.event_loop_awakener.ping(); + } +} + +impl rwh_06::HasWindowHandle for Popup { + fn window_handle(&self) -> Result, rwh_06::HandleError> { + let state = self.popup_state.upgrade().ok_or(rwh_06::HandleError::Unavailable)?; + let raw = rwh_06::WaylandWindowHandle::new({ + let ptr = state.lock().unwrap().window.wl_surface().id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_surface will never be null") + }); + + unsafe { Ok(rwh_06::WindowHandle::borrow_raw(raw.into())) } + } +} + +impl rwh_06::HasDisplayHandle for Popup { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + if self.popup_state.upgrade().is_none() { + return Err(rwh_06::HandleError::Unavailable); + }; + let raw = rwh_06::WaylandDisplayHandle::new({ + let ptr = self.display.id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_proxy should never be null") + }); + + unsafe { Ok(rwh_06::DisplayHandle::borrow_raw(raw.into())) } + } +} + +impl PopupExtWayland for Popup { + fn set_anchor(&self, anchor: crate::PopupAnchor) { + let Some(state) = self.popup_state.upgrade() else { + return; + }; + + if let WindowType::Popup { popup, positioner, .. } = &state.lock().unwrap().window { + positioner.set_anchor(anchor.into()); + popup.reposition(positioner, 0); + } + } + + fn anchor_rect(&self) -> Option<(impl Into, impl Into)> { + let state = self.popup_state.upgrade()?; + if let WindowType::Popup { anchor_rect, .. } = &state.lock().unwrap().window { + Some(*anchor_rect) + } else { + None + } + } + + fn set_anchor_rect(&self, position: impl Into, size: impl Into) { + let Some(state) = self.popup_state.upgrade() else { + return; + }; + + let state = state.lock().unwrap(); + let scale_factor = state.scale_factor(); + let size: LogicalSize = size.into().to_logical(scale_factor); + let position: LogicalPosition = position.into().to_logical(scale_factor); + + if let WindowType::Popup { popup, positioner, parent_origin, .. } = &state.window { + positioner.set_anchor_rect( + position.x - parent_origin.x, + position.y - parent_origin.y, + size.width.max(1), + size.height.max(1), + ); + popup.reposition(positioner, 0); + } + } + + fn set_positioner_offset(&self, position: impl Into) { + let Some(state) = self.popup_state.upgrade() else { + return; + }; + + let scale_factor = state.lock().unwrap().scale_factor(); + let position: LogicalPosition = position.into().to_logical(scale_factor); + if let WindowType::Popup { popup, positioner, .. } = &state.lock().unwrap().window { + positioner.set_offset(position.x, position.y); + popup.reposition(positioner, 0); + } + } + + fn set_constraint_adjustment(&self, constraint_adjustment: crate::PopupConstraintAdjustment) { + let Some(state) = self.popup_state.upgrade() else { + return; + }; + + if let WindowType::Popup { popup, positioner, .. } = &state.lock().unwrap().window { + positioner.set_constraint_adjustment(constraint_adjustment.into()); + popup.reposition(positioner, 0); + } + } + + fn set_gravity(&self, gravity: crate::PopupGravity) { + let Some(state) = self.popup_state.upgrade() else { + return; + }; + + if let WindowType::Popup { popup, positioner, .. } = &state.lock().unwrap().window { + positioner.set_gravity(gravity.into()); + popup.reposition(positioner, 0); + } + } +} diff --git a/winit-wayland/src/seat/keyboard/mod.rs b/winit-wayland/src/seat/keyboard/mod.rs index 9b1d1cd62..feb913ca9 100644 --- a/winit-wayland/src/seat/keyboard/mod.rs +++ b/winit-wayland/src/seat/keyboard/mod.rs @@ -129,9 +129,10 @@ impl Dispatch for WinitState { state.events_sink.push_window_event(WindowEvent::Focused(false), window_id); } }, - WlKeyboardEvent::Key { key, state: WEnum::Value(key_state), .. } + WlKeyboardEvent::Key { serial, key, state: WEnum::Value(key_state), .. } if matches!(key_state, WlKeyState::Repeated | WlKeyState::Pressed) => { + seat_state.latest_input_serial.set(Some(serial)); let key = key + 8; key_input( keyboard_state, @@ -204,7 +205,10 @@ impl Dispatch for WinitState { }) .ok(); }, - WlKeyboardEvent::Key { key, state: WEnum::Value(WlKeyState::Released), .. } => { + WlKeyboardEvent::Key { + serial, key, state: WEnum::Value(WlKeyState::Released), .. + } => { + seat_state.latest_input_serial.set(Some(serial)); let key = key + 8; key_input( diff --git a/winit-wayland/src/seat/mod.rs b/winit-wayland/src/seat/mod.rs index 13d005c0c..53ae5d265 100644 --- a/winit-wayland/src/seat/mod.rs +++ b/winit-wayland/src/seat/mod.rs @@ -1,5 +1,6 @@ //! Seat handling. +use std::cell::Cell; use std::sync::Arc; use foldhash::HashMap; @@ -76,6 +77,11 @@ pub struct WinitSeatState { /// Whether we have pending modifiers. modifiers_pending: bool, + + /// Serial of the most recent input event (keyboard or pointer) on this seat. + /// Written by both keyboard and pointer handlers through a shared reference, + /// so interior mutability is required. + pub(crate) latest_input_serial: Cell>, } impl WinitSeatState { @@ -83,6 +89,12 @@ impl WinitSeatState { Default::default() } + /// Returns the serial of the most recent input event on this seat, or `None` if none has + /// been received yet. + pub fn latest_serial(&self) -> Option { + self.latest_input_serial.get() + } + pub(crate) fn data_device(&self) -> Option<&DataDevice> { self.data_device.as_ref() } diff --git a/winit-wayland/src/seat/pointer/mod.rs b/winit-wayland/src/seat/pointer/mod.rs index e80add88e..d683d449f 100644 --- a/winit-wayland/src/seat/pointer/mod.rs +++ b/winit-wayland/src/seat/pointer/mod.rs @@ -166,7 +166,13 @@ impl PointerHandler for WinitState { ref kind @ PointerEventKind::Press { button, serial, .. } | ref kind @ PointerEventKind::Release { button, serial, .. } => { // Update the last button serial. + pointer.winit_data().data().inner.lock().unwrap().latest_button_serial = serial; + if matches!(kind, PointerEventKind::Press { .. }) { + // For Gnome (Mutter) and possible others only the Press event serial must + // be logged! + seat_state.latest_input_serial.set(Some(serial)); + } let button = wayland_button_to_winit(button); let state = if matches!(kind, PointerEventKind::Press { .. }) { diff --git a/winit-wayland/src/state.rs b/winit-wayland/src/state.rs index c6b5fdb64..3380db486 100644 --- a/winit-wayland/src/state.rs +++ b/winit-wayland/src/state.rs @@ -17,6 +17,7 @@ use sctk::seat::SeatState; use sctk::seat::pointer::ThemedPointer; use sctk::shell::WaylandSurface; use sctk::shell::xdg::XdgShell; +use sctk::shell::xdg::popup::{Popup as XdgPopup, PopupConfigure, PopupHandler}; use sctk::shell::xdg::window::{Window, WindowConfigure, WindowHandler}; use sctk::shm::slot::SlotPool; use sctk::shm::{Shm, ShmHandler}; @@ -37,7 +38,8 @@ use crate::types::wp_tablet_input_v2::TabletManager; use crate::types::wp_viewporter::ViewporterState; use crate::types::xdg_activation::XdgActivationState; use crate::types::xdg_toplevel_icon_manager::XdgToplevelIconManagerState; -use crate::window::{WindowRequests, WindowState}; +use crate::window::WindowState; +use crate::window::handles::WindowRequests; /// Winit's Wayland state. #[derive(Debug)] @@ -314,24 +316,24 @@ impl WindowHandler for WinitState { ) { let window_id = super::make_wid(window.wl_surface()); - let pos = if let Some(pos) = + let index = if let Some(index) = self.window_compositor_updates.iter().position(|update| update.window_id == window_id) { - pos + index } else { self.window_compositor_updates.push(WindowCompositorUpdate::new(window_id)); self.window_compositor_updates.len() - 1 }; // Populate the configure to the window. - self.window_compositor_updates[pos].resized |= self + self.window_compositor_updates[index].resized |= self .windows .get_mut() .get_mut(&window_id) .expect("got configure for dead window.") .lock() .unwrap() - .configure(configure, &self.shm, &self.subcompositor_state); + .configure_window(configure, &self.shm, &self.subcompositor_state); // NOTE: configure demands wl_surface::commit, however winit doesn't commit on behalf of the // users, since it can break a lot of things, thus it'll ask users to redraw instead. @@ -347,6 +349,57 @@ impl WindowHandler for WinitState { } } +impl PopupHandler for WinitState { + fn configure( + &mut self, + _: &Connection, + _: &QueueHandle, + popup: &XdgPopup, + configure: PopupConfigure, + ) { + let window_id = super::make_wid(popup.wl_surface()); + + let index = if let Some(index) = + self.window_compositor_updates.iter().position(|update| update.window_id == window_id) + { + index + } else { + self.window_compositor_updates.push(WindowCompositorUpdate::new(window_id)); + self.window_compositor_updates.len() - 1 + }; + + self.window_compositor_updates[index].resized |= self + .windows + .get_mut() + .get_mut(&window_id) + .expect("got configure for dead window.") + .lock() + .unwrap() + .configure_popup(configure); + + // NOTE: configure demands wl_surface::commit, however winit doesn't commit on behalf of the + // users, since it can break a lot of things, thus it'll ask users to redraw instead + self.window_requests + .get_mut() + .get(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + + // Manually mark that we've got an event, since configure may not generate a resize. + self.dispatched_events = true; + } + + fn done(&mut self, _: &Connection, _: &QueueHandle, popup: &XdgPopup) { + let window_id = super::make_wid(popup.wl_surface()); + let window_requests = self.window_requests.get_mut().iter().find(|r| *r.0 == window_id); + if let Some(window_requests) = window_requests { + window_requests.1.closed.store(true, Ordering::Relaxed); + } + Self::queue_close(&mut self.window_compositor_updates, window_id); + } +} + impl OutputHandler for WinitState { fn output_state(&mut self) -> &mut OutputState { &mut self.output_state diff --git a/winit-wayland/src/window/handles.rs b/winit-wayland/src/window/handles.rs new file mode 100644 index 000000000..5e33aa818 --- /dev/null +++ b/winit-wayland/src/window/handles.rs @@ -0,0 +1,142 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use sctk::compositor::{CompositorState, Region}; +use sctk::reexports::client::QueueHandle; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_v1::XdgActivationV1; +use tracing::warn; +use winit_core::error::RequestError; +use winit_core::event::WindowEvent; +use winit_core::monitor::MonitorHandle as CoreMonitorHandle; +use winit_core::window::{UserAttentionType, WindowId}; + +use super::super::event_loop::sink::EventSink; +use super::super::output::MonitorHandle; +use super::super::state::WinitState; +use super::super::types::xdg_activation::XdgActivationTokenData; + +#[derive(Debug)] +pub struct Handles { + /// Handle to the main queue to perform requests. + pub(crate) queue_handle: QueueHandle, + + /// Window requests to the event loop. + pub(crate) window_requests: Arc, + + /// Observed monitors. + pub(crate) monitors: Arc>>, + + /// Source to wake-up the event-loop for window requests. + pub(crate) event_loop_awakener: calloop::ping::Ping, + + /// The event sink to deliver synthetic events. + pub(crate) window_events_sink: Arc>, + + /// Xdg activation to request user attention. + pub(crate) xdg_activation: Option, + + /// The state of the requested attention from the `xdg_activation`. + pub(crate) attention_requested: Arc, + + /// Compositor to handle WlRegion stuff. + pub(crate) compositor: Arc, +} + +impl Handles { + pub(crate) fn request_redraw(&self) { + // NOTE: try to not wake up the loop when the event was already scheduled and not yet + // processed by the loop, because if at this point the value was `true` it could only + // mean that the loop still haven't dispatched the value to the client and will do + // eventually, resetting it to `false`. + if self + .window_requests + .redraw_requested + .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + self.event_loop_awakener.ping(); + } + } + + pub(crate) fn push_window_event(&self, event: WindowEvent, id: WindowId) { + self.window_events_sink.lock().unwrap().push_window_event(event, id); + self.event_loop_awakener.ping(); + } + + pub(crate) fn available_monitors(&self) -> Box> { + Box::new( + self.monitors + .lock() + .unwrap() + .clone() + .into_iter() + .map(|inner| CoreMonitorHandle(Arc::new(inner))), + ) + } + + pub(crate) fn request_user_attention( + &self, + surface: &WlSurface, + request_type: Option, + ) { + let xdg_activation = match self.xdg_activation.as_ref() { + Some(xdg_activation) => xdg_activation, + None => { + warn!("`request_user_attention` isn't supported"); + return; + }, + }; + + // Urgency is only removed by the compositor and there's no need to raise urgency when it + // was already raised. + if request_type.is_none() || self.attention_requested.load(Ordering::Relaxed) { + return; + } + + self.attention_requested.store(true, Ordering::Relaxed); + let data = XdgActivationTokenData::Attention(( + surface.clone(), + Arc::downgrade(&self.attention_requested), + )); + let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); + xdg_activation_token.set_surface(surface); + xdg_activation_token.commit(); + } + + pub(crate) fn set_cursor_hittest( + &self, + surface: &WlSurface, + hittest: bool, + ) -> Result<(), RequestError> { + if hittest { + surface.set_input_region(None); + Ok(()) + } else { + let region = Region::new(&*self.compositor).map_err(|err| os_error!(err))?; + region.add(0, 0, 0, 0); + surface.set_input_region(Some(region.wl_region())); + Ok(()) + } + } +} + +/// The request from the window to the event loop. +#[derive(Debug)] +pub struct WindowRequests { + /// The window was closed. + pub closed: AtomicBool, + + /// Redraw Requested. + pub redraw_requested: AtomicBool, +} + +impl WindowRequests { + pub fn take_closed(&self) -> bool { + self.closed.swap(false, Ordering::Relaxed) + } + + pub fn take_redraw_requested(&self) -> bool { + self.redraw_requested.swap(false, Ordering::Relaxed) + } +} diff --git a/winit-wayland/src/window/mod.rs b/winit-wayland/src/window/mod.rs index 7078717dd..32e40835f 100644 --- a/winit-wayland/src/window/mod.rs +++ b/winit-wayland/src/window/mod.rs @@ -6,11 +6,11 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use dpi::{LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; -use sctk::compositor::{CompositorState, Region, SurfaceData}; +use rwh_06::RawWindowHandle; +use sctk::compositor::SurfaceData; +use sctk::reexports::client::Proxy; use sctk::reexports::client::protocol::wl_display::WlDisplay; use sctk::reexports::client::protocol::wl_surface::WlSurface; -use sctk::reexports::client::{Proxy, QueueHandle}; -use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_v1::XdgActivationV1; use sctk::shell::WaylandSurface; use sctk::shell::xdg::window::{Window as SctkWindow, WindowDecorations}; use tracing::warn; @@ -26,15 +26,15 @@ use winit_core::window::{ }; use super::ActiveEventLoop; -use super::event_loop::sink::EventSink; use super::output::MonitorHandle; -use super::state::WinitState; use super::types::xdg_activation::XdgActivationTokenData; +use crate::window::state::WindowType; use crate::{WindowAttributesWayland, output}; - pub(crate) mod state; - pub use state::WindowState; +pub(crate) mod handles; +pub use handles::Handles; +use handles::WindowRequests; /// The Wayland window. #[derive(Debug)] @@ -48,33 +48,12 @@ pub struct Window { /// The state of the window. window_state: Arc>, - /// Compositor to handle WlRegion stuff. - compositor: Arc, - /// The wayland display used solely for raw window handle. #[allow(dead_code)] display: WlDisplay, - /// Xdg activation to request user attention. - xdg_activation: Option, - - /// The state of the requested attention from the `xdg_activation`. - attention_requested: Arc, - - /// Handle to the main queue to perform requests. - queue_handle: QueueHandle, - - /// Window requests to the event loop. - window_requests: Arc, - - /// Observed monitors. - monitors: Arc>>, - - /// Source to wake-up the event-loop for window requests. - event_loop_awakener: calloop::ping::Ping, - - /// The event sink to deliver synthetic events. - window_events_sink: Arc>, + /// Common handles like queue, window requests, monitors and so on + handles: Handles, } impl Window { @@ -106,20 +85,32 @@ impl Window { let window = state.xdg_shell.create_window(surface.clone(), default_decorations, &queue_handle); - let WindowAttributesWayland { name: app_name, activation_token, prefer_csd } = *attributes - .platform - .take() - .and_then(|p| p.cast::().ok()) - .unwrap_or_default(); + let WindowAttributesWayland { name: app_name, activation_token, prefer_csd, .. } = + *attributes + .platform + .take() + .and_then(|p| p.cast::().ok()) + .unwrap_or_default(); + + let mut scale_factor = None; + if let Some(RawWindowHandle::Wayland(handle)) = attributes.parent_window() { + if let Some(s) = + state.windows.borrow().get(&WindowId::from_raw(handle.surface.as_ptr() as usize)) + { + scale_factor = Some(s.lock().unwrap().scale_factor()); + } + } + let scale_factor = scale_factor.unwrap_or(1.0); let mut window_state = WindowState::new( - event_loop_window_target.handle.clone(), - &event_loop_window_target.queue_handle, + event_loop_window_target, &state, size, - window.clone(), + state::WindowType::Window { window: window.clone(), last_configure: None }, attributes.preferred_theme, prefer_csd, + scale_factor, + None, ); window_state.set_window_icon(attributes.window_icon); @@ -219,16 +210,22 @@ impl Window { Ok(Self { window, display, - monitors, + window_id, - compositor, window_state, - queue_handle, - xdg_activation, - attention_requested: Arc::new(AtomicBool::new(false)), - event_loop_awakener, - window_requests, - window_events_sink, + + handles: Handles { + queue_handle, + window_requests, + monitors, + event_loop_awakener, + window_events_sink, + + compositor, + + xdg_activation, + attention_requested: Arc::new(AtomicBool::new(false)), + }, }) } @@ -239,7 +236,7 @@ impl Window { impl Window { pub fn request_activation_token(&self) -> Result { - let xdg_activation = match self.xdg_activation.as_ref() { + let xdg_activation = match self.handles.xdg_activation.as_ref() { Some(xdg_activation) => xdg_activation, None => return Err(NotSupportedError::new("xdg_activation_v1 is not available").into()), }; @@ -247,7 +244,8 @@ impl Window { let serial = AsyncRequestSerial::get(); let data = XdgActivationTokenData::Obtain((self.window_id, serial)); - let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); + let xdg_activation_token = + xdg_activation.get_activation_token(&self.handles.queue_handle, data); xdg_activation_token.set_surface(self.surface()); xdg_activation_token.commit(); @@ -262,8 +260,8 @@ impl Window { impl Drop for Window { fn drop(&mut self) { - self.window_requests.closed.store(true, Ordering::Relaxed); - self.event_loop_awakener.ping(); + self.handles.window_requests.closed.store(true, Ordering::Relaxed); + self.handles.event_loop_awakener.ping(); } } @@ -290,23 +288,16 @@ impl rwh_06::HasDisplayHandle for Window { } impl CoreWindow for Window { + fn window_type(&self) -> winit_core::window::WindowType { + winit_core::window::WindowType::Window + } + fn id(&self) -> WindowId { self.window_id } fn request_redraw(&self) { - // NOTE: try to not wake up the loop when the event was already scheduled and not yet - // processed by the loop, because if at this point the value was `true` it could only - // mean that the loop still haven't dispatched the value to the client and will do - // eventually, resetting it to `false`. - if self - .window_requests - .redraw_requested - .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() - { - self.event_loop_awakener.ping(); - } + self.handles.request_redraw(); } #[inline] @@ -449,13 +440,15 @@ impl CoreWindow for Window { } fn is_maximized(&self) -> bool { - self.window_state - .lock() - .unwrap() - .last_configure - .as_ref() - .map(|last_configure| last_configure.is_maximized()) - .unwrap_or_default() + if let WindowType::Window { last_configure, .. } = &self.window_state.lock().unwrap().window + { + last_configure + .as_ref() + .map(|last_configure| last_configure.is_maximized()) + .unwrap_or_default() + } else { + false + } } fn set_fullscreen(&self, fullscreen: Option) { @@ -475,14 +468,16 @@ impl CoreWindow for Window { } fn fullscreen(&self) -> Option { - let is_fullscreen = self - .window_state - .lock() - .unwrap() - .last_configure - .as_ref() - .map(|last_configure| last_configure.is_fullscreen()) - .unwrap_or_default(); + let is_fullscreen = if let WindowType::Window { last_configure, .. } = + &self.window_state.lock().unwrap().window + { + last_configure + .as_ref() + .map(|last_configure| last_configure.is_fullscreen()) + .unwrap_or_default() + } else { + false + }; if is_fullscreen { let current_monitor = self.current_monitor(); @@ -526,8 +521,7 @@ impl CoreWindow for Window { if let Some(allowed) = state_changed { let event = WindowEvent::Ime(if allowed { Ime::Enabled } else { Ime::Disabled }); - self.window_events_sink.lock().unwrap().push_window_event(event, self.window_id); - self.event_loop_awakener.ping(); + self.handles.push_window_event(event, self.window_id); } Ok(()) @@ -545,29 +539,7 @@ impl CoreWindow for Window { } fn request_user_attention(&self, request_type: Option) { - let xdg_activation = match self.xdg_activation.as_ref() { - Some(xdg_activation) => xdg_activation, - None => { - warn!("`request_user_attention` isn't supported"); - return; - }, - }; - - // Urgency is only removed by the compositor and there's no need to raise urgency when it - // was already raised. - if request_type.is_none() || self.attention_requested.load(Ordering::Relaxed) { - return; - } - - self.attention_requested.store(true, Ordering::Relaxed); - let surface = self.surface().clone(); - let data = XdgActivationTokenData::Attention(( - surface.clone(), - Arc::downgrade(&self.attention_requested), - )); - let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); - xdg_activation_token.set_surface(&surface); - xdg_activation_token.commit(); + self.handles.request_user_attention(self.surface(), request_type); } fn set_theme(&self, theme: Option) { @@ -623,17 +595,7 @@ impl CoreWindow for Window { } fn set_cursor_hittest(&self, hittest: bool) -> Result<(), RequestError> { - let surface = self.window.wl_surface(); - - if hittest { - surface.set_input_region(None); - Ok(()) - } else { - let region = Region::new(&*self.compositor).map_err(|err| os_error!(err))?; - region.add(0, 0, 0, 0); - surface.set_input_region(Some(region.wl_region())); - Ok(()) - } + self.handles.set_cursor_hittest(self.surface(), hittest) } fn current_monitor(&self) -> Option { @@ -645,14 +607,7 @@ impl CoreWindow for Window { } fn available_monitors(&self) -> Box> { - Box::new( - self.monitors - .lock() - .unwrap() - .clone() - .into_iter() - .map(|inner| CoreMonitorHandle(Arc::new(inner))), - ) + self.handles.available_monitors() } fn primary_monitor(&self) -> Option { @@ -670,23 +625,3 @@ impl CoreWindow for Window { self } } - -/// The request from the window to the event loop. -#[derive(Debug)] -pub struct WindowRequests { - /// The window was closed. - pub closed: AtomicBool, - - /// Redraw Requested. - pub redraw_requested: AtomicBool, -} - -impl WindowRequests { - pub fn take_closed(&self) -> bool { - self.closed.swap(false, Ordering::Relaxed) - } - - pub fn take_redraw_requested(&self) -> bool { - self.redraw_requested.swap(false, Ordering::Relaxed) - } -} diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index c7bfe2824..67fc1d97d 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -22,8 +22,9 @@ use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as XdgResizeEdge; use sctk::seat::pointer::{PointerData, ThemedPointer}; use sctk::shell::WaylandSurface; -use sctk::shell::xdg::XdgSurface; +use sctk::shell::xdg::popup::{ConfigureKind, Popup, PopupConfigure}; use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure}; +use sctk::shell::xdg::{XdgPositioner, XdgSurface}; use sctk::shm::Shm; use sctk::shm::slot::SlotPool; use sctk::subcompositor::SubcompositorState; @@ -36,7 +37,6 @@ use winit_core::window::{ }; use crate::event_loop::OwnedDisplayHandle; -use crate::logical_to_physical_rounded; use crate::seat::{ PointerConstraintsState, TextInputClientState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext, @@ -45,6 +45,7 @@ use crate::state::{WindowCompositorUpdate, WinitState}; use crate::types::bgr_effects::{BgrEffectManager, SurfaceBlurEffect}; use crate::types::cursor::{CustomCursor, SelectedCursor, WaylandCustomCursor}; use crate::types::xdg_toplevel_icon_manager::ToplevelIcon; +use crate::{ActiveEventLoop, logical_to_physical_rounded}; #[cfg(feature = "sctk-adwaita")] pub type WinitFrame = sctk_adwaita::AdwaitaFrame; @@ -54,6 +55,49 @@ pub type WinitFrame = sctk::shell::xdg::fallback_frame::FallbackFrame = LogicalSize::new(2, 1); +#[derive(Debug)] +pub enum WindowType { + // The option is the last received configure + Window { + window: Window, + last_configure: Option, + }, + Popup { + popup: Popup, + positioner: XdgPositioner, + last_configure: Option, + parent_origin: LogicalPosition, + anchor_rect: (LogicalPosition, LogicalSize), + }, +} + +impl WindowType { + pub fn is_configured(&self) -> bool { + match self { + Self::Window { last_configure, .. } => last_configure.is_some(), + Self::Popup { last_configure, .. } => last_configure.is_some(), + } + } +} + +impl WaylandSurface for WindowType { + fn wl_surface(&self) -> &wayland_client::protocol::wl_surface::WlSurface { + match self { + Self::Window { window, .. } => window.wl_surface(), + Self::Popup { popup, .. } => popup.wl_surface(), + } + } +} + +impl XdgSurface for WindowType { + fn xdg_surface(&self) -> &wayland_protocols::xdg::shell::client::xdg_surface::XdgSurface { + match self { + Self::Window { window, .. } => window.xdg_surface(), + Self::Popup { popup, .. } => popup.xdg_surface(), + } + } +} + /// The state of the window which is being updated from the [`WinitState`]. #[derive(Debug)] pub struct WindowState { @@ -63,9 +107,6 @@ pub struct WindowState { /// The `Shm` to set cursor. pub shm: WlShm, - /// The last received configure. - pub last_configure: Option, - /// The pointers observed on the window. pub pointers: Vec>>, @@ -164,7 +205,7 @@ pub struct WindowState { has_pending_move: Option, /// The underlying SCTK window. - pub window: Window, + pub window: WindowType, // NOTE: The spec says that destroying parent(`window` in our case), will unmap the // subsurfaces. Thus to achieve atomic unmap of the client, drop the decorations @@ -172,19 +213,29 @@ pub struct WindowState { // field drop order guarantees. /// The window frame, which is created from the configure request. frame: Option, + + /// Parent Window if available + parent: Option, + + /// Children of this window like popups, dialogs or other windows + children: Vec, } impl WindowState { /// Create new window state. + #[allow(clippy::too_many_arguments)] pub fn new( - handle: Arc, - queue_handle: &QueueHandle, + active_event_loop: &ActiveEventLoop, winit_state: &WinitState, initial_size: Size, - window: Window, + window: WindowType, theme: Option, prefer_csd: bool, + scale_factor: f64, + parent: Option, ) -> Self { + let handle = active_event_loop.handle.clone(); + let queue_handle = &active_event_loop.queue_handle; let compositor = winit_state.compositor_state.clone(); let pointer_constraints = winit_state.pointer_constraints.clone(); let viewport = winit_state @@ -220,7 +271,6 @@ impl WindowState { seat_focus: Default::default(), has_pending_move: None, text_input_state: None, - last_configure: None, max_surface_size: None, min_surface_size: MIN_WINDOW_SIZE, resize_increments: None, @@ -228,7 +278,7 @@ impl WindowState { pointers: Default::default(), queue_handle: queue_handle.clone(), resizable: true, - scale_factor: 1., + scale_factor, shm: winit_state.shm.wl_shm().clone(), image_pool: winit_state.image_pool.clone(), size: initial_size.to_logical(1.), @@ -240,6 +290,8 @@ impl WindowState { transparent: false, viewport, window, + children: Default::default(), + parent, } } @@ -288,8 +340,47 @@ impl WindowState { FrameCallbackState::Requested => (), } } + pub fn configure_popup(&mut self, configure: PopupConfigure) -> bool { + // NOTE: when using fractional scaling or wl_compositor@v6 the scaling + // should be delivered before the first configure, thus apply it to + // properly scale the physical sizes provided by the users. + if let Some(initial_size) = self.initial_size.take() { + self.size = initial_size.to_logical(self.scale_factor()); + } - pub fn configure( + // The popup was constrained to a different size by the compositor + let constrained = self.size.width != configure.width as u32 + || self.size.height != configure.height as u32; + let new_size = + LogicalSize { width: configure.width as u32, height: configure.height as u32 }; + + // NOTE: Set the configure before doing a resize, since we query it during it. + if let WindowType::Popup { last_configure, .. } = &mut self.window { + let kind = configure.kind.clone(); + *last_configure = Some(configure); + + // Always resize on the initial configure to properly initialize the viewport + // destination and window geometry. This is required for fractional scaling + // to work correctly: without calling resize(), viewport.set_destination() + // is never called, and the compositor would interpret the buffer size as + // logical pixels, making the popup appear at the wrong size. Also resize + // when the compositor constrained us to a different size than requested. + if matches!(kind, ConfigureKind::Initial) || constrained { + self.resize(new_size); + true + } else { + false + } + } else { + tracing::error!( + "configure_popup called for window type unequal of popup. This should never \ + happen, because we start configuring with a popup" + ); + false + } + } + + pub fn configure_window( &mut self, configure: WindowConfigure, shm: &Shm, @@ -407,25 +498,33 @@ impl WindowState { } let new_state = configure.state; - let old_state = self.last_configure.as_ref().map(|configure| configure.state); + if let WindowType::Window { last_configure, .. } = &mut self.window { + let old_state = last_configure.as_ref().map(|configure| configure.state); - let state_change_requires_resize = old_state - .map(|old_state| { - !old_state - .symmetric_difference(new_state) - .difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED) - .is_empty() - }) - // NOTE: `None` is present for the initial configure, thus we must always resize. - .unwrap_or(true); + let state_change_requires_resize = old_state + .map(|old_state| { + !old_state + .symmetric_difference(new_state) + .difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED) + .is_empty() + }) + // NOTE: `None` is present for the initial configure, thus we must always resize. + .unwrap_or(true); - // NOTE: Set the configure before doing a resize, since we query it during it. - self.last_configure = Some(configure); + // NOTE: Set the configure before doing a resize, since we query it during it. + *last_configure = Some(configure); - if state_change_requires_resize || new_size != self.surface_size() { - self.resize(new_size); - true + if state_change_requires_resize || new_size != self.surface_size() { + self.resize(new_size); + true + } else { + false + } } else { + tracing::error!( + "configure_window called for window type unequal of `Window`. This should never \ + happen, because we start configuring with a `Window`" + ); false } } @@ -458,31 +557,45 @@ impl WindowState { /// Start interacting drag resize. pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), RequestError> { - let xdg_toplevel = self.window.xdg_toplevel(); + match &self.window { + WindowType::Window { window, .. } => { + let xdg_toplevel = window.xdg_toplevel(); - // TODO(kchibisov) handle touch serials. - self.apply_on_pointer(|_, data| { - if let Some(serial) = data.latest_button_serial() { - let seat = data.seat(); - xdg_toplevel.resize(seat, serial, resize_direction_to_xdg(direction)); - } - }); + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + if let Some(serial) = data.latest_button_serial() { + let seat = data.seat(); + xdg_toplevel.resize(seat, serial, resize_direction_to_xdg(direction)); + } + }); - Ok(()) + Ok(()) + }, + WindowType::Popup { .. } => Err(RequestError::NotSupported(NotSupportedError::new( + "Drag resize for popup not supported", + ))), + } } /// Start the window drag. pub fn drag_window(&self) -> Result<(), RequestError> { - let xdg_toplevel = self.window.xdg_toplevel(); - // TODO(kchibisov) handle touch serials. - self.apply_on_pointer(|_, data| { - if let Some(serial) = data.latest_button_serial() { - let seat = data.seat(); - xdg_toplevel._move(seat, serial); - } - }); + match &self.window { + WindowType::Window { window, .. } => { + let xdg_toplevel = window.xdg_toplevel(); + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + if let Some(serial) = data.latest_button_serial() { + let seat = data.seat(); + xdg_toplevel._move(seat, serial); + } + }); - Ok(()) + Ok(()) + }, + WindowType::Popup { .. } => Err(RequestError::NotSupported(NotSupportedError::new( + "Drag for popup not supported", + ))), + } } /// Tells whether the window should be closed. @@ -497,32 +610,37 @@ impl WindowState { window_id: WindowId, updates: &mut Vec, ) -> Option { - match self.frame.as_mut()?.on_click(timestamp, click, pressed)? { - FrameAction::Minimize => self.window.set_minimized(), - FrameAction::Maximize => self.window.set_maximized(), - FrameAction::UnMaximize => self.window.unset_maximized(), - FrameAction::Close => WinitState::queue_close(updates, window_id), - FrameAction::Move => self.has_pending_move = Some(serial), - FrameAction::Resize(edge) => { - let edge = match edge { - ResizeEdge::None => XdgResizeEdge::None, - ResizeEdge::Top => XdgResizeEdge::Top, - ResizeEdge::Bottom => XdgResizeEdge::Bottom, - ResizeEdge::Left => XdgResizeEdge::Left, - ResizeEdge::TopLeft => XdgResizeEdge::TopLeft, - ResizeEdge::BottomLeft => XdgResizeEdge::BottomLeft, - ResizeEdge::Right => XdgResizeEdge::Right, - ResizeEdge::TopRight => XdgResizeEdge::TopRight, - ResizeEdge::BottomRight => XdgResizeEdge::BottomRight, - _ => return None, + match &self.window { + WindowType::Window { window, .. } => { + match self.frame.as_mut()?.on_click(timestamp, click, pressed)? { + FrameAction::Minimize => window.set_minimized(), + FrameAction::Maximize => window.set_maximized(), + FrameAction::UnMaximize => window.unset_maximized(), + FrameAction::Close => WinitState::queue_close(updates, window_id), + FrameAction::Move => self.has_pending_move = Some(serial), + FrameAction::Resize(edge) => { + let edge = match edge { + ResizeEdge::None => XdgResizeEdge::None, + ResizeEdge::Top => XdgResizeEdge::Top, + ResizeEdge::Bottom => XdgResizeEdge::Bottom, + ResizeEdge::Left => XdgResizeEdge::Left, + ResizeEdge::TopLeft => XdgResizeEdge::TopLeft, + ResizeEdge::BottomLeft => XdgResizeEdge::BottomLeft, + ResizeEdge::Right => XdgResizeEdge::Right, + ResizeEdge::TopRight => XdgResizeEdge::TopRight, + ResizeEdge::BottomRight => XdgResizeEdge::BottomRight, + _ => return None, + }; + window.resize(seat, serial, edge); + }, + FrameAction::ShowMenu(x, y) => window.show_window_menu(seat, serial, (x, y)), + _ => (), }; - self.window.resize(seat, serial, edge); - }, - FrameAction::ShowMenu(x, y) => self.window.show_window_menu(seat, serial, (x, y)), - _ => (), - }; - Some(false) + Some(false) + }, + WindowType::Popup { .. } => None, + } } pub fn frame_point_left(&mut self) { @@ -540,21 +658,26 @@ impl WindowState { x: f64, y: f64, ) -> Option { - // Take the serial if we had any, so it doesn't stick around. - let serial = self.has_pending_move.take(); + match &self.window { + WindowType::Window { window, .. } => { + // Take the serial if we had any, so it doesn't stick around. + let serial = self.has_pending_move.take(); - if let Some(frame) = self.frame.as_mut() { - let cursor = frame.click_point_moved(timestamp, &surface.id(), x, y); - // If we have a cursor change, that means that cursor is over the decorations, - // so try to apply move. - if let Some(serial) = cursor.is_some().then_some(serial).flatten() { - self.window.move_(seat, serial); - None - } else { - cursor - } - } else { - None + if let Some(frame) = self.frame.as_mut() { + let cursor = frame.click_point_moved(timestamp, &surface.id(), x, y); + // If we have a cursor change, that means that cursor is over the decorations, + // so try to apply move. + if let Some(serial) = cursor.is_some().then_some(serial).flatten() { + window.move_(seat, serial); + None + } else { + cursor + } + } else { + None + } + }, + WindowType::Popup { .. } => None, } } @@ -615,21 +738,25 @@ impl WindowState { /// Whether the window received initial configure event from the compositor. #[inline] pub fn is_configured(&self) -> bool { - self.last_configure.is_some() + self.window.is_configured() } #[inline] pub fn is_decorated(&mut self) -> bool { - let csd = self - .last_configure - .as_ref() - .map(|configure| configure.decoration_mode == DecorationMode::Client) - .unwrap_or(false); - if let Some(frame) = csd.then_some(self.frame.as_ref()).flatten() { - !frame.is_hidden() - } else { - // Server side decorations. - true + match &mut self.window { + WindowType::Window { last_configure, .. } => { + let csd = last_configure + .as_ref() + .map(|configure| configure.decoration_mode == DecorationMode::Client) + .unwrap_or(false); + if let Some(frame) = csd.then_some(self.frame.as_ref()).flatten() { + !frame.is_hidden() + } else { + // Server side decorations. + true + } + }, + WindowType::Popup { .. } => false, // Popup window does not have any decoration } } @@ -642,6 +769,13 @@ impl WindowState { .unwrap_or(self.size) } + /// Get the origin of the content surface by considering the client side decoration if available + /// This is required for example when creating a popup, because as parent a xdg_surface must be + /// passed but the frame is only a wl_surface + pub fn content_surface_origin(&self) -> LogicalPosition { + self.frame.as_ref().map(|frame| frame.location().into()).unwrap_or_else(|| (0, 0).into()) + } + /// Register pointer on the top-level. pub fn pointer_entered(&mut self, added: Weak>) { self.pointers.push(added); @@ -704,8 +838,19 @@ impl WindowState { /// Try to resize the window when the user can do so. pub fn request_surface_size(&mut self, surface_size: Size) -> PhysicalSize { - if self.last_configure.as_ref().map(Self::is_stateless).unwrap_or(true) { - self.resize(surface_size.to_logical(self.scale_factor())) + match &self.window { + WindowType::Window { last_configure, .. } => { + if last_configure.as_ref().map(Self::is_stateless).unwrap_or(true) { + self.resize(surface_size.to_logical(self.scale_factor())) + } + }, + WindowType::Popup { popup, positioner, .. } => { + let size = surface_size.to_logical(self.scale_factor()); + positioner.set_size(size.width, size.height); + if popup.xdg_popup().version() >= 3 { + popup.reposition(positioner, 0); + } + }, } logical_to_physical_rounded(self.surface_size(), self.scale_factor()) @@ -716,8 +861,10 @@ impl WindowState { self.size = surface_size; // Update the stateless size. - if Some(true) == self.last_configure.as_ref().map(Self::is_stateless) { - self.stateless_size = surface_size; + if let WindowType::Window { last_configure, .. } = &mut self.window { + if let Some(true) = last_configure.as_ref().map(Self::is_stateless) { + self.stateless_size = surface_size; + } } // Update the inner frame. @@ -855,33 +1002,37 @@ impl WindowState { /// Set maximum inner window size. pub fn set_min_surface_size(&mut self, size: Option>) { - // Ensure that the window has the right minimum size. - let mut size = size.unwrap_or(MIN_WINDOW_SIZE); - size.width = size.width.max(MIN_WINDOW_SIZE.width); - size.height = size.height.max(MIN_WINDOW_SIZE.height); + if let WindowType::Window { window, .. } = &self.window { + // Ensure that the window has the right minimum size. + let mut size = size.unwrap_or(MIN_WINDOW_SIZE); + size.width = size.width.max(MIN_WINDOW_SIZE.width); + size.height = size.height.max(MIN_WINDOW_SIZE.height); - // Add the borders. - let size = self - .frame - .as_ref() - .map(|frame| frame.add_borders(size.width, size.height).into()) - .unwrap_or(size); + // Add the borders. + let size = self + .frame + .as_ref() + .map(|frame| frame.add_borders(size.width, size.height).into()) + .unwrap_or(size); - self.min_surface_size = size; - self.window.set_min_size(Some(size.into())); + self.min_surface_size = size; + window.set_min_size(Some(size.into())); + } } /// Set maximum inner window size. pub fn set_max_surface_size(&mut self, size: Option>) { - let size = size.map(|size| { - self.frame - .as_ref() - .map(|frame| frame.add_borders(size.width, size.height).into()) - .unwrap_or(size) - }); + if let WindowType::Window { window, .. } = &self.window { + let size = size.map(|size| { + self.frame + .as_ref() + .map(|frame| frame.add_borders(size.width, size.height).into()) + .unwrap_or(size) + }); - self.max_surface_size = size; - self.window.set_max_size(size.map(Into::into)); + self.max_surface_size = size; + window.set_max_size(size.map(Into::into)); + } } /// Set the CSD theme. @@ -983,13 +1134,15 @@ impl WindowState { } pub fn show_window_menu(&self, position: LogicalPosition) { - // TODO(kchibisov) handle touch serials. - self.apply_on_pointer(|_, data| { - if let Some(serial) = data.latest_button_serial() { - let seat = data.seat(); - self.window.show_window_menu(seat, serial, position.into()); - } - }); + if let WindowType::Window { window, .. } = &self.window { + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + if let Some(serial) = data.latest_button_serial() { + let seat = data.seat(); + window.show_window_menu(seat, serial, position.into()); + } + }); + } } /// Set the position of the cursor. @@ -1042,22 +1195,29 @@ impl WindowState { self.decorate = decorate; - match self.last_configure.as_ref().map(|configure| configure.decoration_mode) { - Some(DecorationMode::Server) if !self.decorate => { - // To disable decorations we should request client and hide the frame. - self.window.request_decoration_mode(Some(DecorationMode::Client)) - }, - _ if self.decorate && self.prefer_csd => { - self.window.request_decoration_mode(Some(DecorationMode::Client)) - }, - _ if self.decorate => self.window.request_decoration_mode(Some(DecorationMode::Server)), - _ => (), - } + match &self.window { + WindowType::Window { window, last_configure } => { + match last_configure.as_ref().map(|configure| configure.decoration_mode) { + Some(DecorationMode::Server) if !self.decorate => { + // To disable decorations we should request client and hide the frame. + window.request_decoration_mode(Some(DecorationMode::Client)) + }, + _ if self.decorate && self.prefer_csd => { + window.request_decoration_mode(Some(DecorationMode::Client)) + }, + _ if self.decorate => { + window.request_decoration_mode(Some(DecorationMode::Server)) + }, + _ => (), + } - if let Some(frame) = self.frame.as_mut() { - frame.set_hidden(!decorate); - // Force the resize. - self.resize(self.size); + if let Some(frame) = self.frame.as_mut() { + frame.set_hidden(!decorate); + // Force the resize. + self.resize(self.size); + } + }, + WindowType::Popup { .. } => (), // Popup does not have any decoration } } @@ -1192,46 +1352,51 @@ impl WindowState { frame.set_title(&title); } - self.window.set_title(&title); + match &self.window { + WindowType::Window { window, .. } => window.set_title(&title), + WindowType::Popup { .. } => (), // Popup does not have any title + } self.title = title; } /// Set the window's icon pub fn set_window_icon(&mut self, window_icon: Option) { - let xdg_toplevel_icon_manager = match self.xdg_toplevel_icon_manager.as_ref() { - Some(xdg_toplevel_icon_manager) => xdg_toplevel_icon_manager, - None => { - warn!("`xdg_toplevel_icon_manager_v1` is not supported"); - return; - }, - }; + if let WindowType::Window { window, .. } = &self.window { + let xdg_toplevel_icon_manager = match self.xdg_toplevel_icon_manager.as_ref() { + Some(xdg_toplevel_icon_manager) => xdg_toplevel_icon_manager, + None => { + warn!("`xdg_toplevel_icon_manager_v1` is not supported"); + return; + }, + }; - let (toplevel_icon, xdg_toplevel_icon) = match window_icon { - Some(icon) => { - let mut image_pool = self.image_pool.lock().unwrap(); - let toplevel_icon = match ToplevelIcon::new(icon, &mut image_pool) { - Ok(toplevel_icon) => toplevel_icon, - Err(error) => { - warn!("Error setting window icon: {error}"); - return; - }, - }; + let (toplevel_icon, xdg_toplevel_icon) = match window_icon { + Some(icon) => { + let mut image_pool = self.image_pool.lock().unwrap(); + let toplevel_icon = match ToplevelIcon::new(icon, &mut image_pool) { + Ok(toplevel_icon) => toplevel_icon, + Err(error) => { + warn!("Error setting window icon: {error}"); + return; + }, + }; - let xdg_toplevel_icon = - xdg_toplevel_icon_manager.create_icon(&self.queue_handle, GlobalData); + let xdg_toplevel_icon = + xdg_toplevel_icon_manager.create_icon(&self.queue_handle, GlobalData); - toplevel_icon.add_buffer(&xdg_toplevel_icon); + toplevel_icon.add_buffer(&xdg_toplevel_icon); - (Some(toplevel_icon), Some(xdg_toplevel_icon)) - }, - None => (None, None), - }; + (Some(toplevel_icon), Some(xdg_toplevel_icon)) + }, + None => (None, None), + }; - xdg_toplevel_icon_manager.set_icon(self.window.xdg_toplevel(), xdg_toplevel_icon.as_ref()); - self.toplevel_icon = toplevel_icon; + xdg_toplevel_icon_manager.set_icon(window.xdg_toplevel(), xdg_toplevel_icon.as_ref()); + self.toplevel_icon = toplevel_icon; - if let Some(xdg_toplevel_icon) = xdg_toplevel_icon { - xdg_toplevel_icon.destroy(); + if let Some(xdg_toplevel_icon) = xdg_toplevel_icon { + xdg_toplevel_icon.destroy(); + } } } @@ -1263,6 +1428,22 @@ impl WindowState { pub fn title(&self) -> &str { &self.title } + + pub fn children(&self) -> &Vec { + &self.children + } + + pub fn parent(&self) -> Option { + self.parent + } + + pub fn remove_child(&mut self, child: &WindowId) { + self.children.retain(|w| w != child); + } + + pub fn add_child(&mut self, child: WindowId) { + self.children.push(child); + } } impl Drop for WindowState { diff --git a/winit-web/src/window.rs b/winit-web/src/window.rs index f6d484890..834c846e1 100644 --- a/winit-web/src/window.rs +++ b/winit-web/src/window.rs @@ -13,7 +13,7 @@ use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoremMonitorHandle}; use winit_core::window::{ CursorGrabMode, ImeRequestError, ResizeDirection, Theme, UserAttentionType, - Window as RootWindow, WindowAttributes, WindowButtons, WindowId, WindowLevel, + Window as RootWindow, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType, }; use crate::r#async::Dispatcher; @@ -46,6 +46,12 @@ impl Window { target: &ActiveEventLoop, attr: WindowAttributes, ) -> Result { + if attr.window_type() == WindowType::Popup { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Popups are not implemented for Web", + ))); + } + let id = target.generate_id(); let window = target.runner.window(); @@ -103,6 +109,10 @@ impl Window { } impl RootWindow for Window { + fn window_type(&self) -> WindowType { + WindowType::Window + } + fn id(&self) -> WindowId { self.inner.queue(|inner| inner.id) } diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index 34e6f83a3..71486fd22 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -21,7 +21,7 @@ use windows_sys::Win32::Graphics::Dwm::{ use windows_sys::Win32::Graphics::Gdi::{ CDS_FULLSCREEN, ChangeDisplaySettingsExW, ClientToScreen, CreateRectRgn, DISP_CHANGE_BADFLAGS, DISP_CHANGE_BADMODE, DISP_CHANGE_BADPARAM, DISP_CHANGE_FAILED, DISP_CHANGE_SUCCESSFUL, - DeleteObject, InvalidateRgn, RDW_INTERNALPAINT, RedrawWindow, + DeleteObject, InvalidateRgn, RDW_INTERNALPAINT, RedrawWindow, ScreenToClient, }; use windows_sys::Win32::System::Com::{ CLSCTX_ALL, COINIT_APARTMENTTHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize, @@ -36,7 +36,7 @@ use windows_sys::Win32::UI::Input::Touch::{RegisterTouchWindow, TWF_WANTPALM}; use windows_sys::Win32::UI::WindowsAndMessaging::{ CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, EnableMenuItem, FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY, FLASHWINFO, FlashWindowEx, GWLP_HINSTANCE, - GetClientRect, GetCursorPos, GetForegroundWindow, GetSystemMenu, GetSystemMetrics, + GetClientRect, GetCursorPos, GetForegroundWindow, GetParent, GetSystemMenu, GetSystemMetrics, GetWindowPlacement, GetWindowTextLengthW, GetWindowTextW, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTCAPTION, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IsWindowVisible, LoadCursorW, MENU_ITEM_STATE, MF_BYCOMMAND, MFS_DISABLED, MFS_ENABLED, NID_READY, PM_NOREMOVE, @@ -48,13 +48,13 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ WM_SYSCOMMAND, WNDCLASSEXW, }; use winit_core::cursor::Cursor; -use winit_core::error::RequestError; +use winit_core::error::{NotSupportedError, RequestError}; use winit_core::icon::{Icon, RgbaIcon}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider}; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, - WindowLevel, + WindowLevel, WindowType, }; use crate::dark_mode::try_theme; @@ -100,6 +100,9 @@ pub struct Window { // The events loop proxy. thread_executor: event_loop::EventLoopThreadExecutor, + + /// The type of window of this + window_type: WindowType, } impl Window { @@ -118,6 +121,49 @@ impl Window { self.window_state.lock().unwrap() } + // If we have a popup the position is relative to the parent window and not + // relative to the screen. Therefore we have to translate it from the parent + // coordinate system to the display coordinate system + fn translate_outer_position(&self, position: Position) -> PhysicalPosition { + let position = position.to_physical::(self.scale_factor()); + let mut point = POINT { x: position.x, y: position.y }; + + let window_flags = self.window_state_lock().window_flags; + if window_flags.contains(WindowFlags::POPUP) && !window_flags.contains(WindowFlags::CHILD) { + let parent = unsafe { GetParent(self.hwnd()) }; + if !parent.is_null() { + unsafe { + ClientToScreen(parent, &mut point); + } + } + } + + PhysicalPosition::new(point.x, point.y) + } + + // Inverse of `translate_outer_position`: if we have a popup the position is + // reported relative to the parent window instead of the screen. Therefore we + // translate it from the display coordinate system back to the parent + // coordinate system. Non-popup windows are left in screen coordinates. + fn translate_outer_position_to_parent( + &self, + position: PhysicalPosition, + ) -> PhysicalPosition { + 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 { + ScreenToClient(parent, &mut point); + } + } + } + + PhysicalPosition::new(point.x, point.y) + } + /// Returns the `hwnd` of this window. pub fn hwnd(&self) -> HWND { self.window.hwnd() @@ -416,6 +462,10 @@ impl rwh_06::HasWindowHandle for Window { } impl CoreWindow for Window { + fn window_type(&self) -> WindowType { + self.window_type + } + fn set_title(&self, text: &str) { let wide_text = util::encode_wide(text); unsafe { @@ -464,7 +514,10 @@ impl CoreWindow for Window { fn outer_position(&self) -> Result, RequestError> { util::WindowArea::Outer .get_rect(self.hwnd()) - .map(|rect| Ok(PhysicalPosition::new(rect.left, rect.top))) + .map(|rect| { + Ok(self + .translate_outer_position_to_parent(PhysicalPosition::new(rect.left, rect.top))) + }) .expect( "Unexpected GetWindowRect failure; please report this error to \ rust-windowing/winit", @@ -483,7 +536,7 @@ impl CoreWindow for Window { } fn set_outer_position(&self, position: Position) { - let (x, y): (i32, i32) = position.to_physical::(self.scale_factor()).into(); + let position = self.translate_outer_position(position); let window_state = Arc::clone(&self.window_state); let window = self.window; @@ -498,8 +551,8 @@ impl CoreWindow for Window { SetWindowPos( self.hwnd(), ptr::null_mut(), - x, - y, + position.x, + position.y, 0, 0, SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE, @@ -1211,6 +1264,7 @@ impl InitData<'_> { window: SyncWindowHandle(window), window_state, thread_executor: self.runner.create_thread_executor(), + window_type: self.attributes.window_type, } } @@ -1369,8 +1423,10 @@ unsafe fn init( let class_name = util::encode_wide(&win_attributes.class_name); unsafe { register_window_class(&class_name) }; + let is_popup = matches!(attributes.window_type, WindowType::Popup); let mut window_flags = WindowFlags::empty(); window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations); + window_flags.set(WindowFlags::POPUP, is_popup); window_flags.set(WindowFlags::MARKER_UNDECORATED_SHADOW, win_attributes.decoration_shadow); window_flags .set(WindowFlags::ALWAYS_ON_TOP, attributes.window_level == WindowLevel::AlwaysOnTop); @@ -1380,7 +1436,9 @@ unsafe fn init( window_flags.set(WindowFlags::MARKER_ACTIVATE, attributes.active); window_flags.set(WindowFlags::TRANSPARENT, attributes.transparent); // WindowFlags::VISIBLE and MAXIMIZED are set down below after the window has been configured. - window_flags.set(WindowFlags::RESIZABLE, attributes.resizable); + // Popups are never resizable, matching the Wayland and macOS backends (and avoiding the + // thick `WS_SIZEBOX` resize frame). + window_flags.set(WindowFlags::RESIZABLE, attributes.resizable && !is_popup); // Will be changed later using `window.set_enabled_buttons` but we need to set a default here // so the diffing later can work. window_flags.set(WindowFlags::CLOSABLE, true); @@ -1399,14 +1457,23 @@ unsafe fn init( let parent = match attributes.parent_window() { Some(rwh_06::RawWindowHandle::Win32(handle)) => { - window_flags.set(WindowFlags::CHILD, true); + if !is_popup { + window_flags.set(WindowFlags::CHILD, true); + } if win_attributes.menu.is_some() { warn!("Setting a menu on a child window is unsupported"); } Some(handle.hwnd.get() as HWND) }, Some(raw) => unreachable!("Invalid raw window handle {raw:?} on Windows"), - None => fallback_parent(), + None => { + if is_popup { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Popup without a parent is not supported!", + ))); + } + fallback_parent() + }, }; let menu = win_attributes.menu; diff --git a/winit-win32/src/window_state.rs b/winit-win32/src/window_state.rs index eefb654b0..c0b24f9b9 100644 --- a/winit-win32/src/window_state.rs +++ b/winit-win32/src/window_state.rs @@ -13,9 +13,9 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ SWP_NOREPOSITION, SWP_NOSIZE, SWP_NOZORDER, SendMessageW, SetWindowLongW, SetWindowPos, ShowWindow, WINDOW_EX_STYLE, WINDOW_STYLE, WINDOWPLACEMENT, WS_BORDER, WS_CAPTION, WS_CHILD, WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LAYERED, - WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_EX_WINDOWEDGE, WS_MAXIMIZE, - WS_MAXIMIZEBOX, WS_MINIMIZE, WS_MINIMIZEBOX, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, - WS_SYSMENU, WS_VISIBLE, + WS_EX_NOACTIVATE, WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOPMOST, WS_EX_TRANSPARENT, + WS_EX_WINDOWEDGE, WS_MAXIMIZE, WS_MAXIMIZEBOX, WS_MINIMIZE, WS_MINIMIZEBOX, + WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, WS_VISIBLE, }; use winit_core::icon::Icon; use winit_core::keyboard::ModifiersState; @@ -276,8 +276,18 @@ impl WindowFlags { pub fn to_window_styles(self) -> (WINDOW_STYLE, WINDOW_EX_STYLE) { // Required styles to properly support common window functionality like aero snap. - let mut style = WS_CAPTION | WS_BORDER | WS_CLIPSIBLINGS | WS_SYSMENU; + let mut style = WS_CLIPSIBLINGS; let mut style_ex = WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES; + if self.contains(WindowFlags::POPUP) { + style |= WS_POPUP; + // Don't activate the popup (and thus don't deactivate the parent) when it is shown or + // clicked, unless the caller requested activation via `WindowAttributes::active`. + if !self.contains(WindowFlags::MARKER_ACTIVATE) { + style_ex |= WS_EX_NOACTIVATE; + } + } else { + style |= WS_CAPTION | WS_SYSMENU | WS_BORDER; + }; if self.contains(WindowFlags::RESIZABLE) { style |= WS_SIZEBOX; @@ -300,8 +310,8 @@ impl WindowFlags { if self.contains(WindowFlags::NO_BACK_BUFFER) { style_ex |= WS_EX_NOREDIRECTIONBITMAP; } - if self.contains(WindowFlags::CHILD) { - style |= WS_CHILD; // This is incompatible with WS_POPUP if that gets added eventually. + if self.contains(WindowFlags::CHILD) && !self.contains(WindowFlags::POPUP) { + style |= WS_CHILD; // Remove decorations window styles for child if !self.contains(WindowFlags::MARKER_DECORATIONS) { @@ -309,9 +319,6 @@ impl WindowFlags { style_ex &= !WS_EX_WINDOWEDGE; } } - if self.contains(WindowFlags::POPUP) { - style |= WS_POPUP; - } if self.contains(WindowFlags::MINIMIZED) { style |= WS_MINIMIZE; } diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 4c1a0f356..0e0246f42 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -66,13 +66,26 @@ impl Window { event_loop: &ActiveEventLoop, attribs: WindowAttributes, ) -> Result { - let window = Arc::new(UnownedWindow::new(event_loop, attribs)?); - event_loop.windows.borrow_mut().insert(window.id(), Arc::downgrade(&window)); - Ok(Window(window)) + use winit_core::window::WindowType; + match attribs.window_type() { + WindowType::Window => { + let window = Arc::new(UnownedWindow::new(event_loop, attribs)?); + event_loop.windows.borrow_mut().insert(window.id(), Arc::downgrade(&window)); + Ok(Window(window)) + }, + WindowType::Popup => Err(RequestError::NotSupported(NotSupportedError::new( + "Popups are not implemented for X11", + ))), + _ => Err(RequestError::NotSupported(NotSupportedError::new("Unsupported window type"))), + } } } impl CoreWindow for Window { + fn window_type(&self) -> winit_core::window::WindowType { + winit_core::window::WindowType::Window + } + fn id(&self) -> WindowId { self.0.id() } diff --git a/winit/examples/child_window.rs b/winit/examples/child_window.rs index 24c72ffd8..6b53ac981 100644 --- a/winit/examples/child_window.rs +++ b/winit/examples/child_window.rs @@ -1,4 +1,4 @@ -#[cfg(any(x11_platform, macos_platform, windows_platform))] +#[cfg(any(wayland_platform, x11_platform, macos_platform, windows_platform))] #[allow(deprecated)] fn main() -> Result<(), impl std::error::Error> { use std::collections::HashMap; @@ -10,7 +10,7 @@ fn main() -> Result<(), impl std::error::Error> { 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}; + use winit::window::{Window, WindowAttributes, WindowId, WindowType}; #[path = "util/fill.rs"] mod fill; @@ -39,6 +39,7 @@ fn main() -> Result<(), impl std::error::Error> { fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { let attributes = WindowAttributes::default() .with_title("parent window") + .with_window_type(WindowType::Window) .with_position(Position::Logical(LogicalPosition::new(0.0, 0.0))) .with_surface_size(LogicalSize::new(640.0f32, 480.0f32)); let window = event_loop.create_window(attributes).unwrap(); @@ -131,10 +132,10 @@ fn main() -> Result<(), impl std::error::Error> { event_loop.run_app(Application { context, parent_window_id: None, windows: HashMap::new() }) } -#[cfg(not(any(x11_platform, macos_platform, windows_platform)))] +#[cfg(not(any(wayland_platform, x11_platform, macos_platform, windows_platform)))] fn main() { panic!( - "This example is supported only on x11, macOS, and Windows, with the `rwh_06` feature \ - enabled." + "This example is supported only on wayland, x11, macOS, and Windows, with the `rwh_06` \ + feature enabled." ); } diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index e359af3ba..420859434 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -51,6 +51,7 @@ changelog entry. - On Android, added scancode conversions for more obscure key codes. - On Wayland, added `HoldGesture` event for multi-finger hold gestures - On Wayland, added ext-background-effect-v1 support. +- On Wayland, Windows and macOS, added native popups (`WindowType::Popup`). ### Changed