feat(image_button): add optional removable button overlay
This commit is contained in:
parent
9f27d2b7f5
commit
2c445d820f
5 changed files with 206 additions and 103 deletions
|
|
@ -17,12 +17,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// Messages that are used specifically by our [`App`].
|
/// Messages that are used specifically by our [`App`].
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
Clicked,
|
Clicked(usize),
|
||||||
|
Remove(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [`App`] stores application-specific state.
|
/// The [`App`] stores application-specific state.
|
||||||
pub struct App {
|
pub struct App {
|
||||||
core: Core,
|
core: Core,
|
||||||
|
selected: usize,
|
||||||
|
images: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
/// 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.
|
/// Creates the application, and optionally emits command on initialize.
|
||||||
fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
|
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();
|
let command = app.update_title();
|
||||||
|
|
||||||
|
|
@ -58,8 +68,11 @@ impl cosmic::Application for App {
|
||||||
|
|
||||||
/// Handle application events here.
|
/// Handle application events here.
|
||||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||||
if let Message::Clicked = message {
|
match message {
|
||||||
eprintln!("clicked");
|
Message::Clicked(id) => self.selected = id,
|
||||||
|
Message::Remove(id) => {
|
||||||
|
self.images.remove(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Command::none()
|
Command::none()
|
||||||
|
|
@ -67,22 +80,17 @@ impl cosmic::Application for App {
|
||||||
|
|
||||||
/// Creates a view after each update.
|
/// Creates a view after each update.
|
||||||
fn view(&self) -> Element<Self::Message> {
|
fn view(&self) -> Element<Self::Message> {
|
||||||
let content = cosmic::widget::column()
|
let mut content = cosmic::widget::column().spacing(12);
|
||||||
.spacing(12)
|
|
||||||
.push(
|
for (id, image) in self.images.iter().enumerate() {
|
||||||
cosmic::widget::button::image("/usr/share/backgrounds/pop/kait-herzog-8242.jpg")
|
content = content.push(
|
||||||
.width(600.0)
|
cosmic::widget::button::image(image)
|
||||||
.selected(true)
|
.width(300.0)
|
||||||
.on_press(Message::Clicked),
|
.on_press(Message::Clicked(id))
|
||||||
)
|
.selected(self.selected == id)
|
||||||
.push(
|
.on_remove(Message::Remove(id)),
|
||||||
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 centered = cosmic::widget::container(content)
|
let centered = cosmic::widget::container(content)
|
||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,6 @@ impl StyleSheet for crate::Theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selection_background(&self) -> Background {
|
fn selection_background(&self) -> Background {
|
||||||
Background::Color(self.cosmic().bg_color().into())
|
Background::Color(self.cosmic().primary.base.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,27 @@ use crate::{
|
||||||
widget::{self, image::Handle},
|
widget::{self, image::Handle},
|
||||||
Element,
|
Element,
|
||||||
};
|
};
|
||||||
use apply::Apply;
|
|
||||||
use iced_core::{font::Weight, widget::Id, Length, Padding};
|
use iced_core::{font::Weight, widget::Id, Length, Padding};
|
||||||
use std::borrow::Cow;
|
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> {
|
pub fn image<'a, Message>(handle: impl Into<Handle> + 'a) -> Button<'a, Message> {
|
||||||
Button::new(Image {
|
Button::new(Image {
|
||||||
image: widget::image(handle).border_radius([9.0; 4]),
|
image: widget::image(handle).border_radius([9.0; 4]),
|
||||||
selected: false,
|
selected: false,
|
||||||
|
on_remove: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Image<'a, Handle> {
|
pub struct Image<'a, Handle, Message> {
|
||||||
image: widget::Image<'a, Handle>,
|
image: widget::Image<'a, Handle>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
on_remove: Option<Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message> Button<'a, 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 {
|
Self {
|
||||||
id: Id::unique(),
|
id: Id::unique(),
|
||||||
label: Cow::Borrowed(""),
|
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 {
|
pub fn selected(mut self, selected: bool) -> Self {
|
||||||
self.variant.selected = selected;
|
self.variant.selected = selected;
|
||||||
self
|
self
|
||||||
|
|
@ -56,12 +67,13 @@ where
|
||||||
Message: Clone + 'static,
|
Message: Clone + 'static,
|
||||||
{
|
{
|
||||||
fn from(builder: Button<'a, Message>) -> Element<'a, Message> {
|
fn from(builder: Button<'a, Message>) -> Element<'a, Message> {
|
||||||
builder
|
let content = builder
|
||||||
.variant
|
.variant
|
||||||
.image
|
.image
|
||||||
.width(builder.width)
|
.width(builder.width)
|
||||||
.height(builder.height)
|
.height(builder.height);
|
||||||
.apply(widget::button)
|
|
||||||
|
super::custom_image_button(content, builder.variant.on_remove)
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.selected(builder.variant.selected)
|
.selected(builder.variant.selected)
|
||||||
.id(builder.id)
|
.id(builder.id)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,13 @@ pub fn button<'a, Message>(
|
||||||
Button::new(content)
|
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]
|
#[must_use]
|
||||||
#[derive(Setters)]
|
#[derive(Setters)]
|
||||||
pub struct Builder<'a, Message, Variant> {
|
pub struct Builder<'a, Message, Variant> {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
//! Allow your users to perform actions by pressing a button.
|
//! Allow your users to perform actions by pressing a button.
|
||||||
//!
|
//!
|
||||||
//! A [`Button`] has some local [`State`].
|
//! A [`Button`] has some local [`State`].
|
||||||
|
|
||||||
use iced_runtime::core::widget::Id;
|
use iced_runtime::core::widget::Id;
|
||||||
use iced_runtime::{keyboard, Command};
|
use iced_runtime::{keyboard, Command};
|
||||||
|
|
||||||
|
|
@ -24,44 +25,17 @@ use iced_renderer::core::widget::{operation, OperationOutputWrapper};
|
||||||
|
|
||||||
pub use super::style::{Appearance, StyleSheet};
|
pub use super::style::{Appearance, StyleSheet};
|
||||||
|
|
||||||
struct Selected {
|
/// Internally defines different button widget variants.
|
||||||
icon: svg::Handle,
|
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.
|
/// A generic button which emits 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)
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct Button<'a, Message, Renderer>
|
pub struct Button<'a, Message, Renderer>
|
||||||
|
|
@ -81,8 +55,9 @@ where
|
||||||
width: Length,
|
width: Length,
|
||||||
height: Length,
|
height: Length,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
selected: Option<Selected>,
|
selected: bool,
|
||||||
style: <Renderer::Theme as StyleSheet>::Style,
|
style: <Renderer::Theme as StyleSheet>::Style,
|
||||||
|
variant: Variant<Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
|
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
|
||||||
|
|
@ -92,7 +67,7 @@ where
|
||||||
{
|
{
|
||||||
/// Creates a new [`Button`] with the given content.
|
/// Creates a new [`Button`] with the given content.
|
||||||
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
|
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
|
||||||
Button {
|
Self {
|
||||||
id: Id::unique(),
|
id: Id::unique(),
|
||||||
#[cfg(feature = "a11y")]
|
#[cfg(feature = "a11y")]
|
||||||
name: None,
|
name: None,
|
||||||
|
|
@ -105,8 +80,50 @@ where
|
||||||
width: Length::Shrink,
|
width: Length::Shrink,
|
||||||
height: Length::Shrink,
|
height: Length::Shrink,
|
||||||
padding: Padding::new(5.0),
|
padding: Padding::new(5.0),
|
||||||
selected: None,
|
selected: false,
|
||||||
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
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.
|
/// Displays a selection indicator on image buttons.
|
||||||
pub fn selected(mut self, selected: bool) -> Self {
|
pub fn selected(mut self, selected: bool) -> Self {
|
||||||
self.selected = selected.then(|| Selected {
|
self.selected = 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
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -277,6 +285,27 @@ where
|
||||||
shell: &mut Shell<'_, Message>,
|
shell: &mut Shell<'_, Message>,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) -> event::Status {
|
) -> 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(
|
if let event::Status::Captured = self.content.as_widget_mut().on_event(
|
||||||
&mut tree.children[0],
|
&mut tree.children[0],
|
||||||
event.clone(),
|
event.clone(),
|
||||||
|
|
@ -319,7 +348,7 @@ where
|
||||||
bounds,
|
bounds,
|
||||||
cursor,
|
cursor,
|
||||||
self.on_press.is_some(),
|
self.on_press.is_some(),
|
||||||
self.selected.is_some(),
|
self.selected,
|
||||||
theme,
|
theme,
|
||||||
&self.style,
|
&self.style,
|
||||||
|| tree.state.downcast_ref::<State>(),
|
|| tree.state.downcast_ref::<State>(),
|
||||||
|
|
@ -340,33 +369,71 @@ where
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(ref selected) = self.selected {
|
if let Variant::Image {
|
||||||
renderer.fill_quad(
|
close_icon,
|
||||||
Quad {
|
selection_icon,
|
||||||
bounds: Rectangle {
|
on_remove,
|
||||||
width: 24.0,
|
} = &self.variant
|
||||||
height: 20.0,
|
{
|
||||||
x: bounds.x + styling.border_width,
|
let selection_background = theme.selection_background();
|
||||||
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(),
|
|
||||||
);
|
|
||||||
|
|
||||||
iced_core::svg::Renderer::draw(
|
if self.selected {
|
||||||
renderer,
|
renderer.fill_quad(
|
||||||
selected.icon.clone(),
|
Quad {
|
||||||
styling.icon_color,
|
bounds: Rectangle {
|
||||||
Rectangle {
|
width: 24.0,
|
||||||
width: 16.0,
|
height: 20.0,
|
||||||
height: 16.0,
|
x: bounds.x + styling.border_width,
|
||||||
x: bounds.x + 5.0 + styling.border_width,
|
y: bounds.y + (bounds.height - 20.0 - styling.border_width),
|
||||||
y: bounds.y + (bounds.height - 18.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);
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue