From 5fc4372d877ad2ca6eaab1c0ca5e424f238b9258 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 11 Sep 2026 18:29:31 -0600 Subject: [PATCH] fix: do not report unfocused if focus moved to surface's own popup --- .../wayland/event_loop/mod.rs | 1 + .../wayland/event_loop/state.rs | 68 ++++++++++++++++++- .../wayland/handlers/seat/keyboard.rs | 39 ++++++++++- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/winit/src/platform_specific/wayland/event_loop/mod.rs b/winit/src/platform_specific/wayland/event_loop/mod.rs index 02f7eae7..01bd7514 100644 --- a/winit/src/platform_specific/wayland/event_loop/mod.rs +++ b/winit/src/platform_specific/wayland/event_loop/mod.rs @@ -412,6 +412,7 @@ impl SctkEventLoop { lock_surfaces: Vec::new(), subsurfaces: Vec::new(), touch_points: HashMap::new(), + kbd_leave_to_own_popup: HashSet::new(), sctk_events: Vec::new(), frame_status: HashMap::new(), cursor_requests: HashMap::new(), diff --git a/winit/src/platform_specific/wayland/event_loop/state.rs b/winit/src/platform_specific/wayland/event_loop/state.rs index e408159f..7b1f65c0 100644 --- a/winit/src/platform_specific/wayland/event_loop/state.rs +++ b/winit/src/platform_specific/wayland/event_loop/state.rs @@ -435,6 +435,9 @@ pub struct SctkState { pub(crate) blur_surfaces: HashMap, pub(crate) corner_radii: HashMap)>, pub(crate) touch_points: HashMap, + /// List of layer surfaces whose keyboard leave was ignored because focus moved + /// to one of their own grabbing popups. Their next enter is swallowed too. + pub(crate) kbd_leave_to_own_popup: HashSet, /// Window updates, which are coming from SCTK or the compositor, which require /// calling back to the sctk's downstream. They are handled right in the event loop, @@ -681,6 +684,33 @@ impl SctkState { // TODO winit sets cursor size after handling the change for the window, so maybe that should be done as well. } + /// Popups can only be attached to xdg or layer surfaces. So for subsurfaces, we return the + /// parent, and adjust the offset. + pub(crate) fn subsurface_popup_parent( + &self, + id: core::window::Id, + ) -> Option<(WlSurface, (i32, i32))> { + let mut sub = self.subsurfaces.iter().find(|s| s.id == id)?; + let mut offset = (0, 0); + // bounded by the number of subsurfaces so a corrupt chain cannot spin forever + for _ in 0..=self.subsurfaces.len() { + if let Some(b) = sub.instance.bounds { + offset.0 += b.x as i32; + offset.1 += b.y as i32; + } + let parent = &sub.instance.parent; + match self + .subsurfaces + .iter() + .find(|s| s.instance.wl_surface == *parent) + { + Some(s) => sub = s, + None => return Some((parent.clone(), offset)), + } + } + None + } + pub fn get_popup( &mut self, settings: SctkPopupSettings, @@ -725,6 +755,27 @@ impl SctkState { PopupParent::Popup(parent.popup.wl_surface().clone()), parent.data.toplevel.clone(), ) + } else if let Some((surface, _)) = + self.subsurface_popup_parent(settings.parent) + { + if self + .layer_surfaces + .iter() + .any(|l| *l.surface.wl_surface() == surface) + { + (PopupParent::LayerSurface(surface.clone()), surface) + } else if self + .windows + .iter() + .any(|w| w.wl_surface(&self.connection) == surface) + { + (PopupParent::Window(surface.clone()), surface) + } else if let Some(p) = self.popmgr.popup(&surface) { + (PopupParent::Popup(surface), p.data.toplevel.clone()) + } else { + // e.g. a session lock surface, which cannot parent xdg popups + return Err(PopupCreationError::ParentMissing); + } } else { return Err(PopupCreationError::ParentMissing); }; @@ -1158,6 +1209,7 @@ impl SctkState { } if let Some(i) = self.layer_surfaces.iter().position(|l| l.id == id) { let l = self.layer_surfaces.remove(i); + _ = self.kbd_leave_to_own_popup.remove(&l.surface.wl_surface().id()); if let Some(blurred) = self.blur_surfaces.remove(&l.id) { blurred.destroy(); @@ -1285,13 +1337,19 @@ impl SctkState { }, Action::Popup(action) => { match action { - platform_specific::wayland::popup::Action::Popup { popup: settings } => { + platform_specific::wayland::popup::Action::Popup { popup: mut settings } => { + // anchor rects from widgets inside a subsurface are in that subsurface's coordinates + if let Some((_, (dx, dy))) = self.subsurface_popup_parent(settings.parent) { + settings.positioner.anchor_rect.x += dx; + settings.positioner.anchor_rect.y += dy; + } // first check existing popup // if an existing popup with the same id exists, we just update the positioner. if let Some((chain, existing)) = self.popmgr.popup_id(settings.id).and_then(|p| (p.data.grab == settings.grab && self.popmgr.popups().any(|parent| parent.popup.wl_surface() == p.data.parent.wl_surface() && parent.data.id == settings.parent) || self.windows.iter().any(|w| w.id == settings.parent && *p.data.parent.wl_surface() == w.wl_surface(&self.connection)) - || self.layer_surfaces.iter().any(|l| l.id == settings.parent && p.data.parent.wl_surface() == l.surface.wl_surface())).then(|| + || self.layer_surfaces.iter().any(|l| l.id == settings.parent && p.data.parent.wl_surface() == l.surface.wl_surface()) + || (p.data.parent_window == settings.parent && self.subsurfaces.iter().any(|s| s.id == settings.parent))).then(|| { p.popup.wl_surface().clone() })).and_then(|s| self.popmgr.chain_for_popup_mut(&s)) @@ -1662,9 +1720,13 @@ impl SctkState { } }, subsurface::Action::Reposition { id, x, y } => { - if let Some(subsurface) = self.subsurfaces.iter().find(|s| s.id == id) { + if let Some(subsurface) = self.subsurfaces.iter_mut().find(|s| s.id == id) { subsurface.instance.wl_subsurface.set_position(x, y); subsurface.instance.wl_surface.commit(); + if let Some(b) = subsurface.instance.bounds.as_mut() { + b.x = x as f32; + b.y = y as f32; + } } }, }, diff --git a/winit/src/platform_specific/wayland/handlers/seat/keyboard.rs b/winit/src/platform_specific/wayland/handlers/seat/keyboard.rs index f83e1180..30f9c7a6 100644 --- a/winit/src/platform_specific/wayland/handlers/seat/keyboard.rs +++ b/winit/src/platform_specific/wayland/handlers/seat/keyboard.rs @@ -1,12 +1,16 @@ use crate::platform_specific::wayland::{ - event_loop::state::SctkState, + event_loop::state::{PopupParent, SctkState}, sctk_event::{KeyboardEventVariant, SctkEvent}, }; use cctk::sctk::{ delegate_keyboard, seat::keyboard::{KeyboardHandler, Keysym, Modifiers}, }; -use cctk::sctk::{reexports::client::Proxy, seat::keyboard::RawModifiers}; +use cctk::sctk::{ + reexports::client::Proxy, + seat::keyboard::RawModifiers, + shell::{WaylandSurface, wlr_layer::KeyboardInteractivity}, +}; fn modifiers_from_keysyms( keysyms: &[Keysym], @@ -90,6 +94,11 @@ impl KeyboardHandler for SctkState { } self.request_redraw(&surface); + // Focus is coming back from our own grabbing popup + if self.kbd_leave_to_own_popup.remove(&surface.id()) { + return; + } + let surfaces = self.subsurfaces.iter().filter_map(|s| { (s.instance.parent == *surface).then(|| &s.instance.wl_surface) }); @@ -147,6 +156,32 @@ impl KeyboardHandler for SctkState { _ = my_seat.kbd_focus.take(); (is_active, seat, kbd) }; + + // A grabbing popup parented to this layer surface, or opened from one of + // its subsurfaces that holds focus, takes keyboard focus. + // Only swallow the leave when the layer surface has exclusive keyboard interactivity. + let focus_moved_to_own_popup = self.popmgr.popups().any(|p| { + p.data.grab + && match &p.data.parent { + PopupParent::LayerSurface(s) => { + (s == surface + || self.subsurfaces.iter().any(|sub| { + sub.id == p.data.parent_window + && sub.instance.wl_surface == *surface + })) + && self.layer_surfaces.iter().any(|l| { + l.surface.wl_surface() == s + && l.keyboard_interactivity + == KeyboardInteractivity::Exclusive + }) + } + _ => false, + } + }); + if focus_moved_to_own_popup { + _ = self.kbd_leave_to_own_popup.insert(surface.id()); + return; + } let surfaces = self.subsurfaces.iter().filter_map(|s| { (s.instance.parent == *surface).then(|| &s.instance.wl_surface) });