Compare commits

...
Sign in to create a new pull request.

6 commits

Author SHA1 Message Date
KENZ
aa2a870c35 fix: Forward wayland IME events in SCTK only when single-instance
Some checks failed
Audit / vulnerabilities (push) Has been cancelled
Test / all (ubuntu-latest, 1.88) (push) Has been cancelled
Build / todos_linux (push) Has been cancelled
Build / todos_windows (push) Has been cancelled
Build / todos_macos (push) Has been cancelled
Build / todos_raspberry (push) Has been cancelled
Check / wasm (push) Has been cancelled
Check / widget (push) Has been cancelled
Test / all (ubuntu-latest, beta) (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, 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
feature enabled
2026-04-18 16:12:59 -04:00
Ian Douglas Scott
8f10a3bd34 winit: Set WindowAttributesWayland without cctk feature
There doesn't seem to be any reason for this to require the `cctk`
feature?

Fixes the app ID, and therefore icon, for `cosmic-sync-gui`.
2026-04-16 12:59:35 -06:00
boloto1979
64b97077b7 fix(wayland): add mouse cursor entered event to SctkEvent 2026-04-16 12:27:46 -04:00
Ashley Wulber
13b8d3eab6 fix: just use 0 for position of blur rectangle 2026-04-14 23:21:27 -04:00
Ashley Wulber
1bf1e33317 improv(wayland): handle capabilities for ext_background_effect 2026-04-14 18:21:36 -04:00
Ashley Wulber
d2b2715835 feat: ext-background-effect blur 2026-04-14 18:21:36 -04:00
12 changed files with 568 additions and 331 deletions

593
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
use std::fmt::Debug;
use iced_core::window::Id;
use iced_core::{Rectangle, window::Id};
/// activation Actions
pub mod activation;
@ -35,6 +35,8 @@ pub enum Action {
InhibitShortcuts(bool),
/// Rounded corners in logical space
RoundedCorners(iced_core::window::Id, Option<CornerRadius>),
/// Blur effect for a surface
BlurSurface(Id, Option<Vec<Rectangle>>),
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
@ -70,6 +72,11 @@ impl Debug for Action {
Action::RoundedCorners(id, v) => {
f.debug_tuple("RoundedCorners").field(id).field(v).finish()
}
Action::BlurSurface(id, rectangles) => f
.debug_tuple("BlurSurface")
.field(id)
.field(rectangles)
.finish(),
}
}
}

View file

@ -37,6 +37,8 @@ cctk = [
"xkeysym",
"dep:cctk",
]
# Prevents multiple separate process instances.
single-instance = []
[dependencies]

View file

