// Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 use std::borrow::Borrow; use std::collections::HashMap; use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; #[cfg(wayland_platform)] use crate::core::Auto; #[cfg(wayland_platform)] use crate::surface::action::LiveSettings; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; #[cfg(wayland_platform)] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; #[cfg(wayland_platform)] use enumflags2::BitFlags; #[cfg(not(any(feature = "multi-window", wayland_platform)))] use iced::Application as IcedApplication; #[cfg(wayland_platform)] use iced::event::wayland; use iced::{Task, theme, window}; use iced_futures::event::listen_with; #[cfg(feature = "winit")] use iced_winit::SurfaceIdWrapper; use palette::color_difference::EuclideanDistance; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] #[non_exhaustive] pub enum WindowingSystem { UiKit, AppKit, Orbital, OhosNdk, Xlib, Xcb, Wayland, Drm, Gbm, Win32, WinRt, Web, WebCanvas, WebOffscreenCanvas, AndroidNdk, Haiku, } pub(crate) static WINDOWING_SYSTEM: std::sync::OnceLock = std::sync::OnceLock::new(); pub fn windowing_system() -> Option { WINDOWING_SYSTEM.get().copied() } fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> crate::Action { let raw = handle.as_ref(); let system = match raw { window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit, window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit, window::raw_window_handle::RawWindowHandle::Orbital(_) => WindowingSystem::Orbital, window::raw_window_handle::RawWindowHandle::OhosNdk(_) => WindowingSystem::OhosNdk, window::raw_window_handle::RawWindowHandle::Xlib(_) => WindowingSystem::Xlib, window::raw_window_handle::RawWindowHandle::Xcb(_) => WindowingSystem::Xcb, window::raw_window_handle::RawWindowHandle::Wayland(_) => WindowingSystem::Wayland, window::raw_window_handle::RawWindowHandle::Web(_) => WindowingSystem::Web, window::raw_window_handle::RawWindowHandle::WebCanvas(_) => WindowingSystem::WebCanvas, window::raw_window_handle::RawWindowHandle::WebOffscreenCanvas(_) => { WindowingSystem::WebOffscreenCanvas } window::raw_window_handle::RawWindowHandle::AndroidNdk(_) => WindowingSystem::AndroidNdk, window::raw_window_handle::RawWindowHandle::Haiku(_) => WindowingSystem::Haiku, window::raw_window_handle::RawWindowHandle::Drm(_) => WindowingSystem::Drm, window::raw_window_handle::RawWindowHandle::Gbm(_) => WindowingSystem::Gbm, window::raw_window_handle::RawWindowHandle::Win32(_) => WindowingSystem::Win32, window::raw_window_handle::RawWindowHandle::WinRt(_) => WindowingSystem::WinRt, _ => { tracing::warn!("Unknown windowing system: {raw:?}"); return crate::Action::Cosmic(Action::WindowingSystemInitialized); } }; _ = WINDOWING_SYSTEM.set(system); crate::Action::Cosmic(Action::WindowingSystemInitialized) } #[derive(Default)] pub struct Cosmic { pub app: App, pub surface_views: HashMap< window::Id, ( Option, SurfaceIdWrapper, Box Fn(&'a App) -> crate::surface::action::LiveSettings>, Option< Box< dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync + 'static, >, >, ), >, pub opened_surfaces: HashMap, blur_enabled: bool, } impl Cosmic where T::Message: Send + 'static, { pub fn init( (mut core, flags): (Core, T::Flags), ) -> (Self, iced::Task>) { #[cfg(all(feature = "dbus-config", target_os = "linux"))] { use iced_futures::futures::executor::block_on; core.settings_daemon = block_on(cosmic_config::dbus::settings_daemon_proxy()).ok(); } let id = core.main_window_id().unwrap_or(window::Id::RESERVED); let (model, command) = T::init(core, flags); let existing_theme = THEME.lock().unwrap(); let blur = existing_theme.transparent; drop(existing_theme); let mut cmds = vec![ iced_runtime::window::run_with_handle(id, init_windowing_system), command, ]; if blur { cmds.push(crate::task::message(crate::action::cosmic( Action::BlurEnabled, ))); } (Self::new(model), Task::batch(cmds)) } #[cfg(not(feature = "multi-window"))] pub fn title(&self) -> String { self.app.title().to_string() } #[cfg(feature = "multi-window")] pub fn title(&self, id: window::Id) -> String { self.app.title(id).to_string() } #[allow(clippy::too_many_lines)] pub fn surface_update( &mut self, _surface_message: crate::surface::Action, ) -> iced::Task> { #[cfg(feature = "surface-message")] match _surface_message { #[cfg(wayland_platform)] crate::surface::Action::AppSubsurface(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { tracing::error!("Invalid settings for subsurface"); return Task::none(); }; let settings = settings(&mut self.app); let 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 subsurface view: {err:?}"); None } } }); self.get_subsurface(settings, view.map(|v| *v)) } #[cfg(wayland_platform)] crate::surface::Action::Subsurface(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { tracing::error!("Invalid settings for subsurface"); return Task::none(); }; let settings = settings(); if let Some(view) = view { self.get_subsurface(settings, Some(Box::new(move |_| view()))) } else { self.get_subsurface(settings, None) } } #[cfg(wayland_platform)] crate::surface::Action::AppPopup(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { tracing::error!("Invalid settings for popup"); return Task::none(); }; let Some(live_settings) = std::sync::Arc::try_unwrap(live_settings) .ok() .and_then(|s| { s.downcast:: LiveSettings + Send + Sync>>() .ok() }) else { tracing::error!("Invalid live settings for popup"); return Task::none(); }; let 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 subsurface view: {err:?}"); None } } }); let settings = settings(&mut self.app); self.get_popup(settings, *live_settings, view.map(|v| *v)) } #[cfg(wayland_platform)] crate::surface::Action::DestroyPopup(id) => { iced_winit::commands::popup::destroy_popup(id) } #[cfg(wayland_platform)] crate::surface::Action::DestroyTooltipPopup => { #[cfg(feature = "applet")] { iced_winit::commands::popup::destroy_popup(*crate::applet::TOOLTIP_WINDOW_ID) } #[cfg(not(feature = "applet"))] { Task::none() } } #[cfg(wayland_platform)] crate::surface::Action::DestroySubsurface(id) => { iced_winit::commands::subsurface::destroy_subsurface(id) } #[cfg(wayland_platform)] crate::surface::Action::DestroyWindow(id) => iced::window::close(id), crate::surface::Action::ResponsiveMenuBar { menu_bar, limits, size, } => { let core = self.app.core_mut(); core.menu_bars.insert(menu_bar, (limits, size)); iced::Task::none() } #[cfg(wayland_platform)] crate::surface::Action::Popup(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { tracing::error!("Invalid settings for popup"); return Task::none(); }; let Some(live_settings) = std::sync::Arc::try_unwrap(live_settings) .ok() .and_then(|s| { s.downcast:: LiveSettings + Send + Sync>>() .ok() }) else { tracing::error!("Invalid live settings for popup"); return Task::none(); }; let settings = settings(); let live_settings = Box::new(move |_: &T| live_settings()); if let Some(view) = view { self.get_popup(settings, live_settings, Some(Box::new(move |_| view()))) } else { self.get_popup(settings, live_settings, None) } } #[cfg(wayland_platform)] crate::surface::Action::AppWindow(id, settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() .ok() }) else { tracing::error!("Invalid settings for AppWindow"); return Task::none(); }; let Some(live_settings) = std::sync::Arc::try_unwrap(live_settings) .ok() .and_then(|s| { s.downcast:: LiveSettings + Send + Sync>>() .ok() }) else { tracing::error!("Invalid live settings for popup"); return Task::none(); }; let 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 AppWindow: {err:?}"); None } } }); let settings = settings(&mut self.app); self.get_window(id, settings, *live_settings, view.map(|v| *v)) } #[cfg(wayland_platform)] crate::surface::Action::Window(id, settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() .ok() }) else { tracing::error!("Invalid settings for Window"); return Task::none(); }; let Some(live_settings) = std::sync::Arc::try_unwrap(live_settings) .ok() .and_then(|s| { s.downcast:: LiveSettings + Send + Sync>>() .ok() }) else { tracing::error!("Invalid live settings for popup"); return Task::none(); }; if let Some(view) = view { let settings = settings(); self.get_window( id, settings, Box::new(move |_| live_settings()), Some(Box::new(move |_| view())), ) } else { let settings = settings(); iced_runtime::task::oneshot(|channel| { iced_runtime::Action::Window(iced_runtime::window::Action::Open( id, settings, channel, )) }) .discard() } } crate::surface::Action::Ignore => iced::Task::none(), crate::surface::Action::Task(f) => f().map(crate::Action::Surface), #[cfg(wayland_platform)] crate::surface::Action::AppLayerShell(settings, live_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(); }; let Some(live_settings) = std::sync::Arc::try_unwrap(live_settings) .ok() .and_then(|s| { s.downcast:: LiveSettings + Send + Sync>>() .ok() }) else { tracing::error!("Invalid live settings for popup"); return Task::none(); }; let 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, *live_settings, view.map(|v| *v)) } #[cfg(wayland_platform)] crate::surface::Action::LayerShell(settings, live_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(); }; let Some(live_settings) = std::sync::Arc::try_unwrap(live_settings) .ok() .and_then(|s| { s.downcast:: LiveSettings + Send + Sync>>() .ok() }) else { tracing::error!("Invalid live settings for popup"); return Task::none(); }; let settings = settings(); let live_settings = live_settings(); let live_settings = Box::new(move |_app: &T| live_settings); if let Some(view) = view { self.get_layer_shell(settings, live_settings, Some(Box::new(move |_| view()))) } else { self.get_layer_shell(settings, live_settings, None) } } #[cfg(wayland_platform)] crate::surface::Action::DestroyLayerShell(id) => { iced_winit::commands::layer_surface::destroy_layer_surface(id) } crate::surface::Action::SyncLiveSettings(id) => { if let Some((_, id, live_settings, _)) = self.surface_views.get(&id) { let live_settings = live_settings(&self.app); return self.apply_live_settings(*id, &live_settings); } Task::none() } _ => iced::Task::none(), } #[cfg(not(feature = "surface-message"))] iced::Task::none() } pub fn update( &mut self, message: crate::Action, ) -> iced::Task> { let mut task = match message { crate::Action::App(message) => self.app.update(message), crate::Action::Cosmic(message) => self.cosmic_update(message), crate::Action::Surface(action) => self.surface_update(action), crate::Action::None => iced::Task::none(), #[cfg(feature = "single-instance")] crate::Action::DbusActivation(message) => { let mut task = self.app.dbus_activation(message); if let Some(id) = self.app.core().main_window_id() { let unminimize = iced_runtime::window::minimize::<()>(id, false); task = task.chain(unminimize.discard()); } task } }; // Drain any text context-menu popup teardown requests queued by // widgets during `app.update()` and destroy them through the normal // popup pipeline. Drained before the creation queue so a // destroy-then-recreate (a second right-click reusing the same id) // keeps its order. #[cfg(all(wayland_platform, target_os = "linux"))] for id in crate::widget::text_context_menu::take_popup_destroys() { task = task.chain(iced_winit::commands::popup::destroy_popup(id)); } // Drain any text context-menu popup requests queued by widgets during // `app.update()` and create them through the normal popup pipeline. #[cfg(all(wayland_platform, target_os = "linux"))] for req in crate::widget::text_context_menu::take_popup_requests() { let (live_settings, settings, view) = crate::widget::text_context_menu::into_popup_view::(req); task = task.chain(self.get_popup( settings, Box::new(move |_| live_settings), Some(Box::new(move |_| view())), )); } #[cfg(all(target_env = "gnu", not(target_os = "windows")))] crate::malloc::trim(0); task } #[cfg(not(feature = "multi-window"))] pub fn scale_factor(&self) -> f64 { f64::from(self.app.core().scale_factor()) } #[cfg(feature = "multi-window")] pub fn scale_factor(&self, _id: window::Id) -> f64 { f64::from(self.app.core().scale_factor()) } pub fn style(&self, theme: &Theme) -> theme::Style { if let Some(style) = self.app.style() { style } else { let theme = THEME.lock().unwrap(); if self.app.core().window.is_maximized && !theme.cosmic().frosted_maximized_apps { crate::style::iced::application::style(theme.borrow()) } else { theme::Style { background_color: iced_core::Color::TRANSPARENT, icon_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(), } } } } #[allow(clippy::too_many_lines)] #[cold] pub fn subscription(&self) -> Subscription> { let window_events = listen_with(|event, _, id| { match event { iced::Event::Window(window::Event::Resized(iced::Size { width, height })) => { return Some(Action::WindowResize(id, width, height)); } iced::Event::Window(window::Event::Opened { .. }) => { return Some(Action::Opened(id)); } iced::Event::Window(window::Event::Closed) => { return Some(Action::SurfaceClosed(id)); } iced::Event::Window(window::Event::Focused) => return Some(Action::Focus(id)), iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)), #[cfg(wayland_platform)] iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { match event { wayland::Event::Popup(wayland::PopupEvent::Done, _, id) | wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => { return Some(Action::SurfaceClosed(id)); } #[cfg(feature = "applet")] wayland::Event::Window( iced::event::wayland::WindowEvent::SuggestedBounds(b), ) => { return Some(Action::SuggestedBounds(b)); } #[cfg(wayland_platform)] wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( s, )) => { return Some(Action::WindowState(id, s)); } wayland::Event::BlurEnabled => { return Some(Action::BlurEnabled); } _ => (), } } _ => (), } None }); let mut subscriptions = vec![ self.app.subscription().map(crate::Action::App), self.app .core() .watch_config::(crate::config::ID) .map(|update| { for why in update .errors .into_iter() .filter(cosmic_config::Error::is_err) { if let cosmic_config::Error::GetKey(_, err) = &why { if err.kind() == std::io::ErrorKind::NotFound { // No system default config installed; don't error continue; } } tracing::error!(?why, "cosmic toolkit config update error"); } crate::Action::Cosmic(Action::ToolkitConfig(update.config)) }), self.app .core() .watch_config::( if if let ThemeType::System { prefer_dark, .. } = THEME.lock().unwrap().theme_type { prefer_dark } else { None } .unwrap_or_else(|| self.app.core().system_theme_mode.is_dark) { cosmic_theme::DARK_THEME_ID } else { cosmic_theme::LIGHT_THEME_ID }, ) .map(|update| { for why in update .errors .into_iter() .filter(cosmic_config::Error::is_err) { tracing::error!(?why, "cosmic theme config update error"); } Action::SystemThemeChange( update.keys, crate::theme::Theme::system(Arc::new(update.config)), ) }) .map(crate::Action::Cosmic), self.app .core() .watch_config::(cosmic_theme::THEME_MODE_ID) .map(|update| { for error in update .errors .into_iter() .filter(cosmic_config::Error::is_err) { tracing::error!(?error, "error reading system theme mode update"); } Action::SystemThemeModeChange(update.keys, update.config) }) .map(crate::Action::Cosmic), window_events.map(crate::Action::Cosmic), #[cfg(xdg_portal)] crate::theme::portal::desktop_settings() .map(Action::DesktopSettings) .map(crate::Action::Cosmic), ]; if self.app.core().keyboard_nav { subscriptions.push( keyboard_nav::subscription() .map(Action::KeyboardNav) .map(crate::Action::Cosmic), ); } #[cfg(feature = "single-instance")] if self.app.core().single_instance { subscriptions.push(crate::dbus_activation::subscription::()); } // Drives the text context-menu popup queues: a right-click queues a // popup but publishes no message, so this re-emits `Action::None` to // make `update()` run and drain the queue. #[cfg(all(wayland_platform, target_os = "linux"))] subscriptions.push(crate::widget::text_context_menu::wake_subscription::< T::Message, >()); Subscription::batch(subscriptions) } #[cfg(not(feature = "multi-window"))] pub fn theme(&self) -> Theme { crate::theme::active() } #[cfg(feature = "multi-window")] pub fn theme(&self, _id: window::Id) -> Theme { crate::theme::active() } #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { crate::widget::text_context_menu::set_current_window_id(id); #[cfg(all(wayland_platform, target_os = "linux"))] if let Some((_, _, _, Some(v))) = self.surface_views.get(&id) { return v(&self.app); } if self .app .core() .main_window_id() .is_none_or(|main_id| main_id != id) { return self.app.view_window(id).map(crate::Action::App); } let view = if self.app.core().window.use_template { self.app.view_main() } else { self.app.view().map(crate::Action::App) }; #[cfg(all(target_env = "gnu", not(target_os = "windows")))] crate::malloc::trim(0); view } #[cfg(not(feature = "multi-window"))] pub fn view(&self) -> Element> { if let Some(id) = self.app.core().main_window_id() { crate::widget::text_context_menu::set_current_window_id(id); } let view = self.app.view_main(); #[cfg(all(target_env = "gnu", not(target_os = "windows")))] crate::malloc::trim(0); view } } impl Cosmic { #[allow(clippy::unused_self)] #[cold] pub fn close(&mut self) -> iced::Task> { if let Some(id) = self.app.core().main_window_id() { iced::window::close(id) } else { iced::Task::none() } } #[allow(clippy::too_many_lines)] fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { Action::WindowMaximized(id, maximized) => { #[cfg(not(wayland_platform))] if self .app .core() .main_window_id() .is_some_and(|main_id| main_id == id) { self.app.core_mut().window.sharp_corners = maximized; } } Action::WindowResize(id, width, height) => { if self .app .core() .main_window_id() .is_some_and(|main_id| main_id == id) { self.app.core_mut().set_window_width(width); self.app.core_mut().set_window_height(height); } self.app.on_window_resize(id, width, height); //TODO: more efficient test of maximized (winit has no event for maximize if set by the OS) return iced::window::is_maximized(id).map(move |maximized| { crate::Action::Cosmic(Action::WindowMaximized(id, maximized)) }); } #[cfg(wayland_platform)] Action::WindowState(id, state) => { if self .app .core() .main_window_id() .is_some_and(|main_id| main_id == id) { self.app.core_mut().window.sharp_corners = state.intersects( WindowState::MAXIMIZED | WindowState::FULLSCREEN | WindowState::TILED | WindowState::TILED_RIGHT | WindowState::TILED_LEFT | WindowState::TILED_TOP | WindowState::TILED_BOTTOM, ); self.app.core_mut().window.is_maximized = state.intersects(WindowState::MAXIMIZED | WindowState::FULLSCREEN); } { use iced_winit::platform_specific::commands::corner_radius::corner_radius; let theme = THEME.lock().unwrap(); let rounded = !self.app.core().window.sharp_corners && self.app.core().sync_window_border_radii_to_theme(); let cur_rad = self.app.core().corners(&theme, rounded); return Task::batch([corner_radius(id, cur_rad).discard()]); } } #[cfg(wayland_platform)] Action::WmCapabilities(id, capabilities) => { if self .app .core() .main_window_id() .is_some_and(|main_id| main_id == id) { self.app.core_mut().window.show_maximize = capabilities.contains(WindowManagerCapabilities::MAXIMIZE); self.app.core_mut().window.show_minimize = capabilities.contains(WindowManagerCapabilities::MINIMIZE); self.app.core_mut().window.show_window_menu = capabilities.contains(WindowManagerCapabilities::WINDOW_MENU); } } Action::KeyboardNav(message) => match message { keyboard_nav::Action::FocusNext => { return iced::widget::operation::focus_next().map(crate::Action::Cosmic); } keyboard_nav::Action::FocusPrevious => { return iced::widget::operation::focus_previous().map(crate::Action::Cosmic); } keyboard_nav::Action::Escape => return self.app.on_escape(), keyboard_nav::Action::Search => return self.app.on_search(), keyboard_nav::Action::Fullscreen => return self.app.core().toggle_maximize(None), }, Action::ContextDrawer(show) => { self.app.core_mut().set_show_context(show); return self.app.on_context_drawer(); } Action::Drag => return self.app.core().drag(None), Action::Minimize => return self.app.core().minimize(None), Action::Maximize => return self.app.core().toggle_maximize(None), Action::NavBar(key) => { self.app.core_mut().nav_bar_set_toggled_condensed(false); return self.app.on_nav_select(key); } Action::NavBarContext(key) => { self.app.core_mut().nav_bar_set_context(key); return self.app.on_nav_context(key); } Action::ToggleNavBar => { self.app.core_mut().nav_bar_toggle(); } Action::ToggleNavBarCondensed => { self.app.core_mut().nav_bar_toggle_condensed(); } Action::AppThemeChange(mut theme) => { if let ThemeType::System { theme: _, .. } = theme.theme_type { self.app.core_mut().theme_sub_counter += 1; let portal_accent = self.app.core().portal_accent; if let Some(a) = portal_accent { let t_inner = theme.cosmic(); if a.distance_squared(*t_inner.accent_color()) > 0.00001 { theme = Theme::system(Arc::new(t_inner.with_accent(a))); } } } let new_blur = self.blur_enabled && self.app.core().frosted(theme.cosmic()); theme.transparent = new_blur; let mut guard = THEME.lock().unwrap(); guard.set_theme(theme.theme_type.clone()); guard.transparent = new_blur; drop(guard); #[cfg(wayland_platform)] { let core = self.app.core(); 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 }; 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 { let overriden = wrapper.2(&self.app); if core.blur(&theme, Some(wrapper.1)) && overriden.blur.unwrap_or(true) { cmds.push(blur(*id)); } else if overriden.blur.is_some_and(|b| !b) { cmds.push(iced::window::disable_blur(*id)); } } return Task::batch(cmds); } } Action::SystemThemeChange(keys, mut theme) => { let cur_is_dark = self.app.core().system_theme_mode.is_dark; // Ignore updates if the current theme mode does not match. if cur_is_dark != theme.cosmic().is_dark { return iced::Task::none(); } // update transparent let new_blur = self.blur_enabled && self.app.core().frosted(theme.cosmic()); theme.transparent = new_blur; let cmd = self.app.system_theme_update(&keys, theme.cosmic()); // Record the last-known system theme in event that the current theme is custom. self.app.core_mut().system_theme = theme.clone(); let portal_accent = self.app.core().portal_accent; { let mut cosmic_theme = THEME.lock().unwrap(); // Only apply update if the theme is set to load a system theme if let ThemeType::System { theme: _, prefer_dark, } = cosmic_theme.theme_type { let mut new_theme = if let Some(a) = portal_accent { let t_inner = theme.cosmic(); if a.distance_squared(*t_inner.accent_color()) > 0.00001 { Theme::system(Arc::new(t_inner.with_accent(a))) } else { theme } } else { theme }; new_theme.transparent = new_blur; new_theme.theme_type.prefer_dark(prefer_dark); cosmic_theme.set_theme(new_theme.theme_type); cosmic_theme.transparent = new_blur; #[cfg(wayland_platform)] { use iced_winit::platform_specific::commands::corner_radius::corner_radius; let rounded = self.app.core().sync_window_border_radii_to_theme() && !self.app.core().window.sharp_corners; let cur_rad = self.app.core().corners(&cosmic_theme, rounded); // Update radius for the main window let main_window_id = self .app .core() .main_window_id() .unwrap_or(window::Id::RESERVED); let mut cmds = vec![corner_radius(main_window_id, cur_rad).discard()]; // Update radius for each tracked view with the window surface type for (id, (_, surface_type, live_settings, _)) in &self.surface_views { let overriden = live_settings(&self.app); let cur_rad = if let Some(c) = overriden.corners { Some(c) } else { corners( *surface_type, rounded, &cosmic_theme, self.app.core().auto_corner_radius, ) }; if cur_rad.is_none() { continue; } cmds.push(corner_radius(*id, cur_rad).discard()); } 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, wrapper, ..) in &self.surface_views { let overriden = wrapper.2(&self.app); if self.app.core().blur(&cosmic_theme, Some(wrapper.1)) && overriden.blur.unwrap_or(true) { cmds.push(blur(*id)); } else if overriden.blur.is_some_and(|b| !b) { cmds.push(iced::window::disable_blur(*id)); } } return Task::batch(cmds); } } } return cmd; } Action::ScaleFactor(factor) => { self.app.core_mut().set_scale_factor(factor); } Action::Close => { return match self.app.on_app_exit() { Some(message) => self.app.update(message), None => self.close(), }; } Action::SystemThemeModeChange(keys, mode) => { if match THEME.lock().unwrap().theme_type { ThemeType::System { theme: _, prefer_dark, } => prefer_dark.is_some(), _ => false, } { return iced::Task::none(); } let mut cmds = vec![self.app.system_theme_mode_update(&keys, &mode)]; let core = self.app.core_mut(); core.system_theme_mode = mode; let is_dark = core.system_is_dark(); let changed = core.system_theme_mode.is_dark != is_dark || core.portal_is_dark != Some(is_dark) || core.system_theme.cosmic().is_dark != is_dark; if changed { core.theme_sub_counter += 1; let mut new_theme = if is_dark { crate::theme::system_dark() } else { crate::theme::system_light() }; cmds.push(self.app.system_theme_update(&[], new_theme.cosmic())); let core = self.app.core_mut(); new_theme = if let Some(a) = core.portal_accent { let t_inner = new_theme.cosmic(); if a.distance_squared(*t_inner.accent_color()) > 0.00001 { Theme::system(Arc::new(t_inner.with_accent(a))) } else { new_theme } } else { new_theme }; let new_blur = self.blur_enabled && core.frosted(new_theme.cosmic()); new_theme.transparent = new_blur; core.system_theme = new_theme.clone(); { let mut cosmic_theme = THEME.lock().unwrap(); // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { cosmic_theme.set_theme(new_theme.theme_type); cosmic_theme.transparent = new_blur; #[cfg(wayland_platform)] { use iced_winit::platform_specific::commands::corner_radius::corner_radius; let rounded = self.app.core().sync_window_border_radii_to_theme() && !self.app.core().window.sharp_corners; let cur_rad = self.app.core().corners(&cosmic_theme, rounded); // Update radius for the main window let main_window_id = self .app .core() .main_window_id() .unwrap_or(window::Id::RESERVED); let mut cmds = vec![corner_radius(main_window_id, cur_rad).discard()]; // Update radius for each tracked view with the window surface type for (id, (_, surface_type, live_settings, _)) in &self.surface_views { let overriden = live_settings(&self.app); let cur_rad = if let Some(c) = overriden.corners { Some(c) } else { corners( *surface_type, rounded, &cosmic_theme, self.app.core().auto_corner_radius, ) }; if cur_rad.is_none() { continue; } cmds.push(corner_radius(*id, cur_rad).discard()); } let core = self.app.core(); 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, wrapper, ..) in &self.surface_views { let overriden = wrapper.2(&self.app); if core.blur(&cosmic_theme, Some(wrapper.1)) && overriden.blur.unwrap_or(true) { cmds.push(blur(*id)); } else if overriden.blur.is_some_and(|b| !b) { cmds.push(iced::window::disable_blur(*id)); } } return Task::batch(cmds); } } } } return Task::batch(cmds); } Action::Activate(_token) => { if let Some(id) = self.app.core().main_window_id() { // Unminimize window before requesting to activate it. let mut task = iced_runtime::window::minimize(id, false); #[cfg(wayland_platform)] { task = task.chain( iced_winit::platform_specific::commands::activation::activate( id, #[allow(clippy::used_underscore_binding)] _token, ), ); } #[cfg(not(wayland_platform))] { task = task.chain(iced_runtime::window::gain_focus(id)); } return task; } } Action::SurfaceClosed(id) => { if self.opened_surfaces.get_mut(&id).is_some_and(|v| { *v = v.saturating_sub(1); *v == 0 }) { self.opened_surfaces.remove(&id); self.surface_views.remove(&id); } self.surface_views.shrink_to(self.surface_views.len() * 2); let mut ret = if let Some(msg) = self.app.on_close_requested(id) { self.app.update(msg) } else { Task::none() }; let core = self.app.core(); if core.exit_on_main_window_closed && core.main_window_id().is_some_and(|m_id| id == m_id) { ret = Task::batch([iced::exit::>()]); } return ret; } Action::ShowWindowMenu => { if let Some(id) = self.app.core().main_window_id() { return iced::window::show_system_menu(id); } } #[cfg(feature = "single-instance")] Action::DbusConnection(conn) => { return self.app.dbus_connection(conn); } #[cfg(xdg_portal)] Action::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => { use ashpd::desktop::settings::ColorScheme; if match THEME.lock().unwrap().theme_type { ThemeType::System { theme: _, prefer_dark, } => prefer_dark.is_some(), _ => false, } { return iced::Task::none(); } let is_dark = match s { ColorScheme::NoPreference => None, ColorScheme::PreferDark => Some(true), ColorScheme::PreferLight => Some(false), }; let core = self.app.core_mut(); core.portal_is_dark = is_dark; let is_dark = core.system_is_dark(); let changed = core.system_theme_mode.is_dark != is_dark || core.portal_is_dark != Some(is_dark) || core.system_theme.cosmic().is_dark != is_dark; if changed { core.theme_sub_counter += 1; let mut new_theme = if is_dark { crate::theme::system_dark() } else { crate::theme::system_light() }; if let ThemeType::System { .. } = new_theme.theme_type { let new_blur = self.blur_enabled && core.frosted(new_theme.cosmic()); new_theme.transparent = new_blur; } core.system_theme = new_theme.clone(); let core = self.app.core(); { let mut cosmic_theme = THEME.lock().unwrap(); // 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); #[cfg(wayland_platform)] { let blur = if cosmic_theme.transparent { iced::window::enable_blur } else { iced::window::disable_blur }; 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 { let overriden = wrapper.2(&self.app); if core.blur(&cosmic_theme, Some(wrapper.1)) && overriden.blur.unwrap_or(true) { cmds.push(blur(*id)); } else if overriden.blur.is_some_and(|b| !b) { cmds.push(iced::window::disable_blur(*id)); } } } cosmic_theme.set_theme(new_theme.theme_type); return Task::batch(cmds); } } } } #[cfg(xdg_portal)] Action::DesktopSettings(crate::theme::portal::Desktop::Accent(c)) => { use palette::Srgba; let c = Srgba::new(c.red() as f32, c.green() as f32, c.blue() as f32, 1.0); let core = self.app.core_mut(); core.portal_accent = Some(c); let cur_accent = core.system_theme.cosmic().accent_color(); if cur_accent.distance_squared(*c) < 0.00001 { // skip calculations if we already have the same color return iced::Task::none(); } { let mut cosmic_theme = THEME.lock().unwrap(); // Only apply update if the theme is set to load a system theme if let ThemeType::System { theme: t, prefer_dark, } = cosmic_theme.theme_type.clone() { cosmic_theme.set_theme(ThemeType::System { theme: Arc::new(t.with_accent(c)), prefer_dark, }); } } } #[cfg(xdg_portal)] Action::DesktopSettings(crate::theme::portal::Desktop::Contrast(_)) => { // TODO when high contrast is integrated in settings and all custom themes } Action::ToolkitConfig(config) => { // Change the icon theme if not defined by the application. if !self.app.core().icon_theme_override && crate::icon_theme::default() != config.icon_theme { crate::icon_theme::set_default(config.icon_theme.clone()); } *crate::config::COSMIC_TK.write().unwrap() = config; } Action::Focus(f) => { #[cfg(wayland_platform)] if let Some(( parent, SurfaceIdWrapper::Subsurface(_) | SurfaceIdWrapper::Popup(_), _, _, )) = self.surface_views.get(&f) { // If the parent is already focused, push the new focus // to the end of the focus chain. if parent.is_some_and(|p| self.app.core().focused_window.last() == Some(&p)) { self.app.core_mut().focused_window.push(f); return iced::Task::none(); } else { // set the whole parent chain to the focus chain let mut parent_chain = vec![f]; let mut cur = *parent; while let Some(p) = cur { parent_chain.push(p); cur = self .surface_views .get(&p) .and_then(|(parent, _, _, _)| *parent); } parent_chain.reverse(); self.app.core_mut().focused_window = parent_chain; return iced::Task::none(); } } self.app.core_mut().focused_window = vec![f]; } Action::Unfocus(id) => { let core = self.app.core_mut(); if core.focused_window().as_ref().is_some_and(|cur| *cur == id) { core.focused_window.pop(); } } #[cfg(feature = "applet")] Action::SuggestedBounds(b) => { tracing::info!("Suggested bounds: {b:?}"); let core = self.app.core_mut(); core.applet.suggested_bounds = b; } Action::Opened(id) => { #[cfg(wayland_platform)] { use iced_winit::platform_specific::commands::corner_radius::corner_radius; let mut theme = THEME.lock().unwrap(); // TODO do we need per window sharp corners? let rounded = (!self.app.core().window.sharp_corners && self.app.core().sync_window_border_radii_to_theme()) || self .surface_views .get(&id) .is_some_and(|(_, surface_type, _, _)| { matches!( surface_type, SurfaceIdWrapper::Popup(_) | SurfaceIdWrapper::LayerSurface(_) ) }); let new_blur = self.blur_enabled && self.app.core().frosted(theme.cosmic()); let wrapper = self.surface_views.get(&id).map(|s| s.1); // this will blur untracked windows as if they were the main window let blur_cmd = if self.app.core().blur(&theme, wrapper) && self .surface_views .get(&id) .and_then(|s| s.2(&self.app).blur) .unwrap_or(true) { 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()); cmds.push(blur(id)); Task::batch(cmds) } else { Task::none() }; let corner_task = if let Some((_, cur_rad)) = self.surface_views.get(&id).map(|s| (s.1, &s.2)).and_then( |(s, overriden)| { let overriden = overriden(&self.app); if let Some(c) = overriden.corners { Some((s, c)) } else { corners(s, rounded, &theme, self.app.core().auto_corner_radius) .map(|c| (s, c)) } }, ) { corner_radius(id, Some(cur_rad)).discard() } else if id == self .app .core() .main_window_id() .unwrap_or(window::Id::RESERVED) { corner_radius(id, self.app.core().corners(&theme, rounded)).discard() } else { Task::none() }; return Task::batch([ blur_cmd, corner_task, iced_runtime::window::run_with_handle(id, init_windowing_system), ]); } return iced_runtime::window::run_with_handle(id, init_windowing_system); } #[cfg(wayland_platform)] Action::BlurEnabled => { // TODO do this after blur event confirms support instead of for all wayland windows self.blur_enabled = true; let mut t = THEME.lock().unwrap(); let new_blur = self.blur_enabled && self.app.core().frosted(t.cosmic()); t.transparent = new_blur; self.app.core_mut().system_theme.transparent = new_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 self.app.core().blur(&t, None) { cmds.push(blur( self.app .core() .main_window_id() .unwrap_or(window::Id::RESERVED), )); } for (id, wrapper, ..) in &self.surface_views { let overriden = wrapper.2(&self.app); if self.app.core().blur(&t, Some(wrapper.1)) && overriden.blur.unwrap_or(true) { cmds.push(blur(*id)); } else if overriden.blur.is_some_and(|b| !b) { cmds.push(iced::window::disable_blur(*id)); } } return Task::batch(cmds); } } _ => (), } iced::Task::none() } } impl Cosmic { pub fn new(app: App) -> Self { Self { app, surface_views: HashMap::new(), opened_surfaces: HashMap::new(), blur_enabled: false, } } #[cfg(feature = "surface-message")] /// Apply live setting overrides for a surface pub fn apply_live_settings( &mut self, id_wrapper: SurfaceIdWrapper, live_settings: &crate::surface::action::LiveSettings, ) -> Task> { let id = id_wrapper.inner(); let mut cmds = Vec::with_capacity(2); let t = THEME.try_lock().unwrap(); if let Some(blur) = live_settings.blur { let blur_cmd = if blur { if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::enable_blur(id) } else { #[cfg(wayland_platform)] { iced_winit::commands::blur::blur( id, Some(vec![iced::Rectangle::new( iced::Point::ORIGIN, iced::Size::INFINITE, )]), ) .discard() } #[cfg(not(wayland_platform))] { iced::window::enable_blur(id) } } } else if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::disable_blur(id) } else { #[cfg(wayland_platform)] { iced_winit::commands::blur::blur(id, None).discard() } #[cfg(not(wayland_platform))] { iced::window::disable_blur(id) } }; cmds.push(blur_cmd); } else if self.app.core().blur(&t, Some(id_wrapper)) { cmds.push(if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::enable_blur(id) } else { #[cfg(wayland_platform)] { iced_winit::commands::blur::blur( id, Some(vec![iced::Rectangle::new( iced::Point::ORIGIN, iced::Size::INFINITE, )]), ) .discard() } #[cfg(not(wayland_platform))] { iced::window::enable_blur(id) } }); } #[cfg(wayland_platform)] if let Some(corners) = live_settings.corners { cmds.push( iced_winit::commands::corner_radius::corner_radius(id, Some(corners)).discard(), ); } else { let rounded = !self.app.core().window.sharp_corners && self.app.core().sync_window_border_radii_to_theme(); if let Some(cur_rad) = corners(id_wrapper, rounded, &t, self.app.core().auto_corner_radius) { cmds.push( iced_winit::commands::corner_radius::corner_radius(id, Some(cur_rad)).discard(), ); } } #[cfg(wayland_platform)] if let (SurfaceIdWrapper::LayerSurface(id), Some(padding)) = (id_wrapper, live_settings.padding) { cmds.push(iced_winit::commands::layer_surface::set_padding( id, padding, )); } Task::batch(cmds) } #[cfg(wayland_platform)] /// Create a subsurface pub fn get_subsurface( &mut self, settings: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings, view: Option< Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, >, ) -> Task> { use iced_winit::commands::subsurface::get_subsurface; *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1; let live_settings_task = self.apply_live_settings( SurfaceIdWrapper::Subsurface(settings.id), &LiveSettings { blur: None, corners: None, padding: None, }, ); self.surface_views.insert( settings.id, ( Some(settings.parent), SurfaceIdWrapper::Subsurface(settings.id), Box::new(|_| LiveSettings::default()), view, ), ); Task::batch([live_settings_task, get_subsurface(settings)]) } #[cfg(wayland_platform)] /// Create a subsurface pub fn get_popup( &mut self, settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, view: Option< Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, >, ) -> Task> { use iced_winit::commands::popup::get_popup; *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1; let live_settings_task = self.apply_live_settings( SurfaceIdWrapper::Popup(settings.id), &live_settings(&self.app), ); self.surface_views.insert( settings.id, ( Some(settings.parent), SurfaceIdWrapper::Popup(settings.id), live_settings, view, ), ); live_settings_task.chain(get_popup(settings)).discard() } #[cfg(wayland_platform)] /// Create a window surface pub fn get_window( &mut self, id: iced::window::Id, settings: iced::window::Settings, live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, view: Option< Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, >, ) -> Task> { use iced_winit::SurfaceIdWrapper; *self.opened_surfaces.entry(id).or_insert(0) += 1; let live_settings_task = self.apply_live_settings(SurfaceIdWrapper::Window(id), &live_settings(&self.app)); self.surface_views.insert( id, ( None, // TODO parent for window, platform specific option maybe? SurfaceIdWrapper::Window(id), live_settings, view, ), ); Task::batch([ iced_runtime::task::oneshot(|channel| { iced_runtime::Action::Window(iced_runtime::window::Action::Open( id, settings, channel, )) }) .discard(), // We don't control window creation in the same way live_settings_task, ]) } #[cfg(wayland_platform)] pub fn get_layer_shell( &mut self, settings: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings, live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, view: Option< Box 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; let live_settings_task = self.apply_live_settings( SurfaceIdWrapper::LayerSurface(settings.id), &live_settings(&self.app), ); self.surface_views.insert( settings.id, ( None, // TODO parent for layer shell, platform specific option maybe? SurfaceIdWrapper::LayerSurface(settings.id), live_settings, view, ), ); Task::batch([live_settings_task, get_layer_surface(settings)]) } } #[cfg(wayland_platform)] fn corners( surface_type: SurfaceIdWrapper, rounded: bool, theme: &Theme, auto_corner_radius: BitFlags, ) -> Option { if !match surface_type { SurfaceIdWrapper::Window(_) => auto_corner_radius.contains(Auto::Window), SurfaceIdWrapper::LayerSurface(_) => auto_corner_radius.contains(Auto::System), SurfaceIdWrapper::Popup(_) => auto_corner_radius.contains(Auto::Popup), _ => false, } { return None; } let theme = theme.cosmic(); Some(if let SurfaceIdWrapper::Popup(_) = surface_type { let radius_m = theme.radius_m(); iced_runtime::platform_specific::wayland::CornerRadius { top_left: radius_m[0].round() as u32, top_right: radius_m[1].round() as u32, bottom_right: radius_m[2].round() as u32, bottom_left: radius_m[3].round() as u32, } } else if let SurfaceIdWrapper::Window(_) = surface_type && !rounded { let radius_0 = theme.radius_0(); iced_runtime::platform_specific::wayland::CornerRadius { top_left: radius_0[0].round() as u32, top_right: radius_0[1].round() as u32, bottom_right: radius_0[2].round() as u32, bottom_left: radius_0[3].round() as u32, } } else { let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); iced_runtime::platform_specific::wayland::CornerRadius { top_left: radius_s[0].round() as u32, top_right: radius_s[1].round() as u32, bottom_right: radius_s[2].round() as u32, bottom_left: radius_s[3].round() as u32, } }) }