From 732785a50c5de8fc40034710d13a87612e6a173b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 20 Apr 2026 23:50:27 -0400 Subject: [PATCH] track surfaces that use a normal view method --- Cargo.toml | 1 + src/app/cosmic.rs | 251 ++++++++++++++++++++++++++++++++++-------- src/core.rs | 50 ++++++++- src/surface/action.rs | 79 +++++++++++++ src/surface/mod.rs | 27 +++++ 5 files changed, 361 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e21f986..40230807 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,6 +170,7 @@ url = "2.5.8" zbus = { workspace = true, optional = true } float-cmp = "0.10.0" ron = { workspace = true, optional = true } +enumflags2 = "0.7.12" # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index bdd283c7..56711dbf 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -92,6 +92,7 @@ pub struct Cosmic { Box Fn(&'a App) -> Element<'a, crate::Action>>, ), >, + pub tracked_surfaces: HashMap, pub opened_surfaces: HashMap, blur_enabled: bool, } @@ -165,7 +166,10 @@ where self.get_subsurface(settings, *view) } else { - iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) + let settings = settings(&mut self.app); + self.tracked_surfaces + .insert(settings.id, SurfaceIdWrapper::Subsurface(settings.id)); + iced_winit::commands::subsurface::get_subsurface(settings) } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -193,7 +197,10 @@ where self.get_subsurface(settings, Box::new(move |_| view())) } else { - iced_winit::commands::subsurface::get_subsurface(settings()) + let settings = settings(); + self.tracked_surfaces + .insert(settings.id, SurfaceIdWrapper::Subsurface(settings.id)); + iced_winit::commands::subsurface::get_subsurface(settings) } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -222,7 +229,10 @@ where self.get_popup(settings, *view) } else { - iced_winit::commands::popup::get_popup(settings(&mut self.app)) + let settings = settings(&mut self.app); + self.tracked_surfaces + .insert(settings.id, SurfaceIdWrapper::Popup(settings.id)); + iced_winit::commands::popup::get_popup(settings) } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -279,7 +289,10 @@ where self.get_popup(settings, Box::new(move |_| view())) } else { - iced_winit::commands::popup::get_popup(settings()) + let settings = settings(); + self.tracked_surfaces + .insert(settings.id, SurfaceIdWrapper::Popup(settings.id)); + iced_winit::commands::popup::get_popup(settings) } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -310,7 +323,8 @@ where self.get_window(id, settings, *view) } else { let settings = settings(&mut self.app); - + self.tracked_surfaces + .insert(id, SurfaceIdWrapper::Window(id)); iced_runtime::task::oneshot(|channel| { iced_runtime::Action::Window(iced_runtime::window::Action::Open( id, settings, channel, @@ -345,6 +359,8 @@ where self.get_window(id, settings, Box::new(move |_| view())) } else { let settings = settings(); + self.tracked_surfaces + .insert(id, SurfaceIdWrapper::Window(id)); iced_runtime::task::oneshot(|channel| { iced_runtime::Action::Window(iced_runtime::window::Action::Open( @@ -359,6 +375,74 @@ where crate::surface::Action::Task(f) => { f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) } + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::AppLayerShell(settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for layer surface"); + return Task::none(); + }; + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + + Send + + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for layer surface: {err:?}"); + None + } + } + }) { + let settings = settings(&mut self.app); + + self.get_layer_shell(settings, *view) + } else { + let settings = settings(&mut self.app); + self.tracked_surfaces + .insert(settings.id, SurfaceIdWrapper::LayerSurface(settings.id)); + iced_winit::commands::layer_surface::get_layer_surface(settings) + } + } + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::LayerShell(settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for layer surface"); + return Task::none(); + }; + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for layer surface: {err:?}"); + None + } + } + }) { + let settings = settings(); + + self.get_layer_shell(settings, Box::new(move |_| view())) + } else { + let settings = settings(); + self.tracked_surfaces.insert( + settings.id, + iced_winit::SurfaceIdWrapper::LayerSurface(settings.id), + ); + iced_winit::commands::layer_surface::get_layer_surface(settings) + } + } + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::DestroyLayerShell(id) => { + iced_winit::commands::layer_surface::destroy_layer_surface(id) + } _ => iced::Task::none(), } @@ -775,27 +859,38 @@ impl Cosmic { }; theme.transparent = new_blur; let mut guard = THEME.lock().unwrap(); - guard.set_theme(theme.theme_type); + guard.set_theme(theme.theme_type.clone()); guard.transparent = new_blur; drop(guard); let core = self.app.core(); #[cfg(all(feature = "wayland", target_os = "linux"))] - if core.auto_blur { + { + use crate::widget::wrapper; + let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); let blur = if new_blur { iced::window::enable_blur } else { iced::window::disable_blur }; - cmds.push(blur( - self.app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED), - )); - for (id, ..) in &self.surface_views { - cmds.push(blur(*id)); + if core.blur(&theme, None) { + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + } + for (id, wrapper, ..) in &self.surface_views { + if core.blur(&theme, Some(wrapper.1)) { + cmds.push(blur(*id)); + } + } + for (id, wrapper) in &self.tracked_surfaces { + if core.blur(&theme, Some(*wrapper)) { + cmds.push(blur(*id)); + } } return Task::batch(cmds); } @@ -866,10 +961,14 @@ impl Cosmic { let mut cmds = vec![corner_radius(main_window_id, Some(cur_rad)).discard()]; // Update radius for each tracked view with the window surface type - for (id, (_, surface_type, _)) in self.surface_views.iter() { + for (id, (_, surface_type, _)) in &self.surface_views { let cur_rad = corners(*surface_type, rounded, &cosmic_theme); cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } + for (id, wrapper) in &self.tracked_surfaces { + let cur_rad = corners(*wrapper, rounded, &cosmic_theme); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } return Task::batch(cmds); } @@ -963,26 +1062,37 @@ impl Cosmic { let mut cmds = vec![corner_radius(main_window_id, Some(cur_rad)).discard()]; // Update radius for each tracked view with the window surface type - for (id, (_, surface_type, _)) in self.surface_views.iter() { + for (id, (_, surface_type, _)) in &self.surface_views { let cur_rad = corners(*surface_type, rounded, &cosmic_theme); cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } + for (id, wrapper) in &self.tracked_surfaces { + let cur_rad = corners(*wrapper, rounded, &cosmic_theme); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } let core = self.app.core(); - if core.auto_blur { - let blur = if cosmic_theme.transparent { - iced::window::enable_blur - } else { - iced::window::disable_blur - }; + let blur = if cosmic_theme.transparent { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + if core.blur(&cosmic_theme, None) { cmds.push(blur( self.app .core() .main_window_id() .unwrap_or(window::Id::RESERVED), )); - for (id, ..) in &self.surface_views { + } + for (id, wrapper, ..) in &self.surface_views { + if core.blur(&cosmic_theme, Some(wrapper.1)) { + cmds.push(blur(*id)); + } + } + for (id, wrapper) in &self.tracked_surfaces { + if core.blur(&cosmic_theme, Some(*wrapper)) { cmds.push(blur(*id)); } } @@ -1030,6 +1140,9 @@ impl Cosmic { #[cfg(all(feature = "wayland", target_os = "linux"))] self.surface_views.remove(&id); } + self.tracked_surfaces.remove(&id); + self.tracked_surfaces + .shrink_to(self.tracked_surfaces.len() * 2); let mut ret = if let Some(msg) = self.app.on_close_requested(id) { self.app.update(msg) @@ -1106,22 +1219,28 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type { let mut cmds = Vec::with_capacity(1); - - if core.auto_blur { + #[cfg(all(feature = "wayland", target_os = "linux"))] + { let blur = if cosmic_theme.transparent { iced::window::enable_blur } else { iced::window::disable_blur }; - cmds.push(blur( - self.app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED), - )); - #[cfg(all(feature = "wayland", target_os = "linux"))] - for (id, ..) in &self.surface_views { - cmds.push(blur(*id)); + + if core.blur(&cosmic_theme, None) { + cmds.push(blur( + core.main_window_id().unwrap_or(window::Id::RESERVED), + )); + } + for (id, wrapper, ..) in &self.surface_views { + if core.blur(&cosmic_theme, Some(wrapper.1)) { + cmds.push(blur(*id)); + } + } + for (id, wrapper) in &self.tracked_surfaces { + if core.blur(&cosmic_theme, Some(*wrapper)) { + cmds.push(blur(*id)); + } } } cosmic_theme.set_theme(new_theme.theme_type); @@ -1252,7 +1371,13 @@ impl Cosmic { } }; theme.transparent = new_blur; - let blur_cmd = if core.auto_blur { + let wrapper = self + .surface_views + .get(&id) + .map(|s| s.1) + .or(self.tracked_surfaces.get(&id).copied()); + // this will blur untracked windows as if they were the main window + let blur_cmd = if core.blur(&theme, wrapper) { let blur = if new_blur { iced::window::enable_blur } else { @@ -1266,8 +1391,13 @@ impl Cosmic { Task::none() }; - let corner_task = if let Some(s) = self.surface_views.get(&id) { - corner_radius(id, Some(corners(s.1, rounded, &theme))).discard() + let corner_task = if let Some(s) = self + .surface_views + .get(&id) + .map(|s| s.1) + .or(self.tracked_surfaces.get(&id).copied()) + { + corner_radius(id, Some(corners(s, rounded, &theme))).discard() } else if id == self .app @@ -1305,18 +1435,30 @@ impl Cosmic { t.transparent = matches!(&t.theme_type, ThemeType::System { .. }) && new_blur; - if core.auto_blur { + { let blur = if new_blur { iced::window::enable_blur } else { iced::window::disable_blur }; let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); - if let Some(main_id) = core.main_window_id() { - cmds.push(blur(main_id)); + if core.blur(&t, None) { + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); } - for id in self.surface_views.keys() { - cmds.push(blur(*id)); + for (id, wrapper, ..) in &self.surface_views { + if core.blur(&t, Some(wrapper.1)) { + cmds.push(blur(*id)); + } + } + for (id, wrapper) in &self.tracked_surfaces { + if core.blur(&t, Some(*wrapper)) { + cmds.push(blur(*id)); + } } return Task::batch(cmds); } @@ -1335,6 +1477,7 @@ impl Cosmic { #[cfg(all(feature = "wayland", target_os = "linux"))] surface_views: HashMap::new(), opened_surfaces: HashMap::new(), + tracked_surfaces: HashMap::new(), blur_enabled: false, } } @@ -1409,6 +1552,28 @@ impl Cosmic { }) .discard() } + + #[cfg(all(feature = "wayland", target_os = "linux"))] + pub fn get_layer_shell( + &mut self, + settings: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings, + view: Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + >, + ) -> Task> { + use iced_winit::SurfaceIdWrapper; + use iced_winit::platform_specific::commands::layer_surface::get_layer_surface; + *self.opened_surfaces.entry(settings.id).or_insert(0) += 1; + self.surface_views.insert( + settings.id, + ( + None, // TODO parent for layer shell, platform specific option maybe? + SurfaceIdWrapper::LayerSurface(settings.id), + view, + ), + ); + get_layer_surface(settings) + } } #[cfg(all(feature = "wayland", target_os = "linux"))] diff --git a/src/core.rs b/src/core.rs index e725547f..53e53c94 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,8 +6,10 @@ use std::collections::HashMap; use crate::widget::nav_bar; use cosmic_config::CosmicConfigEntry; use cosmic_theme::ThemeMode; +use enumflags2::{self, BitFlags, bitflags}; use iced::{Limits, Size, window}; use iced_core::window::Id; +use iced_winit::SurfaceIdWrapper; use palette::Srgba; use slotmap::Key; @@ -43,6 +45,18 @@ pub struct Window { width: f32, } +#[bitflags] +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Auto { + /// Automatically apply effect to regular windows + Window, + /// Automatically apply effect to popups + Popup, + /// Automatically apply effect to system interface elements (layer shell surfaces) + System, +} + /// COSMIC-specific application settings #[derive(Clone)] pub struct Core { @@ -102,7 +116,7 @@ pub struct Core { #[cfg(all(feature = "wayland", target_os = "linux"))] pub(crate) sync_window_border_radii_to_theme: bool, - pub(crate) auto_blur: bool, + pub(crate) auto_blur: BitFlags, pub(crate) app_type: AppType, } @@ -214,7 +228,7 @@ impl Default for Core { menu_bars: HashMap::new(), #[cfg(all(feature = "wayland", target_os = "linux"))] sync_window_border_radii_to_theme: true, - auto_blur: true, + auto_blur: Auto::System | Auto::Popup | Auto::Window, app_type: AppType::Window, } } @@ -558,11 +572,11 @@ impl Core { self.sync_window_border_radii_to_theme } - pub fn set_auto_blur(&mut self, auto_blur: bool) { + pub fn set_auto_blur(&mut self, auto_blur: BitFlags) { self.auto_blur = auto_blur; } - pub fn auto_blur(&self) -> bool { + pub fn auto_blur(&self) -> BitFlags { self.auto_blur } @@ -573,4 +587,32 @@ impl Core { pub fn app_type(&self) -> AppType { self.app_type } + + pub fn blur(&self, theme: &Theme, surface_id_wrapper: Option) -> bool { + let theme = theme.cosmic(); + match surface_id_wrapper { + Some(SurfaceIdWrapper::LayerSurface(_)) => { + theme.frosted_system_interface && self.auto_blur.contains(Auto::System) + } + Some(SurfaceIdWrapper::Window(_)) => { + theme.frosted_windows && self.auto_blur.contains(Auto::Window) + } + Some(SurfaceIdWrapper::Popup(_)) + if matches!(self.app_type, AppType::Window | AppType::System) => + { + theme.frosted_windows && self.auto_blur.contains(Auto::Popup) + } + Some(SurfaceIdWrapper::Popup(_)) if matches!(self.app_type, AppType::Applet) => { + theme.frosted_applets && self.auto_blur.contains(Auto::Popup) + } + None => match self.app_type { + AppType::Window => theme.frosted_windows && self.auto_blur.contains(Auto::Window), + AppType::System => { + theme.frosted_system_interface && self.auto_blur.contains(Auto::System) + } + AppType::Applet => false, + }, + _ => false, + } + } } diff --git a/src/surface/action.rs b/src/surface/action.rs index 5bd5a294..300d844b 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -28,6 +28,12 @@ pub fn destroy_window(id: iced_core::window::Id) -> Action { Action::DestroyWindow(id) } +#[cfg(all(feature = "wayland", target_os = "linux"))] +#[must_use] +pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { + Action::DestroyLayerShell(id) +} + #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn app_window( @@ -226,3 +232,76 @@ pub fn subsurface( }), ) } + +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[must_use] +pub fn simple_layer_shell( + settings: impl Fn() + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + view: Option< + impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + >, +) -> Action { + let boxed: Box< + dyn Fn() + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + Action::LayerShell( + Arc::new(boxed), + view.map(|view| { + let boxed: Box< + dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + > = Box::new(view); + let boxed: Box = Box::new(boxed); + Arc::new(boxed) + }), + ) +} + +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[must_use] +pub fn layer_shell( + settings: impl Fn( + &mut App, + ) + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. + view: Option< + Box< + dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, +) -> Action { + let boxed: Box< + dyn Fn( + &mut App, + ) + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + Action::AppLayerShell( + Arc::new(boxed), + view.map(|view| { + let boxed: Box = Box::new(view); + Arc::new(boxed) + }), + ) +} diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 307b5275..29a9eada 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -52,6 +52,21 @@ pub enum Action { /// Destroy a window DestroyWindow(iced::window::Id), + /// Create a layer shell surface with a view function accepting the App as a parameter + AppLayerShell( + std::sync::Arc>, + Option>>, + ), + + /// Create a layer shell surface with a view function + LayerShell( + std::sync::Arc>, + Option>>, + ), + + /// Destroy a layer shell surface + DestroyLayerShell(iced::window::Id), + /// Responsive menu bar update ResponsiveMenuBar { /// Id of the menu bar @@ -111,6 +126,18 @@ impl std::fmt::Debug for Action { .finish(), Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(), Self::Task(_) => f.debug_tuple("Future").finish(), + Self::AppLayerShell(any, any1) => f + .debug_tuple("AppLayerShell") + .field(any) + .field(any1) + .finish(), + Self::LayerShell(any, any1) => { + f.debug_tuple("LayerShell").field(any).field(any1).finish() + } + Self::DestroyLayerShell(arg0) => { + f.debug_tuple("DestroyLayerShell").field(arg0).finish() + } + Self::Task(_) => f.debug_tuple("Task").finish(), } } }