diff --git a/examples/image-button/src/main.rs b/examples/image-button/src/main.rs index 04d64440..bfa51ba2 100644 --- a/examples/image-button/src/main.rs +++ b/examples/image-button/src/main.rs @@ -17,12 +17,15 @@ fn main() -> Result<(), Box> { /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { - Clicked, + Clicked(usize), + Remove(usize), } /// The [`App`] stores application-specific state. pub struct App { core: Core, + selected: usize, + images: Vec, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -49,7 +52,14 @@ impl cosmic::Application for App { /// Creates the application, and optionally emits command on initialize. fn init(core: Core, _input: Self::Flags) -> (Self, Command) { - let mut app = App { core }; + let mut app = App { + core, + selected: 0, + images: vec![ + "/usr/share/backgrounds/pop/kait-herzog-8242.jpg".into(), + "/usr/share/backgrounds/pop/kate-hazen-unleash-your-robot-blue.png".into(), + ], + }; let command = app.update_title(); @@ -58,8 +68,11 @@ impl cosmic::Application for App { /// Handle application events here. fn update(&mut self, message: Self::Message) -> Command { - if let Message::Clicked = message { - eprintln!("clicked"); + match message { + Message::Clicked(id) => self.selected = id, + Message::Remove(id) => { + self.images.remove(id); + } } Command::none() @@ -67,22 +80,17 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element { - let content = cosmic::widget::column() - .spacing(12) - .push( - cosmic::widget::button::image("/usr/share/backgrounds/pop/kait-herzog-8242.jpg") - .width(600.0) - .selected(true) - .on_press(Message::Clicked), - ) - .push( - cosmic::widget::button::image( - "/usr/share/backgrounds/pop/kate-hazen-unleash-your-robot-blue.png", - ) - .width(600.0) - .selected(true) - .on_press(Message::Clicked), + let mut content = cosmic::widget::column().spacing(12); + + for (id, image) in self.images.iter().enumerate() { + content = content.push( + cosmic::widget::button::image(image) + .width(300.0) + .on_press(Message::Clicked(id)) + .selected(self.selected == id) + .on_remove(Message::Remove(id)), ); + } let centered = cosmic::widget::container(content) .width(iced::Length::Fill) diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 499c97ed..879400e2 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -207,6 +207,6 @@ impl StyleSheet for crate::Theme { } fn selection_background(&self) -> Background { - Background::Color(self.cosmic().bg_color().into()) + Background::Color(self.cosmic().primary.base.into()) } } diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index 1fbd6edb..9bfa2880 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -6,26 +6,27 @@ use crate::{ widget::{self, image::Handle}, Element, }; -use apply::Apply; use iced_core::{font::Weight, widget::Id, Length, Padding}; use std::borrow::Cow; -pub type Button<'a, Message> = Builder<'a, Message, Image<'a, Handle>>; +pub type Button<'a, Message> = Builder<'a, Message, Image<'a, Handle, Message>>; pub fn image<'a, Message>(handle: impl Into + 'a) -> Button<'a, Message> { Button::new(Image { image: widget::image(handle).border_radius([9.0; 4]), selected: false, + on_remove: None, }) } -pub struct Image<'a, Handle> { +pub struct Image<'a, Handle, Message> { image: widget::Image<'a, Handle>, selected: bool, + on_remove: Option, } impl<'a, Message> Button<'a, Message> { - pub fn new(variant: Image<'a, Handle>) -> Self { + pub fn new(variant: Image<'a, Handle, Message>) -> Self { Self { id: Id::unique(), label: Cow::Borrowed(""), @@ -44,6 +45,16 @@ impl<'a, Message> Button<'a, Message> { } } + pub fn on_remove(mut self, message: Message) -> Self { + self.variant.on_remove = Some(message); + self + } + + pub fn on_remove_maybe(mut self, message: Option) -> Self { + self.variant.on_remove = message; + self + } + pub fn selected(mut self, selected: bool) -> Self { self.variant.selected = selected; self @@ -56,12 +67,13 @@ where Message: Clone + 'static, { fn from(builder: Button<'a, Message>) -> Element<'a, Message> { - builder + let content = builder .variant .image .width(builder.width) - .height(builder.height) - .apply(widget::button) + .height(builder.height); + + super::custom_image_button(content, builder.variant.on_remove) .padding(0) .selected(builder.variant.selected) .id(builder.id) diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs index ac8ecd3f..32872e0e 100644 --- a/src/widget/button/mod.rs +++ b/src/widget/button/mod.rs @@ -38,6 +38,13 @@ pub fn button<'a, Message>( Button::new(content) } +pub fn custom_image_button<'a, Message>( + content: impl Into>, + on_remove: Option, +) -> Button<'a, Message, crate::Renderer> { + Button::new_image(content, on_remove) +} + #[must_use] #[derive(Setters)] pub struct Builder<'a, Message, Variant> { diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 46a35e51..1a98536f 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -5,6 +5,7 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. + use iced_runtime::core::widget::Id; use iced_runtime::{keyboard, Command}; @@ -24,44 +25,17 @@ use iced_renderer::core::widget::{operation, OperationOutputWrapper}; pub use super::style::{Appearance, StyleSheet}; -struct Selected { - icon: svg::Handle, +/// Internally defines different button widget variants. +enum Variant { + Normal, + Image { + close_icon: svg::Handle, + selection_icon: svg::Handle, + on_remove: Option, + }, } -/// A generic widget that produces a message when pressed. -/// -/// ```no_run -/// # type Button<'a, Message> = -/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer>; -/// # -/// #[derive(Clone)] -/// enum Message { -/// ButtonPressed, -/// } -/// -/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); -/// ``` -/// -/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will -/// be disabled: -/// -/// ``` -/// # type Button<'a, Message> = -/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer>; -/// # -/// #[derive(Clone)] -/// enum Message { -/// ButtonPressed, -/// } -/// -/// fn disabled_button<'a>() -> Button<'a, Message> { -/// Button::new("I'm disabled!") -/// } -/// -/// fn enabled_button<'a>() -> Button<'a, Message> { -/// disabled_button().on_press(Message::ButtonPressed) -/// } -/// ``` +/// A generic button which emits a message when pressed. #[allow(missing_debug_implementations)] #[must_use] pub struct Button<'a, Message, Renderer> @@ -81,8 +55,9 @@ where width: Length, height: Length, padding: Padding, - selected: Option, + selected: bool, style: ::Style, + variant: Variant, } impl<'a, Message, Renderer> Button<'a, Message, Renderer> @@ -92,7 +67,7 @@ where { /// Creates a new [`Button`] with the given content. pub fn new(content: impl Into>) -> Self { - Button { + Self { id: Id::unique(), #[cfg(feature = "a11y")] name: None, @@ -105,8 +80,50 @@ where width: Length::Shrink, height: Length::Shrink, padding: Padding::new(5.0), - selected: None, + selected: false, style: ::Style::default(), + variant: Variant::Normal, + } + } + + pub fn new_image( + content: impl Into>, + on_remove: Option, + ) -> Self { + Self { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, + content: content.into(), + on_press: None, + width: Length::Shrink, + height: Length::Shrink, + padding: Padding::new(5.0), + selected: false, + style: ::Style::default(), + variant: Variant::Image { + on_remove, + close_icon: crate::widget::icon::from_name("window-close-symbolic") + .size(8) + .icon() + .into_svg_handle() + .unwrap_or_else(|| { + let bytes: &'static [u8] = &[]; + iced_core::svg::Handle::from_memory(bytes) + }), + selection_icon: crate::widget::icon::from_name("object-select-symbolic") + .size(16) + .icon() + .into_svg_handle() + .unwrap_or_else(|| { + let bytes: &'static [u8] = &[]; + iced_core::svg::Handle::from_memory(bytes) + }), + }, } } @@ -155,16 +172,7 @@ where /// /// Displays a selection indicator on image buttons. pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected.then(|| Selected { - icon: crate::widget::icon::from_name("object-select-symbolic") - .size(16) - .icon() - .into_svg_handle() - .unwrap_or_else(|| { - let bytes: &'static [u8] = &[]; - iced_core::svg::Handle::from_memory(bytes) - }), - }); + self.selected = selected; self } @@ -277,6 +285,27 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { + if let Variant::Image { + on_remove: Some(on_remove), + .. + } = &self.variant + { + // Capture mouse/touch events on the removal button + match event { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(position) = cursor.position() { + if removal_bounds(layout.bounds(), 4.0).contains(position) { + shell.publish(on_remove.clone()); + return event::Status::Captured; + } + } + } + + _ => (), + } + } + if let event::Status::Captured = self.content.as_widget_mut().on_event( &mut tree.children[0], event.clone(), @@ -319,7 +348,7 @@ where bounds, cursor, self.on_press.is_some(), - self.selected.is_some(), + self.selected, theme, &self.style, || tree.state.downcast_ref::(), @@ -340,33 +369,71 @@ where }, ); - if let Some(ref selected) = self.selected { - renderer.fill_quad( - Quad { - bounds: Rectangle { - width: 24.0, - height: 20.0, - x: bounds.x + styling.border_width, - y: bounds.y + (bounds.height - 20.0 - styling.border_width), - }, - border_radius: [0.0, 8.0, 0.0, 8.0].into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - theme.selection_background(), - ); + if let Variant::Image { + close_icon, + selection_icon, + on_remove, + } = &self.variant + { + let selection_background = theme.selection_background(); - iced_core::svg::Renderer::draw( - renderer, - selected.icon.clone(), - styling.icon_color, - Rectangle { - width: 16.0, - height: 16.0, - x: bounds.x + 5.0 + styling.border_width, - y: bounds.y + (bounds.height - 18.0 - styling.border_width), - }, - ); + if self.selected { + renderer.fill_quad( + Quad { + bounds: Rectangle { + width: 24.0, + height: 20.0, + x: bounds.x + styling.border_width, + y: bounds.y + (bounds.height - 20.0 - styling.border_width), + }, + border_radius: [0.0, 8.0, 0.0, 8.0].into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + selection_background, + ); + + iced_core::svg::Renderer::draw( + renderer, + selection_icon.clone(), + styling.icon_color, + Rectangle { + width: 16.0, + height: 16.0, + x: bounds.x + 5.0 + styling.border_width, + y: bounds.y + (bounds.height - 18.0 - styling.border_width), + }, + ); + } + + if on_remove.is_some() { + if let Some(position) = cursor.position() { + if bounds.contains(position) { + let bounds = removal_bounds(layout.bounds(), 4.0); + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: 20.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + selection_background, + ); + + iced_core::svg::Renderer::draw( + renderer, + close_icon.clone(), + styling.icon_color, + Rectangle { + width: 16.0, + height: 16.0, + x: bounds.x + 4.0, + y: bounds.y + 4.0, + }, + ); + } + } + } } } @@ -750,3 +817,12 @@ impl operation::Focusable for State { State::unfocus(self); } } + +fn removal_bounds(bounds: Rectangle, offset: f32) -> Rectangle { + Rectangle { + x: bounds.x + bounds.width - 12.0 - offset, + y: bounds.y - 12.0 + offset, + width: 24.0, + height: 24.0, + } +}