feat(button): add ImageButton widget variant
This commit is contained in:
parent
470b966e8d
commit
34386561b3
6 changed files with 191 additions and 20 deletions
|
|
@ -24,6 +24,7 @@ pub enum Button {
|
||||||
Link,
|
Link,
|
||||||
Icon,
|
Icon,
|
||||||
IconVertical,
|
IconVertical,
|
||||||
|
Image,
|
||||||
#[default]
|
#[default]
|
||||||
Standard,
|
Standard,
|
||||||
Suggested,
|
Suggested,
|
||||||
|
|
@ -80,6 +81,22 @@ pub fn appearance(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button::Image => {
|
||||||
|
appearance.background = Some(Background::Color(cosmic.bg_color().into()));
|
||||||
|
appearance.text_color = Some(cosmic.accent.base.into());
|
||||||
|
appearance.icon_color = Some(cosmic.accent.base.into());
|
||||||
|
|
||||||
|
corner_radii = &cosmic.corner_radii.radius_s;
|
||||||
|
appearance.border_radius = (*corner_radii).into();
|
||||||
|
|
||||||
|
if focused {
|
||||||
|
appearance.border_width = 3.0;
|
||||||
|
appearance.border_color = cosmic.accent.base.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
return appearance;
|
||||||
|
}
|
||||||
|
|
||||||
Button::Link => {
|
Button::Link => {
|
||||||
appearance.background = None;
|
appearance.background = None;
|
||||||
appearance.icon_color = Some(cosmic.accent.base.into());
|
appearance.icon_color = Some(cosmic.accent.base.into());
|
||||||
|
|
|
||||||
72
src/widget/button/image.rs
Normal file
72
src/widget/button/image.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use super::{Builder, Style};
|
||||||
|
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 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Image<'a, Handle> {
|
||||||
|
image: widget::Image<'a, Handle>,
|
||||||
|
selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message> Button<'a, Message> {
|
||||||
|
pub fn new(variant: Image<'a, Handle>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Id::unique(),
|
||||||
|
label: Cow::Borrowed(""),
|
||||||
|
tooltip: Cow::Borrowed(""),
|
||||||
|
on_press: None,
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
padding: Padding::from(0),
|
||||||
|
spacing: 0,
|
||||||
|
icon_size: 16,
|
||||||
|
line_height: 20,
|
||||||
|
font_size: 14,
|
||||||
|
font_weight: Weight::Normal,
|
||||||
|
style: Style::Image,
|
||||||
|
variant,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.variant.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message> From<Button<'a, Message>> for Element<'a, Message>
|
||||||
|
where
|
||||||
|
Handle: Clone + std::hash::Hash,
|
||||||
|
Message: Clone + 'static,
|
||||||
|
{
|
||||||
|
fn from(builder: Button<'a, Message>) -> Element<'a, Message> {
|
||||||
|
builder
|
||||||
|
.variant
|
||||||
|
.image
|
||||||
|
.width(builder.width)
|
||||||
|
.height(builder.height)
|
||||||
|
.apply(widget::button)
|
||||||
|
.selected(builder.variant.selected)
|
||||||
|
.id(builder.id)
|
||||||
|
.padding(0)
|
||||||
|
.on_press_maybe(builder.on_press)
|
||||||
|
.style(builder.style)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,10 @@ mod icon;
|
||||||
pub use icon::icon;
|
pub use icon::icon;
|
||||||
pub use icon::Button as IconButton;
|
pub use icon::Button as IconButton;
|
||||||
|
|
||||||
|
mod image;
|
||||||
|
pub use image::image;
|
||||||
|
pub use image::Button as ImageButton;
|
||||||
|
|
||||||
mod style;
|
mod style;
|
||||||
pub use style::{Appearance, StyleSheet};
|
pub use style::{Appearance, StyleSheet};
|
||||||
|
|
||||||
|
|
@ -81,7 +85,7 @@ pub struct Builder<'a, Message, Variant> {
|
||||||
/// Sets the preferred font weight.
|
/// Sets the preferred font weight.
|
||||||
font_weight: Weight,
|
font_weight: Weight,
|
||||||
|
|
||||||
// The preferred style of the button.
|
/// The preferred style of the button.
|
||||||
style: Style,
|
style: Style,
|
||||||
|
|
||||||
#[setters(skip)]
|
#[setters(skip)]
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ use apply::Apply;
|
||||||
use iced_core::{font::Weight, text::LineHeight, widget::Id, Alignment, Length, Padding};
|
use iced_core::{font::Weight, text::LineHeight, widget::Id, Alignment, Length, Padding};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
/// A [`Button`] with the highest level of attention.
|
|
||||||
///
|
|
||||||
/// There should only be one primary button used per page.
|
|
||||||
pub type Button<'a, Message> = Builder<'a, Message, Text>;
|
pub type Button<'a, Message> = Builder<'a, Message, Text>;
|
||||||
|
|
||||||
pub fn destructive<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
|
pub fn destructive<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ use iced_runtime::core::widget::Id;
|
||||||
use iced_runtime::{keyboard, Command};
|
use iced_runtime::{keyboard, Command};
|
||||||
|
|
||||||
use iced_core::event::{self, Event};
|
use iced_core::event::{self, Event};
|
||||||
use iced_core::layout;
|
|
||||||
use iced_core::mouse;
|
use iced_core::mouse;
|
||||||
use iced_core::overlay;
|
use iced_core::overlay;
|
||||||
use iced_core::renderer;
|
use iced_core::renderer::{self, Quad};
|
||||||
use iced_core::touch;
|
use iced_core::touch;
|
||||||
use iced_core::widget::tree::{self, Tree};
|
use iced_core::widget::tree::{self, Tree};
|
||||||
use iced_core::widget::Operation;
|
use iced_core::widget::Operation;
|
||||||
|
use iced_core::{layout, svg};
|
||||||
use iced_core::{
|
use iced_core::{
|
||||||
Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell,
|
Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell,
|
||||||
Vector, Widget,
|
Vector, Widget,
|
||||||
|
|
@ -24,6 +24,10 @@ use iced_renderer::core::widget::{operation, OperationOutputWrapper};
|
||||||
|
|
||||||
pub use super::style::{Appearance, StyleSheet};
|
pub use super::style::{Appearance, StyleSheet};
|
||||||
|
|
||||||
|
struct Selected {
|
||||||
|
icon: svg::Handle,
|
||||||
|
}
|
||||||
|
|
||||||
/// A generic widget that produces a message when pressed.
|
/// A generic widget that produces a message when pressed.
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
|
|
@ -77,6 +81,7 @@ where
|
||||||
width: Length,
|
width: Length,
|
||||||
height: Length,
|
height: Length,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
|
selected: Option<Selected>,
|
||||||
style: <Renderer::Theme as StyleSheet>::Style,
|
style: <Renderer::Theme as StyleSheet>::Style,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,10 +105,17 @@ 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,
|
||||||
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Id`] of the [`Button`].
|
||||||
|
pub fn id(mut self, id: Id) -> Self {
|
||||||
|
self.id = id;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the width of the [`Button`].
|
/// Sets the width of the [`Button`].
|
||||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
self.width = width.into();
|
self.width = width.into();
|
||||||
|
|
@ -139,15 +151,27 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the style variant of this [`Button`].
|
/// Sets the widget to a selected state.
|
||||||
pub fn style(mut self, style: <Renderer::Theme as StyleSheet>::Style) -> Self {
|
///
|
||||||
self.style = style;
|
/// Displays a selection indicator on image buttons.
|
||||||
|
pub(super) 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the [`Id`] of the [`Button`].
|
/// Sets the style variant of this [`Button`].
|
||||||
pub fn id(mut self, id: Id) -> Self {
|
pub fn style(mut self, style: <Renderer::Theme as StyleSheet>::Style) -> Self {
|
||||||
self.id = id;
|
self.style = style;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,7 +209,7 @@ where
|
||||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer>
|
impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer>
|
||||||
where
|
where
|
||||||
Message: 'a + Clone,
|
Message: 'a + Clone,
|
||||||
Renderer: 'a + iced_core::Renderer,
|
Renderer: 'a + iced_core::Renderer + svg::Renderer,
|
||||||
Renderer::Theme: StyleSheet,
|
Renderer::Theme: StyleSheet,
|
||||||
{
|
{
|
||||||
fn tag(&self) -> tree::Tag {
|
fn tag(&self) -> tree::Tag {
|
||||||
|
|
@ -295,6 +319,7 @@ where
|
||||||
bounds,
|
bounds,
|
||||||
cursor,
|
cursor,
|
||||||
self.on_press.is_some(),
|
self.on_press.is_some(),
|
||||||
|
self.selected.is_some(),
|
||||||
theme,
|
theme,
|
||||||
&self.style,
|
&self.style,
|
||||||
|| tree.state.downcast_ref::<State>(),
|
|| tree.state.downcast_ref::<State>(),
|
||||||
|
|
@ -313,6 +338,37 @@ where
|
||||||
cursor,
|
cursor,
|
||||||
&bounds,
|
&bounds,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some(ref selected) = self.selected {
|
||||||
|
renderer.fill_quad(
|
||||||
|
Quad {
|
||||||
|
bounds: Rectangle {
|
||||||
|
width: 24.0,
|
||||||
|
height: 20.0,
|
||||||
|
x: bounds.x,
|
||||||
|
y: bounds.y + (bounds.height - 20.0),
|
||||||
|
},
|
||||||
|
border_radius: [0.0, 8.0, 0.0, 8.0].into(),
|
||||||
|
border_width: 0.0,
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
},
|
||||||
|
styling
|
||||||
|
.background
|
||||||
|
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
||||||
|
);
|
||||||
|
|
||||||
|
iced_core::svg::Renderer::draw(
|
||||||
|
renderer,
|
||||||
|
selected.icon.clone(),
|
||||||
|
styling.icon_color,
|
||||||
|
Rectangle {
|
||||||
|
width: 16.0,
|
||||||
|
height: 16.0,
|
||||||
|
x: bounds.x + 4.0,
|
||||||
|
y: bounds.y + (bounds.height - 16.0),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn mouse_interaction(
|
||||||
|
|
@ -417,7 +473,7 @@ where
|
||||||
impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer>
|
impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer>
|
||||||
where
|
where
|
||||||
Message: Clone + 'a,
|
Message: Clone + 'a,
|
||||||
Renderer: iced_core::Renderer + 'a,
|
Renderer: iced_core::Renderer + svg::Renderer + 'a,
|
||||||
Renderer::Theme: StyleSheet,
|
Renderer::Theme: StyleSheet,
|
||||||
{
|
{
|
||||||
fn from(button: Button<'a, Message, Renderer>) -> Self {
|
fn from(button: Button<'a, Message, Renderer>) -> Self {
|
||||||
|
|
@ -538,12 +594,13 @@ pub fn update<'a, Message: Clone>(
|
||||||
event::Status::Ignored
|
event::Status::Ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a [`Button`].
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn draw<'a, Renderer: iced_core::Renderer>(
|
pub fn draw<'a, Renderer: iced_core::Renderer>(
|
||||||
renderer: &mut Renderer,
|
renderer: &mut Renderer,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
is_enabled: bool,
|
is_enabled: bool,
|
||||||
|
is_selected: bool,
|
||||||
style_sheet: &dyn StyleSheet<Style = <Renderer::Theme as StyleSheet>::Style>,
|
style_sheet: &dyn StyleSheet<Style = <Renderer::Theme as StyleSheet>::Style>,
|
||||||
style: &<Renderer::Theme as StyleSheet>::Style,
|
style: &<Renderer::Theme as StyleSheet>::Style,
|
||||||
state: impl FnOnce() -> &'a State,
|
state: impl FnOnce() -> &'a State,
|
||||||
|
|
@ -559,12 +616,12 @@ where
|
||||||
style_sheet.disabled(style)
|
style_sheet.disabled(style)
|
||||||
} else if is_mouse_over {
|
} else if is_mouse_over {
|
||||||
if state.is_pressed {
|
if state.is_pressed {
|
||||||
style_sheet.pressed(state.is_focused, style)
|
style_sheet.pressed(state.is_focused || is_selected, style)
|
||||||
} else {
|
} else {
|
||||||
style_sheet.hovered(state.is_focused, style)
|
style_sheet.hovered(state.is_focused || is_selected, style)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
style_sheet.active(state.is_focused, style)
|
style_sheet.active(state.is_focused || is_selected, style)
|
||||||
};
|
};
|
||||||
|
|
||||||
let doubled_border_width = styling.border_width * 2.0;
|
let doubled_border_width = styling.border_width * 2.0;
|
||||||
|
|
@ -595,7 +652,8 @@ where
|
||||||
bounds: Rectangle {
|
bounds: Rectangle {
|
||||||
x: bounds.x + styling.shadow_offset.x,
|
x: bounds.x + styling.shadow_offset.x,
|
||||||
y: bounds.y + styling.shadow_offset.y,
|
y: bounds.y + styling.shadow_offset.y,
|
||||||
..bounds
|
width: bounds.width,
|
||||||
|
height: bounds.height,
|
||||||
},
|
},
|
||||||
border_radius: styling.border_radius,
|
border_radius: styling.border_radius,
|
||||||
border_width: 0.0,
|
border_width: 0.0,
|
||||||
|
|
@ -607,7 +665,12 @@ where
|
||||||
|
|
||||||
renderer.fill_quad(
|
renderer.fill_quad(
|
||||||
renderer::Quad {
|
renderer::Quad {
|
||||||
bounds,
|
bounds: Rectangle {
|
||||||
|
x: bounds.x + if is_selected { -1.0 } else { 0.0 },
|
||||||
|
y: bounds.y + if is_selected { -1.0 } else { 0.0 },
|
||||||
|
width: bounds.width + if is_selected { 2.0 } else { 0.0 },
|
||||||
|
height: bounds.height + if is_selected { 2.0 } else { 0.0 },
|
||||||
|
},
|
||||||
border_radius: styling.border_radius,
|
border_radius: styling.border_radius,
|
||||||
border_width: styling.border_width,
|
border_width: styling.border_width,
|
||||||
border_color: styling.border_color,
|
border_color: styling.border_color,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,24 @@ pub struct Icon {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Icon {
|
impl Icon {
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_svg_handle(self) -> Option<crate::widget::svg::Handle> {
|
||||||
|
match self.handle.data {
|
||||||
|
Data::Name(named) => {
|
||||||
|
if let Some(path) = named.path() {
|
||||||
|
if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
|
||||||
|
return Some(iced_core::svg::Handle::from_path(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Data::Image(_) => (),
|
||||||
|
Data::Svg(handle) => return Some(handle),
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn into_element<Message: 'static>(self) -> Element<'static, Message> {
|
fn into_element<Message: 'static>(self) -> Element<'static, Message> {
|
||||||
let from_image = |handle| {
|
let from_image = |handle| {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue