fix(wayland): share a grabbing popup's modifiers with its toplevel

This commit is contained in:
Hojjat 2026-09-21 15:02:24 -06:00 • committed by Jeremy Soller
parent 5716b9a691
commit 6f1b3a7170
5 changed files with 90 additions and 1 deletions

View file

@ -1328,6 +1328,17 @@ async fn run_instance<P>(
continue;
}
}
// to avoid race with the reported modifier state from the popup of
// this window, don't empty the modifiers, on focus loss
#[cfg(wayland_platform)]
if let winit::event::WindowEvent::ModifiersChanged(modifiers) =
&event
&& modifiers.state().is_empty()
&& platform_specific_handler.has_popup(id)
{
continue;
}
match event {
winit::event::WindowEvent::SurfaceResized(_) => {
window.raw.request_redraw();

View file

@ -74,6 +74,11 @@ impl PlatformSpecific {
}
}
#[cfg(wayland_platform)]
pub(crate) fn has_popup(&self, toplevel: window::Id) -> bool {
self.wayland.has_popup(toplevel)
}
pub(crate) fn retain_subsurfaces<F: Fn(window::Id) -> bool>(
&mut self,
keep: F,

View file

@ -353,6 +353,31 @@ impl KeyboardHandler for SctkState {
surface: surface.clone(),
});
}
// A popup holds the seat's keyboard on behalf of its toplevel. Report
// the modifiers to the toplevel as well.
let toplevel = self.popmgr.popups().find_map(|p| {
(p.popup.wl_surface() == &surface)
.then(|| p.data.toplevel.clone())
});
if let Some(toplevel) = toplevel {
let id = self
.windows
.iter()
.find(|w| w.wl_surface(&self.connection) == toplevel)
.map(|w| w.id)
.or_else(|| {
self.layer_surfaces
.iter()
.find(|l| *l.surface.wl_surface() == toplevel)
.map(|l| l.id)
});
if let Some(toplevel) = id {
self.sctk_events.push(SctkEvent::PopupModifiers {
toplevel,
modifiers,
});
}
}
}
}
}

View file

@ -20,7 +20,7 @@ use iced_graphics::{Compositor, compositor};
use iced_runtime::core::{Vector, window};
use raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle};
use raw_window_handle::{HasRawDisplayHandle, RawWindowHandle};
use sctk_event::SctkEvent;
use sctk_event::{PopupEventVariant, SctkEvent};
use std::sync::OnceLock;
use std::{collections::HashMap, sync::Arc};
use subsurface_widget::{SubsurfaceInstance, SubsurfaceState};
@ -103,6 +103,7 @@ pub(crate) struct WaylandSpecific {
surface_ids: HashMap<ObjectId, SurfaceIdWrapper>,
subsurface_state: Option<SubsurfaceState>,
surface_subsurfaces: HashMap<window::Id, Vec<SubsurfaceInstance>>,
popup_toplevels: HashMap<ObjectId, window::Id>,
}
impl PlatformSpecific {
@ -173,6 +174,11 @@ impl WaylandSpecific {
self.conn.as_ref()
}
/// Whether a popup opened from `toplevel` is alive.
pub(crate) fn has_popup(&self, toplevel: window::Id) -> bool {
self.popup_toplevels.values().any(|id| *id == toplevel)
}
pub(crate) async fn handle_event<'a, 'b, P>(
&mut self,
e: SctkEvent,
@ -201,8 +207,33 @@ impl WaylandSpecific {
modifiers,
subsurface_state,
surface_subsurfaces,
popup_toplevels,
} = self;
if let SctkEvent::PopupEvent {
variant,
parent_id,
parent_window,
id,
..
} = &e
{
match variant {
PopupEventVariant::Created(..) => {
// A nested popup's parent is itself a popup so inhreit its toplevel
let toplevel = popup_toplevels
.get(&parent_id.id())
.copied()
.unwrap_or(*parent_window);
_ = popup_toplevels.insert(id.id(), toplevel);
}
PopupEventVariant::Done => {
_ = popup_toplevels.remove(&id.id());
}
_ => {}
}
}
match e {
sctk_event => {
let Some(sender) = sender.as_ref() else {

View file

@ -198,6 +198,10 @@ pub enum SctkEvent {
SessionUnlocked,
SurfaceScaleFactorChanged(f64, WlSurface, window::Id),
Winit(WindowId, WindowEvent),
PopupModifiers {
toplevel: window::Id,
modifiers: cctk::sctk::seat::keyboard::Modifiers,
},
Subcompositor(SubsurfaceState),
ShortcutsInhibited(bool),
}
@ -1400,6 +1404,19 @@ impl SctkEvent {
),
)),
SctkEvent::Winit(_, _) => {}
SctkEvent::PopupModifiers {
toplevel,
modifiers,
} => {
events.push((
Some(toplevel),
iced_runtime::core::Event::Keyboard(
keyboard::Event::ModifiersChanged(modifiers_to_native(
modifiers,
)),
),
));
}
SctkEvent::SurfaceScaleFactorChanged(scale, _, id) => {
if let Some(w) = window_manager.get_mut(id) {
w.state.update_scale_factor(scale);