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.
This commit is contained in:
Ashley Wulber 2025-05-23 12:40:10 -04:00 committed by Michael Murphy
parent b61a7ebd5f
commit 1fce5df160
7 changed files with 165 additions and 70 deletions

View file

@ -1,7 +1,7 @@
use cosmic::app::{Core, Task}; use cosmic::app::{Core, Task};
use cosmic::iced::window::Id; use cosmic::iced::window::Id;
use cosmic::iced::Length; use cosmic::iced::{Length, Rectangle};
use cosmic::iced_runtime::core::window; use cosmic::iced_runtime::core::window;
use cosmic::surface::action::{app_popup, destroy_popup}; use cosmic::surface::action::{app_popup, destroy_popup};
use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler};
@ -85,50 +85,62 @@ impl cosmic::Application for Window {
} }
fn view(&self) -> Element<Message> { fn view(&self) -> Element<Message> {
let btn = self.core.applet.icon_button("display-symbolic").on_press( let have_popup = self.popup.clone();
if let Some(id) = self.popup { let btn = self
Message::Surface(destroy_popup(id)) .core
} else { .applet
Message::Surface(app_popup::<Window>( .icon_button("display-symbolic")
|state: &mut Window| { .on_press_with_rectangle(move |offset, bounds| {
let new_id = Id::unique(); if let Some(id) = have_popup {
state.popup = Some(new_id); Message::Surface(destroy_popup(id))
let popup_settings = state.core.applet.get_popup_settings( } else {
state.core.main_window_id().unwrap(), Message::Surface(app_popup::<Window>(
new_id, move |state: &mut Window| {
None, let new_id = Id::unique();
None, state.popup = Some(new_id);
None, let mut popup_settings = state.core.applet.get_popup_settings(
); state.core.main_window_id().unwrap(),
new_id,
None,
None,
None,
);
popup_settings popup_settings.positioner.anchor_rect = Rectangle {
}, x: (bounds.x - offset.x) as i32,
Some(Box::new(move |state: &Window| { y: (bounds.y - offset.y) as i32,
let content_list = list_column() width: bounds.width as i32,
.padding(5) height: bounds.height as i32,
.spacing(0) };
.add(settings::item(
"Example row", popup_settings
cosmic::widget::container( },
toggler(state.example_row) Some(Box::new(move |state: &Window| {
.on_toggle(|value| Message::ToggleExampleRow(value)), let content_list = list_column()
) .padding(5)
.height(Length::Fixed(50.)), .spacing(0)
)) .add(settings::item(
.add(popup_dropdown( "Example row",
&["1", "asdf", "hello", "test"], cosmic::widget::container(
state.selected, toggler(state.example_row)
Message::Selected, .on_toggle(|value| Message::ToggleExampleRow(value)),
state.popup.unwrap_or(Id::NONE), )
Message::Surface, .height(Length::Fixed(50.)),
|m| m, ))
)); .add(popup_dropdown(
Element::from(state.core.applet.popup_container(content_list)) &["1", "asdf", "hello", "test"],
.map(cosmic::Action::App) 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::<Message>( Element::from(self.core.applet.applet_tooltip::<Message>(
btn, btn,

View file

@ -177,7 +177,7 @@ impl Context {
matches!(self.anchor, PanelAnchor::Top | PanelAnchor::Bottom) 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, &self,
icon: widget::icon::Handle, icon: widget::icon::Handle,
) -> crate::widget::Button<'a, Message> { ) -> crate::widget::Button<'a, Message> {
@ -206,7 +206,7 @@ impl Context {
.class(Button::AppletIcon) .class(Button::AppletIcon)
} }
pub fn icon_button<'a, Message: 'static>( pub fn icon_button<'a, Message: Clone + 'static>(
&self, &self,
icon_name: &'a str, icon_name: &'a str,
) -> crate::widget::Button<'a, Message> { ) -> 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<Element<'a, Message>>, content: impl Into<Element<'a, Message>>,
) -> crate::widget::Button<'a, Message> { ) -> crate::widget::Button<'a, Message> {
crate::widget::button::custom(content) crate::widget::button::custom(content)

View file

@ -44,12 +44,14 @@ use iced_core::{Length, Padding};
use std::borrow::Cow; use std::borrow::Cow;
/// A button with a custom element for its content. /// A button with a custom element for its content.
pub fn custom<'a, Message>(content: impl Into<crate::Element<'a, Message>>) -> Button<'a, Message> { pub fn custom<'a, Message: Clone + 'a>(
content: impl Into<crate::Element<'a, Message>>,
) -> Button<'a, Message> {
Button::new(content.into()) Button::new(content.into())
} }
/// An image button which may contain any widget as its content. /// 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<crate::Element<'a, Message>>, content: impl Into<crate::Element<'a, Message>>,
on_remove: Option<Message>, on_remove: Option<Message>,
) -> Button<'a, Message> { ) -> Button<'a, Message> {

View file

@ -47,8 +47,8 @@ pub struct Button<'a, Message> {
#[cfg(feature = "a11y")] #[cfg(feature = "a11y")]
label: Option<Vec<iced_accessibility::accesskit::NodeId>>, label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
content: crate::Element<'a, Message>, content: crate::Element<'a, Message>,
on_press: Option<Message>, on_press: Option<Box<dyn Fn(Vector, Rectangle) -> Message + 'a>>,
on_press_down: Option<Message>, on_press_down: Option<Box<dyn Fn(Vector, Rectangle) -> Message + 'a>>,
width: Length, width: Length,
height: Length, height: Length,
padding: Padding, padding: Padding,
@ -58,7 +58,7 @@ pub struct Button<'a, Message> {
force_enabled: bool, 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. /// Creates a new [`Button`] with the given content.
pub(super) fn new(content: impl Into<crate::Element<'a, Message>>) -> Self { pub(super) fn new(content: impl Into<crate::Element<'a, Message>>) -> Self {
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. /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled.
#[inline] #[inline]
pub fn on_press(mut self, on_press: Message) -> Self { 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 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. /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled.
#[inline] #[inline]
pub fn on_press_down(mut self, on_press: Message) -> Self { 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 self
} }
@ -169,7 +193,49 @@ impl<'a, Message> Button<'a, Message> {
/// If `None`, the [`Button`] will be disabled. /// If `None`, the [`Button`] will be disabled.
#[inline] #[inline]
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self { pub fn on_press_maybe(mut self, on_press: Option<Message>) -> 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<Message>) -> 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 self
} }
@ -350,8 +416,8 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
layout, layout,
cursor, cursor,
shell, shell,
&self.on_press, self.on_press.as_deref(),
&self.on_press_down, self.on_press_down.as_deref(),
|| tree.state.downcast_mut::<State>(), || tree.state.downcast_mut::<State>(),
) )
} }
@ -710,15 +776,15 @@ impl State {
/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] /// Processes the given [`Event`] and updates the [`State`] of a [`Button`]
/// accordingly. /// accordingly.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
pub fn update<'a, Message: Clone>( pub fn update<'a, Message: Clone>(
_id: Id, _id: Id,
event: Event, event: Event,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
on_press: &Option<Message>, on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>,
on_press_down: &Option<Message>, on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>,
state: impl FnOnce() -> &'a mut State, state: impl FnOnce() -> &'a mut State,
) -> event::Status { ) -> event::Status {
match event { match event {
@ -735,7 +801,8 @@ pub fn update<'a, Message: Clone>(
state.is_pressed = true; state.is_pressed = true;
if let Some(on_press_down) = on_press_down { 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; return event::Status::Captured;
@ -753,7 +820,8 @@ pub fn update<'a, Message: Clone>(
let bounds = layout.bounds(); let bounds = layout.bounds();
if cursor.is_over(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; return event::Status::Captured;
@ -771,7 +839,9 @@ pub fn update<'a, Message: Clone>(
.then(|| on_press.clone()) .then(|| on_press.clone())
{ {
state.is_pressed = false; state.is_pressed = false;
shell.publish(on_press); let msg = (on_press)(layout.virtual_offset(), layout.bounds());
shell.publish(msg);
} }
return event::Status::Captured; return event::Status::Captured;
} }
@ -780,7 +850,9 @@ pub fn update<'a, Message: Clone>(
let state = state(); let state = state();
if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) { if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) {
state.is_pressed = true; state.is_pressed = true;
shell.publish(on_press); let msg = (on_press)(layout.virtual_offset(), layout.bounds());
shell.publish(msg);
return event::Status::Captured; return event::Status::Captured;
} }
} }

View file

@ -191,7 +191,7 @@ where
} }
} }
fn date_button<Message>( fn date_button<Message: Clone + 'static>(
date: NaiveDate, date: NaiveDate,
is_currently_viewed_month: bool, is_currently_viewed_month: bool,
is_currently_selected_day: bool, is_currently_selected_day: bool,

View file

@ -122,7 +122,11 @@ impl ColorPickerModel {
/// Get a color picker button that displays the applied color /// 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, &self,
f: T, f: T,
icon_portion: Option<u16>, icon_portion: Option<u16>,
@ -888,7 +892,7 @@ fn color_to_string(c: palette::Hsv, is_hex: bool) -> String {
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
/// A button for selecting a color from a color picker. /// 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<Message>, on_press: Option<Message>,
color: Option<Color>, color: Option<Color>,
icon_portion: Length, icon_portion: Length,

View file

@ -142,9 +142,12 @@ where
} }
} }
pub fn menu_button<'a, Message: 'a>( pub fn menu_button<'a, Message>(
children: Vec<crate::Element<'a, Message>>, children: Vec<crate::Element<'a, Message>>,
) -> crate::widget::Button<'a, Message> { ) -> crate::widget::Button<'a, Message>
where
Message: std::clone::Clone + 'a,
{
widget::button::custom( widget::button::custom(
widget::Row::with_children(children) widget::Row::with_children(children)
.align_y(Alignment::Center) .align_y(Alignment::Center)
@ -195,6 +198,7 @@ pub fn menu_root<'a, Message, Renderer: renderer::Renderer>(
) -> Button<'a, Message> ) -> Button<'a, Message>
where where
Element<'a, Message, crate::Theme, Renderer>: From<widget::Button<'a, Message>>, Element<'a, Message, crate::Theme, Renderer>: From<widget::Button<'a, Message>>,
Message: std::clone::Clone + 'a,
{ {
widget::button::custom(widget::text(label)) widget::button::custom(widget::text(label))
.padding([4, 12]) .padding([4, 12])
@ -215,7 +219,7 @@ pub fn menu_items<
'a, 'a,
A: MenuAction<Message = Message>, A: MenuAction<Message = Message>,
L: Into<Cow<'static, str>> + 'static, L: Into<Cow<'static, str>> + 'static,
Message: 'a, Message,
Renderer: renderer::Renderer + 'a, Renderer: renderer::Renderer + 'a,
>( >(
key_binds: &HashMap<KeyBind, A>, key_binds: &HashMap<KeyBind, A>,
@ -223,6 +227,7 @@ pub fn menu_items<
) -> Vec<MenuTree<'a, Message, Renderer>> ) -> Vec<MenuTree<'a, Message, Renderer>>
where where
Element<'a, Message, crate::Theme, Renderer>: From<widget::button::Button<'a, Message>>, Element<'a, Message, crate::Theme, Renderer>: From<widget::button::Button<'a, Message>>,
Message: 'a + Clone,
{ {
fn find_key<A: MenuAction>(action: &A, key_binds: &HashMap<KeyBind, A>) -> String { fn find_key<A: MenuAction>(action: &A, key_binds: &HashMap<KeyBind, A>) -> String {
for (key_bind, key_action) in key_binds { for (key_bind, key_action) in key_binds {