From bd6dea97b60fcfd491a6a8a2070a536c41694031 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 4 Jun 2026 15:07:46 +0200 Subject: [PATCH 01/17] initial commit to implement popup --- winit-wayland/Cargo.toml | 4 +- winit-wayland/src/event_loop/mod.rs | 9 +- winit-wayland/src/lib.rs | 2 + winit-wayland/src/popup.rs | 524 ++++++++++++++++++++++++++++ winit-wayland/src/popup/state.rs | 122 +++++++ winit-wayland/src/state.rs | 63 +++- winit-wayland/src/window/mod.rs | 36 +- winit-wayland/src/window/state.rs | 442 ++++++++++++++--------- 8 files changed, 1009 insertions(+), 193 deletions(-) create mode 100644 winit-wayland/src/popup.rs create mode 100644 winit-wayland/src/popup/state.rs diff --git a/winit-wayland/Cargo.toml b/winit-wayland/Cargo.toml index 27a5972337..9fada9960c 100644 --- a/winit-wayland/Cargo.toml +++ b/winit-wayland/Cargo.toml @@ -34,10 +34,10 @@ foldhash.workspace = true libc.workspace = true memmap2.workspace = true rustix = { workspace = true, features = ["std", "system", "thread", "process", "event", "pipe"] } -sctk = { package = "smithay-client-toolkit", version = "0.20.0", default-features = false, features = [ +sctk = { package = "smithay-client-toolkit", path = "../../client-toolkit", default-features = false, features = [ "calloop", ] } -sctk-adwaita = { version = "0.11.0", default-features = false, optional = true } +sctk-adwaita = { path="../../sctk-adwaita", default-features = false, optional = true } wayland-backend = { version = "0.3.10", default-features = false, features = ["client_system"] } wayland-client = "0.31.10" wayland-protocols = { version = "0.32.8", features = ["staging"] } diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 3a6ca6982b..a2c71eb56f 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -651,8 +651,13 @@ 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)) + if window_attributes.parent_window().is_some() { + let popup = crate::Popup::new(self, window_attributes)?; + Ok(Box::new(popup)) + } else { + let window = crate::Window::new(self, window_attributes)?; + Ok(Box::new(window)) + } } fn available_monitors(&self) -> Box> { diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 930562b380..594dd0ff1d 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -40,9 +40,11 @@ mod seat; mod state; mod types; mod window; +mod popup; pub use self::event_loop::{ActiveEventLoop, EventLoop}; pub use self::window::Window; +pub use self::popup::Popup; /// Additional methods on [`ActiveEventLoop`] that are specific to Wayland. pub trait ActiveEventLoopExtWayland { diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs new file mode 100644 index 0000000000..bbe6093b10 --- /dev/null +++ b/winit-wayland/src/popup.rs @@ -0,0 +1,524 @@ +use std::sync::{Arc, Mutex}; + +use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; +use rwh_06::RawWindowHandle; +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::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; +use winit_core::window::{ + CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, + UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, + WindowLevel, +}; +mod state; +use std::sync::atomic::AtomicBool; + +pub use state::State; +use wayland_protocols::xdg::shell::client::xdg_positioner::Anchor; + +use super::ActiveEventLoop; +use crate::window::WindowRequests; +use crate::window::state::{WindowState, WindowType}; + +#[derive(Debug)] +pub struct Popup { + /// Reference to the underlying SCTK popup. + popup: SctkPopup, + + // The state of the popup. + popup_state: Arc>, + /// Window id. + window_id: WindowId, + + /// The wayland display used solely for raw window handle. + #[allow(dead_code)] + display: WlDisplay, +} + +impl Popup { + pub(crate) fn new( + event_loop_window_target: &ActiveEventLoop, + attributes: WindowAttributes, + ) -> Result { + macro_rules! error { + ($e:literal) => { + RequestError::NotSupported(NotSupportedError::new($e)) + }; + } + + let window_handle = + attributes.parent_window().ok_or(error!("Popup without a parent is not supported!"))?; + if let RawWindowHandle::Wayland(wayland_window_handle) = window_handle { + let queue_handle = event_loop_window_target.queue_handle.clone(); + let mut state = event_loop_window_target.state.borrow_mut(); + let (popup, popup_state) = if let Some(window_state) = state + .windows + .borrow() + .get(&WindowId::from_raw(wayland_window_handle.surface.as_ptr() as usize)) + { + let position = attributes.position.ok_or(error!("No position specified"))?; + let positioner = XdgPositioner::new(&state.xdg_shell) + .map_err(|_| error!("Failed to create positioner"))?; + let scale_factor = 1.0; // We don't know yet the scale factor + let size = attributes.surface_size.ok_or(error!("Invalid size for popup"))?; + positioner.set_anchor_rect( + position.to_logical(scale_factor).x, + position.to_logical(scale_factor).y, + 10, + 10, + ); + positioner.set_size( + size.to_logical(scale_factor).width, + size.to_logical(scale_factor).height, + ); + positioner.set_anchor(Anchor::TopLeft); + + let window = &window_state.lock().unwrap().window; + let parent_surface = 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"))?; + + let mut popup_state = WindowState::new( + event_loop_window_target.handle.clone(), + &event_loop_window_target.queue_handle, + &state, + size, + WindowType::Popup((popup.clone(), None)), + attributes.preferred_theme, + false, + ); + let popup_state = Arc::new(Mutex::new(popup_state)); + + (popup, popup_state) + } else { + return Err(error!("Parent window id unknown")); + }; + + popup.wl_surface().commit(); + // popup.commit(); Trait not implemented + + 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()); + + 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))?; + } + + Ok(Self { + popup, + popup_state, + window_id, + display: event_loop_window_target.handle.connection.display().clone(), + }) + } else { + Err(RequestError::NotSupported(NotSupportedError::new( + "Not a wayland window handle passed", + ))) + } + } +} + +impl CoreWindow for Popup { + 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(); + // } + } + + #[inline] + fn title(&self) -> String { + self.popup_state.lock().unwrap().title().to_owned() + } + + fn pre_present_notify(&self) { + // self.popup_state.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> { + Err(NotSupportedError::new("window position information is not available on Wayland") + .into()) + } + + fn set_outer_position(&self, _position: Position) { + // Not possible. + } + + fn surface_size(&self) -> PhysicalSize { + let popup_state = self.popup_state.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 mut popup_state = self.popup_state.lock().unwrap(); + // let new_size = popup_state.request_surface_size(size); + // self.request_redraw(); + // Some(new_size) + None + } + + fn outer_size(&self) -> PhysicalSize { + // let popup_state = self.popup_state.lock().unwrap(); + // let scale_factor = popup_state.scale_factor(); + // super::logical_to_physical_rounded(popup_state.outer_size(), scale_factor) + PhysicalSize::new(100, 100) + } + + 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)); + // self.state.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)); + // self.popup_state.lock().unwrap().set_max_surface_size(max_size); + // // NOTE: Requires commit to be applied. + // self.request_redraw(); + } + + fn surface_resize_increments(&self) -> Option> { + // let popup_state = self.popup_state.lock().unwrap(); + // let scale_factor = popup_state.scale_factor(); + // popup_state + // .resize_increments() + // .map(|size| super::logical_to_physical_rounded(size, scale_factor)) + None + } + + fn set_surface_resize_increments(&self, increments: Option) { + // let mut popup_state = self.popup_state.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) { + self.popup_state.lock().unwrap().set_title(title.to_owned()); + } + + #[inline] + fn set_transparent(&self, transparent: bool) { + self.popup_state.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) { + // if self.popup_state.lock().unwrap().set_resizable(resizable) { + // // NOTE: Requires commit to be applied. + // self.request_redraw(); + // } + } + + fn is_resizable(&self) -> bool { + // TODO + // self.popup_state.lock().unwrap().resizable() + 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 { + self.popup_state.lock().unwrap().scale_factor() + } + + #[inline] + fn set_blur(&self, blur: bool) { + // self.popup_state.lock().unwrap().set_blur(blur); + } + + #[inline] + fn set_decorations(&self, decorate: bool) { + // self.popup_state.lock().unwrap().set_decorate(decorate) + } + + #[inline] + fn is_decorated(&self) -> bool { + // self.popup_state.lock().unwrap().is_decorated() + false + } + + fn set_window_level(&self, _level: WindowLevel) {} + + fn set_window_icon(&self, window_icon: Option) { + // self.popup_state.lock().unwrap().set_window_icon(window_icon) + } + + #[inline] + fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> { + // let state_changed = self.popup_state.lock().unwrap().request_ime_update(request)?; + + // 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(); + // } + + Ok(()) + } + + #[inline] + fn ime_capabilities(&self) -> Option { + // self.popup_state.lock().unwrap().ime_allowed() + None + } + + fn focus_window(&self) {} + + fn has_focus(&self) -> bool { + // self.popup_state.lock().unwrap().has_focus() + false + } + + 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(); + } + + fn set_theme(&self, theme: Option) { + // self.popup_state.lock().unwrap().set_theme(theme) + } + + fn theme(&self) -> Option { + // self.popup_state.lock().unwrap().theme() + None + } + + fn set_content_protected(&self, _protected: bool) {} + + fn set_cursor(&self, cursor: Cursor) { + // let popup_state = &mut self.popup_state.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 scale_factor = self.scale_factor(); + // let position = position.to_logical(scale_factor); + // self.popup_state + // .lock() + // .unwrap() + // .set_cursor_position(position) + // // Request redraw on success, since the state is double buffered. + // .map(|_| self.request_redraw()) + Err(RequestError::Ignored) + } + + fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), RequestError> { + // self.popup_state.lock().unwrap().set_cursor_grab(mode) + Err(RequestError::Ignored) + } + + fn set_cursor_visible(&self, visible: bool) { + // self.popup_state.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> { + // TODO: implement + // self.popup_state.lock().unwrap().drag_resize_window(direction) + Err(RequestError::Ignored) + } + + fn show_window_menu(&self, position: Position) { + // let scale_factor = self.scale_factor(); + // let position = position.to_logical(scale_factor); + // self.popup_state.lock().unwrap().show_window_menu(position); + } + + 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(()) + // } + Err(RequestError::Ignored) + } + + fn current_monitor(&self) -> Option { + // let data = self.window.wl_surface().data::()?; + // data.outputs() + // .next() + // .map(MonitorHandle::new) + // .map(|monitor| CoreMonitorHandle(Arc::new(monitor))) + None + } + + fn available_monitors(&self) -> Box> { + // Box::new( + // self.monitors + // .lock() + // .unwrap() + // .clone() + // .into_iter() + // .map(|inner| CoreMonitorHandle(Arc::new(inner))), + // ) + Box::new([].into_iter()) + } + + 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 rwh_06::HasWindowHandle for Popup { + fn window_handle(&self) -> Result, rwh_06::HandleError> { + let raw = rwh_06::WaylandWindowHandle::new({ + let ptr = self.popup.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> { + 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())) } + } +} diff --git a/winit-wayland/src/popup/state.rs b/winit-wayland/src/popup/state.rs new file mode 100644 index 0000000000..d9a801a6e6 --- /dev/null +++ b/winit-wayland/src/popup/state.rs @@ -0,0 +1,122 @@ +use std::sync::Arc; + +use dpi::{LogicalSize, Size}; +use sctk::shell::xdg::XdgPositioner; +use sctk::shell::xdg::popup::{PopupConfigure, PopupHandler}; +use sctk::subcompositor::{self, SubcompositorState}; +use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; + +#[derive(Debug)] +pub struct State { + positioner: XdgPositioner, + + /// The current window title. + title: String, + + scale_factor: f64, + fractional_scale: Option, + + pub last_configure: Option, + + /// The size of the popup + size: LogicalSize, + + /// True if the compositor constrained the size of the popup + constrained: bool, + + /// TODO: maybe this can be removed, because we can use also last_configure to determine + /// If called the first time or not! + /// Initial window size provided by the user. Removed on the first + /// configure. + /// Required because we don't know the scaling yet + /// when constructing the state + initial_size: Option, +} + +impl State { + pub fn new(positioner: XdgPositioner, initial_size: Size) -> Self { + Self { + positioner, + initial_size: Some(initial_size), + constrained: false, + size: initial_size.to_logical(1.), + scale_factor: 1., + last_configure: None, + title: String::default(), + fractional_scale: None, + } + } + + pub fn configure(&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()); + } + + // The popup was constrained to a different size by the compositor + assert!(configure.width >= 0); + assert!(configure.height >= 0); + self.constrained = self.size.width != configure.width as u32 + || self.size.height != configure.height as u32; + let new_size = LogicalSize { + width: (configure.width as u32).into(), + height: (configure.height as u32).into(), + }; + + // NOTE: Set the configure before doing a resize, since we query it during it. + self.last_configure = Some(configure); + + if self.constrained { + self.resize(new_size); + } + + // false + } + + #[inline] + pub fn set_title(&mut self, title: &str) { + self.title = title.to_owned(); + } + + #[inline] + pub fn title(&self) -> &str { + &self.title + } + + /// Set the scale factor for the given window. + #[inline] + pub fn set_scale_factor(&mut self, scale_factor: f64) { + self.scale_factor = scale_factor; + + // NOTE: When fractional scaling is not used update the buffer scale. + // if self.fractional_scale.is_none() { + // let _ = self.window.set_buffer_scale(self.scale_factor as _); + // } + + // if let Some(frame) = self.frame.as_mut() { + // frame.set_scaling_factor(scale_factor); + // } + } + + #[inline] + pub fn is_configured(&self) -> bool { + self.last_configure.is_some() + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + #[inline] + pub fn surface_size(&self) -> LogicalSize { + self.size + } + + fn resize(&mut self, surface_size: LogicalSize) { + self.size = surface_size; + } +} diff --git a/winit-wayland/src/state.rs b/winit-wayland/src/state.rs index 9f5eda4037..5624aff8cb 100644 --- a/winit-wayland/src/state.rs +++ b/winit-wayland/src/state.rs @@ -16,13 +16,13 @@ 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}; use sctk::subcompositor::SubcompositorState; use winit_core::error::OsError; -use crate::WindowId; use crate::event_loop::sink::EventSink; use crate::output::MonitorHandle; use crate::seat::{ @@ -36,6 +36,7 @@ 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::{WindowId, popup}; /// Winit's Wayland state. #[derive(Debug)] @@ -292,24 +293,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. @@ -325,6 +326,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()); + println!("Finished configuring the popup: {:?}", window_id); + + // 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 + }; + + // Populate the configure to the window. + // self.window_compositor_updates[index].resized |= false; + 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()); + println!("Destroying popup with id: {:?}", window_id); + 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 @@ -451,4 +503,5 @@ sctk::delegate_output!(WinitState); sctk::delegate_registry!(WinitState); sctk::delegate_shm!(WinitState); sctk::delegate_xdg_shell!(WinitState); +sctk::delegate_xdg_popup!(WinitState); sctk::delegate_xdg_window!(WinitState); diff --git a/winit-wayland/src/window/mod.rs b/winit-wayland/src/window/mod.rs index 81849be78e..f7d024d9a7 100644 --- a/winit-wayland/src/window/mod.rs +++ b/winit-wayland/src/window/mod.rs @@ -30,6 +30,7 @@ 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; @@ -117,7 +118,7 @@ impl Window { &event_loop_window_target.queue_handle, &state, size, - window.clone(), + state::WindowType::Window((window.clone(), None)), attributes.preferred_theme, prefer_csd, ); @@ -448,13 +449,14 @@ 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) { @@ -474,14 +476,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(); diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 3a1a9b5275..250174f114 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -23,6 +23,7 @@ use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as use sctk::seat::pointer::{PointerDataExt, ThemedPointer}; use sctk::shell::WaylandSurface; use sctk::shell::xdg::XdgSurface; +use sctk::shell::xdg::popup::{Popup, PopupConfigure}; use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure}; use sctk::shm::Shm; use sctk::shm::slot::SlotPool; @@ -37,7 +38,6 @@ use winit_core::window::{ }; use crate::event_loop::OwnedDisplayHandle; -use crate::logical_to_physical_rounded; use crate::seat::{ PointerConstraintsState, TextInputClientState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext, @@ -46,6 +46,7 @@ use crate::state::{WindowCompositorUpdate, WinitState}; use crate::types::cursor::{CustomCursor, SelectedCursor, WaylandCustomCursor}; use crate::types::kwin_blur::KWinBlurManager; use crate::types::xdg_toplevel_icon_manager::ToplevelIcon; +use crate::{logical_to_physical_rounded, popup}; #[cfg(feature = "sctk-adwaita")] pub type WinitFrame = sctk_adwaita::AdwaitaFrame; @@ -55,6 +56,40 @@ 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, Option)), + Popup((Popup, Option)), +} + +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 { @@ -64,9 +99,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>>, @@ -165,7 +197,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 @@ -182,7 +214,7 @@ impl WindowState { queue_handle: &QueueHandle, winit_state: &WinitState, initial_size: Size, - window: Window, + window: WindowType, theme: Option, prefer_csd: bool, ) -> Self { @@ -221,7 +253,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, @@ -281,8 +312,39 @@ impl WindowState { FrameCallbackState::Requested => (), } } + pub fn configure_popup(&mut self, configure: PopupConfigure) { + // 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()); + } + + // The popup was constrained to a different size by the compositor + assert!(configure.width >= 0); + assert!(configure.height >= 0); + 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).into(), + height: (configure.height as u32).into(), + }; + + // NOTE: Set the configure before doing a resize, since we query it during it. + if let WindowType::Popup((_, last_configure)) = &mut self.window { + *last_configure = Some(configure) + } else { + // This should never happen + assert!(false); + return; + } + + if constrained { + self.resize(new_size); + } + } - pub fn configure( + pub fn configure_window( &mut self, configure: WindowConfigure, shm: &Shm, @@ -400,26 +462,32 @@ impl WindowState { } let new_state = configure.state; - let old_state = self.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); - - // NOTE: Set the configure before doing a resize, since we query it during it. - self.last_configure = Some(configure); - - if state_change_requires_resize || new_size != self.surface_size() { - self.resize(new_size); - true + 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); + + // 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 + } else { + false + } } else { - false + // This should never happen + assert!(false); + return false; } } @@ -451,29 +519,40 @@ impl WindowState { /// Start interacting drag resize. pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), RequestError> { - let xdg_toplevel = self.window.xdg_toplevel(); - - // TODO(kchibisov) handle touch serials. - self.apply_on_pointer(|_, data| { - let serial = data.latest_button_serial(); - let seat = data.seat(); - xdg_toplevel.resize(seat, serial, resize_direction_to_xdg(direction)); - }); - - Ok(()) + if let WindowType::Window((window, _)) = &self.window { + let xdg_toplevel = window.xdg_toplevel(); + + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + xdg_toplevel.resize(seat, serial, resize_direction_to_xdg(direction)); + }); + Ok(()) + } else { + // 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| { - let serial = data.latest_button_serial(); - let seat = data.seat(); - xdg_toplevel._move(seat, serial); - }); - - Ok(()) + if let WindowType::Window((window, _)) = &self.window { + let xdg_toplevel = window.xdg_toplevel(); + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + xdg_toplevel._move(seat, serial); + }); + + Ok(()) + } else { + // Popup + Err(RequestError::NotSupported(NotSupportedError::new("Drag for popup not supported"))) + } } /// Tells whether the window should be closed. @@ -488,32 +567,36 @@ 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, - }; - self.window.resize(seat, serial, edge); - }, - FrameAction::ShowMenu(x, y) => self.window.show_window_menu(seat, serial, (x, y)), - _ => (), - }; + if let WindowType::Window((window, _)) = &self.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)), + _ => (), + }; - Some(false) + Some(false) + } else { + None + } } pub fn frame_point_left(&mut self) { @@ -531,18 +614,22 @@ 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(); - - 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 + if let WindowType::Window((window, _)) = &self.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() { + window.move_(seat, serial); + None + } else { + cursor + } } else { - cursor + None } } else { None @@ -606,21 +693,24 @@ 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() + if let WindowType::Window((_, last_configure)) = &mut self.window { + 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 + } } else { - // Server side decorations. - true + false } } @@ -695,8 +785,10 @@ 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())) + if let WindowType::Window((_, last_configure)) = &mut self.window { + if last_configure.as_ref().map(Self::is_stateless).unwrap_or(true) { + self.resize(surface_size.to_logical(self.scale_factor())) + } } logical_to_physical_rounded(self.surface_size(), self.scale_factor()) @@ -707,8 +799,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 Some(true) == last_configure.as_ref().map(Self::is_stateless) { + self.stateless_size = surface_size; + } } // Update the inner frame. @@ -837,33 +931,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); - - // Add the borders. - let 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 { + // 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); - 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) - }); - - self.max_surface_size = size; - self.window.set_max_size(size.map(Into::into)); + 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; + window.set_max_size(size.map(Into::into)); + } } /// Set the CSD theme. @@ -960,12 +1058,14 @@ impl WindowState { } pub fn show_window_menu(&self, position: LogicalPosition) { - // TODO(kchibisov) handle touch serials. - self.apply_on_pointer(|_, data| { - let 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| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + window.show_window_menu(seat, serial, position.into()); + }); + } } /// Set the position of the cursor. @@ -1016,22 +1116,24 @@ 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)), - _ => (), - } + if let WindowType::Window((window, last_configure)) = &self.window { + 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); + } } } @@ -1149,46 +1251,50 @@ impl WindowState { frame.set_title(&title); } - self.window.set_title(&title); + if let WindowType::Window((window, _)) = &self.window { + window.set_title(&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(); + } } } From ae3178cfece26be50284569f16238b8022cabc77 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 26 Mar 2026 09:23:27 +0100 Subject: [PATCH 02/17] use dedicated type to determine if the window is a popup or not Reason: Because a normal window can have a parent window as well, like a Dialog --- winit-core/src/window.rs | 26 ++++++++++++++++++++++++++ winit-wayland/src/event_loop/mod.rs | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 6f564ba900..543fada88f 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -46,6 +46,13 @@ impl fmt::Debug for WindowId { } } +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub enum WindowType { + #[default] + Window, + Popup, +} + /// Attributes used when creating a window. #[derive(Debug)] #[non_exhaustive] @@ -72,6 +79,7 @@ pub struct WindowAttributes { pub(crate) parent_window: Option, pub fullscreen: Option, pub platform: Option>, + pub window_type: WindowType, } impl WindowAttributes { @@ -378,6 +386,22 @@ impl WindowAttributes { self.platform = Some(platform); self } + + /// Sets the window type of the object + /// + /// Currently only wayland is using this type. On X11 popups are also just normal windows + /// Note: If the type is set to `WindowType::Popup` the parent must be set as well with + /// `with_parent_window()`. + pub fn as_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 popup(&self) -> bool { + self.window_type == WindowType::Popup + } } impl Clone for WindowAttributes { @@ -405,6 +429,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 +460,7 @@ impl Default for WindowAttributes { platform: Default::default(), cursor: Cursor::default(), blur: Default::default(), + window_type: Default::default(), } } } diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index a2c71eb56f..a728fa47fb 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -651,7 +651,7 @@ impl RootActiveEventLoop for ActiveEventLoop { &self, window_attributes: winit_core::window::WindowAttributes, ) -> Result, RequestError> { - if window_attributes.parent_window().is_some() { + if window_attributes.popup() { let popup = crate::Popup::new(self, window_attributes)?; Ok(Box::new(popup)) } else { From 8f42bbcc385c023b465b8c57f75672657fb3e885 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 26 Mar 2026 09:44:57 +0100 Subject: [PATCH 03/17] determine the scale factor from the parent window --- winit-wayland/src/popup.rs | 18 ++++++++++-------- winit-wayland/src/window/mod.rs | 18 ++++++++++++++++-- winit-wayland/src/window/state.rs | 3 ++- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs index bbe6093b10..370842a787 100644 --- a/winit-wayland/src/popup.rs +++ b/winit-wayland/src/popup.rs @@ -50,20 +50,21 @@ impl Popup { }; } - let window_handle = + let parent_window_handle = attributes.parent_window().ok_or(error!("Popup without a parent is not supported!"))?; - if let RawWindowHandle::Wayland(wayland_window_handle) = window_handle { + 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 (popup, popup_state) = if let Some(window_state) = state + let (popup, popup_state) = if let Some(parent_window_state) = state .windows .borrow() - .get(&WindowId::from_raw(wayland_window_handle.surface.as_ptr() as usize)) + .get(&WindowId::from_raw(parent_window_handle.surface.as_ptr() as usize)) { + // Use the scale factor of the parent + let scale_factor = parent_window_state.lock().unwrap().scale_factor(); let position = attributes.position.ok_or(error!("No position specified"))?; let positioner = XdgPositioner::new(&state.xdg_shell) .map_err(|_| error!("Failed to create positioner"))?; - let scale_factor = 1.0; // We don't know yet the scale factor let size = attributes.surface_size.ok_or(error!("Invalid size for popup"))?; positioner.set_anchor_rect( position.to_logical(scale_factor).x, @@ -77,8 +78,8 @@ impl Popup { ); positioner.set_anchor(Anchor::TopLeft); - let window = &window_state.lock().unwrap().window; - let parent_surface = window.xdg_surface(); + let parent_window = &parent_window_state.lock().unwrap().window; + let parent_surface = parent_window.xdg_surface(); let surface = state.compositor_state.create_surface(&queue_handle); let popup = SctkPopup::from_surface( Some(parent_surface), @@ -89,7 +90,7 @@ impl Popup { ) .map_err(|_| error!("Failed to create popup"))?; - let mut popup_state = WindowState::new( + let popup_state = WindowState::new( event_loop_window_target.handle.clone(), &event_loop_window_target.queue_handle, &state, @@ -97,6 +98,7 @@ impl Popup { WindowType::Popup((popup.clone(), None)), attributes.preferred_theme, false, + scale_factor, ); let popup_state = Arc::new(Mutex::new(popup_state)); diff --git a/winit-wayland/src/window/mod.rs b/winit-wayland/src/window/mod.rs index f7d024d9a7..7fcae63de9 100644 --- a/winit-wayland/src/window/mod.rs +++ b/winit-wayland/src/window/mod.rs @@ -6,6 +6,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use dpi::{LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; +use rwh_06::RawWindowHandle; use sctk::compositor::{CompositorState, Region, SurfaceData}; use sctk::reexports::client::protocol::wl_display::WlDisplay; use sctk::reexports::client::protocol::wl_surface::WlSurface; @@ -32,9 +33,7 @@ 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; /// The Wayland window. @@ -113,6 +112,20 @@ impl Window { .and_then(|p| p.cast::().ok()) .unwrap_or_default(); + let mut scale_factor = None; + if let Some(handle) = attributes.parent_window() { + if let RawWindowHandle::Wayland(handle) = handle { + 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, @@ -121,6 +134,7 @@ impl Window { state::WindowType::Window((window.clone(), None)), attributes.preferred_theme, prefer_csd, + scale_factor, ); window_state.set_window_icon(attributes.window_icon); diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 250174f114..15b965ee7c 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -217,6 +217,7 @@ impl WindowState { window: WindowType, theme: Option, prefer_csd: bool, + scale_factor: f64, ) -> Self { let compositor = winit_state.compositor_state.clone(); let pointer_constraints = winit_state.pointer_constraints.clone(); @@ -260,7 +261,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.), From 933939a4659f98a64b0c8cec25690569e64f95f4 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 26 Mar 2026 10:00:50 +0100 Subject: [PATCH 04/17] add comment, thtat positioning is now also working on wayland with popups --- winit-core/src/window.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 543fada88f..8a6a81d666 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -153,6 +153,7 @@ 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` /// - **Others:** Ignored. #[inline] pub fn with_position>(mut self, position: P) -> Self { From d43d6ad0af0c6bfa10265ebf4bc470a354a3acc5 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 26 Mar 2026 10:46:56 +0100 Subject: [PATCH 05/17] set the positioner gravity Reason: otherwise the child surface is anchored to the center --- winit-wayland/src/popup.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs index 370842a787..785ae7e94b 100644 --- a/winit-wayland/src/popup.rs +++ b/winit-wayland/src/popup.rs @@ -1,11 +1,12 @@ use std::sync::{Arc, Mutex}; -use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; +use dpi::{LogicalPosition, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use rwh_06::RawWindowHandle; 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 wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; use winit_core::cursor::Cursor; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; @@ -31,6 +32,8 @@ pub struct Popup { // The state of the popup. popup_state: Arc>, + + positioner: XdgPositioner, /// Window id. window_id: WindowId, @@ -55,6 +58,8 @@ impl Popup { 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 positioner = XdgPositioner::new(&state.xdg_shell) + .map_err(|_| error!("Failed to create positioner"))?; let (popup, popup_state) = if let Some(parent_window_state) = state .windows .borrow() @@ -63,20 +68,21 @@ impl Popup { // Use the scale factor of the parent let scale_factor = parent_window_state.lock().unwrap().scale_factor(); let position = attributes.position.ok_or(error!("No position specified"))?; - let positioner = XdgPositioner::new(&state.xdg_shell) - .map_err(|_| error!("Failed to create positioner"))?; let size = attributes.surface_size.ok_or(error!("Invalid size for popup"))?; + + positioner.set_anchor(Anchor::TopLeft); + positioner.set_gravity(Gravity::BottomRight); positioner.set_anchor_rect( position.to_logical(scale_factor).x, position.to_logical(scale_factor).y, - 10, - 10, + 1, + 1, ); + positioner.set_offset(0, 0); positioner.set_size( size.to_logical(scale_factor).width, size.to_logical(scale_factor).height, ); - positioner.set_anchor(Anchor::TopLeft); let parent_window = &parent_window_state.lock().unwrap().window; let parent_surface = parent_window.xdg_surface(); @@ -100,6 +106,10 @@ impl Popup { false, scale_factor, ); + + popup.wl_surface().commit(); + // popup.commit(); Trait not implemented + let popup_state = Arc::new(Mutex::new(popup_state)); (popup, popup_state) @@ -107,9 +117,6 @@ impl Popup { return Err(error!("Parent window id unknown")); }; - popup.wl_surface().commit(); - // popup.commit(); Trait not implemented - let window_id = super::make_wid(&popup.wl_surface()); state.windows.get_mut().insert(window_id, popup_state.clone()); @@ -133,6 +140,7 @@ impl Popup { Ok(Self { popup, popup_state, + positioner, window_id, display: event_loop_window_target.handle.connection.display().clone(), }) From 6dc05a79350a68616a00e7d8278fbe24a286c044 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 4 Jun 2026 15:12:38 +0200 Subject: [PATCH 06/17] cleanup --- winit-wayland/Cargo.toml | 4 +- winit-wayland/src/popup.rs | 12 +-- winit-wayland/src/popup/state.rs | 122 ------------------------------- 3 files changed, 5 insertions(+), 133 deletions(-) delete mode 100644 winit-wayland/src/popup/state.rs diff --git a/winit-wayland/Cargo.toml b/winit-wayland/Cargo.toml index 9fada9960c..27a5972337 100644 --- a/winit-wayland/Cargo.toml +++ b/winit-wayland/Cargo.toml @@ -34,10 +34,10 @@ foldhash.workspace = true libc.workspace = true memmap2.workspace = true rustix = { workspace = true, features = ["std", "system", "thread", "process", "event", "pipe"] } -sctk = { package = "smithay-client-toolkit", path = "../../client-toolkit", default-features = false, features = [ +sctk = { package = "smithay-client-toolkit", version = "0.20.0", default-features = false, features = [ "calloop", ] } -sctk-adwaita = { path="../../sctk-adwaita", default-features = false, optional = true } +sctk-adwaita = { version = "0.11.0", default-features = false, optional = true } wayland-backend = { version = "0.3.10", default-features = false, features = ["client_system"] } wayland-client = "0.31.10" wayland-protocols = { version = "0.32.8", features = ["staging"] } diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs index 785ae7e94b..18be188daf 100644 --- a/winit-wayland/src/popup.rs +++ b/winit-wayland/src/popup.rs @@ -1,3 +1,4 @@ +use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; use dpi::{LogicalPosition, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; @@ -6,7 +7,7 @@ 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 wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; +use wayland_protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; use winit_core::cursor::Cursor; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; @@ -15,11 +16,6 @@ use winit_core::window::{ UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, WindowLevel, }; -mod state; -use std::sync::atomic::AtomicBool; - -pub use state::State; -use wayland_protocols::xdg::shell::client::xdg_positioner::Anchor; use super::ActiveEventLoop; use crate::window::WindowRequests; @@ -33,7 +29,6 @@ pub struct Popup { // The state of the popup. popup_state: Arc>, - positioner: XdgPositioner, /// Window id. window_id: WindowId, @@ -71,7 +66,7 @@ impl Popup { let size = attributes.surface_size.ok_or(error!("Invalid size for popup"))?; positioner.set_anchor(Anchor::TopLeft); - positioner.set_gravity(Gravity::BottomRight); + positioner.set_gravity(Gravity::BottomRight); // Otherwise the child surface will be centered over the anchor point positioner.set_anchor_rect( position.to_logical(scale_factor).x, position.to_logical(scale_factor).y, @@ -140,7 +135,6 @@ impl Popup { Ok(Self { popup, popup_state, - positioner, window_id, display: event_loop_window_target.handle.connection.display().clone(), }) diff --git a/winit-wayland/src/popup/state.rs b/winit-wayland/src/popup/state.rs deleted file mode 100644 index d9a801a6e6..0000000000 --- a/winit-wayland/src/popup/state.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::sync::Arc; - -use dpi::{LogicalSize, Size}; -use sctk::shell::xdg::XdgPositioner; -use sctk::shell::xdg::popup::{PopupConfigure, PopupHandler}; -use sctk::subcompositor::{self, SubcompositorState}; -use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; - -#[derive(Debug)] -pub struct State { - positioner: XdgPositioner, - - /// The current window title. - title: String, - - scale_factor: f64, - fractional_scale: Option, - - pub last_configure: Option, - - /// The size of the popup - size: LogicalSize, - - /// True if the compositor constrained the size of the popup - constrained: bool, - - /// TODO: maybe this can be removed, because we can use also last_configure to determine - /// If called the first time or not! - /// Initial window size provided by the user. Removed on the first - /// configure. - /// Required because we don't know the scaling yet - /// when constructing the state - initial_size: Option, -} - -impl State { - pub fn new(positioner: XdgPositioner, initial_size: Size) -> Self { - Self { - positioner, - initial_size: Some(initial_size), - constrained: false, - size: initial_size.to_logical(1.), - scale_factor: 1., - last_configure: None, - title: String::default(), - fractional_scale: None, - } - } - - pub fn configure(&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()); - } - - // The popup was constrained to a different size by the compositor - assert!(configure.width >= 0); - assert!(configure.height >= 0); - self.constrained = self.size.width != configure.width as u32 - || self.size.height != configure.height as u32; - let new_size = LogicalSize { - width: (configure.width as u32).into(), - height: (configure.height as u32).into(), - }; - - // NOTE: Set the configure before doing a resize, since we query it during it. - self.last_configure = Some(configure); - - if self.constrained { - self.resize(new_size); - } - - // false - } - - #[inline] - pub fn set_title(&mut self, title: &str) { - self.title = title.to_owned(); - } - - #[inline] - pub fn title(&self) -> &str { - &self.title - } - - /// Set the scale factor for the given window. - #[inline] - pub fn set_scale_factor(&mut self, scale_factor: f64) { - self.scale_factor = scale_factor; - - // NOTE: When fractional scaling is not used update the buffer scale. - // if self.fractional_scale.is_none() { - // let _ = self.window.set_buffer_scale(self.scale_factor as _); - // } - - // if let Some(frame) = self.frame.as_mut() { - // frame.set_scaling_factor(scale_factor); - // } - } - - #[inline] - pub fn is_configured(&self) -> bool { - self.last_configure.is_some() - } - - #[inline] - pub fn scale_factor(&self) -> f64 { - self.scale_factor - } - - #[inline] - pub fn surface_size(&self) -> LogicalSize { - self.size - } - - fn resize(&mut self, surface_size: LogicalSize) { - self.size = surface_size; - } -} From 68e33bf2b7b16858ccab23c884d8d6cf55ee837e Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 26 Mar 2026 15:01:26 +0100 Subject: [PATCH 07/17] consider the location of the frame for client side decoration Reason: otherwise the height of the client side decoration is not considered and therefore the location is shifted --- winit-wayland/src/popup.rs | 32 +++++++++++++++++++------------ winit-wayland/src/window/state.rs | 7 +++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs index 18be188daf..93ce690a47 100644 --- a/winit-wayland/src/popup.rs +++ b/winit-wayland/src/popup.rs @@ -60,27 +60,34 @@ impl Popup { .borrow() .get(&WindowId::from_raw(parent_window_handle.surface.as_ptr() as usize)) { - // Use the scale factor of the parent - let scale_factor = parent_window_state.lock().unwrap().scale_factor(); - let position = attributes.position.ok_or(error!("No position specified"))?; let size = attributes.surface_size.ok_or(error!("Invalid size for popup"))?; + let 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 position: LogicalPosition = attributes + .position + .ok_or(error!("No position specified"))? + .to_logical(scale_factor); + let geometry_origin = parent_window_state.content_surface_origin(); + // 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. + let anchor_position = LogicalPosition::new( + position.x - geometry_origin.x, + position.y - geometry_origin.y, + ); + positioner.set_anchor(Anchor::TopLeft); positioner.set_gravity(Gravity::BottomRight); // Otherwise the child surface will be centered over the anchor point - positioner.set_anchor_rect( - position.to_logical(scale_factor).x, - position.to_logical(scale_factor).y, - 1, - 1, - ); + positioner.set_anchor_rect(anchor_position.x, anchor_position.y, 1, 1); positioner.set_offset(0, 0); positioner.set_size( size.to_logical(scale_factor).width, size.to_logical(scale_factor).height, ); - let parent_window = &parent_window_state.lock().unwrap().window; - let parent_surface = parent_window.xdg_surface(); + 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), @@ -90,6 +97,7 @@ impl Popup { &state.xdg_shell, ) .map_err(|_| error!("Failed to create popup"))?; + drop(parent_window_state); let popup_state = WindowState::new( event_loop_window_target.handle.clone(), @@ -103,7 +111,7 @@ impl Popup { ); popup.wl_surface().commit(); - // popup.commit(); Trait not implemented + // popup.commit(); Trait not implemented in Sctk let popup_state = Arc::new(Mutex::new(popup_state)); diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 15b965ee7c..c57f4696c8 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -724,6 +724,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); From a4d299b028f73c48f7345ced3e54c9bb6cb0d310 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Tue, 31 Mar 2026 08:39:03 +0200 Subject: [PATCH 08/17] extend comment --- winit-core/src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 8a6a81d666..82967013d7 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -153,7 +153,7 @@ 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` + /// - **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 { From 3514818e7fc36b579686b932f3fe83154f73ddb0 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Tue, 7 Apr 2026 09:42:02 +0200 Subject: [PATCH 09/17] fix closing popups Reason: There are multiple pointers to the smithay popup. Once in state.windows and one time in the popup object it self. Just dropping the popup object releases only one pointer but we have to notify the state to release also the other --- winit-wayland/src/popup.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs index 93ce690a47..9411490dfb 100644 --- a/winit-wayland/src/popup.rs +++ b/winit-wayland/src/popup.rs @@ -1,10 +1,13 @@ -use std::sync::atomic::AtomicBool; -use std::sync::{Arc, Mutex}; - +use super::ActiveEventLoop; +use crate::window::WindowRequests; +use crate::window::state::{WindowState, WindowType}; +use core::sync::atomic::Ordering; use dpi::{LogicalPosition, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use rwh_06::RawWindowHandle; use sctk::shell::xdg::popup::Popup as SctkPopup; use sctk::shell::xdg::{XdgPositioner, XdgSurface}; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex}; use wayland_client::Proxy; use wayland_client::protocol::wl_display::WlDisplay; use wayland_protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; @@ -17,10 +20,6 @@ use winit_core::window::{ WindowLevel, }; -use super::ActiveEventLoop; -use crate::window::WindowRequests; -use crate::window::state::{WindowState, WindowType}; - #[derive(Debug)] pub struct Popup { /// Reference to the underlying SCTK popup. @@ -35,6 +34,13 @@ pub struct Popup { /// The wayland display used solely for raw window handle. #[allow(dead_code)] display: WlDisplay, + + /// Window requests to the event loop. + /// Used for example to close the popup + window_requests: Arc, + + /// Source to wake-up the event-loop for window requests. + event_loop_awakener: calloop::ping::Ping, } impl Popup { @@ -140,11 +146,16 @@ impl Popup { event_queue.blocking_dispatch(&mut state).map_err(|err| os_error!(err))?; } + + let event_loop_awakener = event_loop_window_target.event_loop_awakener.clone(); + Ok(Self { popup, popup_state, window_id, display: event_loop_window_target.handle.connection.display().clone(), + event_loop_awakener, + window_requests, }) } else { Err(RequestError::NotSupported(NotSupportedError::new( @@ -513,6 +524,13 @@ impl CoreWindow for Popup { } } +impl Drop for Popup { + fn drop(&mut self) { + self.window_requests.closed.store(true, Ordering::Relaxed); + self.event_loop_awakener.ping(); + } +} + impl rwh_06::HasWindowHandle for Popup { fn window_handle(&self) -> Result, rwh_06::HandleError> { let raw = rwh_06::WaylandWindowHandle::new({ From a02e2c392b0efe2df6c47a2a5ea9d23629adb526 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 4 Jun 2026 15:14:25 +0200 Subject: [PATCH 10/17] implement resizing of the popup using the positioner --- winit-wayland/src/popup.rs | 22 +++++++++------------- winit-wayland/src/window/state.rs | 24 ++++++++++++++++-------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs index 9411490dfb..79b482dc25 100644 --- a/winit-wayland/src/popup.rs +++ b/winit-wayland/src/popup.rs @@ -4,6 +4,7 @@ use crate::window::state::{WindowState, WindowType}; use core::sync::atomic::Ordering; use dpi::{LogicalPosition, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use rwh_06::RawWindowHandle; +use sctk::shell::WaylandSurface; use sctk::shell::xdg::popup::Popup as SctkPopup; use sctk::shell::xdg::{XdgPositioner, XdgSurface}; use std::sync::atomic::AtomicBool; @@ -22,10 +23,7 @@ use winit_core::window::{ #[derive(Debug)] pub struct Popup { - /// Reference to the underlying SCTK popup. - popup: SctkPopup, - - // The state of the popup. + /// The state of the popup. popup_state: Arc>, /// Window id. @@ -110,7 +108,7 @@ impl Popup { &event_loop_window_target.queue_handle, &state, size, - WindowType::Popup((popup.clone(), None)), + WindowType::Popup((popup.clone(), positioner, None)), attributes.preferred_theme, false, scale_factor, @@ -146,11 +144,9 @@ impl Popup { event_queue.blocking_dispatch(&mut state).map_err(|err| os_error!(err))?; } - let event_loop_awakener = event_loop_window_target.event_loop_awakener.clone(); Ok(Self { - popup, popup_state, window_id, display: event_loop_window_target.handle.connection.display().clone(), @@ -218,11 +214,10 @@ impl CoreWindow for Popup { } fn request_surface_size(&self, size: Size) -> Option> { - // let mut popup_state = self.popup_state.lock().unwrap(); - // let new_size = popup_state.request_surface_size(size); - // self.request_redraw(); - // Some(new_size) - None + let mut popup_state = self.popup_state.lock().unwrap(); + popup_state.request_surface_size(size); + self.request_redraw(); + Some(size.to_physical(popup_state.scale_factor())) } fn outer_size(&self) -> PhysicalSize { @@ -534,7 +529,8 @@ impl Drop for Popup { impl rwh_06::HasWindowHandle for Popup { fn window_handle(&self) -> Result, rwh_06::HandleError> { let raw = rwh_06::WaylandWindowHandle::new({ - let ptr = self.popup.wl_surface().id().as_ptr(); + let state = self.popup_state.lock().unwrap(); + let ptr = state.window.wl_surface().id().as_ptr(); std::ptr::NonNull::new(ptr as *mut _).expect("wl_surface will never be null") }); diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index c57f4696c8..8b00349ab6 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -24,6 +24,7 @@ use sctk::seat::pointer::{PointerDataExt, ThemedPointer}; use sctk::shell::WaylandSurface; use sctk::shell::xdg::XdgSurface; use sctk::shell::xdg::popup::{Popup, PopupConfigure}; +use sctk::shell::xdg::XdgPositioner; use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure}; use sctk::shm::Shm; use sctk::shm::slot::SlotPool; @@ -60,14 +61,14 @@ const MIN_WINDOW_SIZE: LogicalSize = LogicalSize::new(2, 1); pub enum WindowType { // The option is the last received configure Window((Window, Option)), - Popup((Popup, Option)), + Popup((Popup, XdgPositioner, Option)), } 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(), + Self::Popup((_, _, last_configure)) => last_configure.is_some(), } } } @@ -76,7 +77,7 @@ 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(), + Self::Popup((popup, _, _)) => popup.wl_surface(), } } } @@ -85,7 +86,7 @@ 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(), + Self::Popup((popup, _, _)) => popup.xdg_surface(), } } } @@ -332,7 +333,7 @@ impl WindowState { }; // NOTE: Set the configure before doing a resize, since we query it during it. - if let WindowType::Popup((_, last_configure)) = &mut self.window { + if let WindowType::Popup((_, _, last_configure)) = &mut self.window { *last_configure = Some(configure) } else { // This should never happen @@ -793,9 +794,16 @@ 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 let WindowType::Window((_, last_configure)) = &mut self.window { - if 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); + popup.reposition(positioner, 0); } } From 9371aca5c44ee297e0261e4795fa63c3ed2cf2e3 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 4 Jun 2026 15:16:35 +0200 Subject: [PATCH 11/17] Positioner: Set anchor point once and then use the offset to change the position Reason: Much easier in the resize handle --- winit-core/src/window.rs | 34 ++- winit-wayland/src/event_loop/mod.rs | 18 +- winit-wayland/src/lib.rs | 4 +- winit-wayland/src/popup.rs | 320 +++++++++++++--------------- winit-wayland/src/state.rs | 8 +- winit-wayland/src/window/handles.rs | 142 ++++++++++++ winit-wayland/src/window/mod.rs | 166 ++++----------- winit-wayland/src/window/state.rs | 261 ++++++++++++----------- 8 files changed, 512 insertions(+), 441 deletions(-) create mode 100644 winit-wayland/src/window/handles.rs diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 82967013d7..3c58767f7c 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -46,10 +46,23 @@ impl fmt::Debug for WindowId { } } +/// The role of a window, used to request platform-specific window behavior. #[derive(Debug, Clone, Copy, Default, PartialEq)] +#[non_exhaustive] 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 a frame, + /// shadow contour, or rounded corners for it, so it appears with square corners by default. + /// To get a rounded, native-looking popup, create it transparent (via + /// [`WindowAttributes::with_transparent`]) and draw the background and shadow yourself. Popup, } @@ -153,7 +166,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 + /// - **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 { @@ -388,20 +402,24 @@ impl WindowAttributes { self } - /// Sets the window type of the object + /// Sets the [`WindowType`] (window vs. popup). + /// + /// Used by the Windows, Wayland and macOS backends; on X11 popups are just normal windows. + /// 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. /// - /// Currently only wayland is using this type. On X11 popups are also just normal windows - /// Note: If the type is set to `WindowType::Popup` the parent must be set as well with - /// `with_parent_window()`. - pub fn as_type(mut self, window_type: WindowType) -> Self { + /// See [`WindowType::Popup`] for the per-platform behavior, including how to obtain a + /// rounded, native-looking popup on macOS. + pub fn with_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 popup(&self) -> bool { - self.window_type == WindowType::Popup + pub fn window_type(&self) -> WindowType { + self.window_type } } diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index a728fa47fb..5283aa797e 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -27,7 +27,7 @@ use winit_core::event_loop::{ OwnedDisplayHandle as CoreOwnedDisplayHandle, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; -use winit_core::window::Theme; +use winit_core::window::{Theme, WindowType}; use crate::types::cursor::WaylandCustomCursor; @@ -651,12 +651,16 @@ impl RootActiveEventLoop for ActiveEventLoop { &self, window_attributes: winit_core::window::WindowAttributes, ) -> Result, RequestError> { - if window_attributes.popup() { - let popup = crate::Popup::new(self, window_attributes)?; - Ok(Box::new(popup)) - } else { - 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)) + }, + _ => panic!("Not implemented"), } } diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 594dd0ff1d..267b6e47b4 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -36,15 +36,15 @@ macro_rules! os_error { mod event_loop; mod output; +mod popup; mod seat; mod state; mod types; mod window; -mod popup; pub use self::event_loop::{ActiveEventLoop, EventLoop}; -pub use self::window::Window; pub use self::popup::Popup; +pub use self::window::Window; /// Additional methods on [`ActiveEventLoop`] that are specific to Wayland. pub trait ActiveEventLoopExtWayland { diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs index 79b482dc25..0765a3b65f 100644 --- a/winit-wayland/src/popup.rs +++ b/winit-wayland/src/popup.rs @@ -1,19 +1,20 @@ -use super::ActiveEventLoop; -use crate::window::WindowRequests; -use crate::window::state::{WindowState, WindowType}; use core::sync::atomic::Ordering; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex}; + use dpi::{LogicalPosition, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use rwh_06::RawWindowHandle; +use sctk::compositor::SurfaceData; +use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::shell::WaylandSurface; use sctk::shell::xdg::popup::Popup as SctkPopup; use sctk::shell::xdg::{XdgPositioner, XdgSurface}; -use std::sync::atomic::AtomicBool; -use std::sync::{Arc, Mutex}; use wayland_client::Proxy; use wayland_client::protocol::wl_display::WlDisplay; use wayland_protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; 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, @@ -21,8 +22,18 @@ use winit_core::window::{ WindowLevel, }; +use super::ActiveEventLoop; +use super::output::MonitorHandle; +use crate::WindowAttributesWayland; +use crate::window::Handles; +use crate::window::handles::WindowRequests; +use crate::window::state::{WindowState, WindowType}; + #[derive(Debug)] pub struct Popup { + /// Reference to the underlying SCTK popup + popup: SctkPopup, + /// The state of the popup. popup_state: Arc>, @@ -33,18 +44,13 @@ pub struct Popup { #[allow(dead_code)] display: WlDisplay, - /// Window requests to the event loop. - /// Used for example to close the popup - window_requests: Arc, - - /// Source to wake-up the event-loop for window requests. - event_loop_awakener: calloop::ping::Ping, + handles: Handles, } impl Popup { pub(crate) fn new( event_loop_window_target: &ActiveEventLoop, - attributes: WindowAttributes, + mut attributes: WindowAttributes, ) -> Result { macro_rules! error { ($e:literal) => { @@ -57,6 +63,11 @@ impl Popup { 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 (popup, popup_state) = if let Some(parent_window_state) = state @@ -77,15 +88,13 @@ impl Popup { let geometry_origin = parent_window_state.content_surface_origin(); // 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. - let anchor_position = LogicalPosition::new( - position.x - geometry_origin.x, - position.y - geometry_origin.y, - ); + // This is important for client side decorations + let anchor_position = LogicalPosition::new(-geometry_origin.x, -geometry_origin.y); positioner.set_anchor(Anchor::TopLeft); positioner.set_gravity(Gravity::BottomRight); // Otherwise the child surface will be centered over the anchor point positioner.set_anchor_rect(anchor_position.x, anchor_position.y, 1, 1); - positioner.set_offset(0, 0); + positioner.set_offset(position.x, position.y); positioner.set_size( size.to_logical(scale_factor).width, size.to_logical(scale_factor).height, @@ -104,8 +113,7 @@ impl Popup { drop(parent_window_state); let popup_state = WindowState::new( - event_loop_window_target.handle.clone(), - &event_loop_window_target.queue_handle, + event_loop_window_target, &state, size, WindowType::Popup((popup.clone(), positioner, None)), @@ -114,6 +122,19 @@ impl Popup { scale_factor, ); + 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); + } + popup.wl_surface().commit(); // popup.commit(); Trait not implemented in Sctk @@ -124,7 +145,7 @@ impl Popup { return Err(error!("Parent window id unknown")); }; - let window_id = super::make_wid(&popup.wl_surface()); + let window_id = super::make_wid(popup.wl_surface()); state.windows.get_mut().insert(window_id, popup_state.clone()); let window_requests = WindowRequests { @@ -134,6 +155,9 @@ impl Popup { 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. @@ -144,21 +168,39 @@ impl Popup { event_queue.blocking_dispatch(&mut state).map_err(|err| os_error!(err))?; } + // 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, popup_state, window_id, display: event_loop_window_target.handle.connection.display().clone(), - event_loop_awakener, - window_requests, + 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( - "Not a wayland window handle passed", + "A Popup requires a parent wayland window handle", ))) } } + + #[inline] + pub fn surface(&self) -> &WlSurface { + self.popup.wl_surface() + } } impl CoreWindow for Popup { @@ -167,18 +209,7 @@ impl CoreWindow for Popup { } 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] @@ -187,7 +218,7 @@ impl CoreWindow for Popup { } fn pre_present_notify(&self) { - // self.popup_state.lock().unwrap().request_frame_callback(); + self.popup_state.lock().unwrap().request_frame_callback(); } fn reset_dead_keys(&self) { @@ -203,8 +234,13 @@ impl CoreWindow for Popup { .into()) } - fn set_outer_position(&self, _position: Position) { - // Not possible. + fn set_outer_position(&self, position: Position) { + let state = self.popup_state.lock().unwrap(); + if let WindowType::Popup((popup, positioner, _)) = &state.window { + let position = position.to_logical(state.scale_factor()); + positioner.set_offset(position.x, position.y); + popup.reposition(positioner, 0); + } } fn surface_size(&self) -> PhysicalSize { @@ -221,10 +257,9 @@ impl CoreWindow for Popup { } fn outer_size(&self) -> PhysicalSize { - // let popup_state = self.popup_state.lock().unwrap(); - // let scale_factor = popup_state.scale_factor(); - // super::logical_to_physical_rounded(popup_state.outer_size(), scale_factor) - PhysicalSize::new(100, 100) + let popup_state = self.popup_state.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 { @@ -232,37 +267,36 @@ impl CoreWindow for Popup { } 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)); - // self.state.lock().unwrap().set_min_surface_size(min_size); - // // NOTE: Requires commit to be applied. - // self.request_redraw(); + let scale_factor = self.scale_factor(); + let min_size = min_size.map(|size| size.to_logical(scale_factor)); + self.popup_state.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)); - // self.popup_state.lock().unwrap().set_max_surface_size(max_size); - // // NOTE: Requires commit to be applied. - // self.request_redraw(); + let scale_factor = self.scale_factor(); + let max_size = max_size.map(|size| size.to_logical(scale_factor)); + self.popup_state.lock().unwrap().set_max_surface_size(max_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); } fn surface_resize_increments(&self) -> Option> { - // let popup_state = self.popup_state.lock().unwrap(); - // let scale_factor = popup_state.scale_factor(); - // popup_state - // .resize_increments() - // .map(|size| super::logical_to_physical_rounded(size, scale_factor)) - None + let popup_state = self.popup_state.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 mut popup_state = self.popup_state.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); + let mut popup_state = self.popup_state.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) { @@ -282,16 +316,12 @@ impl CoreWindow for Popup { None } - fn set_resizable(&self, resizable: bool) { - // if self.popup_state.lock().unwrap().set_resizable(resizable) { - // // NOTE: Requires commit to be applied. - // self.request_redraw(); - // } + fn set_resizable(&self, _resizable: bool) { + // A popup cannot be resized with the mouse } fn is_resizable(&self) -> bool { - // TODO - // self.popup_state.lock().unwrap().resizable() + // A popup cannot be resized with the mouse false } @@ -322,7 +352,7 @@ impl CoreWindow for Popup { false } - fn set_fullscreen(&self, fullscreen: Option) { + fn set_fullscreen(&self, _fullscreen: Option) { // Not possible for popups } @@ -337,117 +367,94 @@ impl CoreWindow for Popup { #[inline] fn set_blur(&self, blur: bool) { - // self.popup_state.lock().unwrap().set_blur(blur); + if self.popup_state.lock().unwrap().set_blur(blur) { + self.request_redraw(); + } } #[inline] - fn set_decorations(&self, decorate: bool) { - // self.popup_state.lock().unwrap().set_decorate(decorate) + fn set_decorations(&self, _decorate: bool) { + // Popup does not support decorations } #[inline] fn is_decorated(&self) -> bool { - // self.popup_state.lock().unwrap().is_decorated() + // Popup does not support decorations false } - fn set_window_level(&self, _level: WindowLevel) {} + fn set_window_level(&self, _level: WindowLevel) { + // Popup does not have a window level + } - fn set_window_icon(&self, window_icon: Option) { - // self.popup_state.lock().unwrap().set_window_icon(window_icon) + 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 state_changed = self.popup_state.lock().unwrap().request_ime_update(request)?; + let state_changed = self.popup_state.lock().unwrap().request_ime_update(request)?; - // 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(); - // } + 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 { - // self.popup_state.lock().unwrap().ime_allowed() - None + self.popup_state.lock().unwrap().ime_allowed() } fn focus_window(&self) {} fn has_focus(&self) -> bool { - // self.popup_state.lock().unwrap().has_focus() - false + self.popup_state.lock().unwrap().has_focus() } 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(); - } - - fn set_theme(&self, theme: Option) { - // self.popup_state.lock().unwrap().set_theme(theme) + self.handles.request_user_attention(self.surface(), request_type); + } + + fn set_theme(&self, _theme: Option) { + // A popup does not have a frame } fn theme(&self) -> Option { - // self.popup_state.lock().unwrap().theme() + // A popup does not have a frame None } fn set_content_protected(&self, _protected: bool) {} fn set_cursor(&self, cursor: Cursor) { - // let popup_state = &mut self.popup_state.lock().unwrap(); + let popup_state = &mut self.popup_state.lock().unwrap(); - // match cursor { - // Cursor::Icon(icon) => popup_state.set_cursor(icon), - // Cursor::Custom(cursor) => popup_state.set_custom_cursor(cursor), - // } + 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 scale_factor = self.scale_factor(); - // let position = position.to_logical(scale_factor); - // self.popup_state - // .lock() - // .unwrap() - // .set_cursor_position(position) - // // Request redraw on success, since the state is double buffered. - // .map(|_| self.request_redraw()) - Err(RequestError::Ignored) + let scale_factor = self.scale_factor(); + let position = position.to_logical(scale_factor); + self.popup_state + .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> { - // self.popup_state.lock().unwrap().set_cursor_grab(mode) - Err(RequestError::Ignored) + self.popup_state.lock().unwrap().set_cursor_grab(mode) } fn set_cursor_visible(&self, visible: bool) { - // self.popup_state.lock().unwrap().set_cursor_visible(visible); + self.popup_state.lock().unwrap().set_cursor_visible(visible); } fn drag_window(&self) -> Result<(), RequestError> { @@ -455,52 +462,29 @@ impl CoreWindow for Popup { Err(RequestError::Ignored) } - fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), RequestError> { - // TODO: implement - // self.popup_state.lock().unwrap().drag_resize_window(direction) + fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), RequestError> { + // Popup does not support dragging Err(RequestError::Ignored) } - fn show_window_menu(&self, position: Position) { - // let scale_factor = self.scale_factor(); - // let position = position.to_logical(scale_factor); - // self.popup_state.lock().unwrap().show_window_menu(position); + fn show_window_menu(&self, _position: Position) { + // A popup does not have a menu } 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(()) - // } - Err(RequestError::Ignored) + self.handles.set_cursor_hittest(self.surface(), hittest) } fn current_monitor(&self) -> Option { - // let data = self.window.wl_surface().data::()?; - // data.outputs() - // .next() - // .map(MonitorHandle::new) - // .map(|monitor| CoreMonitorHandle(Arc::new(monitor))) - None + let data = self.surface().data::()?; + data.outputs() + .next() + .map(MonitorHandle::new) + .map(|monitor| CoreMonitorHandle(Arc::new(monitor))) } fn available_monitors(&self) -> Box> { - // Box::new( - // self.monitors - // .lock() - // .unwrap() - // .clone() - // .into_iter() - // .map(|inner| CoreMonitorHandle(Arc::new(inner))), - // ) - Box::new([].into_iter()) + self.handles.available_monitors() } fn primary_monitor(&self) -> Option { @@ -521,8 +505,8 @@ impl CoreWindow for Popup { impl Drop for Popup { 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(); } } diff --git a/winit-wayland/src/state.rs b/winit-wayland/src/state.rs index 5624aff8cb..77c080a05e 100644 --- a/winit-wayland/src/state.rs +++ b/winit-wayland/src/state.rs @@ -23,6 +23,7 @@ use sctk::shm::{Shm, ShmHandler}; use sctk::subcompositor::SubcompositorState; use winit_core::error::OsError; +use crate::WindowId; use crate::event_loop::sink::EventSink; use crate::output::MonitorHandle; use crate::seat::{ @@ -35,8 +36,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::{WindowId, popup}; +use crate::window::WindowState; +use crate::window::handles::WindowRequests; /// Winit's Wayland state. #[derive(Debug)] @@ -335,9 +336,7 @@ impl PopupHandler for WinitState { configure: PopupConfigure, ) { let window_id = super::make_wid(popup.wl_surface()); - println!("Finished configuring the popup: {:?}", window_id); - // let index = if let Some(index) = self.window_compositor_updates.iter().position(|update| update.window_id == window_id) { @@ -372,7 +371,6 @@ impl PopupHandler for WinitState { fn done(&mut self, _: &Connection, _: &QueueHandle, popup: &XdgPopup) { let window_id = super::make_wid(popup.wl_surface()); - println!("Destroying popup with id: {:?}", window_id); Self::queue_close(&mut self.window_compositor_updates, window_id); } } diff --git a/winit-wayland/src/window/handles.rs b/winit-wayland/src/window/handles.rs new file mode 100644 index 0000000000..5e33aa8185 --- /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 7fcae63de9..67b9a81ceb 100644 --- a/winit-wayland/src/window/mod.rs +++ b/winit-wayland/src/window/mod.rs @@ -7,11 +7,10 @@ use std::sync::{Arc, Mutex}; use dpi::{LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use rwh_06::RawWindowHandle; -use sctk::compositor::{CompositorState, Region, SurfaceData}; +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; @@ -27,14 +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 { @@ -113,22 +92,17 @@ impl Window { .unwrap_or_default(); let mut scale_factor = None; - if let Some(handle) = attributes.parent_window() { - if let RawWindowHandle::Wayland(handle) = handle { - 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()); - } + 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, state::WindowType::Window((window.clone(), None)), @@ -233,16 +207,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)), + }, }) } @@ -253,7 +233,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()), }; @@ -261,7 +241,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(); @@ -276,8 +257,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(); } } @@ -309,18 +290,7 @@ impl CoreWindow for Window { } 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] @@ -541,8 +511,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(()) @@ -560,29 +529,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) { @@ -638,21 +585,11 @@ 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 { - let data = self.window.wl_surface().data::()?; + let data = self.surface().data::()?; data.outputs() .next() .map(MonitorHandle::new) @@ -660,14 +597,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 { @@ -685,23 +615,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 8b00349ab6..02a9f98a56 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -22,10 +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::{PointerDataExt, ThemedPointer}; use sctk::shell::WaylandSurface; -use sctk::shell::xdg::XdgSurface; use sctk::shell::xdg::popup::{Popup, PopupConfigure}; -use sctk::shell::xdg::XdgPositioner; 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; @@ -38,7 +37,9 @@ use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, WindowId, }; +use crate::ActiveEventLoop; use crate::event_loop::OwnedDisplayHandle; +use crate::logical_to_physical_rounded; use crate::seat::{ PointerConstraintsState, TextInputClientState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext, @@ -47,7 +48,6 @@ use crate::state::{WindowCompositorUpdate, WinitState}; use crate::types::cursor::{CustomCursor, SelectedCursor, WaylandCustomCursor}; use crate::types::kwin_blur::KWinBlurManager; use crate::types::xdg_toplevel_icon_manager::ToplevelIcon; -use crate::{logical_to_physical_rounded, popup}; #[cfg(feature = "sctk-adwaita")] pub type WinitFrame = sctk_adwaita::AdwaitaFrame; @@ -77,7 +77,7 @@ 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(), + Self::Popup((popup, ..)) => popup.wl_surface(), } } } @@ -86,7 +86,7 @@ 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(), + Self::Popup((popup, ..)) => popup.xdg_surface(), } } } @@ -211,8 +211,7 @@ pub struct WindowState { impl WindowState { /// Create new window state. pub fn new( - handle: Arc, - queue_handle: &QueueHandle, + active_event_loop: &ActiveEventLoop, winit_state: &WinitState, initial_size: Size, window: WindowType, @@ -220,6 +219,8 @@ impl WindowState { prefer_csd: bool, scale_factor: f64, ) -> 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 @@ -327,17 +328,17 @@ impl WindowState { assert!(configure.height >= 0); 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).into(), - height: (configure.height as u32).into(), - }; + 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 { *last_configure = Some(configure) } else { - // This should never happen - assert!(false); + tracing::error!( + "configure_popup called for window type unequal of popup. This should never \ + happen, because we start configuring with a popup" + ); return; } @@ -487,9 +488,11 @@ impl WindowState { false } } else { - // This should never happen - assert!(false); - return false; + tracing::error!( + "configure_window called for window type unequal of `Window`. This should never \ + happen, because we start configuring with a `Window`" + ); + false } } @@ -521,39 +524,41 @@ impl WindowState { /// Start interacting drag resize. pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), RequestError> { - if let WindowType::Window((window, _)) = &self.window { - let xdg_toplevel = 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| { - let serial = data.latest_button_serial(); - let seat = data.seat(); - xdg_toplevel.resize(seat, serial, resize_direction_to_xdg(direction)); - }); - Ok(()) - } else { - // Popup - Err(RequestError::NotSupported(NotSupportedError::new( + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + xdg_toplevel.resize(seat, serial, resize_direction_to_xdg(direction)); + }); + 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> { - if let WindowType::Window((window, _)) = &self.window { - let xdg_toplevel = window.xdg_toplevel(); - // TODO(kchibisov) handle touch serials. - self.apply_on_pointer(|_, data| { - let 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| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + xdg_toplevel._move(seat, serial); + }); - Ok(()) - } else { - // Popup - Err(RequestError::NotSupported(NotSupportedError::new("Drag for popup not supported"))) + Ok(()) + }, + WindowType::Popup(..) => Err(RequestError::NotSupported(NotSupportedError::new( + "Drag for popup not supported", + ))), } } @@ -569,35 +574,36 @@ impl WindowState { window_id: WindowId, updates: &mut Vec, ) -> Option { - if let WindowType::Window((window, _)) = &self.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)), - _ => (), - }; - - Some(false) - } else { - 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)), + _ => (), + }; + + Some(false) + }, + WindowType::Popup(..) => None, } } @@ -616,25 +622,26 @@ impl WindowState { x: f64, y: f64, ) -> Option { - if let WindowType::Window((window, _)) = &self.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() { - window.move_(seat, serial); - None + 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() { + window.move_(seat, serial); + None + } else { + cursor + } } else { - cursor + None } - } else { - None - } - } else { - None + }, + WindowType::Popup(..) => None, } } @@ -700,19 +707,20 @@ impl WindowState { #[inline] pub fn is_decorated(&mut self) -> bool { - if let WindowType::Window((_, last_configure)) = &mut self.window { - 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 - } - } else { - false + match &mut self.window { + WindowType::Window((_, last_configuration)) => { + let csd = last_configuration + .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 } } @@ -799,12 +807,12 @@ impl WindowState { 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); popup.reposition(positioner, 0); - } + }, } logical_to_physical_rounded(self.surface_size(), self.scale_factor()) @@ -815,8 +823,9 @@ impl WindowState { self.size = surface_size; // Update the stateless size. - if let WindowType::Window((_, last_configure)) = &mut self.window { - if Some(true) == last_configure.as_ref().map(Self::is_stateless) { + 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; } } @@ -1132,24 +1141,29 @@ impl WindowState { self.decorate = decorate; - if let WindowType::Window((window, last_configure)) = &self.window { - 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)), - _ => (), - } + 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 } } @@ -1267,8 +1281,9 @@ impl WindowState { frame.set_title(&title); } - if let WindowType::Window((window, _)) = &self.window { - 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; } From 4eb2667de721e9bdd5463141544eb43b09fd5fe6 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 4 Jun 2026 14:58:35 +0200 Subject: [PATCH 12/17] implement popup for macos --- winit-appkit/src/window_delegate.rs | 57 +++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index cdf198d17c..76043522af 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -50,7 +50,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; @@ -107,6 +107,7 @@ pub(crate) struct State { is_simple_fullscreen: Cell, saved_style: Cell>, is_borderless_game: Cell, + is_popup: Cell, } define_class!( @@ -537,6 +538,7 @@ fn new_window( app_state: &Rc, attrs: &WindowAttributes, macos_attrs: &WindowAttributesMacOS, + is_popup: bool, mtm: MainThreadMarker, ) -> Option> { autoreleasepool(|_| { @@ -563,6 +565,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( @@ -620,6 +626,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; @@ -703,7 +711,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(); } @@ -770,13 +779,23 @@ 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 = 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(); + 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() { @@ -795,6 +814,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 => (), } @@ -832,6 +856,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] }; @@ -862,6 +887,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 @@ -1031,6 +1064,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, @@ -1038,6 +1072,21 @@ 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) + } + #[inline] pub fn surface_size(&self) -> PhysicalSize { let content_rect = self.window().contentRectForFrameRect(self.window().frame()); From 3c0d3189fb54f8d5143a99ec49702975da36d0ff Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 4 Jun 2026 14:58:50 +0200 Subject: [PATCH 13/17] implement popup for windows --- winit-win32/src/window.rs | 32 ++++++++++++++++++++++++++++---- winit-win32/src/window_state.rs | 12 +++++++----- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index dba95e9bd8..3c14cad6ba 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -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, @@ -54,7 +54,7 @@ use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, Monito use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, - WindowLevel, + WindowLevel, WindowType, }; use crate::dark_mode::try_theme; @@ -118,6 +118,26 @@ 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) -> (i32, i32) { + 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); + } + } + } + + (point.x, point.y) + } + /// Returns the `hwnd` of this window. pub fn hwnd(&self) -> HWND { self.window.hwnd() @@ -483,7 +503,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 (x, y) = self.translate_outer_position(position); let window_state = Arc::clone(&self.window_state); let window = self.window; @@ -1367,8 +1387,10 @@ unsafe fn init( let class_name = util::encode_wide(&win_attributes.class_name); unsafe { register_window_class(&class_name) }; + let is_popup = 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); @@ -1397,7 +1419,9 @@ 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"); } diff --git a/winit-win32/src/window_state.rs b/winit-win32/src/window_state.rs index ed06d76042..32ea9dc9f0 100644 --- a/winit-win32/src/window_state.rs +++ b/winit-win32/src/window_state.rs @@ -275,8 +275,13 @@ 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_BORDER | WS_CLIPSIBLINGS; let mut style_ex = WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES; + if self.contains(WindowFlags::POPUP) { + style |= WS_POPUP; + } else { + style |= WS_CAPTION | WS_SYSMENU; + }; if self.contains(WindowFlags::RESIZABLE) { style |= WS_SIZEBOX; @@ -299,7 +304,7 @@ impl WindowFlags { if self.contains(WindowFlags::NO_BACK_BUFFER) { style_ex |= WS_EX_NOREDIRECTIONBITMAP; } - if self.contains(WindowFlags::CHILD) { + if self.contains(WindowFlags::CHILD) && !self.contains(WindowFlags::POPUP) { style |= WS_CHILD; // This is incompatible with WS_POPUP if that gets added eventually. // Remove decorations window styles for child @@ -308,9 +313,6 @@ impl WindowFlags { style_ex &= !WS_EX_WINDOWEDGE; } } - if self.contains(WindowFlags::POPUP) { - style |= WS_POPUP; - } if self.contains(WindowFlags::MINIMIZED) { style |= WS_MINIMIZE; } From 39e35d0078c444e0fb06b1588ba534cd86bc3f11 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 4 Jun 2026 14:57:01 +0200 Subject: [PATCH 14/17] Fix popup size for different scalings Reason: we have to call resize to initialize the viewport to map to correct window size --- winit-wayland/src/window/state.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 02a9f98a56..4fc8f25979 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -22,7 +22,7 @@ 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::{PointerDataExt, ThemedPointer}; use sctk::shell::WaylandSurface; -use sctk::shell::xdg::popup::{Popup, PopupConfigure}; +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; @@ -333,7 +333,17 @@ impl WindowState { // NOTE: Set the configure before doing a resize, since we query it during it. if let WindowType::Popup((_, _, last_configure)) = &mut self.window { - *last_configure = Some(configure) + 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); + } } else { tracing::error!( "configure_popup called for window type unequal of popup. This should never \ @@ -341,10 +351,6 @@ impl WindowState { ); return; } - - if constrained { - self.resize(new_size); - } } pub fn configure_window( From 9c33bdcb2db66c8c33d96c9198f5b43645516f26 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 4 Jun 2026 15:24:33 +0200 Subject: [PATCH 15/17] add changelog entry --- winit/src/changelog/unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index 729bfd639b..4a01afcc19 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -16,6 +16,7 @@ on how to add them: - On X11, add `Window::even_more_rare_api`. - On Wayland, add `Window::common_api`. - On Windows, add `Window::some_rare_api`. +- Native popups for Wayland, Windows and MacOS ``` When the change requires non-trivial amount of work for users to comply From 08ac037fae7b19b51d3a1fdf2f5f2e39ff2ab2db Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 4 Jun 2026 15:35:41 +0200 Subject: [PATCH 16/17] update example to show how to use the WindowType --- winit/examples/child_window.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/winit/examples/child_window.rs b/winit/examples/child_window.rs index 4077bca36b..ddf528f7cc 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; @@ -9,7 +9,7 @@ fn main() -> Result<(), impl std::error::Error> { use winit::event::{ElementState, KeyEvent, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoop}; 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; @@ -36,6 +36,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_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(); @@ -121,10 +122,10 @@ fn main() -> Result<(), impl std::error::Error> { event_loop.run_app(Application::default()) } -#[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 \ + "This example is supported only on wayland, x11, macOS, and Windows, with the `rwh_06` feature \ enabled." ); } From 74bfada41c5e6eca8f971f43e4d6a0d0a6a5e38a Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Thu, 4 Jun 2026 15:44:24 +0200 Subject: [PATCH 17/17] fix formatting --- winit-wayland/src/window/state.rs | 17 ++++++++--------- winit/examples/child_window.rs | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index c26c5e4bcf..ba6e16ce26 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -36,9 +36,7 @@ use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, WindowId, }; -use crate::ActiveEventLoop; use crate::event_loop::OwnedDisplayHandle; -use crate::logical_to_physical_rounded; use crate::seat::{ PointerConstraintsState, TextInputClientState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext, @@ -47,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; @@ -335,11 +334,12 @@ impl WindowState { 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. + // 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); } @@ -828,8 +828,7 @@ impl WindowState { self.size = surface_size; // Update the stateless size. - if let WindowType::Window((_, last_configure)) = &mut self.window - { + 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; } diff --git a/winit/examples/child_window.rs b/winit/examples/child_window.rs index ddf528f7cc..80953cd109 100644 --- a/winit/examples/child_window.rs +++ b/winit/examples/child_window.rs @@ -125,7 +125,7 @@ fn main() -> Result<(), impl std::error::Error> { #[cfg(not(any(wayland_platform, x11_platform, macos_platform, windows_platform)))] fn main() { panic!( - "This example is supported only on wayland, 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." ); }