feat(button): add ImageButton widget variant

This commit is contained in:
Michael Aaron Murphy 2023-10-30 16:32:10 +01:00 committed by Michael Murphy
parent 470b966e8d
commit 34386561b3
6 changed files with 191 additions and 20 deletions

View file

@ -24,6 +24,7 @@ pub enum Button {
Link,
Icon,
IconVertical,
Image,
#[default]
Standard,
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 => {
appearance.background = None;
appearance.icon_color = Some(cosmic.accent.base.into());

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

View file

@ -12,6 +12,10 @@ mod icon;
pub use icon::icon;
pub use icon::Button as IconButton;
mod image;
pub use image::image;
pub use image::Button as ImageButton;
mod style;
pub use style::{Appearance, StyleSheet};
@ -81,7 +85,7 @@ pub struct Builder<'a, Message, Variant> {
/// Sets the preferred font weight.
font_weight: Weight,
// The preferred style of the button.
/// The preferred style of the button.
style: Style,
#[setters(skip)]

View file

@ -8,9 +8,6 @@ use apply::Apply;
use iced_core::{font::Weight, text::LineHeight, widget::Id, Alignment, Length, Padding};
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 fn destructive<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {

View file

@ -9,13 +9,13 @@ use iced_runtime::core::widget::Id;
use iced_runtime::{keyboard, Command};
use iced_core::event::{self, Event};
use iced_core::layout;
use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::renderer::{self, Quad};
use iced_core::touch;
use iced_core::widget::tree::{self, Tree};
use iced_core::widget::Operation;
use iced_core::{layout, svg};
use iced_core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell,
Vector, Widget,
@ -24,6 +24,10 @@ use iced_renderer::core::widget::{operation, OperationOutputWrapper};
pub use super::style::{Appearance, StyleSheet};
struct Selected {
icon: svg::Handle,
}
/// A generic widget that produces a message when pressed.
///
/// ```no_run
@ -77,6 +81,7 @@ where
width: Length,
height: Length,
padding: Padding,
selected: Option<Selected>,
style: <Renderer::Theme as StyleSheet>::Style,
}
@ -100,10 +105,17 @@ where
width: Length::Shrink,
height: Length::Shrink,
padding: Padding::new(5.0),
selected: None,
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`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
@ -139,15 +151,27 @@ where
self
}
/// Sets the style variant of this [`Button`].
pub fn style(mut self, style: <Renderer::Theme as StyleSheet>::Style) -> Self {
self.style = style;
/// Sets the widget to a selected state.
///
/// 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
}
/// Sets the [`Id`] of the [`Button`].
pub fn id(mut self, id: Id) -> Self {
self.id = id;
/// Sets the style variant of this [`Button`].
pub fn style(mut self, style: <Renderer::Theme as StyleSheet>::Style) -> Self {
self.style = style;
self
}
@ -185,7 +209,7 @@ where
impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer>
where
Message: 'a + Clone,
Renderer: 'a + iced_core::Renderer,
Renderer: 'a + iced_core::Renderer + svg::Renderer,
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
@ -295,6 +319,7 @@ where
bounds,
cursor,
self.on_press.is_some(),
self.selected.is_some(),
theme,
&self.style,
|| tree.state.downcast_ref::<State>(),
@ -313,6 +338,37 @@ where
cursor,
&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(
@ -417,7 +473,7 @@ where
impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer>
where
Message: Clone + 'a,
Renderer: iced_core::Renderer + 'a,
Renderer: iced_core::Renderer + svg::Renderer + 'a,
Renderer::Theme: StyleSheet,
{
fn from(button: Button<'a, Message, Renderer>) -> Self {
@ -538,12 +594,13 @@ pub fn update<'a, Message: Clone>(
event::Status::Ignored
}
/// Draws a [`Button`].
#[allow(clippy::too_many_arguments)]
pub fn draw<'a, Renderer: iced_core::Renderer>(
renderer: &mut Renderer,
bounds: Rectangle,
cursor: mouse::Cursor,
is_enabled: bool,
is_selected: bool,
style_sheet: &dyn StyleSheet<Style = <Renderer::Theme as StyleSheet>::Style>,
style: &<Renderer::Theme as StyleSheet>::Style,
state: impl FnOnce() -> &'a State,
@ -559,12 +616,12 @@ where
style_sheet.disabled(style)
} else if is_mouse_over {
if state.is_pressed {
style_sheet.pressed(state.is_focused, style)
style_sheet.pressed(state.is_focused || is_selected, style)
} else {
style_sheet.hovered(state.is_focused, style)
style_sheet.hovered(state.is_focused || is_selected, style)
}
} 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;
@ -595,7 +652,8 @@ where
bounds: Rectangle {
x: bounds.x + styling.shadow_offset.x,
y: bounds.y + styling.shadow_offset.y,
..bounds
width: bounds.width,
height: bounds.height,
},
border_radius: styling.border_radius,
border_width: 0.0,
@ -607,7 +665,12 @@ where
renderer.fill_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_width: styling.border_width,
border_color: styling.border_color,

View file

@ -50,6 +50,24 @@ pub struct 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]
fn into_element<Message: 'static>(self) -> Element<'static, Message> {
let from_image = |handle| {