feat(image_button): add optional removable button overlay

This commit is contained in:
Michael Aaron Murphy 2023-11-15 16:09:17 +01:00 committed by Michael Murphy
parent 9f27d2b7f5
commit 2c445d820f
5 changed files with 206 additions and 103 deletions

View file

@ -17,12 +17,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
/// 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<String>,
}
/// 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<Self::Message>) {
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<Self::Message> {
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<Self::Message> {
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)

View file

@ -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())
}
}

View file

@ -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<Handle> + '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<Message>,
}
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<Message>) -> 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)

View file

@ -38,6 +38,13 @@ pub fn button<'a, Message>(
Button::new(content)
}
pub fn custom_image_button<'a, Message>(
content: impl Into<Element<'a, Message>>,
on_remove: Option<Message>,
) -> Button<'a, Message, crate::Renderer> {
Button::new_image(content, on_remove)
}
#[must_use]
#[derive(Setters)]
pub struct Builder<'a, Message, Variant> {

View file

@ -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<Message> {
Normal,
Image {
close_icon: svg::Handle,
selection_icon: svg::Handle,
on_remove: Option<Message>,
},
}
/// A generic widget that produces a message when pressed.
///
/// ```no_run
/// # type Button<'a, Message> =
/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[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<iced_widget::style::Theme>>;
/// #
/// #[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>,
selected: bool,
style: <Renderer::Theme as StyleSheet>::Style,
variant: Variant<Message>,
}
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<Element<'a, Message, Renderer>>) -> 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: <Renderer::Theme as StyleSheet>::Style::default(),
variant: Variant::Normal,
}
}
pub fn new_image(
content: impl Into<Element<'a, Message, Renderer>>,
on_remove: Option<Message>,
) -> 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: <Renderer::Theme as StyleSheet>::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::<State>(),
@ -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,
}
}