@ -178,7 +178,7 @@ pub fn window_attributes(
),
));
}
#[cfg(all(feature = "cctk", target_os = "linux"))]
#[cfg(target_os = "linux")]
{
use winit::platform::wayland::WindowAttributesWayland;

View file

@ -7,7 +7,10 @@ use crate::platform_specific::SurfaceIdWrapper;
use crate::{
Control,
futures::futures::channel::mpsc,
handlers::{overlap::OverlapNotifyV1, text_input::TextInputManager},
handlers::{
ext_background_effect, overlap::OverlapNotifyV1,
text_input::TextInputManager,
},
platform_specific::wayland::{
handlers::{
wp_fractional_scaling::FractionalScalingManager,
@ -346,26 +349,26 @@ impl SctkEventLoop {
&qh,
),
corner_radius_manager: registry_state.bind_one::<CosmicCornerRadiusManagerV1, _, _>(
&qh,
1..=1,
(),
).ok(),
&qh,
1..=1,
(),
).ok(),
toplevel_manager: ToplevelManagerState::try_new(
&registry_state,
&qh,
),
inhibitor_manager: registry_state.bind_one::<zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1, _, _>(
&qh,
1..=1,
(),
).ok(),
inhibitor_manager: registry_state.bind_one::<zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1, _, _>(
&qh,
1..=1,
(),
).ok(),
text_input_manager: TextInputManager::try_new(&registry_state, &qh),
ext_background_effect_manager: ext_background_effect::ExtBackgroundEffectManager::new(&globals, &qh).ok(),
registry_state,
queue_handle: qh,
loop_handle,
inhibitor: None,
inhibited: false,
_cursor_surface: None,
@ -373,6 +376,7 @@ impl SctkEventLoop {
outputs: Vec::new(),
seats: Vec::new(),
windows: Vec::new(),
blur_surfaces: HashMap::new(),
layer_surfaces: Vec::new(),
popups: Vec::new(),
lock_surfaces: Vec::new(),

View file

@ -2,6 +2,7 @@ use crate::{
Control,
handlers::{
activation::IcedRequestData,
ext_background_effect,
overlap::{OverlapNotificationV1, OverlapNotifyV1},
text_input::{Preedit, TextInputManager},
},
@ -25,7 +26,7 @@ use iced_futures::{
};
use raw_window_handle::HasWindowHandle;
use std::{
collections::{HashMap, HashSet},
collections::{HashMap, HashSet, hash_map::Entry},
convert::Infallible,
fmt::Debug,
sync::{Arc, Mutex, atomic::AtomicU32},
@ -108,6 +109,7 @@ use iced_runtime::{
},
};
use wayland_protocols::{
ext::background_effect::v1::client::ext_background_effect_surface_v1::ExtBackgroundEffectSurfaceV1,
wp::{
fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1,
keyboard_shortcuts_inhibit::zv1::client::{
@ -443,6 +445,7 @@ pub struct SctkState {
pub(crate) popups: Vec<SctkPopup>,
pub(crate) subsurfaces: Vec<SctkSubsurface>,
pub(crate) lock_surfaces: Vec<SctkLockSurface>,
pub(crate) blur_surfaces: HashMap<core::window::Id, Vec<ExtBackgroundEffectSurfaceV1>>,
pub(crate) touch_points: HashMap<touch::Finger, (WlSurface, Point)>,
/// Window updates, which are coming from SCTK or the compositor, which require
@ -485,6 +488,7 @@ pub struct SctkState {
pub(crate) toplevel_info: Option<ToplevelInfoState>,
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) activation_token_ctr: u32,
pub(crate) token_senders: HashMap<u32, oneshot::Sender<Option<String>>>,
@ -666,9 +670,7 @@ impl SctkState {
// TODO winit sets cursor size after handling the change for the window, so maybe that should be done as well.
}
}
impl SctkState {
pub fn get_popup(
&mut self,
settings: SctkPopupSettings,
@ -1091,6 +1093,12 @@ impl SctkState {
if let Some(i) = self.layer_surfaces.iter().position(|l| l.id == id) {
let l = self.layer_surfaces.remove(i);
if let Some(blurred) = self.blur_surfaces.remove(&l.id) {
for s in blurred {
s.destroy();
}
}
let (removed, remaining): (Vec<_>, Vec<_>) = self
.subsurfaces
.drain(..)
@ -1344,6 +1352,12 @@ impl SctkState {
_ = self.destroyed.insert(id);
}
if let Some(blurred) = self.blur_surfaces.remove(&id) {
for s in blurred {
s.destroy();
}
}
let (removed, remaining): (Vec<_>, Vec<_>) = self
.subsurfaces
.drain(..)
@ -1455,6 +1469,7 @@ impl SctkState {
// TODO how to handle this when there's no lock?
if let Some((surface, _)) = self.get_lock_surface(id, &output) {
let wl_surface = surface.wl_surface();
receive_frame(&mut self.frame_status, &wl_surface);
}
}
@ -1465,6 +1480,11 @@ impl SctkState {
})
{
let surface = self.lock_surfaces.remove(i);
if let Some(blurred) = self.blur_surfaces.remove(&surface.id) {
for s in blurred {
s.destroy();
}
}
let (removed, remaining): (Vec<_>, Vec<_>) = self
.subsurfaces
.drain(..)
@ -1536,7 +1556,7 @@ impl SctkState {
let mut destroyed = vec![];
if let Some(subsurface) = self.subsurfaces.iter().position(|s| s.id == id) {
let subsurface = self.subsurfaces.remove(subsurface);
destroyed.push((subsurface.instance.wl_surface.clone(), subsurface.instance.parent.clone()));
destroyed.push((subsurface.instance.wl_surface.clone(), subsurface.instance.parent.clone(), subsurface.id));
subsurface.instance.wl_surface.attach(None, 0, 0);
subsurface.instance.wl_surface.commit();
@ -1544,7 +1564,12 @@ impl SctkState {
SctkEvent::SubsurfaceEvent( crate::sctk_event::SubsurfaceEventVariant::Destroyed(subsurface.instance) )
);
}
for (destroyed, parent) in destroyed {
for (destroyed, parent, id) in destroyed {
if let Some(blurred) = self.blur_surfaces.remove(&id) {
for s in blurred {
s.destroy();
}
}
if let Some((wl_surface, f)) = self.seats.iter_mut().find(|f| {
f.kbd_focus.as_ref().is_some_and(|f| *f == destroyed)
}).and_then(|f| Some((parent, &mut f.kbd_focus))) {
@ -1574,7 +1599,7 @@ impl SctkState {
if let Some(manager) = self.corner_radius_manager.as_ref() {
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
let geo_size: LogicalSize<f64> = w.window.surface_size().cast::<f64>().to_logical(w.window.scale_factor());
let half_min_dim = ((geo_size.width as u32).min(geo_size.height as u32) / 2);
let half_min_dim = (geo_size.width as u32).min(geo_size.height as u32) / 2;
if let Some(radii) = v {
let adjusted_radii = CornerRadius {
@ -1621,6 +1646,97 @@ impl SctkState {
}
}
}
Action::BlurSurface(id, rectangles) => {
use wayland_protocols::ext::background_effect::v1::client::ext_background_effect_manager_v1;
if let Some(bg_effect_mgr) = self.ext_background_effect_manager.as_mut() && !bg_effect_mgr.capabilities().contains(ext_background_effect_manager_v1::Capability::Blur) {
bg_effect_mgr.enqueue(id, rectangles.clone());
return Ok(());
}
let s = if let Some(s) = self.popups.iter().find(|s| s.data.id == id) {
s.popup.wl_surface()
} else if let Some(s) = self.layer_surfaces.iter().find(|s| s.id == id) {
s.surface.wl_surface()
} else if let Some(s) = self.lock_surfaces.iter().find(|s| s.id == id) {
s.session_lock_surface.wl_surface()
} else if let Some(subsurface) = self.subsurfaces.iter().find(|s| s.id == id) {
&subsurface.instance.wl_surface
} else {
log::error!("Failed to find surface for blur action");
return Ok(());
};
let existing_blur = self.blur_surfaces.entry(id);
match (existing_blur, rectangles) {
(Entry::Occupied(occupied_entry), None) => {
let blur_surfaces = occupied_entry.remove();
for region in blur_surfaces {
region.destroy();
}
},
(Entry::Occupied(mut occupied_entry), Some(rectangles)) => {
let blur_surfaces = occupied_entry.get_mut();
let regions = rectangles.into_iter().map(|rect| {
let region = self
.compositor_state
.wl_compositor()
.create_region(&self.queue_handle, ());
region.add(
rect.x.round() as i32,
rect.y.round() as i32,
rect.width.round() as i32,
rect.height.round() as i32,
);
region
}).collect::<Vec<_>>();
if regions.len() > blur_surfaces.len() {
// add extra blur surfaces if needed
for _ in blur_surfaces.len()..regions.len() {
let Some(extra_region) = self.ext_background_effect_manager.as_mut().map(|mgr| mgr.blur(s, &self.queue_handle)) else {
log::error!("Failed to create blur effect for surface");
return Ok(());
};
blur_surfaces.push(extra_region);
}
} else if regions.len() < blur_surfaces.len() {
for surface in blur_surfaces.iter().skip(regions.len()) {
surface.destroy();
}
}
// update existing blur surfaces
for (blur_surface, region) in blur_surfaces.iter().zip(regions.into_iter()) {
blur_surface.set_blur_region(Some(&region));
}
},
(Entry::Vacant(..), None) => {
// nothing to remove
},
(Entry::Vacant(vacant_entry), Some(rectangles)) => {
if self.ext_background_effect_manager.is_none() {
log::error!("Blur effect is not supported.");
return Ok(());
}
let blur_surfaces = rectangles.into_iter().map(|rect| {
let region = self
.compositor_state
.wl_compositor()
.create_region(&self.queue_handle, ());
region.add(
rect.x.round() as i32,
rect.y.round() as i32,
rect.width.round() as i32,
rect.height.round() as i32,
);
let blur_manager = self.ext_background_effect_manager.as_mut().unwrap();
let blur_surface = blur_manager.blur(s, &self.queue_handle);
blur_surface.set_blur_region(Some(&region));
blur_surface
}).collect::<Vec<_>>();
_ = vacant_entry.insert(blur_surfaces);
},
}
},
};
Ok(())
}

View file

@ -0,0 +1,107 @@
use std::collections::HashMap;
use cctk::sctk;
use iced_runtime::core::Rectangle;
use iced_runtime::platform_specific::wayland::Action;
use sctk::globals::GlobalData;
use sctk::reexports::client::globals::{BindError, GlobalList};
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle, delegate_dispatch};
use wayland_protocols::ext::background_effect::v1::client::ext_background_effect_manager_v1::{Capability, Event, ExtBackgroundEffectManagerV1};
use wayland_protocols::ext::background_effect::v1::client::ext_background_effect_surface_v1::ExtBackgroundEffectSurfaceV1;
use crate::event_loop::state::SctkState;
use crate::window;
#[derive(Debug, Clone)]
pub struct ExtBackgroundEffectManager {
manager: ExtBackgroundEffectManagerV1,
capabilities: Capability,
queued_blur_actions: HashMap<window::Id, Option<Vec<Rectangle>>>,
}
impl ExtBackgroundEffectManager {
pub fn new(
globals: &GlobalList,
queue_handle: &QueueHandle<SctkState>,
) -> Result<Self, BindError> {
let manager = globals.bind(queue_handle, 1..=1, GlobalData)?;
Ok(Self {
manager,
capabilities: Capability::empty(),
queued_blur_actions: HashMap::new(),
})
}
pub fn blur(
&mut self,
surface: &WlSurface,
queue_handle: &QueueHandle<SctkState>,
) -> ExtBackgroundEffectSurfaceV1 {
self.manager
.get_background_effect(surface, queue_handle, ())
}
pub fn enqueue(&mut self, id: window::Id, rects: Option<Vec<Rectangle>>) {
_ = self.queued_blur_actions.insert(id, rects);
}
pub fn capabilities(&self) -> Capability {
self.capabilities
}
}
impl Dispatch<ExtBackgroundEffectManagerV1, GlobalData, SctkState>
for ExtBackgroundEffectManager
{
fn event(
state: &mut SctkState,
_: &ExtBackgroundEffectManagerV1,
event: <ExtBackgroundEffectManagerV1 as Proxy>::Event,
_: &GlobalData,
_: &Connection,
_: &QueueHandle<SctkState>,
) {
match event {
Event::Capabilities { flags } => match flags {
wayland_client::WEnum::Value(capability) => {
let mut queued_actions = Vec::new();
if let Some(bg_effect_mgr) =
state.ext_background_effect_manager.as_mut()
{
bg_effect_mgr.capabilities = capability;
queued_actions =
bg_effect_mgr.queued_blur_actions.drain().collect();
}
for (id, rects) in queued_actions {
_ = state.handle_action(Action::BlurSurface(id, rects));
}
}
wayland_client::WEnum::Unknown(u) => {
log::warn!("Unknown value: {u:?}");
}
},
e => {
log::warn!("Ignored event {e:?}");
}
}
}
}
impl Dispatch<ExtBackgroundEffectSurfaceV1, (), SctkState>
for ExtBackgroundEffectManager
{
fn event(
_: &mut SctkState,
_: &ExtBackgroundEffectSurfaceV1,
_: <ExtBackgroundEffectSurfaceV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<SctkState>,
) {
// There is no event
}
}
delegate_dispatch!(SctkState: [ExtBackgroundEffectManagerV1: GlobalData] => ExtBackgroundEffectManager);
delegate_dispatch!(SctkState: [ExtBackgroundEffectSurfaceV1: ()] => ExtBackgroundEffectManager);

View file

@ -1,6 +1,7 @@
// handlers
pub mod activation;
pub mod compositor;
pub mod ext_background_effect;
pub mod output;
pub mod overlap;
pub mod seat;
@ -12,14 +13,14 @@ pub mod toplevel;
pub mod wp_fractional_scaling;
pub mod wp_viewporter;
use cctk::{sctk::{
use cctk::sctk::{
delegate_registry, delegate_shm,
output::OutputState,
registry::{ProvidesRegistryState, RegistryState},
registry_handlers,
seat::SeatState,
shm::{Shm, ShmHandler},
}};
};
use crate::platform_specific::wayland::event_loop::state::SctkState;

View file

@ -69,6 +69,10 @@ impl Dispatch<ZwpTextInputV3, (), SctkState> for TextInputManager {
_conn: &Connection,
_qhandle: &QueueHandle<SctkState>,
) {
if cfg!(not(feature = "single-instance")) {
return;
}
let kbd_focus =
match state.seats.iter_mut().find_map(|s| s.kbd_focus.clone()) {
Some(surface) => surface,

View file

@ -380,6 +380,12 @@ impl SctkEvent {
(variant.position.0, variant.position.1).into(),
)
}
events.push((
id.clone(),
iced_runtime::core::Event::Mouse(
mouse::Event::CursorEntered,
),
));
events.push((
id,
iced_runtime::core::Event::Mouse(

View file

@ -7,7 +7,6 @@ use crate::core::{
widget::{self, Widget},
};
use std::{
borrow::BorrowMut,
cell::RefCell,
collections::HashMap,
fmt::Debug,

View file

@ -287,7 +287,21 @@ impl winit::window::Window for SctkWinitWindow {
}
fn set_blur(&self, blur: bool) {
// TODO
_ = self.tx.send(Action::Action(
iced_runtime::platform_specific::wayland::Action::BlurSurface(
self.id.inner(),
if blur {
Some(vec![iced_runtime::core::Rectangle {
x: 0.,
y: 0.,
width: f32::MAX,
height: f32::MAX,
}])
} else {
None
},
),
));
}
fn set_visible(&self, visible: bool) {}