diff --git a/Cargo.toml b/Cargo.toml index 5d22afc..2edab9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ ashpd = { version = "0.12.0", default-features = false, optional = true } async-fs = { version = "2.1", optional = true } async-std = { version = "1.13", optional = true } auto_enums = "0.8.7" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "6254f50", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "633beb0", optional = true } chrono = "0.4.42" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } @@ -229,6 +229,6 @@ libcosmic = { path = "./" } # FIXME update winit deps where necessary to use this # [patch.crates-io] -# [patch."https://github.com/pop-os/winit.git"] -# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" } -# winit = { path = "../../winit" } +[patch."https://github.com/pop-os/winit.git"] +winit = { git = "https://github.com/pop-os/winit.git//", branch = "xdg-toplevel" } +# winit = { path = "../winit" } diff --git a/examples/multi-window/Cargo.toml b/examples/multi-window/Cargo.toml index 7a8e305..168bd4e 100644 --- a/examples/multi-window/Cargo.toml +++ b/examples/multi-window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window", "dbus-config", "wgpu"] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window", "dbus-config", "wgpu", "wayland"] } diff --git a/iced b/iced index d050875..788be2f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d05087507a7a0e37e26f174cfc97629c960b4383 +Subproject commit 788be2f7825b648ec3ce33697c6e675a7b7265ec diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 1733183..9c5a39f 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use std::borrow::Borrow; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; @@ -92,6 +92,7 @@ pub struct Cosmic { Box Fn(&'a App) -> Element<'a, crate::Action>>, ), >, + pub tracked_windows: HashSet, } impl Cosmic @@ -139,11 +140,11 @@ where #[cfg(feature = "wayland")] crate::surface::Action::AppSubsurface(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(); - }; + .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(); + }; if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { 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(); - }; + .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(); + }; if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { 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(); - }; + .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(); + }; if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { iced_winit::commands::subsurface::destroy_subsurface(id) } + #[cfg(feature = "wayland")] + crate::surface::Action::DestroyWindow(id) => iced::window::close(id), crate::surface::Action::ResponsiveMenuBar { menu_bar, limits, @@ -241,11 +244,11 @@ where #[cfg(feature = "wayland")] crate::surface::Action::Popup(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(); - }; + .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(); + }; if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { + 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(); + }; + + 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 AppWindow: {err:?}"); + None + } + } + }) { + let settings = settings(&mut self.app); + self.get_window(id, settings, *view) + } else { + let settings = settings(&mut self.app); + + self.tracked_windows.insert(id); + iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open( + id, settings, channel, + )) + }) + .discard() + } + } + #[cfg(feature = "wayland")] + crate::surface::Action::Window(id, 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(); + }; + + 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 Window: {err:?}"); + None + } + } + }) { + let settings = settings(); + self.get_window(id, settings, Box::new(move |_| view())) + } else { + let settings = settings(); + + self.tracked_windows.insert(id); + + 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(|sm| crate::Action::Cosmic(Action::Surface(sm))) @@ -667,6 +744,42 @@ impl Cosmic { new_theme.theme_type.prefer_dark(prefer_dark); cosmic_theme.set_theme(new_theme.theme_type); + #[cfg(feature = "wayland")] + if self.app.core().sync_window_border_radii_to_theme() { + use iced_runtime::platform_specific::wayland::CornerRadius; + use iced_winit::platform_specific::commands::corner_radius::corner_radius; + + let t = cosmic_theme.cosmic(); + + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_left: radii[2].round() as u32, + bottom_right: radii[3].round() as u32, + }; + + // 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, Some(cur_rad)).discard()]; + // Update radius for each tracked view with the window surface type + for (id, (_, surface_type, _)) in self.surface_views.iter() { + if let SurfaceIdWrapper::Window(_) = surface_type { + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } + } + // Update radius for all tracked windows + for id in self.tracked_windows.iter() { + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } + + return Task::batch(cmds); + } } } @@ -725,9 +838,46 @@ impl Cosmic { 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 { theme: _, .. } = cosmic_theme.theme_type { + if let ThemeType::System { .. } = cosmic_theme.theme_type { cosmic_theme.set_theme(new_theme.theme_type); + #[cfg(feature = "wayland")] + if self.app.core().sync_window_border_radii_to_theme() { + use iced_runtime::platform_specific::wayland::CornerRadius; + use iced_winit::platform_specific::commands::corner_radius::corner_radius; + + let t = cosmic_theme.cosmic(); + + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_left: radii[2].round() as u32, + bottom_right: radii[3].round() as u32, + }; + + // 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, Some(cur_rad)).discard()]; + // Update radius for each tracked view with the window surface type + for (id, (_, surface_type, _)) in self.surface_views.iter() { + if let SurfaceIdWrapper::Window(_) = surface_type { + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } + } + // Update radius for all tracked windows + for id in self.tracked_windows.iter() { + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } + + return Task::batch(cmds); + } } } } @@ -748,6 +898,10 @@ impl Cosmic { Action::Surface(action) => return self.surface_update(action), Action::SurfaceClosed(id) => { + #[cfg(feature = "wayland")] + self.surface_views.remove(&id); + self.tracked_windows.remove(&id); + let mut ret = if let Some(msg) = self.app.on_close_requested(id) { self.app.update(msg) } else { @@ -910,6 +1064,26 @@ impl Cosmic { core.applet.suggested_bounds = b; } Action::Opened(id) => { + self.tracked_windows.insert(id); + #[cfg(feature = "wayland")] + if self.app.core().sync_window_border_radii_to_theme() { + use iced_runtime::platform_specific::wayland::CornerRadius; + use iced_winit::platform_specific::commands::corner_radius::corner_radius; + + let theme = THEME.lock().unwrap(); + let t = theme.cosmic(); + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_left: radii[2].round() as u32, + bottom_right: radii[3].round() as u32, + }; + return Task::batch(vec![ + corner_radius(id, Some(cur_rad)).discard(), + iced_runtime::window::run_with_handle(id, init_windowing_system), + ]); + } return iced_runtime::window::run_with_handle(id, init_windowing_system); } _ => {} @@ -925,6 +1099,7 @@ impl Cosmic { app, #[cfg(feature = "wayland")] surface_views: HashMap::new(), + tracked_windows: HashSet::new(), } } @@ -971,4 +1146,30 @@ impl Cosmic { ); get_popup(settings) } + + #[cfg(feature = "wayland")] + /// Create a window surface + pub fn get_window( + &mut self, + id: iced::window::Id, + settings: iced::window::Settings, + view: Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + >, + ) -> Task> { + use iced_winit::SurfaceIdWrapper; + + self.surface_views.insert( + id.clone(), + ( + None, // TODO parent for window, platform specific option maybe? + SurfaceIdWrapper::Window(id), + view, + ), + ); + iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open(id, settings, channel)) + }) + .discard() + } } diff --git a/src/core.rs b/src/core.rs index c82aa83..6069a83 100644 --- a/src/core.rs +++ b/src/core.rs @@ -97,6 +97,9 @@ pub struct Core { pub(crate) exit_on_main_window_closed: bool, pub(crate) menu_bars: HashMap, + + #[cfg(feature = "wayland")] + pub(crate) sync_window_border_radii_to_theme: bool, } impl Default for Core { @@ -154,6 +157,8 @@ impl Default for Core { main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), + #[cfg(feature = "wayland")] + sync_window_border_radii_to_theme: true } } } @@ -476,4 +481,15 @@ impl Core { crate::command::toggle_maximize(id) } + + // TODO should we emit tasks setting the corner radius or unsetting it if this is changed? + #[cfg(feature = "wayland")] + pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) { + self.sync_window_border_radii_to_theme = sync; + } + + #[cfg(feature = "wayland")] + pub fn sync_window_border_radii_to_theme(&self) -> bool { + self.sync_window_border_radii_to_theme + } } diff --git a/src/surface/action.rs b/src/surface/action.rs index fdf2680..25c45ce 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -5,6 +5,7 @@ use super::Action; #[cfg(feature = "winit")] use crate::Application; +use iced::window; use std::{any::Any, sync::Arc}; /// Used to produce a destroy popup message from within a widget. @@ -20,6 +21,90 @@ pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { Action::DestroySubsurface(id) } +#[cfg(feature = "wayland")] +#[must_use] +pub fn destroy_window(id: iced_core::window::Id) -> Action { + Action::DestroyWindow(id) +} + +#[cfg(all(feature = "wayland", feature = "winit"))] +#[must_use] +pub fn app_window( + settings: impl Fn(&mut App) -> window::Settings + + Send + + Sync + + 'static, + view: Option< + Box< + dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, +) -> (window::Id, Action) { + let id = window::Id::unique(); + + let boxed: Box< + dyn Fn(&mut App) -> window::Settings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + ( + id, + Action::AppWindow( + id, + Arc::new(boxed), + view.map(|view| { + let boxed: Box = Box::new(view); + Arc::new(boxed) + }), + ) + ) +} + +/// Used to create a window message from within a widget. +#[cfg(all(feature = "wayland", feature = "winit"))] +#[must_use] +pub fn simple_window( + settings: impl Fn() -> window::Settings + + Send + + Sync + + 'static, + view: Option< + impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + >, +) -> (window::Id, Action) { + let id = window::Id::unique(); + + let boxed: Box< + dyn Fn() -> window::Settings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + ( + id, + Action::Window( + id, + 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", feature = "winit"))] #[must_use] pub fn app_popup( diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 3041fa5..b4ef63b 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -36,6 +36,22 @@ pub enum Action { ), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), + + /// Create a window with a view function accepting the App as a parameter + AppWindow( + iced::window::Id, + std::sync::Arc>, + Option>>, + ), + /// Create a window with a view function + Window( + iced::window::Id, + std::sync::Arc>, + Option>>, + ), + /// Destroy a window + DestroyWindow(iced::window::Id), + /// Responsive menu bar update ResponsiveMenuBar { /// Id of the menu bar @@ -80,6 +96,15 @@ impl std::fmt::Debug for Action { .field("size", size) .finish(), Self::Ignore => write!(f, "Ignore"), + Self::AppWindow(id, arg0, arg1) => { + f.debug_tuple("AppWindow").field(id).field(arg0).field(arg1).finish() + } + Self::Window(id, arg0, arg1) => { + f.debug_tuple("Window").field(id).field(arg0).field(arg1).finish() + } + Self::DestroyWindow(arg0) => { + f.debug_tuple("DestroyWindow").field(arg0).finish() + } Self::Task(_) => f.debug_tuple("Future").finish(), } }