refactor(widget): improvements to button and icon widgets

This commit is contained in:
Michael Aaron Murphy 2023-09-13 15:47:32 +02:00 committed by Michael Murphy
parent 7f0943924a
commit 9dbc1be269
20 changed files with 399 additions and 558 deletions

View file

@ -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,

View file

@ -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};

View file

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