fix: do not report unfocused if focus moved to surface's own popup

This commit is contained in:
Hojjat 2026-09-11 18:29:31 -06:00 • committed by Ashley Wulber
parent 87e1787fbb
commit 5fc4372d87
3 changed files with 103 additions and 5 deletions

View file

@ -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(),

View file

@ -435,6 +435,9 @@ pub struct SctkState {
pub(crate) blur_surfaces: HashMap<core::window::Id, ExtBackgroundEffectSurfaceV1>,
pub(crate) corner_radii: HashMap<core::window::Id, (SctkCornerRadius, Option<CornerRadius>)>,
pub(crate) touch_points: HashMap<touch::Finger, (WlSurface, Point)>,
/// 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<ObjectId>,
/// 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;
}
}
},
},

View file

@ -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)
});