From c916c89b033c98dfddb1933da0c763f3f726ce08 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 10 Mar 2025 22:38:02 -0400 Subject: [PATCH] refactor: more ergonomic dropdown popups --- examples/applet/src/window.rs | 90 +++-------------- iced | 2 +- src/process.rs | 1 - src/widget/dropdown/menu/mod.rs | 21 +++- src/widget/dropdown/mod.rs | 28 +++++- src/widget/dropdown/widget.rs | 167 +++++++++++++++++--------------- 6 files changed, 149 insertions(+), 160 deletions(-) diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 90676751..15085d85 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -1,29 +1,19 @@ use cosmic::app::{Core, Task}; -use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; -use cosmic::iced::event::listen_with; + use cosmic::iced::window::Id; -use cosmic::iced::{self, Length, Limits}; +use cosmic::iced::Length; use cosmic::iced_runtime::core::window; -use cosmic::iced_runtime::platform_specific::wayland::popup::SctkPopupSettings; -use cosmic::iced_runtime::platform_specific::wayland::subsurface; use cosmic::surface::action::{app_popup, destroy_popup}; -use cosmic::widget::dropdown::DropdownView; -use cosmic::widget::{autosize, dropdown, layer_container, list_column, settings, toggler}; -use cosmic::{iced_core, Element}; -use once_cell::sync::Lazy; +use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; +use cosmic::Element; const ID: &str = "com.system76.CosmicAppletExample"; -static SUBSURFACE_ID: Lazy = - Lazy::new(|| cosmic::widget::Id::new("subsurface")); - pub struct Window { core: Core, popup: Option, example_row: bool, selected: Option, - subsurface_id: Id, - dropdown_id: Id, } impl Default for Window { @@ -33,52 +23,18 @@ impl Default for Window { popup: None, example_row: false, selected: None, - subsurface_id: Id::unique(), - dropdown_id: Id::unique(), } } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Message { PopupClosed(Id), - PopupCloseRequested(Id), ToggleExampleRow(bool), Selected(usize), Surface(cosmic::surface::Action), - OpenDropdown(SctkPopupSettings, DropdownView), } -impl std::fmt::Debug for Message { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::PopupClosed(arg0) => f.debug_tuple("PopupClosed").field(arg0).finish(), - Self::PopupCloseRequested(arg0) => { - f.debug_tuple("PopupCloseRequested").field(arg0).finish() - } - Self::ToggleExampleRow(arg0) => f.debug_tuple("ToggleExampleRow").field(arg0).finish(), - Self::Selected(arg0) => f.debug_tuple("Selected").field(arg0).finish(), - Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(), - Self::OpenDropdown(arg0, _) => f.debug_tuple("OpenDropdown").field(arg0).finish(), - } - } -} - -// impl From for MessageWrapper { -// fn from(value: Message) -> Self { -// match value { -// Message::Surface(s) => MessageWrapper::Surface(s), -// m => MessageWrapper::Message(m), -// } -// } -// } - -// impl From for Message { -// fn from(value: SurfaceMessage) -> Self { -// Message::Surface(value) -// } -// } - impl cosmic::Application for Window { type Executor = cosmic::SingleThreadExecutor; type Flags = (); @@ -123,25 +79,6 @@ impl cosmic::Application for Window { } Message::Selected(i) => { self.selected = Some(i); - return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(cosmic::surface::action::destroy_popup( - self.dropdown_id, - )), - )); - } - Message::OpenDropdown(sctk_popup_settings, view) => { - self.dropdown_id = sctk_popup_settings.id; - return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(cosmic::surface::action::app_popup( - move |_: &mut Window| sctk_popup_settings.clone(), - Some(Box::new(move |_: &Window| view().map(cosmic::Action::App))), - )), - )); - } - Message::PopupCloseRequested(id) => { - return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(cosmic::surface::action::destroy_popup(id)), - )); } }; Task::none() @@ -178,15 +115,14 @@ impl cosmic::Application for Window { ) .height(Length::Fixed(50.)), )) - .add( - dropdown( - &["1", "asdf", "hello", "test"], - state.selected, - Message::Selected, - ) - .with_popup(state.popup.unwrap_or(Id::NONE), Message::OpenDropdown) - .on_close_popup(Message::PopupCloseRequested), - ); + .add(popup_dropdown( + &["1", "asdf", "hello", "test"], + state.selected, + Message::Selected, + state.popup.unwrap_or(Id::NONE), + Message::Surface, + |m| m, + )); Element::from(state.core.applet.popup_container(content_list)) .map(cosmic::Action::App) })), diff --git a/iced b/iced index fd229ebf..ea5570fd 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit fd229ebf90e0873f0f40a3906f6d92bceae56ba2 +Subproject commit ea5570fdd756f599637a60c7fed4b8a4f03f049e diff --git a/src/process.rs b/src/process.rs index 87d9e1cb..f76dad7e 100644 --- a/src/process.rs +++ b/src/process.rs @@ -43,7 +43,6 @@ pub async fn spawn(mut command: Command) -> Option { return None; }; - match unsafe { libc::fork() } { // Parent process 1.. => { diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 3d4a8c41..681a4c37 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -8,6 +8,7 @@ use std::sync::{Arc, Mutex}; pub use appearance::{Appearance, StyleSheet}; +use crate::surface; use crate::widget::{icon, Container, RcWrapper}; use iced_core::event::{self, Event}; use iced_core::layout::{self, Layout}; @@ -32,6 +33,7 @@ where hovered_option: Arc>>, selected_option: Option, on_selected: Box Message + 'a>, + close_on_selected: Option, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, width: f32, padding: Padding, @@ -54,6 +56,7 @@ where selected_option: Option, on_selected: impl FnMut(usize) -> Message + 'a, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, + close_on_selected: Option, ) -> Self { Menu { state, @@ -68,6 +71,7 @@ where text_size: None, text_line_height: text::LineHeight::default(), style: Default::default(), + close_on_selected, } } @@ -153,7 +157,7 @@ struct Overlay<'a, Message> { position: Point, } -impl<'a, Message: 'a> Overlay<'a, Message> { +impl<'a, Message: Clone + 'a> Overlay<'a, Message> { pub fn new>( menu: Menu<'a, S, Message>, target_height: f32, @@ -175,6 +179,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> { text_size, text_line_height, style, + close_on_selected, } = menu; let mut container = Container::new(Scrollable::new( @@ -184,6 +189,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> { hovered_option, selected_option, on_selected, + close_on_selected, on_option_hovered, text_size, text_line_height, @@ -297,7 +303,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> { } } -impl<'a, Message: 'a> iced_core::Overlay +impl<'a, Message: Clone + 'a> iced_core::Overlay for Overlay<'a, Message> { fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { @@ -338,7 +344,7 @@ impl<'a, Message: 'a> iced_core::Overlay } } -impl<'a, Message: 'a> crate::widget::Widget +impl<'a, Message: Clone + 'a> crate::widget::Widget for Overlay<'a, Message> { fn size(&self) -> Size { @@ -411,6 +417,7 @@ where hovered_option: Arc>>, selected_option: Option, on_selected: Box Message + 'a>, + close_on_selected: Option, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, padding: Padding, text_size: Option, @@ -420,6 +427,7 @@ where impl, Message> Widget for List<'_, S, Message> where [S]: std::borrow::ToOwned, + Message: Clone, { fn size(&self) -> Size { Size::new(Length::Fill, Length::Shrink) @@ -469,6 +477,9 @@ where if cursor.is_over(layout.bounds()) { if let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); + if let Some(close_on_selected) = self.close_on_selected.clone() { + shell.publish(close_on_selected); + } return event::Status::Captured; } } @@ -510,6 +521,9 @@ where if let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); + if let Some(close_on_selected) = self.close_on_selected.clone() { + shell.publish(close_on_selected); + } return event::Status::Captured; } } @@ -680,6 +694,7 @@ impl<'a, S: AsRef, Message: 'a> From> for Element<'a, Message, crate::Theme, crate::Renderer> where [S]: std::borrow::ToOwned, + Message: Clone, { fn from(list: List<'a, S, Message>) -> Self { Element::new(list) diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index 3480e494..9857f2ae 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -5,6 +5,7 @@ //! Displays a list of options in a popover menu on select. pub mod menu; +use iced_core::window; pub use menu::Menu; pub mod multi; @@ -12,6 +13,8 @@ pub mod multi; mod widget; pub use widget::*; +use crate::surface; + /// Displays a list of options in a popover menu on select. pub fn dropdown< S: AsRef + std::clone::Clone + Send + Sync + 'static, @@ -20,6 +23,29 @@ pub fn dropdown< selections: &[S], selected: Option, on_selected: impl Fn(usize) -> Message + Send + Sync + 'static, -) -> Dropdown<'_, S, Message> { +) -> Dropdown<'_, S, Message, Message> { Dropdown::new(selections, selected, on_selected) } + +#[cfg(all(feature = "winit", feature = "wayland"))] +/// Displays a list of options in a popover menu on select. +/// AppMessage must be the App's toplevel message. +pub fn popup_dropdown< + 'a, + S: AsRef + std::clone::Clone + Send + Sync + 'static, + Message: 'static + Clone, + AppMessage: 'static + Clone, +>( + selections: &'a [S], + 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, + map_action: impl Fn(Message) -> AppMessage + Send + Sync + 'static, +) -> Dropdown<'a, S, Message, AppMessage> { + Dropdown::new(selections, selected, on_selected).with_popup( + parent_id, + on_surface_action, + map_action, + ) +} diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 66fc4cdd..8b71004c 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -4,7 +4,7 @@ use super::menu::{self, Menu}; use crate::widget::icon::{self, Handle}; -use crate::Element; +use crate::{surface, Element}; use derive_setters::Setters; use iced::window; use iced_core::event::{self, Event}; @@ -18,6 +18,7 @@ use iced_widget::pick_list::{self, Catalog}; use std::borrow::Cow; use std::ffi::OsStr; use std::hash::{DefaultHasher, Hash, Hasher}; +use std::marker::PhantomData; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, LazyLock, Mutex}; @@ -26,7 +27,7 @@ static AUTOSIZE_ID: LazyLock = LazyLock::new(|| crate::widget::Id::new("cosmic-applet-autosize")); /// A widget for selecting a single value from a list of selections. #[derive(Setters)] -pub struct Dropdown<'a, S: AsRef + Send + Sync + Clone + 'static, Message> +pub struct Dropdown<'a, S: AsRef + Send + Sync + Clone + 'static, Message, AppMessage> where [S]: std::borrow::ToOwned, { @@ -48,26 +49,18 @@ where text_line_height: text::LineHeight, #[setters(strip_option)] font: Option, - #[cfg(feature = "wayland")] #[setters(skip)] - on_open: Option< - Arc< - dyn Fn( - iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, - DropdownView, - ) -> Message - + 'a, - >, - >, + on_surface_action: Option Message + Send + Sync + 'static>>, #[setters(skip)] - on_close_popup: Option Message + 'a>>, + action_map: Option AppMessage + 'static + Send + Sync>>, #[setters(strip_option)] window_id: Option, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "winit", feature = "wayland"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, } -impl<'a, S: AsRef + Send + Sync + Clone + 'static, Message: 'static> Dropdown<'a, S, Message> +impl<'a, S: AsRef + Send + Sync + Clone + 'static, Message: 'static, AppMessage: 'static> + Dropdown<'a, S, Message, AppMessage> where [S]: std::borrow::ToOwned, { @@ -95,33 +88,30 @@ where text_size: None, text_line_height: text::LineHeight::Relative(1.2), font: None, - #[cfg(feature = "wayland")] - on_open: None, window_id: None, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "winit", feature = "wayland"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), - on_close_popup: None, + on_surface_action: None, + action_map: None, } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "winit", feature = "wayland"))] /// Handle dropdown requests for popup creation. /// Intended to be used with [`crate::app::message::get_popup`] pub fn with_popup( mut self, parent_id: window::Id, - on_open: impl Fn( - iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, - DropdownView, - ) -> Message - + 'a, + on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static, + action_map: impl Fn(Message) -> AppMessage + Send + Sync + 'static, ) -> Self { self.window_id = Some(parent_id); - self.on_open = Some(Arc::new(on_open)); + self.on_surface_action = Some(Arc::new(on_surface_action)); + self.action_map = Some(Arc::new(action_map)); self } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "winit", feature = "wayland"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -129,18 +119,13 @@ where self.positioner = positioner; self } - - #[cfg(feature = "wayland")] - /// Handle dropdown requests for popup removal. - /// Intended to be used with [`crate::app::message::destroy_popup`] - pub fn on_close_popup(mut self, on_close: impl Fn(window::Id) -> Message + 'a) -> Self { - self.on_close_popup = Some(Box::new(on_close)); - self - } } -impl + Send + Sync + Clone + 'static, Message: 'static + std::clone::Clone> - Widget for Dropdown<'_, S, Message> +impl< + S: AsRef + Send + Sync + Clone + 'static, + Message: 'static + Clone, + AppMessage: 'static + Clone, + > Widget for Dropdown<'_, S, Message, AppMessage> where [S]: std::borrow::ToOwned, { @@ -225,21 +210,20 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( + update::( &event, layout, cursor, shell, - #[cfg(feature = "wayland")] - self.on_open.clone(), - #[cfg(feature = "wayland")] + #[cfg(all(feature = "winit", feature = "wayland"))] self.positioner.clone(), self.on_selected.clone(), self.selected, self.selections, || tree.state.downcast_mut::(), self.window_id, - self.on_close_popup.as_deref(), + self.on_surface_action.clone(), + self.action_map.clone(), self.icons, self.gap, self.padding, @@ -295,8 +279,8 @@ where renderer: &crate::Renderer, translation: Vector, ) -> Option> { - #[cfg(feature = "wayland")] - if self.on_open.is_some() || self.window_id.is_some() || self.on_close_popup.is_some() { + #[cfg(all(feature = "winit", feature = "wayland"))] + if self.window_id.is_some() || self.on_surface_action.is_some() { return None; } @@ -316,6 +300,7 @@ where self.selected, self.on_selected.as_ref(), translation, + None, ) } @@ -331,12 +316,16 @@ where // } } -impl<'a, S: AsRef + Send + Sync + Clone + 'static, Message: 'static + std::clone::Clone> - From> for crate::Element<'a, Message> +impl< + 'a, + S: AsRef + Send + Sync + Clone + 'static, + Message: 'static + std::clone::Clone, + AppMessage: 'static + std::clone::Clone, + > From> for crate::Element<'a, Message> where [S]: std::borrow::ToOwned, { - fn from(pick_list: Dropdown<'a, S, Message>) -> Self { + fn from(pick_list: Dropdown<'a, S, Message, AppMessage>) -> Self { Self::new(pick_list) } } @@ -441,33 +430,26 @@ pub fn layout( /// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`] /// accordingly. -#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments, clippy::too_many_lines)] pub fn update< 'a, - S: AsRef + Send + Sync + std::clone::Clone + 'static, + S: AsRef + Send + Sync + Clone + 'static, Message: Clone + 'static, + AppMessage: Clone + 'static, >( event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - #[cfg(feature = "wayland")] on_open: Option< - Arc< - dyn Fn( - iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, - DropdownView, - ) -> Message - + 'a, - >, - >, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "winit", feature = "wayland"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, on_selected: Arc Message + Send + Sync + 'static>, selected: Option, selections: &[S], state: impl FnOnce() -> &'a mut State, - window_id: Option, - on_close: Option<&'a dyn Fn(window::Id) -> Message>, + _window_id: Option, + on_surface_action: Option Message + Send + Sync + 'static>>, + action_map: Option AppMessage + Send + Sync + 'static>>, icons: &[icon::Handle], gap: f32, padding: Padding, @@ -484,8 +466,9 @@ pub fn update< // Event wasn't processed by overlay, so cursor was clicked either outside it's // bounds or on the drop-down, either way we close the overlay. state.is_open.store(false, Ordering::Relaxed); - if let Some(on_close) = on_close { - shell.publish(on_close(state.popup_id)); + #[cfg(all(feature = "winit", feature = "wayland"))] + if let Some(on_close) = on_surface_action { + shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } event::Status::Captured } else if cursor.is_over(layout.bounds()) { @@ -494,8 +477,10 @@ pub fn update< *hovered_guard = selected; let id = window::Id::unique(); state.popup_id = id; - #[cfg(feature = "wayland")] - if let Some((on_open, parent)) = on_open.zip(window_id) { + #[cfg(all(feature = "winit", feature = "wayland"))] + if let Some(((on_surface_action, parent), action_map)) = + on_surface_action.zip(_window_id).zip(action_map) + { use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; @@ -521,9 +506,19 @@ pub fn update< let icons: Cow<'static, [Handle]> = Cow::Owned(icons.to_vec()); let selections: Cow<'static, [S]> = Cow::Owned(selections.to_vec()); let state = state.clone(); - - shell.publish(on_open( - SctkPopupSettings { + let on_close = surface::action::destroy_popup(id); + let on_surface_action_clone = on_surface_action.clone(); + let get_popup_action = surface::action::simple_popup::< + AppMessage, + Box< + dyn Fn() -> Element<'static, crate::Action> + + Send + + Sync + + 'static, + >, + >( + move || { + SctkPopupSettings { parent, id, input_zone: None, @@ -538,15 +533,29 @@ pub fn update< parent_size: None, grab: true, close_with_children: true, + } }, - Arc::new(move || { - Element::from( - menu_widget( - bounds, &state, gap, padding,text_size.unwrap_or(14.0), selections.clone(), icons.clone(), selected_option, on_selected.clone() - ) - ) - }), - )); + Some(Box::new(move || { + let action_map = action_map.clone(); + let on_selected = on_selected.clone(); + let e: Element<'static, crate::Action> = + Element::from(menu_widget( + bounds, + &state, + gap, + padding, + text_size.unwrap_or(14.0), + selections.clone(), + icons.clone(), + selected_option, + Arc::new(move |i| on_selected.clone()(i)), + Some(on_surface_action_clone(on_close.clone())), + )) + .map(move |m| crate::Action::App(action_map.clone()(m))); + e + })), + ); + shell.publish(on_surface_action(get_popup_action)); } event::Status::Captured } else { @@ -595,7 +604,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } -#[cfg(feature = "wayland")] +#[cfg(all(feature = "winit", feature = "wayland"))] /// Returns the current menu widget of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn menu_widget< @@ -611,6 +620,7 @@ pub fn menu_widget< icons: Cow<'static, [icon::Handle]>, selected_option: Option, on_selected: Arc Message + Send + Sync + 'static>, + close_on_selected: Option, ) -> crate::Element<'static, Message> where [S]: std::borrow::ToOwned, @@ -640,6 +650,7 @@ where (on_selected)(option) }, None, + close_on_selected, ) .width(width) .padding(padding) @@ -672,6 +683,7 @@ pub fn overlay<'a, S: AsRef + Send + Sync + Clone + 'static, Message: std:: selected_option: Option, on_selected: &'a dyn Fn(usize) -> Message, translation: Vector, + close_on_selected: Option, ) -> Option> where [S]: std::borrow::ToOwned, @@ -691,6 +703,7 @@ where (on_selected)(option) }, None, + close_on_selected, ) .width({ let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {