merge: pop-os/iced a l'etat pinne par libcosmic 08a09e3df (epoch-1.9.0)
Some checks failed
Audit / vulnerabilities (push) Has been cancelled
Check / wasm (push) Has been cancelled
Check / widget (push) Has been cancelled
Document / all (push) Has been cancelled
Format / all (push) Has been cancelled
Lint / all (push) Has been cancelled
Test / all (macOS-latest, 1.88) (push) Has been cancelled
Test / all (macOS-latest, beta) (push) Has been cancelled
Test / all (macOS-latest, stable) (push) Has been cancelled
Test / all (ubuntu-latest, 1.88) (push) Has been cancelled
Test / all (ubuntu-latest, beta) (push) Has been cancelled
Test / all (ubuntu-latest, stable) (push) Has been cancelled
Test / all (windows-latest, 1.88) (push) Has been cancelled
Test / all (windows-latest, beta) (push) Has been cancelled
Test / all (windows-latest, stable) (push) Has been cancelled

# Conflicts:
#	Cargo.toml
This commit is contained in:
Lionel DARNIS 2026-09-24 20:45:11 +02:00
commit 24da1dc0e4
23 changed files with 1602 additions and 1231 deletions

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

@ -138,3 +138,14 @@ pub fn set_input_zone<Message>(
)),
))
}
pub fn set_show_on_lock<Message>(
id: SurfaceId,
value: bool,
) -> Task<Message> {
task::effect(Action::PlatformSpecific(
platform_specific::Action::Wayland(wayland::Action::LayerSurface(
wayland::layer_surface::Action::ShowOnLock { id, value },
)),
))
}

View file

@ -392,6 +392,7 @@ impl SctkEventLoop {
).ok(),
text_input_manager: TextInputManager::try_new(&registry_state, &qh),
ext_background_effect_manager: ext_background_effect::ExtBackgroundEffectManager::new(&globals, &qh).ok(),
cosmic_session_lock_layer_manager: registry_state.bind_one(&qh, 1..=1, ()).ok(),
registry_state,
queue_handle: qh,
@ -411,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

@ -44,6 +44,7 @@ use cctk::{
cosmic_protocols::{
corner_radius::v1::client::cosmic_corner_radius_manager_v1::CosmicCornerRadiusManagerV1,
overlap_notify::v1::client::zcosmic_overlap_notification_v1::ZcosmicOverlapNotificationV1,
session_lock_layer::v1::client::cosmic_session_lock_layer_manager_v1::CosmicSessionLockLayerManagerV1,
},
sctk::{
activation::{ActivationState, RequestData},
@ -434,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,
@ -477,6 +481,7 @@ pub struct SctkState {
pub(crate) toplevel_manager: Option<ToplevelManagerState>,
pub(crate) subsurface_state: Option<SubsurfaceState>,
pub(crate) ext_background_effect_manager: Option<ext_background_effect::ExtBackgroundEffectManager>,
pub(crate) cosmic_session_lock_layer_manager: Option<CosmicSessionLockLayerManagerV1>,
pub(crate) activation_token_ctr: u32,
pub(crate) token_senders: HashMap<u32, oneshot::Sender<Option<String>>>,
@ -679,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,
@ -723,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);
};
@ -1156,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();
@ -1266,16 +1320,36 @@ impl SctkState {
}
}
},
platform_specific::wayland::layer_surface::Action::ShowOnLock { id, value } => {
if let Some(layer_surface) = self.layer_surfaces.iter_mut().find(|l| l.id == id) {
if let SurfaceKind::Wlr(wlr) = layer_surface.surface.kind() {
if let Some(manager) = &self.cosmic_session_lock_layer_manager {
if value {
manager.set_show_on_lock(wlr);
} else {
manager.unset_show_on_lock(wlr);
}
_ = self.to_commit.insert(id, layer_surface.surface.wl_surface().clone());
}
}
}
}
},
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))
@ -1646,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;
}
}
},
},
@ -2165,3 +2243,4 @@ pub(crate) fn send_event(
delegate_noop!(SctkState: ignore WlSubsurface);
delegate_noop!(SctkState: ignore WlRegion);
delegate_noop!(SctkState: CosmicSessionLockLayerManagerV1);

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)
});
@ -318,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),
}
@ -623,7 +627,12 @@ impl SctkEvent {
keyboard::Event::KeyPressed {
key: key.clone(),
location: location,
text: ke.utf8.map(|s| s.into()),
// sctk reports keys without a text representation (e.g. arrows, Home) as
// Some("") but winit reports None. Widgets are written against winit's contract.
text: ke
.utf8
.filter(|s| !s.is_empty())
.map(|s| s.into()),
modifiers: modifiers_to_native(*modifiers),
physical_key,
repeat: false,
@ -649,7 +658,9 @@ impl SctkEvent {
keyboard::Event::KeyPressed {
key: key.clone(),
location: location,
text: utf8.map(|s| s.into()),
text: utf8
.filter(|s| !s.is_empty())
.map(|s| s.into()),
modifiers: modifiers_to_native(*modifiers),
physical_key,
repeat: true,
@ -1400,6 +1411,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);