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-icons b/cosmic-icons index 343c007f..52520957 160000 --- a/cosmic-icons +++ b/cosmic-icons @@ -1 +1 @@ -Subproject commit 343c007f37cd71716e68f01c43ecf2764b7f7c47 +Subproject commit 5252095787cc96e2aed64604158f94e450703455 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/iced b/iced index d12bb8e0..e891f34b 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d12bb8e056712ab87efbdd4296261331772ec413 +Subproject commit e891f34b3224b25fc4bdf286b3f9dba26c8abe5b 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 22f07a08..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) @@ -472,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) => { @@ -562,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( @@ -1189,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); 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 a4308306..b2864d52 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -1,8 +1,7 @@ // Copyright 2025 System76 // SPDX-License-Identifier: MPL-2.0 -use super::{Action, View}; -#[cfg(feature = "winit")] +use super::Action; use crate::Application; use iced::{Rectangle, window}; @@ -11,31 +10,30 @@ 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) } @@ -67,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> = @@ -101,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); @@ -116,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) + }), ), ) } @@ -130,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 @@ -164,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 @@ -176,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) + }), ) } @@ -192,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 @@ -207,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) + }), ) } @@ -223,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, @@ -257,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 @@ -271,7 +287,13 @@ pub fn simple_layer_shell( Action::LayerShell( 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) + }), ) } @@ -288,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 09243da3..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,7 +50,7 @@ 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), @@ -75,74 +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), - 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) - } -} - -#[cfg(feature = "winit")] -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 { @@ -152,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() @@ -167,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(), @@ -193,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(), @@ -208,11 +139,11 @@ 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() 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/widget/context_menu.rs b/src/widget/context_menu.rs index 2169c5ba..15c78ef8 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -33,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, }; @@ -56,33 +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( @@ -115,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; @@ -136,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), @@ -235,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 @@ -255,7 +235,6 @@ impl Widget context_cursor: Point::default(), fingers_pressed: Default::default(), menu_bar_state: Default::default(), - reported_open: false, }) } @@ -290,11 +269,9 @@ 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() { - state.menu_bar_state.inner.with_data_mut(|inner| { - menu_roots_diff(context_menu, &mut inner.tree); - }); - } + 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 @@ -342,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, @@ -401,20 +346,6 @@ impl Widget 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(); - } - }); - } - // 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 @@ -494,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)) @@ -532,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 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")] @@ -645,5 +560,4 @@ pub struct LocalState { context_cursor: Point, fingers_pressed: HashSet, menu_bar_state: MenuBarState, - reported_open: 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 b20fc94a..50dc9b64 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -176,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 @@ -328,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 @@ -377,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 { @@ -580,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 diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index e9e585ba..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> { diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 36943759..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,84 +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(); - let key = key_binds - .iter() - .find(|(_, a)| **a == action) - .map_or_else(String::new, |(k, _)| k.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. @@ -376,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; @@ -393,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(); 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 fb7fea66..b9704244 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -157,17 +157,9 @@ where ) { if self.popup.is_some() { if self.modal { - 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; - } - _ => {} + if matches!(event, Event::Mouse(_) | Event::Touch(_)) { + shell.capture_event(); + return; } } else if let Some(on_close) = self.on_close.as_ref() && matches!( 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/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/text_input/input.rs b/src/widget/text_input/input.rs index 262d9389..d7a3634a 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -7,7 +7,6 @@ //! A [`TextInput`] has some local [`State`]. use std::borrow::Cow; use std::cell::{Cell, LazyCell}; -use unicode_segmentation::UnicodeSegmentation; use crate::ext::ColorExt; use crate::theme::THEME; @@ -2540,63 +2539,6 @@ fn input_method<'b>( } } -#[allow(clippy::too_many_arguments)] -fn render_graphemes( - value: &Value, - state: &State, - left: usize, - right: usize, - text: &str, - text_color: Color, - bounds: Rectangle, - size: f32, - renderer: &mut crate::Renderer, - font: iced_core::Font, -) { - let lo_byte = value.byte_index_at_grapheme(left); - let hi_byte = value.byte_index_at_grapheme(right); - - let rects = state.value.raw().highlight( - 0, - (lo_byte, text::Affinity::After), - (hi_byte, text::Affinity::Before), - ); - - if !rects.is_empty() { - let grapheme_range = text[lo_byte..hi_byte].to_string(); - let origin = bounds.position(); - - let start_pos = origin + (rects.first().unwrap().position() - Point::ORIGIN); - - for rect in rects { - let absolute_rect = Rectangle { - x: rect.x + origin.x, - y: rect.y + origin.y, - width: rect.width, - height: rect.height, - }; - - renderer.fill_text( - Text { - content: grapheme_range.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, - }, - start_pos, - text_color, - absolute_rect, - ); - } - } -} - /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. /// @@ -2805,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, _) = 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 { @@ -2879,6 +2821,7 @@ pub fn draw<'a, Message>( (lo_byte, text::Affinity::After), (hi_byte, text::Affinity::Before), ); + let cursors: Vec<(renderer::Quad, Color)> = rects .into_iter() .map(|r| { @@ -2932,6 +2875,7 @@ pub fn draw<'a, Message>( state.value.raw().min_width(), effective_alignment(state.value.raw()), ); + if cursors.is_empty() { renderer.with_translation(Vector::ZERO, |_| {}); } else { @@ -2948,77 +2892,33 @@ pub fn draw<'a, Message>( width: actual_width, ..text_bounds }; - let tcolor = if text.is_empty() { + let color = if text.is_empty() { appearance.placeholder_color } else { text_color }; - if let cursor::State::Selection { start, end } = state.cursor.state(value) - && state.is_focused() - { - let left = start.min(end); - let right = end.max(start); - let grapheme_len = text.graphemes(true).count(); - - if left > 0 { - render_graphemes( - value, state, 0, left, &text, tcolor, bounds, size, renderer, font, - ); - } - - if grapheme_len >= right { - render_graphemes( - value, - state, - left, - right, - &text, - appearance.selected_text_color, - bounds, - size, - renderer, - font, - ); - } - - if left < grapheme_len { - render_graphemes( - value, - state, - right, - grapheme_len, - &text, - tcolor, - bounds, - size, - renderer, - font, - ); - } - } else { - 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, + renderer.fill_text( + Text { + content: if text.is_empty() { + placeholder.to_string() + } else { + text.clone() }, - bounds.position(), - tcolor, - text_bounds, - ); - } + 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, + }, + 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. diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 7b7bbc08..03bdf1bd 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -142,26 +142,23 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - let (content_state, toasts_state) = state.children.split_at_mut(1); - let content = self.content.as_widget_mut().overlay( - &mut content_state[0], - layout, - renderer, - viewport, - translation, - ); + //TODO: this hides the overlay of the content during the toast if self.is_empty { - return content; + self.content.as_widget_mut().overlay( + &mut state.children[0], + layout, + renderer, + viewport, + translation, + ) + } else { + let _bounds = layout.bounds(); + + Some(overlay::Element::new(Box::new(ToasterOverlay::new( + &mut state.children[1], + &mut self.toasts, + )))) } - // keep the content's overlays (e.g. popovers, menus) visible while a toast is shown - let toaster = overlay::Element::new(Box::new(ToasterOverlay::new( - &mut toasts_state[0], - &mut self.toasts, - ))); - Some(match content { - Some(content) => overlay::Group::with_children(vec![content, toaster]).overlay(), - None => toaster, - }) } fn drag_destinations( 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)); }