refactor(widget): improvements to button and icon widgets
This commit is contained in:
parent
7f0943924a
commit
9dbc1be269
20 changed files with 399 additions and 558 deletions
|
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 408 B |
|
|
@ -1,50 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::Builder;
|
||||
use super::Style;
|
||||
use crate::widget::icon::{self, Handle};
|
||||
use iced_core::{font::Weight, widget::Id, Length, Padding};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub type Button<'a, Message> = Builder<'a, Message, Hyperlink>;
|
||||
|
||||
pub struct Hyperlink {
|
||||
trailing_icon: bool,
|
||||
}
|
||||
|
||||
pub fn hyperlink<'a, Message>() -> Button<'a, Message> {
|
||||
Button::new(Hyperlink {
|
||||
trailing_icon: false,
|
||||
})
|
||||
}
|
||||
|
||||
impl<'a, Message> Button<'a, Message> {
|
||||
pub fn new(link: Hyperlink) -> Self {
|
||||
Self {
|
||||
id: Id::unique(),
|
||||
label: 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::Link,
|
||||
variant: link,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn with_icon(mut self) -> Self {
|
||||
self.variant.trailing_icon = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon() -> Handle {
|
||||
icon::handle::from_svg_bytes(&include_bytes!("../../../res/external-link.svg")[..])
|
||||
.symbolic(true)
|
||||
}
|
||||
|
|
@ -2,7 +2,11 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::{button, Builder, Style};
|
||||
use crate::{widget::icon::Handle, Element};
|
||||
use crate::widget::{
|
||||
icon::{self, Handle},
|
||||
tooltip,
|
||||
};
|
||||
use crate::Element;
|
||||
use apply::Apply;
|
||||
use iced_core::{font::Weight, text::LineHeight, widget::Id, Alignment, Length, Padding};
|
||||
use std::borrow::Cow;
|
||||
|
|
@ -11,14 +15,12 @@ pub type Button<'a, Message> = Builder<'a, Message, Icon>;
|
|||
|
||||
pub struct Icon {
|
||||
handle: Handle,
|
||||
selected: bool,
|
||||
vertical: bool,
|
||||
}
|
||||
|
||||
pub fn icon<Message>(handle: impl Into<Handle>) -> Button<'static, Message> {
|
||||
pub fn icon<'a, Message>(handle: impl Into<Handle>) -> Button<'a, Message> {
|
||||
Button::new(Icon {
|
||||
handle: handle.into(),
|
||||
selected: false,
|
||||
vertical: false,
|
||||
})
|
||||
}
|
||||
|
|
@ -33,12 +35,13 @@ impl<'a, Message> Button<'a, Message> {
|
|||
Self {
|
||||
id: Id::unique(),
|
||||
label: Cow::Borrowed(""),
|
||||
tooltip: Cow::Borrowed(""),
|
||||
on_press: None,
|
||||
width: Length::Shrink,
|
||||
height: Length::Fixed(46.0),
|
||||
padding: Padding::from(padding),
|
||||
spacing: theme.space_xxxs(),
|
||||
icon_size: 16,
|
||||
icon_size: if icon.handle.symbolic { 16 } else { 24 },
|
||||
line_height: 20,
|
||||
font_size: 14,
|
||||
font_weight: Weight::Normal,
|
||||
|
|
@ -91,7 +94,7 @@ impl<'a, Message> Button<'a, Message> {
|
|||
let theme = theme.cosmic();
|
||||
|
||||
self.font_size = 28;
|
||||
self.font_weight = Weight::Light;
|
||||
self.font_weight = Weight::Normal;
|
||||
self.icon_size = 40;
|
||||
self.line_height = 36;
|
||||
self.height = Length::Fixed(64.0);
|
||||
|
|
@ -121,16 +124,21 @@ impl<'a, Message> Button<'a, Message> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.variant.selected = selected;
|
||||
pub fn vertical(mut self, vertical: bool) -> Self {
|
||||
self.variant.vertical = vertical;
|
||||
self.style = Style::IconVertical;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Message> {
|
||||
fn from(builder: Button<'a, Message>) -> Element<'a, Message> {
|
||||
fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> {
|
||||
let mut content = Vec::with_capacity(2);
|
||||
|
||||
if let icon::Data::Name(ref mut named) = builder.variant.handle.data {
|
||||
named.size = Some(builder.icon_size);
|
||||
}
|
||||
|
||||
content.push(
|
||||
crate::widget::icon(builder.variant.handle.clone())
|
||||
.size(builder.icon_size)
|
||||
|
|
@ -154,8 +162,8 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
|
|||
let button = if builder.variant.vertical {
|
||||
crate::widget::column::with_children(content)
|
||||
.padding(builder.padding)
|
||||
.width(builder.width)
|
||||
.height(builder.height)
|
||||
// .width(builder.width)
|
||||
// .height(builder.height)
|
||||
.spacing(builder.spacing)
|
||||
.align_items(Alignment::Center)
|
||||
.apply(button)
|
||||
|
|
@ -169,11 +177,23 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
|
|||
.apply(button)
|
||||
};
|
||||
|
||||
button
|
||||
let button = button
|
||||
.padding(0)
|
||||
.id(builder.id)
|
||||
.on_press_maybe(builder.on_press)
|
||||
.style(builder.style)
|
||||
.into()
|
||||
.style(builder.style);
|
||||
|
||||
if builder.tooltip.is_empty() {
|
||||
button.into()
|
||||
} else {
|
||||
tooltip(button, builder.tooltip, tooltip::Position::Top)
|
||||
.size(builder.font_size)
|
||||
.font({
|
||||
let mut font = crate::font::DEFAULT;
|
||||
font.weight = builder.font_weight;
|
||||
font
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
101
src/widget/button/link.rs
Normal file
101
src/widget/button/link.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::Builder;
|
||||
use super::Style;
|
||||
use crate::prelude::*;
|
||||
use crate::widget::icon::{self, Handle};
|
||||
use crate::widget::{button, row, tooltip};
|
||||
use crate::Element;
|
||||
use iced_core::text::LineHeight;
|
||||
use iced_core::{font::Weight, widget::Id, Alignment, Length, Padding};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub type Button<'a, Message> = Builder<'a, Message, Hyperlink>;
|
||||
|
||||
pub struct Hyperlink {
|
||||
trailing_icon: bool,
|
||||
}
|
||||
|
||||
pub fn link<'a, Message>(label: impl Into<Cow<'a, str>> + 'static) -> Button<'a, Message> {
|
||||
Button::new(
|
||||
label,
|
||||
Hyperlink {
|
||||
trailing_icon: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
impl<'a, Message> Button<'a, Message> {
|
||||
pub fn new(label: impl Into<Cow<'a, str>> + 'static, link: Hyperlink) -> Self {
|
||||
Self {
|
||||
id: Id::unique(),
|
||||
label: label.into(),
|
||||
tooltip: Cow::Borrowed(""),
|
||||
on_press: None,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
padding: Padding::from(4),
|
||||
spacing: 0,
|
||||
icon_size: 16,
|
||||
line_height: 20,
|
||||
font_size: 14,
|
||||
font_weight: Weight::Normal,
|
||||
style: Style::Link,
|
||||
variant: link,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn trailing_icon(mut self, set: bool) -> Self {
|
||||
self.variant.trailing_icon = set;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon() -> Handle {
|
||||
icon::from_svg_bytes(&include_bytes!("external-link.svg")[..]).symbolic(true)
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Message> {
|
||||
fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> {
|
||||
let button: super::Button<'a, Message, crate::Renderer> = row::with_capacity(2)
|
||||
.push({
|
||||
let mut font = crate::font::DEFAULT;
|
||||
font.weight = builder.font_weight;
|
||||
|
||||
// TODO: Avoid allocation
|
||||
crate::widget::text(builder.label.to_string())
|
||||
.size(builder.font_size)
|
||||
.line_height(LineHeight::Absolute(builder.line_height.into()))
|
||||
.font(font)
|
||||
})
|
||||
.push_maybe(if builder.variant.trailing_icon {
|
||||
Some(icon().icon().size(builder.icon_size))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.padding(builder.padding)
|
||||
.width(builder.width)
|
||||
.height(builder.height)
|
||||
.spacing(builder.spacing)
|
||||
.align_items(Alignment::Center)
|
||||
.apply(button)
|
||||
.padding(0)
|
||||
.id(builder.id)
|
||||
.on_press_maybe(builder.on_press.take())
|
||||
.style(builder.style);
|
||||
|
||||
if builder.tooltip.is_empty() {
|
||||
button.into()
|
||||
} else {
|
||||
tooltip(button, builder.tooltip, tooltip::Position::Top)
|
||||
.size(builder.font_size)
|
||||
.font({
|
||||
let mut font = crate::font::DEFAULT;
|
||||
font.weight = builder.font_weight;
|
||||
font
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
pub mod hyperlink;
|
||||
pub use hyperlink::Button as LinkButton;
|
||||
pub use crate::theme::Button as Style;
|
||||
|
||||
pub mod link;
|
||||
pub use link::link;
|
||||
pub use link::Button as LinkButton;
|
||||
|
||||
mod icon;
|
||||
pub use icon::icon;
|
||||
|
|
@ -18,7 +21,6 @@ pub use text::{destructive, standard, suggested, text};
|
|||
mod widget;
|
||||
pub use widget::{draw, focus, layout, mouse_interaction, Button};
|
||||
|
||||
pub use crate::theme::Button as Style;
|
||||
use crate::Element;
|
||||
use iced_core::font::Weight;
|
||||
use iced_core::widget::Id;
|
||||
|
|
@ -35,7 +37,7 @@ pub fn button<'a, Message>(
|
|||
pub struct Builder<'a, Message, Variant> {
|
||||
id: Id,
|
||||
label: Cow<'a, str>,
|
||||
// tooltip: Cow<'a, str>,
|
||||
tooltip: Cow<'a, str>,
|
||||
on_press: Option<Message>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
|
|
@ -49,13 +51,8 @@ pub struct Builder<'a, Message, Variant> {
|
|||
variant: Variant,
|
||||
}
|
||||
|
||||
// /// A [`Button`] with an icon, which may be used in place of text.
|
||||
// pub const fn icon<'a>(selected: bool) -> Button<'a> {
|
||||
// Builder::new(Standard::Icon { selected })
|
||||
// }
|
||||
|
||||
impl<'a, Message, Variant> Builder<'a, Message, Variant> {
|
||||
/// Sets the [`Id`] of the [`Button`].
|
||||
/// Sets the [`Id`] of the button.
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
|
|
@ -66,48 +63,50 @@ impl<'a, Message, Variant> Builder<'a, Message, Variant> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Button`].
|
||||
/// Sets the width of the button.
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Button`].
|
||||
/// Sets the height of the button.
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Button`].
|
||||
/// Sets the [`Padding`] of the button.
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when the [`Button`] is pressed.
|
||||
/// Sets the message that will be produced when the button is pressed.
|
||||
///
|
||||
/// Unless `on_press` is called, the [`Button`] will be disabled.
|
||||
/// Unless `on_press` is called, the button will be disabled.
|
||||
pub fn on_press(mut self, on_press: Message) -> Self {
|
||||
self.on_press = Some(on_press);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when the [`Button`] is pressed,
|
||||
/// Sets the message that will be produced when the button is pressed,
|
||||
/// if `Some`.
|
||||
///
|
||||
/// If `None`, the [`Button`] will be disabled.
|
||||
/// If `None`, the button will be disabled.
|
||||
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
|
||||
self.on_press = on_press;
|
||||
self
|
||||
}
|
||||
|
||||
/// Overrides the preferred style of the button.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tooltip(mut self, label: impl Into<Cow<'a, str>>) -> Self {
|
||||
self.label = label.into();
|
||||
/// Adds a tooltip to the button.
|
||||
pub fn tooltip(mut self, tooltip: impl Into<Cow<'a, str>>) -> Self {
|
||||
self.tooltip = tooltip.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,4 @@ pub trait StyleSheet {
|
|||
|
||||
/// Produces the pressed [`Appearance`] of a button.
|
||||
fn pressed(&self, focused: bool, style: &Self::Style) -> Appearance;
|
||||
|
||||
/// [`Appearance`] when a button is selected.
|
||||
fn selected(&self, focused: bool, style: &Self::Style) -> Appearance;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::{button, Builder, Style};
|
||||
use crate::widget::{self, icon::Handle, row};
|
||||
use crate::widget::{icon, row, tooltip};
|
||||
use crate::{ext::CollectionWidget, Element};
|
||||
use apply::Apply;
|
||||
use iced_core::{font::Weight, text::LineHeight, widget::Id, Alignment, Length, Padding};
|
||||
|
|
@ -34,8 +34,8 @@ pub fn text<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message>
|
|||
}
|
||||
|
||||
pub struct Text {
|
||||
pub(super) leading_icon: Option<crate::widget::icon::Handle>,
|
||||
pub(super) trailing_icon: Option<crate::widget::icon::Handle>,
|
||||
pub(super) leading_icon: Option<icon::Handle>,
|
||||
pub(super) trailing_icon: Option<icon::Handle>,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
|
|
@ -55,6 +55,7 @@ impl<'a, Message> Button<'a, Message> {
|
|||
Self {
|
||||
id: Id::unique(),
|
||||
label: Cow::Borrowed(""),
|
||||
tooltip: Cow::Borrowed(""),
|
||||
on_press: None,
|
||||
width: Length::Shrink,
|
||||
height: Length::Fixed(theme.space_l().into()),
|
||||
|
|
@ -70,54 +71,76 @@ impl<'a, Message> Button<'a, Message> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn leading_icon(mut self, icon: impl Into<Handle>) -> Self {
|
||||
pub fn leading_icon(mut self, icon: impl Into<icon::Handle>) -> Self {
|
||||
self.variant.leading_icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn trailing_icon(mut self, icon: impl Into<Handle>) -> Self {
|
||||
pub fn trailing_icon(mut self, icon: impl Into<icon::Handle>) -> Self {
|
||||
self.variant.trailing_icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Message> {
|
||||
fn from(mut b: Button<'a, Message>) -> Element<'a, Message> {
|
||||
// TODO: Determine why this needs to be set before the label to prevent lifetime conflict.
|
||||
let trailing_icon = b
|
||||
.variant
|
||||
.trailing_icon
|
||||
.map(|i| Element::from(widget::icon(i).size(b.icon_size)));
|
||||
fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> {
|
||||
let trailing_icon = builder.variant.trailing_icon.map(|mut i| {
|
||||
if let icon::Data::Name(ref mut named) = i.data {
|
||||
named.size = Some(builder.icon_size);
|
||||
}
|
||||
|
||||
row::with_capacity(3)
|
||||
i.icon()
|
||||
});
|
||||
|
||||
let leading_icon = builder.variant.leading_icon.map(|mut i| {
|
||||
if let icon::Data::Name(ref mut named) = i.data {
|
||||
named.size = Some(builder.icon_size);
|
||||
}
|
||||
|
||||
i.icon()
|
||||
});
|
||||
|
||||
let label: Option<Element<'_, _>> = (!builder.label.is_empty()).then(|| {
|
||||
let mut font = crate::font::DEFAULT;
|
||||
font.weight = builder.font_weight;
|
||||
|
||||
// TODO: Avoid allocation
|
||||
crate::widget::text(builder.label.to_string())
|
||||
.size(builder.font_size)
|
||||
.line_height(LineHeight::Absolute(builder.line_height.into()))
|
||||
.font(font)
|
||||
.into()
|
||||
});
|
||||
|
||||
let button: super::Button<'a, Message, crate::Renderer> = row::with_capacity(3)
|
||||
// Optional icon to place before label.
|
||||
.push_maybe(
|
||||
b.variant
|
||||
.leading_icon
|
||||
.map(|i| widget::icon(i).size(b.icon_size)),
|
||||
)
|
||||
.push_maybe(leading_icon)
|
||||
// Optional label between icons.
|
||||
.push_maybe((!b.label.is_empty()).then(|| {
|
||||
let mut font = crate::font::DEFAULT;
|
||||
font.weight = b.font_weight;
|
||||
|
||||
crate::widget::text(b.label)
|
||||
.size(b.font_size)
|
||||
.line_height(LineHeight::Absolute(b.line_height.into()))
|
||||
.font(font)
|
||||
}))
|
||||
.push_maybe(label)
|
||||
// Optional icon to place behind the label.
|
||||
.push_maybe(trailing_icon)
|
||||
.padding(b.padding)
|
||||
.width(b.width)
|
||||
.height(b.height)
|
||||
.spacing(b.spacing)
|
||||
.padding(builder.padding)
|
||||
.width(builder.width)
|
||||
.height(builder.height)
|
||||
.spacing(builder.spacing)
|
||||
.align_items(Alignment::Center)
|
||||
.apply(button)
|
||||
.padding(0)
|
||||
.id(b.id)
|
||||
.on_press_maybe(b.on_press.take())
|
||||
.style(b.style)
|
||||
.into()
|
||||
.id(builder.id)
|
||||
.on_press_maybe(builder.on_press.take())
|
||||
.style(builder.style);
|
||||
|
||||
if builder.tooltip.is_empty() {
|
||||
button.into()
|
||||
} else {
|
||||
tooltip(button, builder.tooltip, tooltip::Position::Top)
|
||||
.size(builder.font_size)
|
||||
.font({
|
||||
let mut font = crate::font::DEFAULT;
|
||||
font.weight = builder.font_weight;
|
||||
font
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
/// Creates the widget for window controls.
|
||||
fn window_controls(&mut self) -> Element<'a, Message> {
|
||||
let icon = |name, size, on_press| {
|
||||
widget::icon::handle::from_name(name)
|
||||
widget::icon::from_name(name)
|
||||
.size(size)
|
||||
.handle()
|
||||
.apply(widget::button::icon)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::{Builder, Icon};
|
||||
use super::{Icon, Named};
|
||||
use crate::widget::{image, svg};
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::OsStr;
|
||||
|
|
@ -13,7 +13,7 @@ use std::path::PathBuf;
|
|||
pub struct Handle {
|
||||
pub symbolic: bool,
|
||||
#[setters(skip)]
|
||||
pub variant: Variant,
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
|
|
@ -24,16 +24,12 @@ impl Handle {
|
|||
|
||||
#[must_use]
|
||||
#[derive(Clone, Debug, Hash)]
|
||||
pub enum Variant {
|
||||
pub enum Data {
|
||||
Name(Named),
|
||||
Image(image::Handle),
|
||||
Svg(svg::Handle),
|
||||
}
|
||||
|
||||
/// Create an icon handle from its XDG icon name.
|
||||
pub fn from_name(name: &str) -> Builder {
|
||||
Builder::new(name)
|
||||
}
|
||||
|
||||
/// Create an icon handle from its path.
|
||||
pub fn from_path(path: PathBuf) -> Handle {
|
||||
Handle {
|
||||
|
|
@ -41,10 +37,10 @@ pub fn from_path(path: PathBuf) -> Handle {
|
|||
.file_stem()
|
||||
.and_then(OsStr::to_str)
|
||||
.is_some_and(|name| name.ends_with("-symbolic")),
|
||||
variant: if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
|
||||
Variant::Svg(svg::Handle::from_path(path))
|
||||
data: if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
|
||||
Data::Svg(svg::Handle::from_path(path))
|
||||
} else {
|
||||
Variant::Image(image::Handle::from_path(path))
|
||||
Data::Image(image::Handle::from_path(path))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +55,7 @@ pub fn from_raster_bytes(
|
|||
) -> Handle {
|
||||
Handle {
|
||||
symbolic: false,
|
||||
variant: Variant::Image(image::Handle::from_memory(bytes)),
|
||||
data: Data::Image(image::Handle::from_memory(bytes)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +71,7 @@ pub fn from_raster_pixels(
|
|||
) -> Handle {
|
||||
Handle {
|
||||
symbolic: false,
|
||||
variant: Variant::Image(image::Handle::from_pixels(width, height, pixels)),
|
||||
data: Data::Image(image::Handle::from_pixels(width, height, pixels)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,6 +79,6 @@ pub fn from_raster_pixels(
|
|||
pub fn from_svg_bytes(bytes: impl Into<Cow<'static, [u8]>>) -> Handle {
|
||||
Handle {
|
||||
symbolic: false,
|
||||
variant: Variant::Svg(svg::Handle::from_memory(bytes)),
|
||||
data: Data::Svg(svg::Handle::from_memory(bytes)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,19 @@
|
|||
|
||||
//! Lazily-generated SVG icon widget for Iced.
|
||||
|
||||
mod builder;
|
||||
pub use builder::Builder;
|
||||
mod named;
|
||||
use std::ffi::OsStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod handle;
|
||||
pub use handle::Handle;
|
||||
pub use named::Named;
|
||||
|
||||
mod handle;
|
||||
pub use handle::{from_path, from_raster_bytes, from_raster_pixels, from_svg_bytes, Data, Handle};
|
||||
|
||||
use crate::{Element, Renderer};
|
||||
use derive_setters::Setters;
|
||||
use iced::widget::{Image, Svg};
|
||||
use iced::{ContentFit, Length};
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Create an [`Icon`] from a pre-existing [`Handle`]
|
||||
pub fn icon(handle: Handle) -> Icon {
|
||||
|
|
@ -28,38 +29,9 @@ pub fn icon(handle: Handle) -> Icon {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create an [`Icon`] from its path.
|
||||
pub fn from_path(path: PathBuf) -> Icon {
|
||||
icon(handle::from_path(path))
|
||||
}
|
||||
|
||||
/// Create an image [`Icon`] from memory.
|
||||
pub fn from_raster_bytes(
|
||||
bytes: impl Into<Cow<'static, [u8]>>
|
||||
+ std::convert::AsRef<[u8]>
|
||||
+ std::marker::Send
|
||||
+ std::marker::Sync
|
||||
+ 'static,
|
||||
) -> Icon {
|
||||
icon(handle::from_raster_bytes(bytes))
|
||||
}
|
||||
|
||||
/// Create an image [`Icon`] from RGBA data, where you must define the width and height.
|
||||
pub fn from_raster_pixels(
|
||||
width: u32,
|
||||
height: u32,
|
||||
pixels: impl Into<Cow<'static, [u8]>>
|
||||
+ std::convert::AsRef<[u8]>
|
||||
+ std::marker::Send
|
||||
+ std::marker::Sync
|
||||
+ 'static,
|
||||
) -> Icon {
|
||||
icon(handle::from_raster_pixels(width, height, pixels))
|
||||
}
|
||||
|
||||
/// Create a SVG [`Icon`] from memory.
|
||||
pub fn from_svg_bytes(bytes: impl Into<Cow<'static, [u8]>>) -> Icon {
|
||||
icon(handle::from_svg_bytes(bytes))
|
||||
/// Create an icon handle from its XDG icon name.
|
||||
pub fn from_name(name: impl Into<Arc<str>>) -> Named {
|
||||
Named::new(name)
|
||||
}
|
||||
|
||||
/// An image which may be an SVG or PNG.
|
||||
|
|
@ -80,19 +52,40 @@ pub struct Icon {
|
|||
impl Icon {
|
||||
#[must_use]
|
||||
fn into_element<Message: 'static>(self) -> Element<'static, Message> {
|
||||
match self.handle.variant {
|
||||
handle::Variant::Image(handle) => Image::new(handle)
|
||||
let from_image = |handle| {
|
||||
Image::new(handle)
|
||||
.width(self.width.unwrap_or(Length::Fixed(f32::from(self.size))))
|
||||
.height(self.height.unwrap_or(Length::Fixed(f32::from(self.size))))
|
||||
.content_fit(self.content_fit)
|
||||
.into(),
|
||||
handle::Variant::Svg(handle) => Svg::<Renderer>::new(handle)
|
||||
.into()
|
||||
};
|
||||
|
||||
let from_svg = |handle| {
|
||||
Svg::<Renderer>::new(handle)
|
||||
.style(self.style.clone())
|
||||
.width(self.width.unwrap_or(Length::Fixed(f32::from(self.size))))
|
||||
.height(self.height.unwrap_or(Length::Fixed(f32::from(self.size))))
|
||||
.content_fit(self.content_fit)
|
||||
.symbolic(self.handle.symbolic)
|
||||
.into(),
|
||||
.into()
|
||||
};
|
||||
|
||||
match self.handle.data {
|
||||
Data::Name(named) => {
|
||||
if let Some(path) = named.path() {
|
||||
if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
|
||||
from_svg(iced_core::svg::Handle::from_path(path))
|
||||
} else {
|
||||
from_image(iced_core::image::Handle::from_path(path))
|
||||
}
|
||||
} else {
|
||||
let bytes: &'static [u8] = &[];
|
||||
from_svg(iced_core::svg::Handle::from_memory(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
Data::Image(handle) => from_image(handle),
|
||||
Data::Svg(handle) => from_svg(handle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,38 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::{handle, Handle, Icon};
|
||||
use std::path::PathBuf;
|
||||
use super::{Handle, Icon};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
#[must_use]
|
||||
#[derive(derive_setters::Setters)]
|
||||
pub struct Builder<'a> {
|
||||
#[derive(derive_setters::Setters, Clone, Debug, Hash)]
|
||||
pub struct Named {
|
||||
/// Name of icon to locate in an XDG icon path.
|
||||
name: &'a str,
|
||||
pub(super) name: Arc<str>,
|
||||
|
||||
/// Checks for a fallback if the icon was not found.
|
||||
fallback: bool,
|
||||
pub fallback: bool,
|
||||
|
||||
/// Restrict the lookup to a given scale.
|
||||
#[setters(strip_option)]
|
||||
scale: Option<u16>,
|
||||
pub scale: Option<u16>,
|
||||
|
||||
/// Restrict the lookup to a given size.
|
||||
#[setters(strip_option)]
|
||||
size: Option<u16>,
|
||||
pub size: Option<u16>,
|
||||
|
||||
/// Whether the icon is symbolic or not.
|
||||
pub symbolic: bool,
|
||||
|
||||
/// Prioritizes SVG over PNG
|
||||
prefer_svg: bool,
|
||||
pub prefer_svg: bool,
|
||||
}
|
||||
|
||||
impl<'a> Builder<'a> {
|
||||
pub const fn new(name: &'a str) -> Self {
|
||||
impl Named {
|
||||
pub fn new(name: impl Into<Arc<str>>) -> Self {
|
||||
let name = name.into();
|
||||
Self {
|
||||
symbolic: name.ends_with("-symbolic"),
|
||||
name,
|
||||
fallback: true,
|
||||
size: None,
|
||||
|
|
@ -37,12 +42,13 @@ impl<'a> Builder<'a> {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn path(mut self) -> Option<PathBuf> {
|
||||
pub fn path(self) -> Option<PathBuf> {
|
||||
let mut name = &*self.name;
|
||||
crate::icon_theme::DEFAULT.with(|theme| {
|
||||
let theme = theme.borrow();
|
||||
|
||||
let locate = || {
|
||||
let mut lookup = freedesktop_icons::lookup(self.name)
|
||||
let mut lookup = freedesktop_icons::lookup(name)
|
||||
.with_theme(theme.as_ref())
|
||||
.with_cache();
|
||||
|
||||
|
|
@ -65,10 +71,8 @@ impl<'a> Builder<'a> {
|
|||
|
||||
// On failure, attempt to locate fallback icon.
|
||||
if result.is_none() && self.fallback {
|
||||
let name = std::mem::take(&mut self.name);
|
||||
|
||||
for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) {
|
||||
self.name = name;
|
||||
for new_name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) {
|
||||
name = new_name;
|
||||
result = locate();
|
||||
if result.is_some() {
|
||||
break;
|
||||
|
|
@ -81,11 +85,9 @@ impl<'a> Builder<'a> {
|
|||
}
|
||||
|
||||
pub fn handle(self) -> Handle {
|
||||
if let Some(path) = self.path() {
|
||||
handle::from_path(path)
|
||||
} else {
|
||||
let bytes: &'static [u8] = &[];
|
||||
handle::from_svg_bytes(bytes)
|
||||
Handle {
|
||||
symbolic: self.symbolic,
|
||||
data: super::Data::Name(self),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,14 +103,20 @@ impl<'a> Builder<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Builder<'a>> for Handle {
|
||||
fn from(builder: Builder<'a>) -> Self {
|
||||
impl From<Named> for Handle {
|
||||
fn from(builder: Named) -> Self {
|
||||
builder.handle()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Builder<'a>> for Icon {
|
||||
fn from(builder: Builder<'a>) -> Self {
|
||||
impl From<Named> for Icon {
|
||||
fn from(builder: Named) -> Self {
|
||||
builder.icon()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static> From<Named> for crate::Element<'a, Message> {
|
||||
fn from(builder: Named) -> Self {
|
||||
builder.icon().into()
|
||||
}
|
||||
}
|
||||
|
|
@ -26,12 +26,10 @@ pub fn nav_bar_toggle<Message>() -> NavBarToggle<Message> {
|
|||
impl<'a, Message: 'static + Clone> From<NavBarToggle<Message>> for Element<'a, Message> {
|
||||
fn from(nav_bar_toggle: NavBarToggle<Message>) -> Self {
|
||||
let icon = if nav_bar_toggle.active {
|
||||
widget::icon::handle::from_svg_bytes(
|
||||
&include_bytes!("../../res/sidebar-active.svg")[..],
|
||||
)
|
||||
.symbolic(true)
|
||||
widget::icon::from_svg_bytes(&include_bytes!("../../res/sidebar-active.svg")[..])
|
||||
.symbolic(true)
|
||||
} else {
|
||||
widget::icon::handle::from_name("open-menu-symbolic")
|
||||
widget::icon::from_name("open-menu-symbolic")
|
||||
.size(16)
|
||||
.handle()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ impl<'a, Message: 'static + Clone> Field<'a, Message> {
|
|||
|
||||
row::with_capacity(3)
|
||||
.push(
|
||||
icon::handle::from_svg_bytes(&include_bytes!("search.svg")[..])
|
||||
icon::from_svg_bytes(&include_bytes!("search.svg")[..])
|
||||
.symbolic(true)
|
||||
.icon()
|
||||
.size(16),
|
||||
|
|
@ -67,7 +67,7 @@ impl<'a, Message: 'static + Clone> From<Field<'a, Message>> for crate::Element<'
|
|||
}
|
||||
|
||||
fn clear_button<Message: 'static>() -> crate::widget::IconButton<'static, Message> {
|
||||
icon::handle::from_name("edit-clear-symbolic")
|
||||
icon::from_name("edit-clear-symbolic")
|
||||
.size(16)
|
||||
.apply(crate::widget::button::icon)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ mod button {
|
|||
/// A search button which converts to a search [`field`] on click.
|
||||
#[must_use]
|
||||
pub fn button<Message: 'static + Clone>(on_press: Message) -> crate::Element<'static, Message> {
|
||||
icon::handle::from_svg_bytes(&include_bytes!("search.svg")[..])
|
||||
icon::from_svg_bytes(&include_bytes!("search.svg")[..])
|
||||
.symbolic(true)
|
||||
.apply(crate::widget::button::icon)
|
||||
.on_press(on_press)
|
||||
|
|
|
|||
|
|
@ -122,9 +122,7 @@ where
|
|||
Self {
|
||||
model,
|
||||
id: None,
|
||||
close_icon: icon::handle::from_name("window-close-symbolic")
|
||||
.size(16)
|
||||
.icon(),
|
||||
close_icon: icon::from_name("window-close-symbolic").size(16).icon(),
|
||||
show_close_icon_on_hover: false,
|
||||
button_padding: [4, 4, 4, 4],
|
||||
button_height: 32,
|
||||
|
|
|
|||
|
|
@ -42,9 +42,8 @@ impl<'a, Message: 'static> SpinButton<'a, Message> {
|
|||
let Self { on_change, label } = self;
|
||||
container(
|
||||
row::with_children(vec![
|
||||
icon::handle::from_name("list-remove-symbolic")
|
||||
icon::from_name("list-remove-symbolic")
|
||||
.size(24)
|
||||
.icon()
|
||||
.apply(container)
|
||||
.width(Length::Fixed(32.0))
|
||||
.height(Length::Fixed(32.0))
|
||||
|
|
@ -62,9 +61,8 @@ impl<'a, Message: 'static> SpinButton<'a, Message> {
|
|||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center)
|
||||
.into(),
|
||||
icon::handle::from_name("list-add-symbolic")
|
||||
icon::from_name("list-add-symbolic")
|
||||
.size(24)
|
||||
.icon()
|
||||
.apply(container)
|
||||
.width(Length::Fixed(32.0))
|
||||
.height(Length::Fixed(32.0))
|
||||
|
|
|
|||
|
|
@ -68,23 +68,23 @@ where
|
|||
let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs());
|
||||
let input = TextInput::new(placeholder, value)
|
||||
.padding([0, spacing, 0, spacing])
|
||||
.style(super::style::TextInput::Search)
|
||||
.start_icon(
|
||||
iced_widget::container(
|
||||
crate::widget::icon::handle::from_name("system-search-symbolic")
|
||||
.size(16)
|
||||
.icon(),
|
||||
)
|
||||
.padding([spacing, spacing, spacing, spacing])
|
||||
.into(),
|
||||
.style(crate::theme::TextInput::Search)
|
||||
.leading_icon(
|
||||
crate::widget::icon::from_name("system-search-symbolic")
|
||||
.size(16)
|
||||
.apply(crate::widget::container)
|
||||
.padding([spacing, spacing, spacing, spacing])
|
||||
.into(),
|
||||
);
|
||||
|
||||
if let Some(msg) = on_clear {
|
||||
input.end_icon(
|
||||
crate::widget::icon::handle::from_name("edit-clear-symbolic")
|
||||
input.trailing_icon(
|
||||
crate::widget::icon::from_name("edit-clear-symbolic")
|
||||
.size(16)
|
||||
.handle()
|
||||
.apply(crate::widget::button::icon)
|
||||
.apply(crate::widget::button)
|
||||
.style(crate::theme::Button::Icon)
|
||||
.width(32)
|
||||
.height(32)
|
||||
.on_press(msg)
|
||||
.padding([spacing, spacing, spacing, spacing])
|
||||
.into(),
|
||||
|
|
@ -108,12 +108,11 @@ where
|
|||
let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs());
|
||||
let mut input = TextInput::new(placeholder, value)
|
||||
.padding([0, spacing, 0, spacing])
|
||||
.style(super::style::TextInput::Default)
|
||||
.start_icon(
|
||||
crate::widget::icon::handle::from_name("system-lock-screen-symbolic")
|
||||
.style(crate::theme::TextInput::Default)
|
||||
.leading_icon(
|
||||
crate::widget::icon::from_name("system-lock-screen-symbolic")
|
||||
.size(16)
|
||||
.icon()
|
||||
.apply(iced_widget::container)
|
||||
.apply(crate::widget::container)
|
||||
.padding([spacing, spacing, spacing, spacing])
|
||||
.into(),
|
||||
);
|
||||
|
|
@ -121,11 +120,11 @@ where
|
|||
input = input.password();
|
||||
}
|
||||
if let Some(msg) = on_visible_toggle {
|
||||
input.end_icon(
|
||||
crate::widget::icon::handle::from_name("document-properties-symbolic")
|
||||
input.trailing_icon(
|
||||
crate::widget::icon::from_name("document-properties-symbolic")
|
||||
.size(16)
|
||||
.handle()
|
||||
.apply(crate::widget::button::icon)
|
||||
.apply(crate::widget::button)
|
||||
.style(crate::theme::Button::Icon)
|
||||
.on_press(msg)
|
||||
.padding([spacing, spacing, spacing, spacing])
|
||||
.into(),
|
||||
|
|
@ -145,7 +144,7 @@ where
|
|||
let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs());
|
||||
|
||||
TextInput::new("", value)
|
||||
.style(super::style::TextInput::Inline)
|
||||
.style(crate::theme::TextInput::Inline)
|
||||
.padding([spacing, spacing, spacing, spacing])
|
||||
}
|
||||
|
||||
|
|
@ -204,8 +203,8 @@ pub struct TextInput<'a, Message> {
|
|||
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
||||
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
||||
on_submit: Option<Message>,
|
||||
start_icon: Option<Element<'a, Message, crate::Renderer>>,
|
||||
end_element: Option<Element<'a, Message, crate::Renderer>>,
|
||||
leading_icon: Option<Element<'a, Message, crate::Renderer>>,
|
||||
trailing_icon: Option<Element<'a, Message, crate::Renderer>>,
|
||||
style: <<crate::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
|
||||
// (text_input::State, mime_type, dnd_action) -> Message
|
||||
on_create_dnd_source: Option<Box<dyn Fn(State) -> Message + 'a>>,
|
||||
|
|
@ -242,10 +241,10 @@ where
|
|||
on_input: None,
|
||||
on_paste: None,
|
||||
on_submit: None,
|
||||
start_icon: None,
|
||||
end_element: None,
|
||||
leading_icon: None,
|
||||
trailing_icon: None,
|
||||
error: None,
|
||||
style: super::style::TextInput::default(),
|
||||
style: crate::theme::TextInput::default(),
|
||||
on_dnd_command_produced: None,
|
||||
on_create_dnd_source: None,
|
||||
surface_ids: None,
|
||||
|
|
@ -333,14 +332,14 @@ where
|
|||
}
|
||||
|
||||
/// Sets the start [`Icon`] of the [`TextInput`].
|
||||
pub fn start_icon(mut self, icon: Element<'a, Message, crate::Renderer>) -> Self {
|
||||
self.start_icon = Some(icon);
|
||||
pub fn leading_icon(mut self, icon: Element<'a, Message, crate::Renderer>) -> Self {
|
||||
self.leading_icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the end [`Icon`] of the [`TextInput`].
|
||||
pub fn end_icon(mut self, icon: Element<'a, Message, crate::Renderer>) -> Self {
|
||||
self.end_element = Some(icon);
|
||||
pub fn trailing_icon(mut self, icon: Element<'a, Message, crate::Renderer>) -> Self {
|
||||
self.trailing_icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -398,8 +397,8 @@ where
|
|||
self.font,
|
||||
self.on_input.is_none(),
|
||||
self.is_secure,
|
||||
self.start_icon.as_ref(),
|
||||
self.end_element.as_ref(),
|
||||
self.leading_icon.as_ref(),
|
||||
self.trailing_icon.as_ref(),
|
||||
&self.style,
|
||||
self.dnd_icon,
|
||||
self.line_height,
|
||||
|
|
@ -485,18 +484,18 @@ where
|
|||
state.dragging_state = None;
|
||||
}
|
||||
let mut children: Vec<_> = self
|
||||
.start_icon
|
||||
.leading_icon
|
||||
.iter_mut()
|
||||
.chain(self.end_element.iter_mut())
|
||||
.chain(self.trailing_icon.iter_mut())
|
||||
.map(iced_core::Element::as_widget_mut)
|
||||
.collect();
|
||||
tree.diff_children(children.as_mut_slice());
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.start_icon
|
||||
self.leading_icon
|
||||
.iter()
|
||||
.chain(self.end_element.iter())
|
||||
.chain(self.trailing_icon.iter())
|
||||
.map(|icon| Tree::new(icon))
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -536,8 +535,8 @@ where
|
|||
self.width,
|
||||
self.padding,
|
||||
self.size,
|
||||
self.start_icon.as_ref(),
|
||||
self.end_element.as_ref(),
|
||||
self.leading_icon.as_ref(),
|
||||
self.trailing_icon.as_ref(),
|
||||
self.line_height,
|
||||
self.label,
|
||||
self.helper_text,
|
||||
|
|
@ -573,13 +572,13 @@ where
|
|||
) -> event::Status {
|
||||
let text_layout = self.text_layout(layout);
|
||||
let mut child_state = tree.children.iter_mut();
|
||||
if let (Some(start_icon), Some(tree)) = (self.start_icon.as_mut(), child_state.next()) {
|
||||
if let (Some(leading_icon), Some(tree)) = (self.leading_icon.as_mut(), child_state.next()) {
|
||||
let mut children = text_layout.children();
|
||||
children.next();
|
||||
let start_icon_layout = children.next().unwrap();
|
||||
let leading_icon_layout = children.next().unwrap();
|
||||
|
||||
if cursor_position.is_over(start_icon_layout.bounds()) {
|
||||
return start_icon.as_widget_mut().on_event(
|
||||
if cursor_position.is_over(leading_icon_layout.bounds()) {
|
||||
return leading_icon.as_widget_mut().on_event(
|
||||
tree,
|
||||
event.clone(),
|
||||
layout,
|
||||
|
|
@ -591,14 +590,15 @@ where
|
|||
);
|
||||
}
|
||||
}
|
||||
if let (Some(end_icon), Some(tree)) = (self.end_element.as_mut(), child_state.next()) {
|
||||
if let (Some(trailing_icon), Some(tree)) = (self.trailing_icon.as_mut(), child_state.next())
|
||||
{
|
||||
let mut children = text_layout.children();
|
||||
children.next();
|
||||
children.next();
|
||||
let end_icon_layout = children.next().unwrap();
|
||||
let trailing_icon_layout = children.next().unwrap();
|
||||
|
||||
if cursor_position.is_over(end_icon_layout.bounds()) {
|
||||
return end_icon.as_widget_mut().on_event(
|
||||
if cursor_position.is_over(trailing_icon_layout.bounds()) {
|
||||
return trailing_icon.as_widget_mut().on_event(
|
||||
tree,
|
||||
event.clone(),
|
||||
layout,
|
||||
|
|
@ -656,8 +656,8 @@ where
|
|||
self.font,
|
||||
self.on_input.is_none(),
|
||||
self.is_secure,
|
||||
self.start_icon.as_ref(),
|
||||
self.end_element.as_ref(),
|
||||
self.leading_icon.as_ref(),
|
||||
self.trailing_icon.as_ref(),
|
||||
&self.style,
|
||||
self.dnd_icon,
|
||||
self.line_height,
|
||||
|
|
@ -681,15 +681,15 @@ where
|
|||
) -> mouse::Interaction {
|
||||
let layout = self.text_layout(layout);
|
||||
let mut index = 0;
|
||||
if let (Some(start_icon), Some(tree)) =
|
||||
(self.start_icon.as_ref(), state.children.get(index))
|
||||
if let (Some(leading_icon), Some(tree)) =
|
||||
(self.leading_icon.as_ref(), state.children.get(index))
|
||||
{
|
||||
let mut children = layout.children();
|
||||
children.next();
|
||||
let start_icon_layout = children.next().unwrap();
|
||||
let leading_icon_layout = children.next().unwrap();
|
||||
|
||||
if cursor_position.is_over(start_icon_layout.bounds()) {
|
||||
return start_icon.mouse_interaction(
|
||||
if cursor_position.is_over(leading_icon_layout.bounds()) {
|
||||
return leading_icon.mouse_interaction(
|
||||
tree,
|
||||
layout,
|
||||
cursor_position,
|
||||
|
|
@ -700,15 +700,16 @@ where
|
|||
index += 1;
|
||||
}
|
||||
|
||||
if let (Some(end_icon), Some(tree)) = (self.end_element.as_ref(), state.children.get(index))
|
||||
if let (Some(trailing_icon), Some(tree)) =
|
||||
(self.trailing_icon.as_ref(), state.children.get(index))
|
||||
{
|
||||
let mut children = layout.children();
|
||||
children.next();
|
||||
children.next();
|
||||
let end_icon_layout = children.next().unwrap();
|
||||
let trailing_icon_layout = children.next().unwrap();
|
||||
|
||||
if cursor_position.is_over(end_icon_layout.bounds()) {
|
||||
return end_icon.mouse_interaction(
|
||||
if cursor_position.is_over(trailing_icon_layout.bounds()) {
|
||||
return trailing_icon.mouse_interaction(
|
||||
tree,
|
||||
layout,
|
||||
cursor_position,
|
||||
|
|
@ -770,8 +771,8 @@ pub fn layout<Message>(
|
|||
width: Length,
|
||||
padding: Padding,
|
||||
size: Option<f32>,
|
||||
start_icon: Option<&Element<'_, Message, crate::Renderer>>,
|
||||
end_icon: Option<&Element<'_, Message, crate::Renderer>>,
|
||||
leading_icon: Option<&Element<'_, Message, crate::Renderer>>,
|
||||
trailing_icon: Option<&Element<'_, Message, crate::Renderer>>,
|
||||
line_height: text::LineHeight,
|
||||
label: Option<&str>,
|
||||
helper_text: Option<&str>,
|
||||
|
|
@ -804,13 +805,13 @@ pub fn layout<Message>(
|
|||
let mut text_input_height = text_size * 1.2;
|
||||
let padding = padding.fit(Size::ZERO, limits.max());
|
||||
|
||||
let helper_pos = if start_icon.is_some() || end_icon.is_some() {
|
||||
let helper_pos = if leading_icon.is_some() || trailing_icon.is_some() {
|
||||
// TODO configurable icon spacing, maybe via appearance
|
||||
let limits_copy = limits;
|
||||
|
||||
let limits = limits.pad(padding);
|
||||
let icon_spacing = 8.0;
|
||||
let (start_icon_width, mut start_icon) = if let Some(icon) = start_icon.as_ref() {
|
||||
let (leading_icon_width, mut leading_icon) = if let Some(icon) = leading_icon.as_ref() {
|
||||
let icon_node = icon.layout(
|
||||
renderer,
|
||||
&Limits::NONE
|
||||
|
|
@ -823,7 +824,7 @@ pub fn layout<Message>(
|
|||
(0.0, None)
|
||||
};
|
||||
|
||||
let (end_icon_width, mut end_icon) = if let Some(icon) = end_icon.as_ref() {
|
||||
let (trailing_icon_width, mut trailing_icon) = if let Some(icon) = trailing_icon.as_ref() {
|
||||
let icon_node = icon.layout(
|
||||
renderer,
|
||||
&Limits::NONE
|
||||
|
|
@ -839,11 +840,12 @@ pub fn layout<Message>(
|
|||
|
||||
let text_bounds = text_limits.resolve(Size::ZERO);
|
||||
|
||||
let mut text_node =
|
||||
layout::Node::new(text_bounds - Size::new(start_icon_width + end_icon_width, 0.0));
|
||||
let mut text_node = layout::Node::new(
|
||||
text_bounds - Size::new(leading_icon_width + trailing_icon_width, 0.0),
|
||||
);
|
||||
|
||||
text_node.move_to(Point::new(
|
||||
padding.left + start_icon_width,
|
||||
padding.left + leading_icon_width,
|
||||
padding.top + ((text_input_height - text_size * 1.2) / 2.0).max(0.0),
|
||||
));
|
||||
let mut node_list: Vec<_> = Vec::with_capacity(3);
|
||||
|
|
@ -851,23 +853,23 @@ pub fn layout<Message>(
|
|||
let text_node_bounds = text_node.bounds();
|
||||
node_list.push(text_node);
|
||||
|
||||
if let Some(mut start_icon) = start_icon.take() {
|
||||
start_icon.move_to(Point::new(
|
||||
if let Some(mut leading_icon) = leading_icon.take() {
|
||||
leading_icon.move_to(Point::new(
|
||||
padding.left,
|
||||
padding.top + ((text_size * 1.2 - start_icon.bounds().height) / 2.0).max(0.0),
|
||||
padding.top + ((text_size * 1.2 - leading_icon.bounds().height) / 2.0).max(0.0),
|
||||
));
|
||||
node_list.push(start_icon);
|
||||
node_list.push(leading_icon);
|
||||
}
|
||||
if let Some(mut end_icon) = end_icon.take() {
|
||||
end_icon.move_to(Point::new(
|
||||
if let Some(mut trailing_icon) = trailing_icon.take() {
|
||||
trailing_icon.move_to(Point::new(
|
||||
text_node_bounds.x + text_node_bounds.width + f32::from(spacing),
|
||||
padding.top + ((text_size * 1.2 - end_icon.bounds().height) / 2.0).max(0.0),
|
||||
padding.top + ((text_size * 1.2 - trailing_icon.bounds().height) / 2.0).max(0.0),
|
||||
));
|
||||
node_list.push(end_icon);
|
||||
node_list.push(trailing_icon);
|
||||
}
|
||||
|
||||
let text_input_size = Size::new(
|
||||
text_node_bounds.x + text_node_bounds.width + end_icon_width,
|
||||
text_node_bounds.x + text_node_bounds.width + trailing_icon_width,
|
||||
text_input_height,
|
||||
)
|
||||
.pad(padding);
|
||||
|
|
@ -987,6 +989,7 @@ where
|
|||
|
||||
let font: <Renderer as text::Renderer>::Font =
|
||||
font.unwrap_or_else(|| renderer.default_font());
|
||||
|
||||
if is_clicked {
|
||||
let Some(pos) = cursor_position.position() else {
|
||||
return event::Status::Ignored;
|
||||
|
|
@ -1737,7 +1740,7 @@ pub fn draw<'a, Message>(
|
|||
is_disabled: bool,
|
||||
is_secure: bool,
|
||||
icon: Option<&Element<'a, Message, crate::Renderer>>,
|
||||
end_element: Option<&Element<'a, Message, crate::Renderer>>,
|
||||
trailing_icon: Option<&Element<'a, Message, crate::Renderer>>,
|
||||
style: &<crate::Theme as StyleSheet>::Style,
|
||||
dnd_icon: bool,
|
||||
line_height: text::LineHeight,
|
||||
|
|
@ -1848,9 +1851,9 @@ pub fn draw<'a, Message>(
|
|||
});
|
||||
}
|
||||
let mut child_index = 0;
|
||||
let start_icon_tree = children.get(child_index);
|
||||
let leading_icon_tree = children.get(child_index);
|
||||
// draw the start icon in the text input
|
||||
if let (Some(icon), Some(tree)) = (icon, start_icon_tree) {
|
||||
if let (Some(icon), Some(tree)) = (icon, leading_icon_tree) {
|
||||
let icon_layout = children_layout.next().unwrap();
|
||||
|
||||
icon.as_widget().draw(
|
||||
|
|
@ -1858,7 +1861,7 @@ pub fn draw<'a, Message>(
|
|||
renderer,
|
||||
theme,
|
||||
&renderer::Style {
|
||||
icon_color: renderer_style.icon_color,
|
||||
icon_color: appearance.icon_color,
|
||||
text_color: appearance.text_color,
|
||||
scale_factor: renderer_style.scale_factor,
|
||||
},
|
||||
|
|
@ -1977,12 +1980,6 @@ pub fn draw<'a, Message>(
|
|||
text::Shaping::Advanced,
|
||||
);
|
||||
|
||||
let color = if text.is_empty() {
|
||||
theme.placeholder_color(style)
|
||||
} else {
|
||||
appearance.text_color
|
||||
};
|
||||
|
||||
let render = |renderer: &mut crate::Renderer| {
|
||||
if let Some((cursor, color)) = cursor {
|
||||
renderer.fill_quad(cursor, color);
|
||||
|
|
@ -1992,7 +1989,11 @@ pub fn draw<'a, Message>(
|
|||
|
||||
renderer.fill_text(Text {
|
||||
content: if text.is_empty() { placeholder } else { &text },
|
||||
color,
|
||||
color: if text.is_empty() {
|
||||
appearance.placeholder_color
|
||||
} else {
|
||||
appearance.text_color
|
||||
},
|
||||
font,
|
||||
bounds: Rectangle {
|
||||
y: text_bounds.center_y(),
|
||||
|
|
@ -2015,10 +2016,10 @@ pub fn draw<'a, Message>(
|
|||
render(renderer);
|
||||
}
|
||||
|
||||
let end_icon_tree = children.get(child_index);
|
||||
let trailing_icon_tree = children.get(child_index);
|
||||
|
||||
// draw the end icon in the text input
|
||||
if let (Some(icon), Some(tree)) = (end_element, end_icon_tree) {
|
||||
if let (Some(icon), Some(tree)) = (trailing_icon, trailing_icon_tree) {
|
||||
let icon_layout = children_layout.next().unwrap();
|
||||
|
||||
icon.as_widget().draw(
|
||||
|
|
@ -2042,7 +2043,7 @@ pub fn draw<'a, Message>(
|
|||
content: helper_text,
|
||||
size: helper_text_size,
|
||||
font,
|
||||
color,
|
||||
color: appearance.text_color,
|
||||
bounds: helper_text_layout.bounds(),
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
|
|
|
|||
|
|
@ -10,5 +10,6 @@ mod input;
|
|||
mod style;
|
||||
pub mod value;
|
||||
|
||||
pub use crate::theme::TextInput as Style;
|
||||
pub use input::*;
|
||||
pub use style::{Appearance as TextInputAppearance, StyleSheet as TextInputStyleSheet};
|
||||
pub use style::{Appearance, StyleSheet};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Change the appearance of a text input.
|
||||
|
||||
use iced_core::{Background, BorderRadius, Color};
|
||||
|
||||
/// The appearance of a text input.
|
||||
|
|
@ -18,8 +19,12 @@ pub struct Appearance {
|
|||
pub border_width: f32,
|
||||
/// The border [`Color`] of the text input.
|
||||
pub border_color: Color,
|
||||
/// The [`Color`] of symbolic icons.
|
||||
pub icon_color: Color,
|
||||
/// The label [`Color`] of the text input.
|
||||
pub label_color: Color,
|
||||
/// The placeholder text [`Color`].
|
||||
pub placeholder_color: Color,
|
||||
/// The text [`Color`] of the text input.
|
||||
pub selected_text_color: Color,
|
||||
/// The text [`Color`] of the text input.
|
||||
|
|
@ -42,9 +47,6 @@ pub trait StyleSheet {
|
|||
/// Produces the style of a focused text input.
|
||||
fn focused(&self, style: &Self::Style) -> Appearance;
|
||||
|
||||
/// Produces the [`Color`] of the placeholder of a text input.
|
||||
fn placeholder_color(&self, style: &Self::Style) -> Color;
|
||||
|
||||
/// Produces the style of an hovered text input.
|
||||
fn hovered(&self, style: &Self::Style) -> Appearance {
|
||||
self.focused(style)
|
||||
|
|
@ -53,247 +55,3 @@ pub trait StyleSheet {
|
|||
/// Produces the style of a disabled text input.
|
||||
fn disabled(&self, style: &Self::Style) -> Appearance;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum TextInput {
|
||||
#[default]
|
||||
Default,
|
||||
ExpandableSearch,
|
||||
Search,
|
||||
Inline,
|
||||
Custom {
|
||||
active: Box<dyn Fn(&crate::Theme) -> Appearance>,
|
||||
error: Box<dyn Fn(&crate::Theme) -> Appearance>,
|
||||
hovered: Box<dyn Fn(&crate::Theme) -> Appearance>,
|
||||
focused: Box<dyn Fn(&crate::Theme) -> Appearance>,
|
||||
disabled: Box<dyn Fn(&crate::Theme) -> Appearance>,
|
||||
placeholder_color: Box<dyn Fn(&crate::Theme) -> Color>,
|
||||
},
|
||||
}
|
||||
|
||||
impl StyleSheet for crate::Theme {
|
||||
type Style = TextInput;
|
||||
|
||||
fn active(&self, style: &Self::Style) -> Appearance {
|
||||
let palette = self.cosmic();
|
||||
let mut bg = palette.palette.neutral_7;
|
||||
bg.alpha = 0.25;
|
||||
let corner = palette.corner_radii;
|
||||
let label_color = palette.palette.neutral_9;
|
||||
match style {
|
||||
TextInput::Default => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_s.into(),
|
||||
border_width: 1.0,
|
||||
border_offset: None,
|
||||
border_color: self.current_container().component.divider.into(),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::ExpandableSearch => Appearance {
|
||||
background: Color::TRANSPARENT.into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Search => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_width: 1.0,
|
||||
border_offset: None,
|
||||
border_color: self.current_container().component.divider.into(),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Inline => Appearance {
|
||||
background: Color::TRANSPARENT.into(),
|
||||
border_radius: corner.radius_0.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Custom { active, .. } => active(self),
|
||||
}
|
||||
}
|
||||
|
||||
fn error(&self, style: &Self::Style) -> Appearance {
|
||||
let palette = self.cosmic();
|
||||
let mut bg = palette.palette.neutral_7;
|
||||
bg.alpha = 0.25;
|
||||
let corner = palette.corner_radii;
|
||||
let label_color = palette.palette.neutral_9;
|
||||
|
||||
match style {
|
||||
TextInput::Default => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_s.into(),
|
||||
border_width: 1.0,
|
||||
border_offset: Some(2.0),
|
||||
border_color: Color::from(palette.destructive_color()),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Search | TextInput::ExpandableSearch => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Inline => Appearance {
|
||||
background: Color::TRANSPARENT.into(),
|
||||
border_radius: corner.radius_0.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Custom { error, .. } => error(self),
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, style: &Self::Style) -> Appearance {
|
||||
let palette = self.cosmic();
|
||||
let mut bg = palette.palette.neutral_7;
|
||||
bg.alpha = 0.25;
|
||||
let corner = palette.corner_radii;
|
||||
let label_color = palette.palette.neutral_9;
|
||||
|
||||
match style {
|
||||
TextInput::Default => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_s.into(),
|
||||
border_width: 1.0,
|
||||
border_offset: None,
|
||||
border_color: palette.accent.base.into(),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Search => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_offset: None,
|
||||
border_width: 1.0,
|
||||
border_color: palette.accent.base.into(),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::ExpandableSearch => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_offset: None,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Inline => Appearance {
|
||||
background: Color::from(self.current_container().component.hover).into(),
|
||||
border_radius: corner.radius_0.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Custom { hovered, .. } => hovered(self),
|
||||
}
|
||||
}
|
||||
|
||||
fn focused(&self, style: &Self::Style) -> Appearance {
|
||||
let palette = self.cosmic();
|
||||
let mut bg = palette.palette.neutral_7;
|
||||
bg.alpha = 0.25;
|
||||
let corner = palette.corner_radii;
|
||||
let label_color = palette.palette.neutral_9;
|
||||
|
||||
match style {
|
||||
TextInput::Default => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_s.into(),
|
||||
border_width: 1.0,
|
||||
border_offset: Some(2.0),
|
||||
border_color: palette.accent.base.into(),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Search | TextInput::ExpandableSearch => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_width: 1.0,
|
||||
border_offset: Some(2.0),
|
||||
border_color: palette.accent.base.into(),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Inline => Appearance {
|
||||
background: Color::from(palette.accent.base).into(),
|
||||
border_radius: corner.radius_0.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
// TODO use regular text color here after text rendering handles multiple colors
|
||||
// in this case, for selected and unselected text
|
||||
text_color: palette.on_accent_color().into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Custom { focused, .. } => focused(self),
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_color(&self, style: &Self::Style) -> Color {
|
||||
if let TextInput::Custom {
|
||||
placeholder_color, ..
|
||||
} = style
|
||||
{
|
||||
return placeholder_color(self);
|
||||
}
|
||||
let palette = self.cosmic();
|
||||
let mut neutral_9 = palette.palette.neutral_9;
|
||||
neutral_9.alpha = 0.7;
|
||||
neutral_9.into()
|
||||
}
|
||||
|
||||
fn disabled(&self, style: &Self::Style) -> Appearance {
|
||||
if let TextInput::Custom { disabled, .. } = style {
|
||||
return disabled(self);
|
||||
}
|
||||
self.active(style)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ impl<'a, Message: 'static + Clone> Warning<'a, Message> {
|
|||
pub fn into_widget(self) -> widget::Container<'a, Message, Renderer> {
|
||||
let label = widget::container(crate::widget::text(self.message)).width(Length::Fill);
|
||||
|
||||
let close_button = icon::handle::from_name("window-close-symbolic")
|
||||
let close_button = icon::from_name("window-close-symbolic")
|
||||
.size(16)
|
||||
.apply(widget::button::icon)
|
||||
.on_press_maybe(self.on_close);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue