diff --git a/Cargo.toml b/Cargo.toml index 23ceccb3..07b73c19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,7 +176,6 @@ zbus = { workspace = true, optional = true } float-cmp = "0.10.0" ron = { workspace = true, optional = true } enumflags2 = "0.7.12" -roxmltree = "0.20" # Enable DBus feature on Linux targets [target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_vendor = "apple"))))'.dependencies] diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index 1ca2a3f6..b49cf447 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -1,5 +1,6 @@ use proc_macro::TokenStream; use quote::quote; +use syn; #[proc_macro_derive(CosmicConfigEntry, attributes(version, id, cosmic_config_entry))] pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { @@ -99,18 +100,16 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { Ok(value) => { default.#field_name = value.into(); } - Err(why) => if why.is_err() { - errors.push(why); - } + Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), + Err(e) => errors.push(e), } } } else { quote! { match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { Ok(#field_name) => default.#field_name = #field_name, - Err(why) => if why.is_err() { - errors.push(why); - } + Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), + Err(e) => errors.push(e), } } } diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 71f3d2d2..e51b4f6a 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -4,7 +4,7 @@ use notify::event::{EventKind, ModifyKind, RenameMode}; use notify::{RecommendedWatcher, Watcher}; use serde::Serialize; use serde::de::DeserializeOwned; -use std::io::{self, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::{env, fmt, fs}; @@ -417,11 +417,10 @@ impl ConfigGet for Config { match self.key_path(key) { Ok(key_path) if key_path.is_file() => { // Load user override - match fs::read_to_string(key_path) { - Ok(data) => Ok(ron::from_str(&data)?), - Err(why) if why.kind() == io::ErrorKind::NotFound => Err(Error::NotFound), - Err(why) => Err(Error::GetKey(key.to_string(), why)), - } + let data = fs::read_to_string(key_path) + .map_err(|err| Error::GetKey(key.to_string(), err))?; + + Ok(ron::from_str(&data)?) } _ => { @@ -437,11 +436,9 @@ impl ConfigGet for Config { fn get_system_default(&self, key: &str) -> Result { // Load system default let default_path = self.default_path(key)?; - match fs::read_to_string(default_path) { - Ok(data) => Ok(ron::from_str(&data)?), - Err(why) if why.kind() == io::ErrorKind::NotFound => Err(Error::NotFound), - Err(why) => Err(Error::GetKey(key.to_string(), why)), - } + let data = + fs::read_to_string(default_path).map_err(|err| Error::GetKey(key.to_string(), err))?; + Ok(ron::from_str(&data)?) } } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 124b432a..f0387877 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -1,5 +1,5 @@ -use iced_futures::futures::SinkExt; use iced_futures::futures::channel::mpsc; +use iced_futures::futures::{SinkExt, Stream}; use iced_futures::stream; use notify::RecommendedWatcher; use std::borrow::Cow; diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 43e4ec27..6fce9b89 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -756,7 +756,6 @@ impl Theme { } /// Get the active theme based on the current theme mode. - #[allow(clippy::result_large_err)] pub fn get_active() -> Result, Self)> { (|| { (if ThemeMode::is_dark(&Config::new(Self::id(), Self::VERSION)?)? { @@ -769,6 +768,7 @@ impl Theme { .and_then(|theme_config| Self::get_entry(&theme_config)) } + #[must_use] /// Rebuild the current theme with the provided accent pub fn with_accent(&self, c: Srgba) -> Self { let mut oklcha: Oklcha = c.into_color(); @@ -1103,7 +1103,7 @@ impl ThemeBuilder { let container_alpha = alpha_map.blurred_alpha(frosted); let actual_alpha = - if frosted_windows || frosted_system_interface || frosted_panel || frosted_applets { + if (frosted_windows || frosted_system_interface || frosted_panel || frosted_applets) { container_alpha } else { 1.0 @@ -1173,14 +1173,9 @@ impl ThemeBuilder { control_steps_array[0] }; component_hovered_overlay.alpha = 0.1; - let mut background_pressed_overlay = component_hovered_overlay; - background_pressed_overlay.alpha = 0.2; - #[allow(unused_assignments)] + let mut component_pressed_overlay = component_hovered_overlay; - #[allow(unused_assignments)] - { - component_pressed_overlay.alpha = 0.2; - } + component_pressed_overlay.alpha = 0.2; // Standard button background is neutral 7 with 25% opacity let button_bg = control_steps_array[7].with_alpha(0.25); @@ -1215,7 +1210,7 @@ impl ThemeBuilder { ); let primary = { - let container_bg = if let Some(primary_container_bg_color) = primary_container_bg { + let mut container_bg = if let Some(primary_container_bg_color) = primary_container_bg { primary_container_bg_color } else { get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) @@ -1233,7 +1228,7 @@ impl ThemeBuilder { }; component_hovered_overlay.alpha = 0.1; - let mut component_pressed_overlay = component_hovered_overlay; + component_pressed_overlay = component_hovered_overlay; component_pressed_overlay.alpha = 0.2; Container::new( @@ -1368,7 +1363,7 @@ impl ThemeBuilder { accent, on_bg_component, component_hovered_overlay, - background_pressed_overlay, + component_pressed_overlay, is_high_contrast, control_steps_array[8], ), @@ -1664,7 +1659,7 @@ impl ThemeBuilder { /// but this represents the strength of the blur effect. #[allow(missing_docs)] #[repr(u8)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum BlurStrength { ExtremelyLow, ExtremelyLow2, @@ -1672,7 +1667,6 @@ pub enum BlurStrength { VeryLow2, Low, Low2, - #[default] Medium, Medium2, High, @@ -1683,6 +1677,12 @@ pub enum BlurStrength { ExtremelyHigh2, } +impl Default for BlurStrength { + fn default() -> Self { + Self::Medium + } +} + impl TryFrom for BlurStrength { type Error = (); @@ -1707,7 +1707,6 @@ impl TryFrom for BlurStrength { } } -#[allow(missing_docs)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct AlphaMap { pub extremely_low: f32, @@ -1726,7 +1725,6 @@ pub struct AlphaMap { pub extremely_high_2: f32, } -#[allow(missing_docs)] impl AlphaMap { pub fn blurred_alpha(&self, blur: BlurStrength) -> f32 { match blur { diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index 8fed090e..bbb4f246 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -321,15 +321,17 @@ impl Theme { return Ok(None); } - if let Ok(metadata) = fs::symlink_metadata(path) - && metadata.file_type().is_symlink() - && let Ok(actual_cosmic_css) = fs::read_link(path) - { - let canonical_target = fs::canonicalize(&actual_cosmic_css)?; - let canonical_base = fs::canonicalize(cosmic_css)?; - return Ok(Some( - canonical_target == canonical_base || canonical_target.starts_with(&canonical_base), - )); + if let Ok(metadata) = fs::symlink_metadata(path) { + if metadata.file_type().is_symlink() { + if let Ok(actual_cosmic_css) = fs::read_link(path) { + let canonical_target = fs::canonicalize(&actual_cosmic_css)?; + let canonical_base = fs::canonicalize(cosmic_css)?; + return Ok(Some( + canonical_target == canonical_base + || canonical_target.starts_with(&canonical_base), + )); + } + } } Ok(Some(false)) } diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index a4c0b54b..16df211f 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -111,6 +111,7 @@ inactive_colors={} qt6ct_res?; Ok(()) } + #[must_use] #[cold] fn write_ct(&self, ct: &str, qpalette: &str) -> Result<(), OutputError> { let file_path = Self::get_qpalette_path(ct, self.is_dark)?; @@ -138,6 +139,7 @@ inactive_colors={} qt6ct_res?; Ok(()) } + #[must_use] #[cold] fn apply_ct(ct: &str, is_dark: bool) -> Result<(), OutputError> { let path = Self::get_conf_path(ct)?; @@ -211,6 +213,7 @@ inactive_colors={} qt6ct_res?; Ok(()) } + #[must_use] #[cold] fn reset_ct(ct: &str) -> Result<(), OutputError> { let path = Self::get_conf_path(ct)?; @@ -246,7 +249,7 @@ inactive_colors={} let Some(mut config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; - config_dir.push(ct); + config_dir.push(&ct); if !config_dir.exists() { fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; } @@ -269,7 +272,7 @@ inactive_colors={} let Some(mut config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; - config_dir.push(ct); + config_dir.push(&ct); config_dir.push("colors"); if !config_dir.exists() { fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; diff --git a/cosmic-theme/src/output/vs_code.rs b/cosmic-theme/src/output/vs_code.rs index b57b9003..f49c888f 100644 --- a/cosmic-theme/src/output/vs_code.rs +++ b/cosmic-theme/src/output/vs_code.rs @@ -275,7 +275,6 @@ impl Theme { /// Returns an `OutputError` if the user config dir is missing, the /// settings file cannot be read/written, or its JSON is invalid. #[cold] - /// Applies the theme to VS Code's user settings. pub fn apply_vs_code(self) -> Result<(), OutputError> { let vs_theme = VsTheme::from(self); let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; @@ -308,7 +307,6 @@ impl Theme { /// Returns an `OutputError` if the user config dir is missing, the /// settings file cannot be read/written, or its JSON is invalid. #[cold] - /// Removes COSMIC theme settings from VS Code's user settings. pub fn reset_vs_code() -> Result<(), OutputError> { let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; config_dir.extend(["Code", "User", "settings.json"]); diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index e1163f1b..f77bf512 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -35,7 +35,7 @@ pub enum Message { PopupClosed(Id), ToggleExampleRow(bool), Selected(usize), - Surface(cosmic::surface::Action), + Surface(cosmic::surface::Action), Toggle(bool), } @@ -76,7 +76,9 @@ impl cosmic::Application for Window { self.example_row = toggled; } Message::Surface(a) => { - return cosmic::task::message(cosmic::Action::Surface(a)); + return cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(a), + )); } Message::Selected(i) => { self.selected = Some(i); diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 18ed32a4..af4200c9 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -98,7 +98,7 @@ pub enum Message { Input2(String), Ignore, ToggleHide, - Surface(cosmic::surface::Action), + Surface(cosmic::surface::Action), Hi, Hi2, Hi3, @@ -255,7 +255,9 @@ impl cosmic::Application for App { self.hidden = !self.hidden; } Message::Surface(a) => { - return cosmic::task::message(cosmic::Action::Surface(a)); + return cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(a), + )); } Message::Hi => { dbg!("hi"); diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index e10bb48c..50c76bfb 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -28,7 +28,7 @@ fn main() -> Result<(), Box> { pub enum Message { Clicked, WindowClose, - Surface(cosmic::surface::Action), + Surface(cosmic::surface::Action), ToggleHideContent, ToggleSomeAction, WindowNew, @@ -89,7 +89,9 @@ impl cosmic::Application for App { self.button_label = format!("Clicked {message:?}"); } Message::Surface(action) => { - return cosmic::task::message(cosmic::Action::Surface(action)); + return cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(action), + )); } Message::WindowClose | Message::ToggleHideContent diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 3a12aaa5..b4b5343f 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -34,7 +34,7 @@ pub enum Message { OpenError(Arc), OpenFile, Selected(Url), - Surface(cosmic::surface::Action), + Surface(cosmic::surface::Action), } /// The [`App`] stores application-specific state. @@ -187,7 +187,9 @@ impl cosmic::Application for App { self.error_status = None; } Message::Surface(action) => { - return cosmic::task::message(cosmic::Action::Surface(action)); + return cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(action), + )); } } diff --git a/i18n/ca/libcosmic.ftl b/i18n/ca/libcosmic.ftl new file mode 100644 index 00000000..0fbc5ca5 --- /dev/null +++ b/i18n/ca/libcosmic.ftl @@ -0,0 +1,3 @@ +license = Llicència +close = Tancar +links = Enllaços diff --git a/i18n/ca/libcosmic_yoda.ftl b/i18n/ca/libcosmic_yoda.ftl index 0fbc5ca5..e69de29b 100644 --- a/i18n/ca/libcosmic_yoda.ftl +++ b/i18n/ca/libcosmic_yoda.ftl @@ -1,3 +0,0 @@ -license = Llicència -close = Tancar -links = Enllaços diff --git a/i18n/gl/libcosmic_yoda.ftl b/i18n/gl/libcosmic.ftl similarity index 100% rename from i18n/gl/libcosmic_yoda.ftl rename to i18n/gl/libcosmic.ftl diff --git a/i18n/fil/libcosmic_yoda.ftl b/i18n/lo/libcosmic.ftl similarity index 100% rename from i18n/fil/libcosmic_yoda.ftl rename to i18n/lo/libcosmic.ftl diff --git a/i18n/oc/libcosmic.ftl b/i18n/oc/libcosmic.ftl new file mode 100644 index 00000000..e1d98649 --- /dev/null +++ b/i18n/oc/libcosmic.ftl @@ -0,0 +1,14 @@ +monday = Diluns +mon = Dil +tuesday = Dimars +tue = Dim +wednesday = Dimècres +wed = Dma +thursday = Dijòus +thu = Dij +friday = Divendres +fri = Div +saturday = Dissabte +sat = Dis +sunday = Dimenge +sun = Dim diff --git a/i18n/oc/libcosmic_yoda.ftl b/i18n/oc/libcosmic_yoda.ftl index e1d98649..e69de29b 100644 --- a/i18n/oc/libcosmic_yoda.ftl +++ b/i18n/oc/libcosmic_yoda.ftl @@ -1,14 +0,0 @@ -monday = Diluns -mon = Dil -tuesday = Dimars -tue = Dim -wednesday = Dimècres -wed = Dma -thursday = Dijòus -thu = Dij -friday = Divendres -fri = Div -saturday = Dissabte -sat = Dis -sunday = Dimenge -sun = Dim diff --git a/i18n/lo/libcosmic_yoda.ftl b/i18n/sat/libcosmic.ftl similarity index 100% rename from i18n/lo/libcosmic_yoda.ftl rename to i18n/sat/libcosmic.ftl diff --git a/i18n/sat/libcosmic_yoda.ftl b/i18n/sat/libcosmic_yoda.ftl deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/th/libcosmic_yoda.ftl b/i18n/th/libcosmic_yoda.ftl index a94a2b12..e69de29b 100644 --- a/i18n/th/libcosmic_yoda.ftl +++ b/i18n/th/libcosmic_yoda.ftl @@ -1 +0,0 @@ -close = ปิด diff --git a/iced b/iced index 24da1dc0..d12bb8e0 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 24da1dc0e4e5c1b7f803bf8584d10939fe5acdec +Subproject commit d12bb8e056712ab87efbdd4296261331772ec413 diff --git a/src/action.rs b/src/action.rs index a3d583a7..19e228b8 100644 --- a/src/action.rs +++ b/src/action.rs @@ -16,11 +16,6 @@ pub const fn none() -> Action { Action::None } -/// Wrap a surface action, typically produced by a widget, to be handled by libcosmic. -pub const fn surface(action: crate::surface::Action) -> Action { - Action::Surface(action) -} - #[derive(Clone, Debug)] #[must_use] pub enum Action { @@ -31,45 +26,10 @@ pub enum Action { #[cfg(feature = "single-instance")] /// Dbus activation messages DbusActivation(dbus_activation::Message), - /// Surface (popup, subsurface, window, layer shell) requests, handled by libcosmic. - Surface(crate::surface::Action), /// Do nothing None, } -impl Action { - /// Map the application message inside, leaving libcosmic's own variants untouched. - #[must_use] - pub fn map(self, f: impl Fn(M) -> N + Clone + Send + Sync + 'static) -> Action { - match self { - Action::App(message) => Action::App(f(message)), - #[cfg(feature = "winit")] - Action::Cosmic(action) => Action::Cosmic(action), - #[cfg(feature = "single-instance")] - Action::DbusActivation(message) => Action::DbusActivation(message), - Action::Surface(action) => Action::Surface(action.map(f)), - Action::None => Action::None, - } - } -} - -impl Action> { - /// Collapse a doubly wrapped action, as produced by widgets whose message type is already - /// an [`Action`], into a single one. - #[must_use] - pub fn flatten(self) -> Action { - match self { - Action::App(action) => action, - #[cfg(feature = "winit")] - Action::Cosmic(action) => Action::Cosmic(action), - #[cfg(feature = "single-instance")] - Action::DbusActivation(message) => Action::DbusActivation(message), - Action::Surface(action) => Action::Surface(action.flatten()), - Action::None => Action::None, - } - } -} - impl From for Action { fn from(value: M) -> Self { Self::App(value) diff --git a/src/app/action.rs b/src/app/action.rs index bf240dda..873622da 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -45,6 +45,8 @@ pub enum Action { /// Tracks updates to window suggested size. #[cfg(feature = "applet")] SuggestedBounds(Option), + /// Internal surface message + Surface(surface::Action), /// Notifies that a surface was closed. /// Any data relating to the surface should be cleaned up. SurfaceClosed(iced::window::Id), diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 5dfe909a..90f6c1a7 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -153,7 +153,7 @@ where #[allow(clippy::too_many_lines)] pub fn surface_update( &mut self, - _surface_message: crate::surface::Action, + _surface_message: crate::surface::Action, ) -> iced::Task> { #[cfg(feature = "surface-message")] match _surface_message { @@ -194,7 +194,18 @@ where }; let settings = settings(); - if let Some(view) = view { + 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 subsurface view: {err:?}"); + + None + } + } + }) { self.get_subsurface(settings, Some(Box::new(move |_| view()))) } else { self.get_subsurface(settings, None) @@ -290,7 +301,17 @@ where let settings = settings(); let live_settings = Box::new(move |_: &T| live_settings()); - if let Some(view) = view { + 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 subsurface view: {err:?}"); + None + } + } + }) { self.get_popup(settings, live_settings, Some(Box::new(move |_| view()))) } else { self.get_popup(settings, live_settings, None) @@ -356,7 +377,17 @@ where return Task::none(); }; - if let Some(view) = view { + 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( @@ -378,7 +409,9 @@ where } crate::surface::Action::Ignore => iced::Task::none(), - crate::surface::Action::Task(f) => f().map(crate::Action::Surface), + crate::surface::Action::Task(f) => { + f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) + } #[cfg(wayland_platform)] crate::surface::Action::AppLayerShell(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) @@ -441,7 +474,17 @@ where let live_settings = live_settings(); let live_settings = Box::new(move |_app: &T| live_settings); - if let Some(view) = view { + 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 + } + } + }) { self.get_layer_shell(settings, live_settings, Some(Box::new(move |_| view()))) } else { self.get_layer_shell(settings, live_settings, None) @@ -451,33 +494,6 @@ where crate::surface::Action::DestroyLayerShell(id) => { iced_winit::commands::layer_surface::destroy_layer_surface(id) } - #[cfg(wayland_platform)] - crate::surface::Action::Lock(id, output, live_settings, view) => { - 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 live_settings = live_settings(); - let live_settings = Box::new(move |_app: &T| live_settings); - - if let Some(view) = view { - self.get_lock(id, output, live_settings, Some(Box::new(move |_| view()))) - } else { - self.get_lock(id, output, live_settings, None) - } - } - #[cfg(wayland_platform)] - crate::surface::Action::DestroyLock(id) => { - iced_winit::commands::session_lock::destroy_lock_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); @@ -499,7 +515,6 @@ where 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) => { @@ -589,13 +604,9 @@ where #[cfg(wayland_platform)] iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { match event { - wayland::Event::Popup(wayland::PopupEvent::Done, _, popup) => { - if popup == id { - return Some(Action::SurfaceClosed(popup)); - } - } - wayland::Event::Layer(wayland::LayerEvent::Done, _, layer) => { - return Some(Action::SurfaceClosed(layer)); + 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( @@ -1216,6 +1227,8 @@ impl Cosmic { } } + Action::Surface(action) => return self.surface_update(action), + Action::SurfaceClosed(id) => { if self.opened_surfaces.get_mut(&id).is_some_and(|v| { *v = v.saturating_sub(1); @@ -1672,7 +1685,7 @@ impl Cosmic { view, ), ); - live_settings_task.chain(get_subsurface(settings)) + Task::batch([live_settings_task, get_subsurface(settings)]) } #[cfg(wayland_platform)] @@ -1728,13 +1741,16 @@ impl Cosmic { view, ), ); - - // We don't control window creation in the same way - iced_runtime::task::oneshot(|channel| { - iced_runtime::Action::Window(iced_runtime::window::Action::Open(id, settings, channel)) - }) - .discard() - .chain(live_settings_task) + 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)] @@ -1762,35 +1778,7 @@ impl Cosmic { view, ), ); - live_settings_task.chain(get_layer_surface(settings)) - } - - #[cfg(wayland_platform)] - pub fn get_lock( - &mut self, - id: window::Id, - output: cctk::wayland_client::protocol::wl_output::WlOutput, - 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::session_lock::get_lock_surface; - *self.opened_surfaces.entry(id).or_insert(0) += 1; - let live_settings_task = - self.apply_live_settings(SurfaceIdWrapper::SessionLock(id), &live_settings(&self.app)); - self.surface_views.insert( - id, - ( - None, // TODO parent for layer shell, platform specific option maybe? - SurfaceIdWrapper::SessionLock(id), - live_settings, - view, - ), - ); - - live_settings_task.chain(get_lock_surface(id, output)) + Task::batch([live_settings_task, get_layer_surface(settings)]) } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 91da0a16..ded57543 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -394,7 +394,7 @@ where { nav = nav .window_id_maybe(self.core().main_window_id()) - .on_surface_action(|action| crate::Action::Surface(action.flatten())) + .on_surface_action(|m| crate::Action::Cosmic(crate::app::Action::Surface(m))) } let mut nav = nav .into_container() diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 555ec837..37635bf4 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -295,7 +295,7 @@ impl Context { content: impl Into>, tooltip: impl Into>, has_popup: bool, - on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static, + on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static, parent_id: Option, ) -> crate::widget::wayland::tooltip::widget::Tooltip<'a, Message, Message> { let window_id = *TOOLTIP_WINDOW_ID; diff --git a/src/surface/action.rs b/src/surface/action.rs index c2b9ed82..b2864d52 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -1,40 +1,39 @@ // Copyright 2025 System76 // SPDX-License-Identifier: MPL-2.0 -use super::{Action, View}; +use super::Action; use crate::Application; -use iced::window; +use iced::{Rectangle, window}; #[cfg(all(wayland_platform, feature = "winit"))] use iced_runtime::platform_specific::wayland::CornerRadius; #[cfg(wayland_platform)] use iced_runtime::platform_specific::wayland::layer_surface::IcedMargin; use std::any::Any; - use std::sync::Arc; /// Used to produce a destroy popup message from within a widget. #[cfg(wayland_platform)] #[must_use] -pub fn destroy_popup(id: iced_core::window::Id) -> Action { +pub fn destroy_popup(id: iced_core::window::Id) -> Action { Action::DestroyPopup(id) } #[cfg(wayland_platform)] #[must_use] -pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { +pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { Action::DestroySubsurface(id) } #[cfg(wayland_platform)] #[must_use] -pub fn destroy_window(id: iced_core::window::Id) -> Action { +pub fn destroy_window(id: iced_core::window::Id) -> Action { Action::DestroyWindow(id) } #[cfg(wayland_platform)] #[must_use] -pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { +pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { Action::DestroyLayerShell(id) } @@ -66,7 +65,7 @@ pub fn app_window( live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, view: BoxedView, -) -> (window::Id, Action) { +) -> (window::Id, Action) { let id = window::Id::unique(); let boxed: Box window::Settings + Send + Sync + 'static> = @@ -100,7 +99,7 @@ pub fn simple_window( view: Option< impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, >, -) -> (window::Id, Action) { +) -> (window::Id, Action) { let id = window::Id::unique(); let boxed: Box window::Settings + Send + Sync + 'static> = Box::new(settings); @@ -115,7 +114,16 @@ pub fn simple_window( id, Arc::new(boxed), Arc::new(boxed_live), - view.map(|view| Arc::new(view) as View), + 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) + }), ), ) } @@ -129,7 +137,7 @@ pub fn app_popup( + Sync + 'static, view: BoxedView, -) -> Action { +) -> Action { let boxed: Box< dyn Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send @@ -163,7 +171,7 @@ pub fn simple_subsurface( view: Option< Box crate::Element<'static, crate::Action> + Send + Sync + 'static>, >, -) -> Action { +) -> Action { let boxed: Box< dyn Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send @@ -175,7 +183,10 @@ pub fn simple_subsurface( Action::Subsurface( Arc::new(boxed), Arc::new(Box::new(LiveSettings::default)), - view.map(|view| Arc::from(view) as View), + view.map(|view| { + let boxed: Box = Box::new(view); + Arc::new(boxed) + }), ) } @@ -191,7 +202,7 @@ pub fn simple_popup( view: Option< impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, >, -) -> Action { +) -> Action { let boxed: Box< dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send @@ -206,7 +217,13 @@ pub fn simple_popup( Action::Popup( Arc::new(boxed), Arc::new(boxed_live), - view.map(|view| Arc::new(view) as View), + 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) + }), ) } @@ -222,7 +239,7 @@ pub fn subsurface( + 'static, // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. view: BoxedView, -) -> Action { +) -> Action { let boxed: Box< dyn Fn( &mut App, @@ -256,7 +273,7 @@ pub fn simple_layer_shell( view: Option< impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, >, -) -> Action { +) -> Action { let boxed: Box< dyn Fn() -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings @@ -270,27 +287,13 @@ pub fn simple_layer_shell( Action::LayerShell( Arc::new(boxed), Arc::new(boxed_live), - view.map(|view| Arc::new(view) as View), - ) -} - -#[cfg(wayland_platform)] -#[must_use] -pub fn lock( - live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, - id: window::Id, - output: cctk::wayland_client::protocol::wl_output::WlOutput, - view: Option< - impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, - >, -) -> Action { - let boxed_live: Box LiveSettings + Send + Sync + 'static> = Box::new(live_settings); - let boxed_live: Box = Box::new(boxed_live); - Action::Lock( - id, - output, - Arc::new(boxed_live), - view.map(|view| Arc::new(view) as View), + 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) + }), ) } @@ -307,7 +310,7 @@ pub fn app_layer_shell( + 'static, // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. view: BoxedView, -) -> Action { +) -> Action { let boxed: Box< dyn Fn( &mut App, diff --git a/src/surface/mod.rs b/src/surface/mod.rs index ee3b231f..7cb2476c 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -11,28 +11,19 @@ use std::sync::Arc; type BoxedSetting = Arc>; -/// Produces the content of a surface created from within a widget. -/// -/// Typed on the message the widget publishes. -pub type View = - Arc crate::Element<'static, crate::Action> + Send + Sync + 'static>; - /// Ignore this message in your application. It will be intercepted. -/// -/// `M` is the message type of whoever created the action. The ones prefixed with `App` take the -/// application itself and are type-erased, the others carry a [`View`] typed on `M`. #[derive(Clone)] -pub enum Action { +pub enum Action { /// Create a subsurface with a view function accepting the App as a parameter AppSubsurface(BoxedSetting, BoxedSetting, Option), /// Create a subsurface with a view function - Subsurface(BoxedSetting, BoxedSetting, Option>), + Subsurface(BoxedSetting, BoxedSetting, Option), /// Destroy a subsurface with a view function DestroySubsurface(iced::window::Id), /// Create a popup with a view function accepting the App as a parameter AppPopup(BoxedSetting, BoxedSetting, Option), /// Create a popup - Popup(BoxedSetting, BoxedSetting, Option>), + Popup(BoxedSetting, BoxedSetting, Option), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), /// Destroys the global tooltip popup subsurface @@ -50,7 +41,7 @@ pub enum Action { iced::window::Id, BoxedSetting, BoxedSetting, - Option>, + Option, ), /// Destroy a window DestroyWindow(iced::window::Id), @@ -59,23 +50,11 @@ pub enum Action { AppLayerShell(BoxedSetting, BoxedSetting, Option), /// Create a layer shell surface with a view function - LayerShell(BoxedSetting, BoxedSetting, Option>), + LayerShell(BoxedSetting, BoxedSetting, Option), /// Destroy a layer shell surface DestroyLayerShell(iced::window::Id), - #[cfg(wayland_platform)] - /// Create a lock shell surface with a view function - Lock( - iced::window::Id, - cctk::wayland_client::protocol::wl_output::WlOutput, - BoxedSetting, - Option>, - ), - - /// Destroy a lock surface - DestroyLock(iced::window::Id), - /// Responsive menu bar update ResponsiveMenuBar { /// Id of the menu bar @@ -87,76 +66,14 @@ pub enum Action { }, Ignore, SyncLiveSettings(iced::window::Id), - Task(Arc Task> + Send + Sync>), + Task(Arc Task + Send + Sync>), } -impl Action { - /// Re-type the action for a component whose messages are wrapped by `f`. - /// - /// Similar to [`iced::Element::map`]. A component that maps a widget's messages must - /// map the widget's surface actions too. - #[must_use] - pub fn map(self, f: impl Fn(M) -> N + Clone + Send + Sync + 'static) -> Action { - self.map_actions(move |action| action.map(f.clone())) - } - - fn map_actions( - self, - g: impl Fn(crate::Action) -> crate::Action + Clone + Send + Sync + 'static, - ) -> Action { - let map_view = |view: Option>| -> Option> { - let view = view?; - let g = g.clone(); - Some(Arc::new(move || view().map(g.clone()))) - }; - match self { - Action::AppSubsurface(a, b, c) => Action::AppSubsurface(a, b, c), - Action::Subsurface(a, b, view) => Action::Subsurface(a, b, map_view(view)), - Action::DestroySubsurface(id) => Action::DestroySubsurface(id), - Action::AppPopup(a, b, c) => Action::AppPopup(a, b, c), - Action::Popup(a, b, view) => Action::Popup(a, b, map_view(view)), - Action::DestroyPopup(id) => Action::DestroyPopup(id), - Action::DestroyTooltipPopup => Action::DestroyTooltipPopup, - Action::AppWindow(id, a, b, c) => Action::AppWindow(id, a, b, c), - Action::Window(id, a, b, view) => Action::Window(id, a, b, map_view(view)), - Action::DestroyWindow(id) => Action::DestroyWindow(id), - Action::AppLayerShell(a, b, c) => Action::AppLayerShell(a, b, c), - Action::LayerShell(a, b, view) => Action::LayerShell(a, b, map_view(view)), - Action::DestroyLayerShell(id) => Action::DestroyLayerShell(id), - #[cfg(wayland_platform)] - Action::Lock(id, output, a, view) => Action::Lock(id, output, a, map_view(view)), - Action::DestroyLock(id) => Action::DestroyLock(id), - Action::ResponsiveMenuBar { - menu_bar, - limits, - size, - } => Action::ResponsiveMenuBar { - menu_bar, - limits, - size, - }, - Action::Ignore => Action::Ignore, - Action::SyncLiveSettings(id) => Action::SyncLiveSettings(id), - Action::Task(task) => Action::Task(Arc::new(move || { - let g = g.clone(); - task().map(move |action| action.map_actions(g.clone())) - })), - } - } +pub fn surface_task(action: Action) -> Task> { + crate::task::message(crate::Action::Cosmic(crate::app::Action::Surface(action))) } -impl Action> { - #[must_use] - pub fn flatten(self) -> Action { - self.map_actions(crate::Action::flatten) - } -} - -pub fn surface_task(action: Action) -> Task> { - crate::task::message(crate::Action::Surface(action)) -} - -impl std::fmt::Debug for Action { +impl std::fmt::Debug for Action { #[cold] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -166,11 +83,11 @@ impl std::fmt::Debug for Action { .field(arg1) .field(arg2) .finish(), - Self::Subsurface(arg0, arg1, view) => f + Self::Subsurface(arg0, arg1, arg2) => f .debug_tuple("Subsurface") .field(arg0) .field(arg1) - .field(&view.as_ref().map(|_| "view")) + .field(arg2) .finish(), Self::DestroySubsurface(arg0) => { f.debug_tuple("DestroySubsurface").field(arg0).finish() @@ -181,11 +98,11 @@ impl std::fmt::Debug for Action { .field(arg1) .field(arg2) .finish(), - Self::Popup(arg0, arg1, view) => f + Self::Popup(arg0, arg1, arg2) => f .debug_tuple("Popup") .field(arg0) .field(arg1) - .field(&view.as_ref().map(|_| "view")) + .field(arg2) .finish(), Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(), @@ -207,12 +124,12 @@ impl std::fmt::Debug for Action { .field(arg1) .field(arg2) .finish(), - Self::Window(id, arg0, arg1, view) => f + Self::Window(id, arg0, arg1, arg2) => f .debug_tuple("Window") .field(id) .field(arg0) .field(arg1) - .field(&view.as_ref().map(|_| "view")) + .field(arg2) .finish(), Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(), Self::Task(_) => f.debug_tuple("Future").finish(), @@ -222,24 +139,15 @@ impl std::fmt::Debug for Action { .field(arg1) .field(arg2) .finish(), - Self::LayerShell(arg0, arg1, view) => f + Self::LayerShell(arg, arg1, arg2) => f .debug_tuple("LayerShell") - .field(arg0) + .field(arg) .field(arg1) - .field(&view.as_ref().map(|_| "view")) + .field(arg2) .finish(), Self::DestroyLayerShell(arg0) => { f.debug_tuple("DestroyLayerShell").field(arg0).finish() } - #[cfg(wayland_platform)] - Self::Lock(id, output, arg0, view) => f - .debug_tuple("Lock") - .field(id) - .field(output) - .field(arg0) - .field(&view.as_ref().map(|_| "view")) - .finish(), - Self::DestroyLock(arg0) => f.debug_tuple("DestroyLock").field(arg0).finish(), Self::SyncLiveSettings(arg0) => f.debug_tuple("SyncLiveSettings").field(arg0).finish(), } } diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index f361512d..76798104 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -25,7 +25,6 @@ pub enum Button { IconVertical, Image, Link, - LinkActive, ListItem([f32; 4]), MenuFolder, MenuItem, @@ -122,13 +121,6 @@ pub fn appearance( corner_radii = &cosmic.corner_radii.radius_0; } - Button::LinkActive => { - appearance.background = Some(Background::Color(cosmic.text_button.hover.into())); - appearance.icon_color = Some(cosmic.accent_text_color().into()); - appearance.text_color = Some(cosmic.accent_text_color().into()); - corner_radii = &cosmic.corner_radii.radius_xs; - } - Button::Custom { .. } => (), Button::AppletMenu => { let (background, _, _) = color(&cosmic.text_button); diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index ffaea2a6..178b5b37 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -1368,22 +1368,18 @@ impl iced_widget::text::Catalog for Theme { fn style(&self, class: &Self::Class<'_>) -> iced_widget::text::Style { let selected_fill = self.cosmic().accent.base.into(); - let selected_text_color = Some(self.cosmic().on_accent_color().into()); match class { Text::Accent => iced_widget::text::Style { color: Some(self.cosmic().accent_text_color().into()), selected_fill, - selected_text_color, }, Text::Default => iced_widget::text::Style { color: None, selected_fill, - selected_text_color, }, Text::Color(c) => iced_widget::text::Style { color: Some(*c), selected_fill, - selected_text_color, }, Text::Custom(f) => f(self), } diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index a57babf3..15c78ef8 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -17,8 +17,6 @@ use iced_core::{Length, Point, Size, mouse, touch}; use std::collections::HashSet; use std::sync::Arc; -use crate::widget::RcWrapper; - /// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. pub fn context_menu<'a, Message: 'static + Clone>( content: impl Into>, @@ -35,9 +33,6 @@ pub fn context_menu<'a, Message: 'static + Clone>( }), close_on_escape: true, window_id: window::Id::RESERVED, - item_width: ItemWidth::Uniform(240), - on_open: None, - on_close: None, on_surface_action: None, }; @@ -58,37 +53,12 @@ pub struct ContextMenu<'a, Message> { context_menu: Option>>, pub window_id: window::Id, pub close_on_escape: bool, - /// Width of each menu item, and therefore of the menu. - pub item_width: ItemWidth, - /// Emitted when the menu opens, so the application can mark what was right-clicked. - #[setters(strip_option)] - pub on_open: Option, - /// Emitted when the menu closes by any path, including the compositor dismissing it. - #[setters(strip_option)] - pub on_close: Option, #[setters(skip)] pub(crate) on_surface_action: - Option) -> Message + Send + Sync + 'static>>, + Option Message + Send + Sync + 'static>>, } impl ContextMenu<'_, Message> { - /// Publish `on_open`/`on_close` when the open state changed since the last report. - fn report_open_state( - &self, - state: &mut LocalState, - shell: &mut iced_core::Shell<'_, Message>, - ) { - let open = state.menu_bar_state.inner.with_data(|d| d.open); - if open == state.reported_open { - return; - } - state.reported_open = open; - let message = if open { &self.on_open } else { &self.on_close }; - if let Some(message) = message.clone() { - shell.publish(message); - } - } - #[cfg(wayland_platform)] #[allow(clippy::too_many_lines)] fn create_popup( @@ -98,7 +68,7 @@ impl ContextMenu<'_, Message> { renderer: &crate::Renderer, shell: &mut iced_core::Shell<'_, Message>, viewport: &iced::Rectangle, - my_state: &mut LocalState, + my_state: &mut LocalState, ) { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { use crate::surface::action::{LiveSettings, destroy_popup}; @@ -121,12 +91,16 @@ impl ContextMenu<'_, Message> { shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id))); state.view_cursor = view_cursor; + ( + id, + layout.children().map(|lo| lo.bounds()).collect::>(), + ) + } else { + ( + window::Id::unique(), + layout.children().map(|lo| lo.bounds()).collect(), + ) } - // A fresh id per popup, so the old popup's Done cannot be mistaken for the new one's - ( - window::Id::unique(), - layout.children().map(|lo| lo.bounds()).collect::>(), - ) }); let Some(context_menu) = self.context_menu.as_mut() else { return; @@ -142,7 +116,7 @@ impl ContextMenu<'_, Message> { click_outside: true, click_inside: true, }, - item_width: self.item_width, + item_width: ItemWidth::Uniform(240), item_height: ItemHeight::Dynamic(40), bar_bounds: bounds, main_offset: -(bounds.height as i32), @@ -203,7 +177,6 @@ impl ContextMenu<'_, Message> { ..Default::default() }; let parent = self.window_id; - let roots = my_state.roots.clone(); let t = THEME.lock().unwrap(); let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); drop(t); @@ -230,12 +203,8 @@ impl ContextMenu<'_, Message> { input_zone: None, }, Some(move || { - // Latest roots from the owner widget - let mut popup_menu = popup_menu.clone(); - popup_menu.menu_roots = - std::borrow::Cow::Owned(roots.with_data(Clone::clone)); crate::Element::from( - crate::widget::container(popup_menu).center(Length::Fill), + crate::widget::container(popup_menu.clone()).center(Length::Fill), ) .map(crate::action::app) }), @@ -246,7 +215,7 @@ impl ContextMenu<'_, Message> { pub fn on_surface_action( mut self, - handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, ) -> Self { self.on_surface_action = Some(Arc::new(handler)); self @@ -257,7 +226,7 @@ impl Widget for ContextMenu<'_, Message> { fn tag(&self) -> tree::Tag { - tree::Tag::of::>() + tree::Tag::of::() } fn state(&self) -> tree::State { @@ -266,9 +235,6 @@ impl Widget context_cursor: Point::default(), fingers_pressed: Default::default(), menu_bar_state: Default::default(), - reported_open: false, - roots: RcWrapper::new(self.context_menu.clone().unwrap_or_default()), - reshape: false, }) } @@ -302,32 +268,10 @@ impl Widget fn diff(&mut self, tree: &mut Tree) { tree.diff_children(std::slice::from_mut(&mut self.content)); - let state = tree.state.downcast_mut::>(); - if let Some(context_menu) = self.context_menu.as_mut() { - // The popup's item slots were measured at open from the item widgets. Items of - // another kind or count, such as a divider in a new place, do not fit those slots, - // so rebuild the popup on the next `update`. Same-shaped items, such as a - // relabeled button, update in place. - let popup_open = state - .menu_bar_state - .inner - .with_data(|d| !d.popup_id.is_empty()); - let shape = |roots: &Vec>| -> Vec { - roots.first().map_or_else(Vec::new, |root| { - root.flattern().iter().map(|mt| mt.item.tag()).collect() - }) - }; - if popup_open && state.roots.with_data(shape) != shape(context_menu) { - state.reshape = true; - return; - } - state.menu_bar_state.inner.with_data_mut(|inner| { - menu_roots_diff(context_menu, &mut inner.tree); - }); - state - .roots - .with_data_mut(|roots| roots.clone_from(context_menu)); - } + let state = tree.state.downcast_mut::(); + state.menu_bar_state.inner.with_data_mut(|inner| { + menu_roots_diff(self.context_menu.as_mut().unwrap(), &mut inner.tree); + }); // if let Some(ref mut context_menus) = self.context_menu { // for (menu, tree) in context_menus @@ -375,38 +319,6 @@ impl Widget ); } - fn mouse_interaction( - &self, - tree: &Tree, - layout: iced_core::Layout<'_>, - cursor: iced_core::mouse::Cursor, - viewport: &iced::Rectangle, - renderer: &crate::Renderer, - ) -> mouse::Interaction { - self.content.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor, - viewport, - renderer, - ) - } - - fn drag_destinations( - &self, - tree: &Tree, - layout: iced_core::Layout<'_>, - renderer: &crate::Renderer, - dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, - ) { - self.content.as_widget().drag_destinations( - &tree.children[0], - layout, - renderer, - dnd_rectangles, - ); - } - fn operate( &mut self, tree: &mut Tree, @@ -431,41 +343,9 @@ impl Widget shell: &mut iced_core::Shell<'_, Message>, viewport: &iced::Rectangle, ) { - let state = tree.state.downcast_mut::>(); + let state = tree.state.downcast_mut::(); let bounds = layout.bounds(); - // The compositor dismissed our popup: nothing else tells this state about it. - #[cfg(wayland_platform)] - if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( - iced::event::wayland::Event::Popup(iced::event::wayland::PopupEvent::Done, _, popup), - )) = event - { - state.menu_bar_state.inner.with_data_mut(|d| { - if d.popup_id.get(&self.window_id) == Some(popup) { - d.popup_id.remove(&self.window_id); - d.reset(); - } - }); - } - - // The menu changed shape while open - #[cfg(wayland_platform)] - if state.reshape { - state.reshape = false; - if state.menu_bar_state.inner.with_data(|d| d.open) { - if let Some(context_menu) = self.context_menu.as_mut() { - state.menu_bar_state.inner.with_data_mut(|inner| { - menu_roots_diff(context_menu, &mut inner.tree); - }); - state - .roots - .with_data_mut(|roots| roots.clone_from(context_menu)); - } - let view_cursor = state.menu_bar_state.inner.with_data(|d| d.view_cursor); - self.create_popup(layout, view_cursor, renderer, shell, viewport, state); - } - } - // XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open. let reset = self.window_id != window::Id::NONE && state @@ -535,7 +415,7 @@ impl Widget && (right_button_released(event) || (touch_lifted(event) && fingers_pressed == 2)) { state.context_cursor = cursor.position().unwrap_or_default(); - let state = tree.state.downcast_mut::>(); + let state = tree.state.downcast_mut::(); state.menu_bar_state.inner.with_data_mut(|state| { state.open = true; state.view_cursor = cursor; @@ -545,9 +425,7 @@ impl Widget self.create_popup(layout, cursor, renderer, shell, viewport, state); } - shell.request_redraw(); shell.capture_event(); - self.report_open_state(tree.state.downcast_mut::>(), shell); return; } else if !was_open && right_button_released(event) || (touch_lifted(event)) @@ -583,76 +461,62 @@ impl Widget shell, viewport, ); - self.report_open_state(tree.state.downcast_mut::>(), shell); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: iced_core::Layout<'b>, - renderer: &crate::Renderer, - viewport: &iced::Rectangle, + layout: iced_core::Layout<'_>, + _renderer: &crate::Renderer, + _viewport: &iced::Rectangle, translation: Vector, ) -> Option> { - // The wrapped content's overlays (tooltips, dropdowns, ...) always pass through - let content = self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - viewport, - translation, - ); - #[cfg(wayland_platform)] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - return content; + return None; } - let state = tree.state.downcast_ref::>(); - let Some(context_menu) = self.context_menu.as_mut() else { - return content; - }; + let state = tree.state.downcast_ref::(); + + let context_menu = self.context_menu.as_mut()?; + if !state.menu_bar_state.inner.with_data(|state| state.open) { - return content; + return None; } - // Anchor the menu to a 1x1 rectangle at the click, like the popup path does - let bounds = iced::Rectangle::new(state.context_cursor, Size::new(1.0, 1.0)); - let menu = crate::widget::menu::Menu { - tree: state.menu_bar_state.clone(), - menu_roots: std::borrow::Cow::Owned(context_menu.clone()), - bounds_expand: 16, - menu_overlays_parent: true, - close_condition: CloseCondition { - leave: false, - click_outside: true, - click_inside: true, - }, - item_width: self.item_width, - item_height: ItemHeight::Dynamic(40), - bar_bounds: bounds, - main_offset: 0, - cross_offset: 0, - root_bounds_list: vec![bounds], - path_highlight: Some(PathHighlight::MenuActive), - style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), - position: Point::new(translation.x, translation.y), - is_overlay: true, - window_id: window::Id::NONE, - depth: 0, - on_surface_action: None, - } - .overlay(); - - Some(match content { - Some(content) => { - iced_core::overlay::Group::with_children(vec![content, menu]).overlay() + let mut bounds = layout.bounds(); + bounds.x = state.context_cursor.x; + bounds.y = state.context_cursor.y; + Some( + crate::widget::menu::Menu { + tree: state.menu_bar_state.clone(), + menu_roots: std::borrow::Cow::Owned(context_menu.clone()), + bounds_expand: 16, + menu_overlays_parent: true, + close_condition: CloseCondition { + leave: false, + click_outside: true, + click_inside: true, + }, + item_width: ItemWidth::Uniform(240), + item_height: ItemHeight::Dynamic(40), + bar_bounds: bounds, + main_offset: -(bounds.height as i32), + cross_offset: 0, + root_bounds_list: vec![bounds], + path_highlight: Some(PathHighlight::MenuActive), + style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), + position: Point::new(translation.x, translation.y), + is_overlay: true, + window_id: window::Id::NONE, + depth: 0, + on_surface_action: None, } - None => menu, - }) + .overlay(), + ) } #[cfg(feature = "a11y")] @@ -692,12 +556,8 @@ fn touch_lifted(event: &Event) -> bool { matches!(event, Event::Touch(touch::Event::FingerLifted { .. })) } -pub struct LocalState { +pub struct LocalState { context_cursor: Point, fingers_pressed: HashSet, menu_bar_state: MenuBarState, - reported_open: bool, - roots: RcWrapper>>, - /// menu shape has changed, rebuild it on the next `update` - reshape: bool, } diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index a5f0162b..d0ec175a 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -44,7 +44,7 @@ pub fn popup_dropdown< selected: Option, on_selected: impl Fn(usize) -> Message + Send + Sync + 'static, _parent_id: window::Id, - _on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static, + _on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static, _map_action: impl Fn(Message) -> AppMessage + Send + Sync + 'static, ) -> Dropdown<'a, S, Message, AppMessage> { let dropdown: Dropdown<'_, S, Message, AppMessage> = diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 7d8d1dfc..994b28dd 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -54,8 +54,7 @@ where #[setters(strip_option)] font: Option, #[setters(skip)] - on_surface_action: - Option) -> Message + Send + Sync + 'static>>, + on_surface_action: Option Message + Send + Sync + 'static>>, #[setters(skip)] action_map: Option AppMessage + 'static + Send + Sync>>, #[setters(strip_option)] @@ -109,7 +108,7 @@ where pub fn with_popup( self, parent_id: window::Id, - on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static, + on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static, action_map: impl Fn(Message) -> NewAppMessage + Send + Sync + 'static, ) -> Dropdown<'a, S, Message, NewAppMessage> { let Self { @@ -552,9 +551,7 @@ pub fn update< selections: &[S], state: impl FnOnce() -> &'a mut State, _window_id: Option, - on_surface_action: Option< - Arc) -> Message + Send + Sync + 'static>, - >, + on_surface_action: Option Message + Send + Sync + 'static>>, action_map: Option AppMessage + Send + Sync + 'static>>, icons: &[icon::Handle], gap: f32, diff --git a/src/widget/icon/handle.rs b/src/widget/icon/handle.rs index 1fe334f7..7e0bab02 100644 --- a/src/widget/icon/handle.rs +++ b/src/widget/icon/handle.rs @@ -5,10 +5,8 @@ use super::Icon; use crate::widget::{image, svg}; use std::borrow::Cow; use std::ffi::OsStr; -use std::fs::File; use std::hash::Hash; -use std::io::Read; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; #[must_use] #[derive(Clone, Debug, Hash, derive_setters::Setters)] @@ -33,52 +31,6 @@ pub enum Data { Svg(svg::Handle), } -enum SvgSource { - Path, - Bytes(Vec), -} - -fn svg_source(path: &Path) -> Option { - if path - .extension() - .and_then(OsStr::to_str) - .is_some_and(|extension| extension.eq_ignore_ascii_case("svg")) - { - return Some(SvgSource::Path); - } - - let Ok(mut file) = File::open(path) else { - return None; - }; - - let Ok(metadata) = file.metadata() else { - return None; - }; - const MAX_SVG_SIZE: u64 = 16 * 1024 * 1024; - if !metadata.file_type().is_file() || metadata.len() > MAX_SVG_SIZE { - return None; - } - - let mut prefix = [0; 32]; - let Ok(length) = file.read(&mut prefix) else { - return None; - }; - let prefix = &prefix[..length]; - - if ::image::guess_format(prefix).is_ok() { - return None; - } - - let mut bytes = Vec::with_capacity(metadata.len() as usize); - bytes.extend_from_slice(prefix); - if file.read_to_end(&mut bytes).is_err() { - return None; - } - - let document = roxmltree::Document::parse(std::str::from_utf8(&bytes).ok()?).ok()?; - (document.root_element().tag_name().name() == "svg").then_some(SvgSource::Bytes(bytes)) -} - /// Create an icon handle from its path. pub fn from_path(path: PathBuf) -> Handle { Handle { @@ -86,10 +38,10 @@ pub fn from_path(path: PathBuf) -> Handle { .file_stem() .and_then(OsStr::to_str) .is_some_and(|name| name.ends_with("-symbolic")), - data: match svg_source(&path) { - Some(SvgSource::Path) => Data::Svg(svg::Handle::from_path(path)), - Some(SvgSource::Bytes(bytes)) => Data::Svg(svg::Handle::from_memory(bytes)), - None => Data::Image(image::Handle::from_path(path)), + data: if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { + Data::Svg(svg::Handle::from_path(path)) + } else { + Data::Image(image::Handle::from_path(path)) }, } } diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index 118b4f12..89050301 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -3,6 +3,7 @@ use super::{Handle, Icon}; use std::borrow::Cow; +use std::ffi::OsStr; use std::path::PathBuf; use std::sync::Arc; @@ -137,7 +138,11 @@ impl Named { Handle { symbolic: self.symbolic, data: if let Some(path) = self.path() { - super::from_path(path).data + if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { + super::Data::Svg(iced_core::svg::Handle::from_path(path)) + } else { + super::Data::Image(iced_core::image::Handle::from_path(path)) + } } else { super::bundle::get(&name).unwrap_or_else(|| { let bytes: &'static [u8] = &[]; diff --git a/src/widget/menu.rs b/src/widget/menu.rs index 6ddb8f37..17d47cdd 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -71,8 +71,8 @@ pub mod menu_column; mod menu_inner; mod menu_tree; pub use menu_tree::{ - Entry, IconSlot, MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, - menu_root as root, nav_context, + MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, menu_root as root, + nav_context, }; pub use crate::style::menu_bar::{Appearance, StyleSheet}; diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index be4dadbf..50dc9b64 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use super::menu_inner::{ CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight, - close_innermost, }; use super::menu_tree::MenuTree; use crate::Renderer; @@ -177,7 +176,7 @@ pub struct MenuBar { #[cfg(wayland_platform)] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: - Option) -> Message + Send + Sync + 'static>>, + Option Message + Send + Sync + 'static>>, } impl MenuBar @@ -329,7 +328,7 @@ where #[must_use] pub fn on_surface_action( mut self, - handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, ) -> Self { self.on_surface_action = Some(Arc::new(handler)); self @@ -378,12 +377,13 @@ where state.active_root.clear(); shell.publish(surface_action(destroy_popup(id))); state.view_cursor = view_cursor; + (id, layout.children().map(|lo| lo.bounds()).collect()) + } else { + ( + window::Id::unique(), + layout.children().map(|lo| lo.bounds()).collect(), + ) } - // A fresh id per popup, so the old popup's Done cannot be mistaken for the new one's - ( - window::Id::unique(), - layout.children().map(|lo| lo.bounds()).collect(), - ) }); let mut popup_menu: Menu<'static, _> = Menu { @@ -581,21 +581,6 @@ where let my_state = tree.state.downcast_mut::(); - // The compositor dismissed our popup: nothing else tells this state about it. - #[cfg(wayland_platform)] - if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( - iced::event::wayland::Event::Popup(iced::event::wayland::PopupEvent::Done, _, popup), - )) = event - { - my_state.inner.with_data_mut(|d| { - if d.popup_id.get(&self.window_id) == Some(popup) { - // submenus were dismissed with it - d.popup_id.clear(); - d.reset(); - } - }); - } - // XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open. let reset = self.window_id != window::Id::NONE && my_state @@ -613,23 +598,6 @@ where }); match event { - event::Event::Keyboard(iced_widget::core::keyboard::Event::KeyPressed { - key: - iced_widget::core::keyboard::Key::Named( - iced_widget::core::keyboard::key::Named::Escape, - ), - .. - }) if open => { - my_state.inner.with_data_mut(|state| { - let handler = self.on_surface_action.as_ref(); - close_innermost(state, &mut |id| { - if let Some(handler) = handler { - shell.publish((handler)(crate::surface::Action::DestroyPopup(id))); - } - }); - }); - shell.capture_event(); - } Mouse(mouse::Event::ButtonPressed(Left)) | Touch(touch::Event::FingerPressed { .. }) if view_cursor.is_over(layout.bounds()) => diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index b79684c5..0bc3f6cf 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -455,7 +455,7 @@ pub(crate) struct Menu<'b, Message: std::clone::Clone> { pub(crate) window_id: window::Id, pub(crate) depth: usize, pub(crate) on_surface_action: - Option) -> Message + Send + Sync + 'static>>, + Option Message + Send + Sync + 'static>>, } impl<'b, Message: Clone + 'static> Menu<'b, Message> { pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, crate::Renderer> { @@ -546,48 +546,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { }) } - /// Close every menu and, on Wayland, destroy the root popup this menu belongs to. - /// Used when clicking outside a menu. #[allow(clippy::too_many_lines)] - fn close_all( - &self, - state: &mut super::menu_bar::MenuBarStateInner, - shell: &mut Shell<'_, Message>, - ) { - #[cfg(wayland_platform)] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) - && let Some(handler) = self.on_surface_action.as_ref() - { - let mut root = self.window_id; - let mut depth = self.depth; - while let Some(parent) = state.popup_id.iter().find(|(_, v)| **v == root) { - if depth == 0 { - break; - } - root = *parent.0; - depth = depth.saturating_sub(1); - } - shell.publish((handler)(crate::surface::Action::DestroyPopup(root))); - } - - state.reset(); - } - - /// Close the innermost open menu, see [`close_innermost`]. - /// Used when pressing `esc`. - fn close( - &self, - state: &mut super::menu_bar::MenuBarStateInner, - shell: &mut Shell<'_, Message>, - ) { - let handler = self.on_surface_action.as_ref(); - close_innermost(state, &mut |id| { - if let Some(handler) = handler { - shell.publish((handler)(crate::surface::Action::DestroyPopup(id))); - } - }); - } - fn update( &mut self, event: &event::Event, @@ -642,19 +601,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { ); match event { - event::Event::Keyboard(iced_widget::core::keyboard::Event::KeyPressed { - key: - iced_widget::core::keyboard::Key::Named( - iced_widget::core::keyboard::key::Named::Escape, - ), - .. - }) if !self.is_overlay => { - self.tree - .inner - .with_data_mut(|state| self.close(state, shell)); - shell.capture_event(); - } - Mouse(WheelScrolled { delta }) => process_scroll_events( self, shell, @@ -730,7 +676,27 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { needs_reset |= self.close_condition.click_outside && !is_inside; if needs_reset { - self.close_all(state, shell); + #[cfg(wayland_platform)] + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && let Some(handler) = self.on_surface_action.as_ref() + { + let mut root = self.window_id; + let mut depth = self.depth; + while let Some(parent) = + state.popup_id.iter().find(|(_, v)| **v == root) + { + // parent of root popup is the window, so we stop. + if depth == 0 { + break; + } + root = *parent.0; + depth = depth.saturating_sub(1); + } + shell + .publish((handler)(crate::surface::Action::DestroyPopup(root))); + } + + state.reset(); } } @@ -1816,45 +1782,6 @@ fn get_children_layout( (Size::new(width, height), child_positions, child_sizes) } -/// Close the innermost open menu level -pub(super) fn close_innermost( - state: &mut super::menu_bar::MenuBarStateInner, - destroy: &mut dyn FnMut(window::Id), -) { - let outermost = state - .popup_id - .iter() - .find(|(parent, _)| !state.popup_id.values().any(|id| id == *parent)) - .map(|(_, id)| *id); - let innermost = state - .popup_id - .values() - .find(|id| !state.popup_id.contains_key(id)) - .copied(); - - let levels = state.menu_states.len(); - if levels <= 1 { - if let Some(id) = outermost { - destroy(id); - } - state.popup_id.clear(); - state.reset(); - return; - } - - state.active_root.truncate(levels - 1); - state.menu_states.truncate(levels - 1); - - // Forget the parent's hovered item, so hovering it again reopens the submenu. - if let Some(parent) = state.menu_states.last_mut() { - parent.index = None; - } - if let Some(id) = innermost { - state.popup_id.retain(|_, popup| *popup != id); - destroy(id); - } -} - fn search_bound( default: usize, default_left: usize, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 901d50ae..bd4f1e6c 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -180,84 +180,6 @@ pub enum MenuItem>> { Folder(L, Vec>), /// Represents a divider between menu items. Divider, - /// A menu entry with every option available; see [`Entry`]. - Entry(Entry), -} - -impl>> MenuItem { - /// Create an [`Entry`] menu item, configure it with the builder methods on [`Entry`]. - pub fn entry(label: L, action: A) -> Self { - MenuItem::Entry(Entry::new(label, action)) - } -} - -/// The leading icon column of a menu entry. -#[derive(Clone, Debug, Default)] -pub enum IconSlot { - /// No icon and no space reserved for one. - #[default] - None, - /// No icon, but the space for an icon is reserved (indented entry) - Reserved, - /// An icon. - Icon(icon::Handle), -} - -impl From> for IconSlot { - fn from(icon: Option) -> Self { - icon.map_or(IconSlot::None, IconSlot::Icon) - } -} - -/// A menu entry: label, optional leading icon, optional check column, enabled state and action. -#[derive(Clone)] -pub struct Entry { - label: L, - icon: IconSlot, - /// `Some` draws the check column - checked: Option, - enabled: bool, - action: A, -} - -impl Entry { - pub fn new(label: L, action: A) -> Self { - Self { - label, - icon: IconSlot::None, - checked: None, - enabled: true, - action, - } - } - - /// Draw a leading icon - #[must_use] - pub fn icon(mut self, icon: icon::Handle) -> Self { - self.icon = IconSlot::Icon(icon); - self - } - - /// Draw no icon, but resever the space - #[must_use] - pub fn reserve_icon(mut self) -> Self { - self.icon = IconSlot::Reserved; - self - } - - /// Show a check column, ticked when `checked` is true, empty sapce when false - #[must_use] - pub fn checked(mut self, checked: bool) -> Self { - self.checked = Some(checked); - self - } - - /// Disabled entries are drawn dimmed and do not react to presses - #[must_use] - pub fn enabled(mut self, enabled: bool) -> Self { - self.enabled = enabled; - self - } } /// Create a root menu item. @@ -279,97 +201,6 @@ where .class(theme::Button::MenuRoot) } -fn entry_tree< - A: MenuAction, - L: Into>, - Message: Clone + 'static, ->( - entry: Entry, - key_binds: &HashMap, - key_class: theme::Text, -) -> MenuTree { - let Entry { - label, - icon, - checked, - enabled, - action, - } = entry; - let spacing = crate::theme::spacing(); - // Several bindings may map to one action - // choose based on these rules: - // - the fewest modifiers win (Delete vs Ctrl+D) - // - if equal then character key over a named one (Ctrl+C over Ctrl+Insert) - // - if equal then `Ord` to stay consistent since hashmap is arbitrary - let rank = |k: &KeyBind| { - ( - k.modifiers.len(), - matches!(k.key, iced_core::keyboard::Key::Named(_)), - ) - }; - let key = key_binds - .iter() - .filter(|(_, a)| **a == action) - .map(|(k, _)| k) - .min_by(|a, b| rank(a).cmp(&rank(b)).then_with(|| a.cmp(b))) - .map_or_else(String::new, ToString::to_string); - - let mut items: Vec> = Vec::with_capacity(7); - - if let Some(checked) = checked { - items.push(if checked { - widget::icon::from_name("object-select-symbolic") - .size(16) - .icon() - .class(theme::Svg::Custom(Rc::new(|theme| { - iced_widget::svg::Style { - color: Some(theme.cosmic().accent_text_color().into()), - } - }))) - .width(Length::Fixed(16.0)) - .into() - } else { - widget::space::horizontal() - .width(Length::Fixed(16.0)) - .into() - }); - items.push(widget::space::horizontal().width(spacing.space_xxs).into()); - } - - match icon { - IconSlot::Icon(icon) => { - items.push(widget::icon::icon(icon).size(14).into()); - items.push(widget::space::horizontal().width(spacing.space_xxs).into()); - } - IconSlot::Reserved => { - items.push( - widget::space::horizontal() - .width(Length::Fixed(14.0)) - .into(), - ); - items.push(widget::space::horizontal().width(spacing.space_xxs).into()); - } - IconSlot::None => {} - } - - let ellipsize = - iced_core::text::Ellipsize::Middle(iced_core::text::EllipsizeHeightLimit::Lines(1)); - items.push(widget::text(label.into()).ellipsize(ellipsize).into()); - items.push(widget::space::horizontal().into()); - items.push( - widget::text(key) - .class(key_class) - .ellipsize(ellipsize) - .into(), - ); - - let mut button = menu_button(items); - if enabled { - button = button.on_press(action.message()); - } - MenuTree::from(Element::from(button)) -} - /// Create a list of menu items from a vector of `MenuItem`. /// /// The `MenuItem` can be either an action or a separator. @@ -389,6 +220,15 @@ pub fn menu_items< key_binds: &HashMap, children: Vec>, ) -> Vec> { + fn find_key(action: &A, key_binds: &HashMap) -> String { + for (key_bind, key_action) in key_binds { + if action == key_action { + return key_bind.to_string(); + } + } + String::new() + } + fn key_style(theme: &crate::Theme) -> TextStyle { let mut color = theme.cosmic().background(theme.transparent).component.on; color.alpha *= 0.75; @@ -406,25 +246,117 @@ pub fn menu_items< .enumerate() .flat_map(|(i, item)| { let mut trees = vec![]; + let spacing = crate::theme::spacing(); match item { MenuItem::Button(label, icon, action) => { - let mut entry = Entry::new(label, action); - entry.icon = icon.into(); - trees.push(entry_tree(entry, key_binds, key_class.clone())); + let l: Cow<'static, str> = label.into(); + let key = find_key(&action, key_binds); + let mut items = vec![ + widget::text(l) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), + widget::space::horizontal().into(), + widget::text(key) + .class(key_class) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), + ]; + + if let Some(icon) = icon { + items.insert(0, widget::icon::icon(icon).size(14).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); + } + + let menu_button = menu_button(items).on_press(action.message()); + + trees.push(MenuTree::::from(Element::from(menu_button))); } MenuItem::ButtonDisabled(label, icon, action) => { - let mut entry = Entry::new(label, action).enabled(false); - entry.icon = icon.into(); - trees.push(entry_tree(entry, key_binds, key_class.clone())); + let l: Cow<'static, str> = label.into(); + + let key = find_key(&action, key_binds); + + let mut items = vec![ + widget::text(l) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), + widget::space::horizontal().into(), + widget::text(key) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .class(key_class) + .into(), + ]; + + if let Some(icon) = icon { + items.insert(0, widget::icon::icon(icon).size(14).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); + } + + let menu_button = menu_button(items); + + trees.push(MenuTree::::from(Element::from(menu_button))); } MenuItem::CheckBox(label, icon, value, action) => { - let mut entry = Entry::new(label, action).checked(value); - entry.icon = icon.into(); - trees.push(entry_tree(entry, key_binds, key_class.clone())); - } - MenuItem::Entry(entry) => { - trees.push(entry_tree(entry, key_binds, key_class.clone())); + let key = find_key(&action, key_binds); + let mut items = vec![ + if value { + widget::icon::from_name("object-select-symbolic") + .size(16) + .icon() + .class(theme::Svg::Custom(Rc::new(|theme| { + iced_widget::svg::Style { + color: Some(theme.cosmic().accent_text_color().into()), + } + }))) + .width(Length::Fixed(16.0)) + .into() + } else { + widget::space::horizontal() + .width(Length::Fixed(16.0)) + .into() + }, + widget::space::horizontal().width(spacing.space_xxs).into(), + widget::text(label) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .align_x(iced::Alignment::Start) + .into(), + widget::space::horizontal().into(), + widget::text(key) + .class(key_class) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), + ]; + + if let Some(icon) = icon { + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); + items.insert(2, widget::icon::icon(icon).size(14).into()); + } + + trees.push(MenuTree::from(Element::from( + menu_button(items).on_press(action.message()), + ))); } MenuItem::Folder(label, children) => { let l: Cow<'static, str> = label.into(); @@ -458,8 +390,7 @@ pub fn menu_items< )); } MenuItem::Divider => { - // A divider at either end separates nothing - if i != 0 && i != size - 1 { + if i != size - 1 { trees.push(MenuTree::::from(Element::from( widget::divider::horizontal::light(), ))); diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 84dd4a08..14790866 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -159,7 +159,7 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> { #[must_use] pub fn on_surface_action( mut self, - handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, ) -> Self { self.segmented_button = self.segmented_button.on_surface_action(handler); self diff --git a/src/widget/popover.rs b/src/widget/popover.rs index b9704244..fb7fea66 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -157,9 +157,17 @@ where ) { if self.popup.is_some() { if self.modal { - if matches!(event, Event::Mouse(_) | Event::Touch(_)) { - shell.capture_event(); - return; + match event { + Event::Mouse(_) | Event::Touch(_) => { + shell.capture_event(); + return; + } + // app-level keyboard navigation (Tab focus cycling, Escape) + // still receives keyboard events + Event::Keyboard(_) => { + return; + } + _ => {} } } else if let Some(on_close) = self.on_close.as_ref() && matches!( diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index b1fd8836..8da4be14 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -1,6 +1,6 @@ //! Show a circular progress indicator. use super::animation::{Animation, Progress}; -use super::style::{self, Catalog}; +use super::style::StyleSheet; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer}; use iced::widget::canvas; @@ -15,11 +15,11 @@ const MAX_WRAP: f32 = 1.0 - MIN_GAP_ANGLE.0 / (2.0 * PI); #[must_use] pub struct Circular where - Theme: Catalog, + Theme: StyleSheet, { size: f32, bar_height: Option, - class: Theme::Class, + style: Theme::Style, cycle_duration: Duration, period: Duration, progress: Option, @@ -27,14 +27,14 @@ where impl Circular where - Theme: Catalog, + Theme: StyleSheet, { /// Creates a new [`Circular`] with the given content. pub fn new() -> Self { Circular { size: 48.0, bar_height: None, - class: Theme::Class::default(), + style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), period: Duration::from_secs(2), progress: None, @@ -54,9 +54,9 @@ where self } - /// Sets the style class of this [`Circular`]. - pub fn class(mut self, class: Theme::Class) -> Self { - self.class = class; + /// Sets the style variant of this [`Circular`]. + pub fn style(mut self, style: Theme::Style) -> Self { + self.style = style; self } @@ -78,29 +78,11 @@ where self.progress = Some(progress.clamp(0.0, 1.0)); self } - - /// Sets the track color of this [`Circular`]. - pub fn track_color(mut self, color: impl Into) -> Self { - self.class = self.class.track_color(color); - self - } - - /// Sets the bar color of this [`Circular`]. - pub fn bar_color(mut self, color: impl Into) -> Self { - self.class = self.class.bar_color(color); - self - } - - /// Sets the border color of this [`Circular`]. - pub fn border_color(mut self, color: impl Into) -> Self { - self.class = self.class.border_color(color); - self - } } impl Default for Circular where - Theme: Catalog, + Theme: StyleSheet, { fn default() -> Self { Self::new() @@ -117,7 +99,7 @@ struct State { impl Widget for Circular where Message: Clone, - Theme: Catalog, + Theme: StyleSheet, { fn tag(&self) -> tree::Tag { tree::Tag::of::() @@ -188,7 +170,7 @@ where let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); - let custom_style = theme.style(&self.class, self.progress.is_some(), true); + let custom_style = Theme::appearance(theme, &self.style, self.progress.is_some(), true); let geometry = state.cache.draw(renderer, bounds.size(), |frame| { let bar_height = self.bar_height.unwrap_or((frame.width() / 12.0).max(2.0)); @@ -258,7 +240,7 @@ where impl<'a, Message, Theme> From> for Element<'a, Message, Theme, Renderer> where Message: Clone + 'a, - Theme: Catalog + 'a, + Theme: StyleSheet + 'a, { fn from(circular: Circular) -> Self { Self::new(circular) diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index c5072b4d..6c06e2f7 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -1,6 +1,6 @@ //! Show a linear progress indicator. use super::animation::{Animation, Progress}; -use super::style::{self, Catalog}; +use super::style::StyleSheet; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer}; use iced::{Border, Color, Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; @@ -13,11 +13,11 @@ const WRAP_LENGTH: f32 = 0.618; // avoids animation repetition #[must_use] pub struct Linear where - Theme: Catalog, + Theme: StyleSheet, { width: Length, girth: Length, - class: Theme::Class, + style: Theme::Style, cycle_duration: Duration, period: Duration, progress: Option, @@ -27,14 +27,14 @@ where impl Linear where - Theme: Catalog, + Theme: StyleSheet, { /// Creates a new [`Linear`] with the given content. pub fn new() -> Self { Linear { width: Length::Fixed(100.0), girth: Length::Fixed(4.0), - class: Theme::Class::default(), + style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), period: Duration::from_secs(2), progress: None, @@ -55,9 +55,9 @@ where self } - /// Sets the style class of this [`Linear`]. - pub fn class(mut self, class: Theme::Class) -> Self { - self.class = class; + /// Sets the style variant of this [`Linear`]. + pub fn style(mut self, style: impl Into) -> Self { + self.style = style.into(); self } @@ -99,35 +99,11 @@ where self.segment_spacing = spacing.into().0.max(1.0); self } - - /// Sets the track color of this [`Linear`]. - pub fn track_color(mut self, color: impl Into) -> Self { - self.class = self.class.track_color(color); - self - } - - /// Sets the bar color of this [`Linear`]. - pub fn bar_color(mut self, color: impl Into) -> Self { - self.class = self.class.bar_color(color); - self - } - - /// Sets the border color of this [`Linear`]. - pub fn border_color(mut self, color: impl Into) -> Self { - self.class = self.class.border_color(color); - self - } - - /// Sets the border radius of this [`Linear`]. - pub fn border_radius(mut self, radius: f32) -> Self { - self.class = self.class.border_radius(radius); - self - } } impl Default for Linear where - Theme: Catalog, + Theme: StyleSheet, { fn default() -> Self { Self::new() @@ -143,7 +119,7 @@ struct State { impl Widget for Linear where Message: Clone, - Theme: Catalog, + Theme: StyleSheet, Renderer: advanced::Renderer, { fn tag(&self) -> tree::Tag { @@ -210,7 +186,7 @@ where _viewport: &Rectangle, ) { let bounds = layout.bounds(); - let custom_style = theme.style(&self.class, self.progress.is_some(), false); + let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); let state = tree.state.downcast_ref::(); let border_width = if custom_style.border_color.is_some() { @@ -331,7 +307,7 @@ where impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Message: Clone + 'a, - Theme: Catalog + 'a, + Theme: StyleSheet + 'a, Renderer: iced::advanced::Renderer + 'a, { fn from(linear: Linear) -> Self { diff --git a/src/widget/progress_bar/style.rs b/src/widget/progress_bar/style.rs index 5447fdec..db2fe64d 100644 --- a/src/widget/progress_bar/style.rs +++ b/src/widget/progress_bar/style.rs @@ -1,8 +1,7 @@ use iced::Color; -use palette::WithAlpha; -#[derive(Clone, Copy, Debug)] -pub struct Style { +#[derive(Debug, Clone, Copy)] +pub struct Appearance { /// The track [`Color`] of the progress indicator. pub track_color: Color, /// The bar [`Color`] of the progress indicator. @@ -13,101 +12,94 @@ pub struct Style { pub border_radius: f32, } -/// [`Style`] field overrides -#[derive(Clone, Copy, Debug, Default)] -pub struct Class { - pub track_color: Option, - pub bar_color: Option, - pub border_color: Option, - pub border_radius: Option, -} - -impl Class { - pub fn track_color(mut self, color: impl Into) -> Self { - self.track_color = Some(color.into()); - self - } - - pub fn bar_color(mut self, color: impl Into) -> Self { - self.bar_color = Some(color.into()); - self - } - - pub fn border_color(mut self, color: impl Into) -> Self { - self.border_color = Some(color.into()); - self - } - - pub fn border_radius(mut self, radius: f32) -> Self { - self.border_radius = Some(radius); - self - } - - fn resolve(&self, base: Style) -> Style { - Style { - track_color: self.track_color.unwrap_or(base.track_color), - bar_color: self.bar_color.unwrap_or(base.bar_color), - border_color: self.border_color.or(base.border_color), - border_radius: self.border_radius.unwrap_or(base.border_radius), +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + track_color: Color::TRANSPARENT, + bar_color: Color::BLACK, + border_color: None, + border_radius: 0.0, } } } /// A set of rules that dictate the style of an indicator. -pub trait Catalog: Sized { - /// The supported class of the [`Catalog`]. - type Class: Default; +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; - /// Produces the active [`Style`] of an indicator. - fn style(&self, class: &Self::Class, is_determinate: bool, is_circular: bool) -> Style; + /// Produces the active [`Appearance`] of a indicator. + fn appearance( + &self, + style: &Self::Style, + is_determinate: bool, + is_circular: bool, + ) -> Appearance; } -impl Catalog for iced::Theme { - type Class = Class; +impl StyleSheet for iced::Theme { + type Style = (); - fn style(&self, class: &Self::Class, _is_determinate: bool, _is_circular: bool) -> Style { + fn appearance( + &self, + _style: &Self::Style, + _is_determinate: bool, + _is_circular: bool, + ) -> Appearance { let palette = self.extended_palette(); - class.resolve(Style { + + Appearance { track_color: palette.background.weak.color, bar_color: palette.primary.base.color, border_color: None, border_radius: 0.0, - }) + } } } -impl Catalog for crate::Theme { - type Class = Class; +impl StyleSheet for crate::Theme { + type Style = (); - fn style(&self, class: &Self::Class, is_determinate: bool, is_circular: bool) -> Style { + fn appearance( + &self, + _style: &Self::Style, + is_determinate: bool, + is_circular: bool, + ) -> Appearance { + let cur = self.current_container(); + let mut cur_divider = cur.divider; + cur_divider.alpha = 0.5; let theme = self.cosmic(); - let (mut track_color, bar_color) = match (theme.is_dark, theme.is_high_contrast) { - (true, true) => ( + let (mut track_color, bar_color) = if theme.is_dark && theme.is_high_contrast { + ( theme.palette.neutral_6.into(), theme.accent_text_color().into(), - ), - (true, false) => (theme.palette.neutral_5.into(), theme.accent_color().into()), - (false, true) => ( + ) + } else if theme.is_dark { + (theme.palette.neutral_5.into(), theme.accent_color().into()) + } else if theme.is_high_contrast { + ( theme.palette.neutral_4.into(), theme.accent_text_color().into(), - ), - (false, false) => (theme.palette.neutral_3.into(), theme.accent_color().into()), + ) + } else { + (theme.palette.neutral_3.into(), theme.accent_color().into()) }; if !is_determinate && is_circular { track_color = Color::TRANSPARENT; } - class.resolve(Style { + Appearance { track_color, bar_color, border_color: if is_determinate && theme.is_high_contrast { - Some(self.current_container().divider.with_alpha(0.5).into()) + Some(cur_divider.into()) } else { None }, border_radius: theme.corner_radii.radius_xl[0], - }) + } } } diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index 4560114c..d200a444 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -11,7 +11,7 @@ use iced_core::{ pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>( content: E, id: Id, - on_action: impl Fn(crate::surface::Action) -> Message + 'static, + on_action: impl Fn(crate::surface::Action) -> Message + 'static, ) -> ResponsiveContainer<'a, Message, Theme, crate::Renderer> where E: Into>, @@ -32,7 +32,7 @@ where content: Element<'a, Message, Theme, Renderer>, id: Id, size: Option, - on_action: Box) -> Message>, + on_action: Box Message>, } impl<'a, Message, Theme, Renderer> ResponsiveContainer<'a, Message, Theme, Renderer> @@ -43,7 +43,7 @@ where pub(crate) fn new( content: T, id: Id, - on_action: impl Fn(crate::surface::Action) -> Message + 'static, + on_action: impl Fn(crate::surface::Action) -> Message + 'static, ) -> Self where T: Into>, diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index c698f571..b7b416ef 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -79,11 +79,7 @@ impl ResponsiveMenuBar { core: &Core, key_binds: &HashMap, id: crate::widget::Id, - action_message: impl Fn(crate::surface::Action) -> Message - + Send - + Sync - + Clone - + 'static, + action_message: impl Fn(crate::surface::Action) -> Message + Send + Sync + Clone + 'static, trees: Vec<(S, Vec>)>, ) -> Element<'a, Message> { use crate::widget::id_container; diff --git a/src/widget/scrollable/scrollable.rs b/src/widget/scrollable/scrollable.rs index 6565279f..a3fa4edd 100644 --- a/src/widget/scrollable/scrollable.rs +++ b/src/widget/scrollable/scrollable.rs @@ -2,28 +2,30 @@ // SPDX-License-Identifier: MPL-2.0 use crate::{Element, Renderer}; -use iced::widget::scrollable::{Direction, Scrollable, Scrollbar}; +use iced::widget; pub fn scrollable<'a, Message>( element: impl Into>, -) -> Scrollable<'a, Message, crate::Theme, Renderer> { +) -> widget::Scrollable<'a, Message, crate::Theme, Renderer> { vertical(element) } pub fn vertical<'a, Message>( element: impl Into>, -) -> Scrollable<'a, Message, crate::Theme, Renderer> { - iced::widget::scrollable(element) - .padding(8.0) - .direction(Direction::Vertical( - Scrollbar::new().width(8.0).scroller_width(8.0), - )) +) -> widget::Scrollable<'a, Message, crate::Theme, Renderer> { + widget::scrollable(element) + .scroller_width(8.0) + .scrollbar_width(8.0) + .scrollbar_padding(8.0) } pub fn horizontal<'a, Message>( element: impl Into>, -) -> Scrollable<'a, Message, crate::Theme, Renderer> { - iced::widget::scrollable(element).direction(Direction::Horizontal( - Scrollbar::new().width(8.0).scroller_width(8.0), - )) +) -> widget::Scrollable<'a, Message, crate::Theme, Renderer> { + widget::scrollable(element) + .direction(widget::scrollable::Direction::Horizontal( + widget::scrollable::Scrollbar::new(), + )) + .scroller_width(8.0) + .scrollbar_width(8.0) } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 1a6466b7..ed90328f 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -203,7 +203,7 @@ where positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, #[setters(skip)] pub(crate) on_surface_action: - Option) -> Message + Send + Sync + 'static>>, + Option Message + Send + Sync + 'static>>, /// Defines the implementation of this struct variant: PhantomData, @@ -914,7 +914,7 @@ where #[must_use] pub fn on_surface_action( mut self, - handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, ) -> Self { self.on_surface_action = Some(Arc::new(handler)); self @@ -953,9 +953,10 @@ where state.active_root.clear(); shell.publish(surface_action(destroy_popup(id))); state.view_cursor = view_cursor; + id + } else { + window::Id::unique() } - // A fresh id per popup, so the old popup's Done cannot be mistaken for the new one's - window::Id::unique() }); let Some(entity) = state.show_context else { return; @@ -1087,7 +1088,7 @@ where + Sync + 'static, view: Option crate::Element<'static, Message> + Send + Sync + 'static>, - ) -> crate::surface::Action { + ) -> crate::surface::Action { use std::any::Any; let boxed: Box< @@ -1106,8 +1107,11 @@ where Arc::new(boxed), Arc::new(boxed_live), view.map(|view| { - Arc::new(move || view().map(crate::Action::App)) - as crate::surface::View + let boxed: Box< + dyn Fn() -> crate::Element<'static, Message> + Send + Sync + 'static, + > = Box::new(view); + let boxed: Box = Box::new(boxed); + Arc::new(boxed) }), ) } @@ -1256,29 +1260,6 @@ where let my_bounds = layout.bounds(); let state = tree.state.downcast_mut::(); - // The compositor dismissed our context menu popup: nothing else tells this state about it. - #[cfg(wayland_platform)] - if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( - iced::event::wayland::Event::Popup(iced::event::wayland::PopupEvent::Done, _, popup), - )) = &event - { - let dismissed = state.menu_state.inner.with_data_mut(|data| { - if data.popup_id.get(&self.window_id) == Some(popup) { - data.popup_id.clear(); - data.reset(); - true - } else { - false - } - }); - if dismissed { - state.show_context = None; - for key in self.model.order.iter().copied() { - self.update_entity_paragraph(state, key); - } - } - } - let hovered_before = state.hovered; let my_id = self.get_drag_id(); @@ -2259,7 +2240,12 @@ where let menu_open = || { state.show_context == Some(key) - && state.menu_state.inner.with_data(|data| data.open) + && !tree.children.is_empty() + && tree.children[0] + .state + .downcast_ref::() + .inner + .with_data(|data| data.open) }; let key_is_active = self.model.is_active(key); diff --git a/src/widget/selectable_text.rs b/src/widget/selectable_text.rs index 67645d38..18328c40 100644 --- a/src/widget/selectable_text.rs +++ b/src/widget/selectable_text.rs @@ -355,8 +355,6 @@ impl<'a, Message: Clone + 'static> Widget for S selected_text, false, has_selection, - self.inner.has_text(&tree.children[0]), - self.inner.clipboard_has_text(&tree.children[0]), &menu_bar_state, &pending_action, renderer, diff --git a/src/widget/text_context_menu.rs b/src/widget/text_context_menu.rs index 0bf1eca9..2e908331 100644 --- a/src/widget/text_context_menu.rs +++ b/src/widget/text_context_menu.rs @@ -217,12 +217,7 @@ where let selected_text = widget.selected_text(tree); let is_editable = widget.is_editable(); - let mut menu_roots = build_menu_roots( - is_editable, - selected_text.is_some(), - widget.has_text(tree), - widget.clipboard_has_text(tree), - ); + let mut menu_roots = build_menu_roots(is_editable, selected_text.is_some()); menu_roots.iter_mut().for_each(menu::Tree::set_index); let bounds = Rectangle { @@ -288,28 +283,28 @@ pub(crate) enum TextCtxAction { SelectAll, } -fn build_menu_roots( - is_editable: bool, - has_selection: bool, - has_text: bool, - clipboard_has_text: bool, -) -> Vec> { - let item = |label: &'static str, action: TextCtxAction, enabled: bool| { - menu::Tree::from(crate::Element::from( - menu::menu_button(vec![widget::text(label).into()]) - .on_press_maybe(enabled.then_some(action)), - )) - }; - +fn build_menu_roots(is_editable: bool, has_selection: bool) -> Vec> { let mut items = Vec::with_capacity(4); - if is_editable { - items.push(item("Cut", TextCtxAction::Cut, has_selection)); + + if is_editable && has_selection { + items.push(menu::Tree::from(crate::Element::from( + menu::menu_button(vec![widget::text("Cut").into()]).on_press(TextCtxAction::Cut), + ))); } - items.push(item("Copy", TextCtxAction::Copy, has_selection)); - if is_editable { - items.push(item("Paste", TextCtxAction::Paste, clipboard_has_text)); + if has_selection { + items.push(menu::Tree::from(crate::Element::from( + menu::menu_button(vec![widget::text("Copy").into()]).on_press(TextCtxAction::Copy), + ))); } - items.push(item("Select All", TextCtxAction::SelectAll, has_text)); + if is_editable { + items.push(menu::Tree::from(crate::Element::from( + menu::menu_button(vec![widget::text("Paste").into()]).on_press(TextCtxAction::Paste), + ))); + } + items.push(menu::Tree::from(crate::Element::from( + menu::menu_button(vec![widget::text("Select All").into()]) + .on_press(TextCtxAction::SelectAll), + ))); vec![menu::Tree::with_children( RcElementWrapper::new(crate::Element::from(widget::Row::new())), @@ -491,8 +486,6 @@ pub(crate) fn create_text_context_popup( selected_text: Option, is_editable: bool, has_selection: bool, - has_text: bool, - clipboard_has_text: bool, menu_bar_state: &MenuBarState, pending_action: &PendingAction, renderer: &crate::Renderer, @@ -506,7 +499,7 @@ pub(crate) fn create_text_context_popup( return; } - let mut menu_roots = build_menu_roots(is_editable, has_selection, has_text, clipboard_has_text); + let mut menu_roots = build_menu_roots(is_editable, has_selection); menu_roots.iter_mut().for_each(menu::Tree::set_index); let id = menu_bar_state.inner.with_data_mut(|state| { diff --git a/src/widget/text_editor.rs b/src/widget/text_editor.rs index 1d303faf..f25cb277 100644 --- a/src/widget/text_editor.rs +++ b/src/widget/text_editor.rs @@ -297,8 +297,6 @@ impl<'a, Message: Clone + 'static> Widget( state.focus(); } state.context_menu_position = Some(pos); - state.clipboard_has_text = iced_core::widget::text::clipboard_has_text(clipboard); shell.capture_event(); return; } @@ -2752,7 +2747,7 @@ pub fn draw<'a, Message>( let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); #[cfg(not(wayland_platform))] let handling_dnd_offer = false; - let (cursors, offset, is_selecting) = if let Some(focus) = + let (cursors, offset, _is_selecting) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { let now = Instant::now(); handling_dnd_offer.then_some(Focus { @@ -2881,17 +2876,14 @@ pub fn draw<'a, Message>( effective_alignment(state.value.raw()), ); - let shift = Vector::new(alignment_offset - offset, 0.0); - let fill_cursors = |renderer: &mut crate::Renderer| { - renderer.with_translation(shift, |renderer| { + if cursors.is_empty() { + renderer.with_translation(Vector::ZERO, |_| {}); + } else { + renderer.with_translation(Vector::new(alignment_offset - offset, 0.0), |renderer| { for (quad, color) in &cursors { renderer.fill_quad(*quad, *color); } }); - }; - - if !is_selecting { - fill_cursors(renderer); } let bounds = Rectangle { @@ -2906,39 +2898,27 @@ pub fn draw<'a, Message>( text_color }; - let text = Text { - content: if text.is_empty() { - placeholder.to_string() - } else { - text.clone() + renderer.fill_text( + Text { + content: if text.is_empty() { + placeholder.to_string() + } else { + text.clone() + }, + font, + bounds: bounds.size(), + size: iced::Pixels(size), + align_x: text::Alignment::Default, + align_y: alignment::Vertical::Center, + line_height: text::LineHeight::default(), + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }, - font, - bounds: bounds.size(), - size: iced::Pixels(size), - align_x: text::Alignment::Default, - align_y: alignment::Vertical::Center, - line_height: text::LineHeight::default(), - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::None, - ellipsize: text::Ellipsize::None, - }; - renderer.fill_text(text.clone(), bounds.position(), color, text_bounds); - - // Redraw the same text in the selected color, clipped to the selection quads, - // so glyph shaping and positioning stay identical to the unselected pass. - if is_selecting { - fill_cursors(renderer); - for (quad, _) in &cursors { - renderer.with_layer(quad.bounds + shift, |renderer| { - renderer.fill_text( - text.clone(), - bounds.position(), - appearance.selected_text_color, - text_bounds, - ); - }); - } - } + bounds.position(), + color, + text_bounds, + ); }; // FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue. @@ -3082,7 +3062,6 @@ pub struct State { keyboard_modifiers: keyboard::Modifiers, scroll_offset: f32, context_menu_position: Option, - clipboard_has_text: bool, pub(crate) menu_bar_state: crate::widget::menu::MenuBarState, pub(crate) pending_action: crate::widget::text_context_menu::PendingAction, } @@ -3178,7 +3157,6 @@ impl State { scroll_offset: 0.0, dirty: false, context_menu_position: None, - clipboard_has_text: false, menu_bar_state: crate::widget::menu::MenuBarState::default(), pending_action: crate::widget::text_context_menu::pending_action(), } @@ -3542,14 +3520,6 @@ impl iced_core::widget::text::HasSelectableText true } - fn has_text(&self, tree: &WidgetTree) -> bool { - !tree.state.downcast_ref::().tracked_value.is_empty() - } - - fn clipboard_has_text(&self, tree: &WidgetTree) -> bool { - tree.state.downcast_ref::().clipboard_has_text - } - fn is_focused(&self, tree: &WidgetTree) -> bool { tree.state.downcast_ref::().is_focused() } diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index c621d966..b0287eed 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -40,7 +40,7 @@ pub struct Tooltip<'a, Message, TopLevelMessage> { label: Option>, content: crate::Element<'a, Message>, on_leave: Message, - on_surface_action: Box) -> Message>, + on_surface_action: Box Message>, width: Length, height: Length, padding: Padding, @@ -75,7 +75,7 @@ impl<'a, Message, TopLevelMessage> Tooltip<'a, Message, TopLevelMessage> { + Sync + 'static, on_leave: Message, - on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static, + on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static, ) -> Self { Self { id: Id::unique(), @@ -440,7 +440,7 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( >, delay: Option, on_leave: &Message, - on_surface_action: &dyn Fn(crate::surface::Action) -> Message, + on_surface_action: &dyn Fn(crate::surface::Action) -> Message, state: impl FnOnce() -> &'a mut State, ) { match event { @@ -534,8 +534,19 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( crate::surface::Action::Popup( Arc::new(boxed), Arc::new(boxed_live), - Some(Arc::new(move || view()) - as crate::surface::View), + Some({ + let boxed: Box< + dyn Fn() -> crate::Element< + 'static, + crate::Action, + > + Send + + Sync + + 'static, + > = Box::new(move || view()); + let boxed: Box = + Box::new(boxed); + Arc::new(boxed) + }), ) }) })); @@ -572,8 +583,19 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( let sm = crate::surface::Action::Popup( Arc::new(boxed), Arc::new(boxed_live), - Some(Arc::new(move || view()) - as crate::surface::View), + Some({ + let boxed: Box< + dyn Fn() -> crate::Element< + 'static, + crate::Action, + > + Send + + Sync + + 'static, + > = Box::new(move || view()); + let boxed: Box = + Box::new(boxed); + Arc::new(boxed) + }), ); shell.publish((on_surface_action)(sm)); }