From 1fce5df160f595d1b1e5a8e2bb2a24775419f82d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 23 May 2025 12:40:10 -0400 Subject: [PATCH] refactor: add optional parameter for layout offset and bounds for button handlers Buttons are often used for toggling popups, so something allowing more straightforward positioning is important. --- examples/applet/src/window.rs | 100 ++++++++++++++++++-------------- src/applet/mod.rs | 6 +- src/widget/button/mod.rs | 6 +- src/widget/button/widget.rs | 102 ++++++++++++++++++++++++++++----- src/widget/calendar.rs | 2 +- src/widget/color_picker/mod.rs | 8 ++- src/widget/menu/menu_tree.rs | 11 +++- 7 files changed, 165 insertions(+), 70 deletions(-) diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 4a822cea..66b2040a 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -1,7 +1,7 @@ use cosmic::app::{Core, Task}; use cosmic::iced::window::Id; -use cosmic::iced::Length; +use cosmic::iced::{Length, Rectangle}; use cosmic::iced_runtime::core::window; use cosmic::surface::action::{app_popup, destroy_popup}; use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; @@ -85,50 +85,62 @@ impl cosmic::Application for Window { } fn view(&self) -> Element { - let btn = self.core.applet.icon_button("display-symbolic").on_press( - if let Some(id) = self.popup { - Message::Surface(destroy_popup(id)) - } else { - Message::Surface(app_popup::( - |state: &mut Window| { - let new_id = Id::unique(); - state.popup = Some(new_id); - let popup_settings = state.core.applet.get_popup_settings( - state.core.main_window_id().unwrap(), - new_id, - None, - None, - None, - ); + let have_popup = self.popup.clone(); + let btn = self + .core + .applet + .icon_button("display-symbolic") + .on_press_with_rectangle(move |offset, bounds| { + if let Some(id) = have_popup { + Message::Surface(destroy_popup(id)) + } else { + Message::Surface(app_popup::( + move |state: &mut Window| { + let new_id = Id::unique(); + state.popup = Some(new_id); + let mut popup_settings = state.core.applet.get_popup_settings( + state.core.main_window_id().unwrap(), + new_id, + None, + None, + None, + ); - popup_settings - }, - Some(Box::new(move |state: &Window| { - let content_list = list_column() - .padding(5) - .spacing(0) - .add(settings::item( - "Example row", - cosmic::widget::container( - toggler(state.example_row) - .on_toggle(|value| Message::ToggleExampleRow(value)), - ) - .height(Length::Fixed(50.)), - )) - .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) - })), - )) - }, - ); + popup_settings.positioner.anchor_rect = Rectangle { + x: (bounds.x - offset.x) as i32, + y: (bounds.y - offset.y) as i32, + width: bounds.width as i32, + height: bounds.height as i32, + }; + + popup_settings + }, + Some(Box::new(move |state: &Window| { + let content_list = list_column() + .padding(5) + .spacing(0) + .add(settings::item( + "Example row", + cosmic::widget::container( + toggler(state.example_row) + .on_toggle(|value| Message::ToggleExampleRow(value)), + ) + .height(Length::Fixed(50.)), + )) + .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) + })), + )) + } + }); Element::from(self.core.applet.applet_tooltip::( btn, diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 8f8cbfc0..02a8c004 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -177,7 +177,7 @@ impl Context { matches!(self.anchor, PanelAnchor::Top | PanelAnchor::Bottom) } - pub fn icon_button_from_handle<'a, Message: 'static>( + pub fn icon_button_from_handle<'a, Message: Clone + 'static>( &self, icon: widget::icon::Handle, ) -> crate::widget::Button<'a, Message> { @@ -206,7 +206,7 @@ impl Context { .class(Button::AppletIcon) } - pub fn icon_button<'a, Message: 'static>( + pub fn icon_button<'a, Message: Clone + 'static>( &self, icon_name: &'a str, ) -> crate::widget::Button<'a, Message> { @@ -503,7 +503,7 @@ pub fn style() -> iced_runtime::Appearance { } } -pub fn menu_button<'a, Message>( +pub fn menu_button<'a, Message: Clone + 'a>( content: impl Into>, ) -> crate::widget::Button<'a, Message> { crate::widget::button::custom(content) diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs index 721ee8b7..d9a4df94 100644 --- a/src/widget/button/mod.rs +++ b/src/widget/button/mod.rs @@ -44,12 +44,14 @@ use iced_core::{Length, Padding}; use std::borrow::Cow; /// A button with a custom element for its content. -pub fn custom<'a, Message>(content: impl Into>) -> Button<'a, Message> { +pub fn custom<'a, Message: Clone + 'a>( + content: impl Into>, +) -> Button<'a, Message> { Button::new(content.into()) } /// An image button which may contain any widget as its content. -pub fn custom_image_button<'a, Message>( +pub fn custom_image_button<'a, Message: Clone + 'a>( content: impl Into>, on_remove: Option, ) -> Button<'a, Message> { diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index bbf5e821..5a1da458 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -47,8 +47,8 @@ pub struct Button<'a, Message> { #[cfg(feature = "a11y")] label: Option>, content: crate::Element<'a, Message>, - on_press: Option, - on_press_down: Option, + on_press: Option Message + 'a>>, + on_press_down: Option Message + 'a>>, width: Length, height: Length, padding: Padding, @@ -58,7 +58,7 @@ pub struct Button<'a, Message> { force_enabled: bool, } -impl<'a, Message> Button<'a, Message> { +impl<'a, Message: Clone + 'a> Button<'a, Message> { /// Creates a new [`Button`] with the given content. pub(super) fn new(content: impl Into>) -> Self { Self { @@ -150,7 +150,19 @@ impl<'a, Message> Button<'a, Message> { /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. #[inline] pub fn on_press(mut self, on_press: Message) -> Self { - self.on_press = Some(on_press); + self.on_press = Some(Box::new(move |_, _| on_press.clone())); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed and released. + /// + /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. + #[inline] + pub fn on_press_with_rectangle( + mut self, + on_press: impl Fn(Vector, Rectangle) -> Message + 'a, + ) -> Self { + self.on_press = Some(Box::new(on_press)); self } @@ -159,7 +171,19 @@ impl<'a, Message> Button<'a, Message> { /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. #[inline] pub fn on_press_down(mut self, on_press: Message) -> Self { - self.on_press_down = Some(on_press); + self.on_press_down = Some(Box::new(move |_, _| on_press.clone())); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed, + /// + /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. + #[inline] + pub fn on_press_down_with_rectange( + mut self, + on_press: impl Fn(Vector, Rectangle) -> Message + 'a, + ) -> Self { + self.on_press_down = Some(Box::new(on_press)); self } @@ -169,7 +193,49 @@ impl<'a, Message> Button<'a, Message> { /// If `None`, the [`Button`] will be disabled. #[inline] pub fn on_press_maybe(mut self, on_press: Option) -> Self { - self.on_press = on_press; + if let Some(m) = on_press { + self.on_press(m) + } else { + self.on_press = None; + self + } + } + + /// Sets the message that will be produced when the [`Button`] is pressed and released. + /// + /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. + #[inline] + pub fn on_press_maybe_with_rectangle( + mut self, + on_press: impl Fn(Vector, Rectangle) -> Message + 'a, + ) -> Self { + self.on_press = Some(Box::new(on_press)); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed, + /// if `Some`. + /// + /// If `None`, the [`Button`] will be disabled. + #[inline] + pub fn on_press_down_maybe(mut self, on_press: Option) -> Self { + if let Some(m) = on_press { + self.on_press(m) + } else { + self.on_press_down = None; + self + } + } + + /// Sets the message that will be produced when the [`Button`] is pressed and released. + /// + /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled. + #[inline] + pub fn on_press_down_maybe_with_rectangle( + mut self, + on_press: impl Fn(Vector, Rectangle) -> Message + 'a, + ) -> Self { + self.on_press_down = Some(Box::new(on_press)); self } @@ -350,8 +416,8 @@ impl<'a, Message: 'a + Clone> Widget layout, cursor, shell, - &self.on_press, - &self.on_press_down, + self.on_press.as_deref(), + self.on_press_down.as_deref(), || tree.state.downcast_mut::(), ) } @@ -710,15 +776,15 @@ impl State { /// Processes the given [`Event`] and updates the [`State`] of a [`Button`] /// accordingly. -#[allow(clippy::needless_pass_by_value)] +#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)] pub fn update<'a, Message: Clone>( _id: Id, event: Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - on_press: &Option, - on_press_down: &Option, + on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>, + on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>, state: impl FnOnce() -> &'a mut State, ) -> event::Status { match event { @@ -735,7 +801,8 @@ pub fn update<'a, Message: Clone>( state.is_pressed = true; if let Some(on_press_down) = on_press_down { - shell.publish(on_press_down.clone()); + let msg = (on_press_down)(layout.virtual_offset(), layout.bounds()); + shell.publish(msg); } return event::Status::Captured; @@ -753,7 +820,8 @@ pub fn update<'a, Message: Clone>( let bounds = layout.bounds(); if cursor.is_over(bounds) { - shell.publish(on_press); + let msg = (on_press)(layout.virtual_offset(), layout.bounds()); + shell.publish(msg); } return event::Status::Captured; @@ -771,7 +839,9 @@ pub fn update<'a, Message: Clone>( .then(|| on_press.clone()) { state.is_pressed = false; - shell.publish(on_press); + let msg = (on_press)(layout.virtual_offset(), layout.bounds()); + + shell.publish(msg); } return event::Status::Captured; } @@ -780,7 +850,9 @@ pub fn update<'a, Message: Clone>( let state = state(); if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) { state.is_pressed = true; - shell.publish(on_press); + let msg = (on_press)(layout.virtual_offset(), layout.bounds()); + + shell.publish(msg); return event::Status::Captured; } } diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 826c46f9..83b1dcfd 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -191,7 +191,7 @@ where } } -fn date_button( +fn date_button( date: NaiveDate, is_currently_viewed_month: bool, is_currently_selected_day: bool, diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 1762330d..7d088515 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -122,7 +122,11 @@ impl ColorPickerModel { /// Get a color picker button that displays the applied color /// - pub fn picker_button<'a, Message: 'static, T: Fn(ColorPickerUpdate) -> Message>( + pub fn picker_button< + 'a, + Message: 'static + std::clone::Clone, + T: Fn(ColorPickerUpdate) -> Message, + >( &self, f: T, icon_portion: Option, @@ -888,7 +892,7 @@ fn color_to_string(c: palette::Hsv, is_hex: bool) -> String { #[allow(clippy::too_many_lines)] /// A button for selecting a color from a color picker. -pub fn color_button<'a, Message: 'static>( +pub fn color_button<'a, Message: Clone + 'static>( on_press: Option, color: Option, icon_portion: Length, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 921b4dba..1f3fd4ab 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -142,9 +142,12 @@ where } } -pub fn menu_button<'a, Message: 'a>( +pub fn menu_button<'a, Message>( children: Vec>, -) -> crate::widget::Button<'a, Message> { +) -> crate::widget::Button<'a, Message> +where + Message: std::clone::Clone + 'a, +{ widget::button::custom( widget::Row::with_children(children) .align_y(Alignment::Center) @@ -195,6 +198,7 @@ pub fn menu_root<'a, Message, Renderer: renderer::Renderer>( ) -> Button<'a, Message> where Element<'a, Message, crate::Theme, Renderer>: From>, + Message: std::clone::Clone + 'a, { widget::button::custom(widget::text(label)) .padding([4, 12]) @@ -215,7 +219,7 @@ pub fn menu_items< 'a, A: MenuAction, L: Into> + 'static, - Message: 'a, + Message, Renderer: renderer::Renderer + 'a, >( key_binds: &HashMap, @@ -223,6 +227,7 @@ pub fn menu_items< ) -> Vec> where Element<'a, Message, crate::Theme, Renderer>: From>, + Message: 'a + Clone, { fn find_key(action: &A, key_binds: &HashMap) -> String { for (key_bind, key_action) in key_binds {