use iced_core::{
Background, Border, Clipboard, Color, Element, Event, Layout, Length, Padding, Rectangle,
Shadow, Shell, Size, Theme, Vector, Widget,
layout::{self, Limits, Node},
mouse::{self, Cursor, Interaction},
overlay,
renderer::Quad,
touch,
widget::{
Operation, Tree,
tree::{self, Tag},
},
window,
};
const INDICATOR_HEIGHT: f32 = 2.0;
pub struct Item<'a, Id, Message, Theme = iced_core::Theme, Renderer = iced_widget::Renderer>
where
Theme: Catalog,
{
width: Length,
height: Length,
pub(super) id: Id,
content: Element<'a, Message, Theme, Renderer>,
pub(super) padding: Padding,
clip: bool,
pub(super) class: Theme::Class<'a>,
pub(super) status: Option<Status>,
}
impl<'a, Id, Message, Theme, Renderer> Item<'a, Id, Message, Theme, Renderer>
where
Theme: Catalog,
{
pub fn new(id: Id, content: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
Self {
width: Length::Shrink,
height: 32.into(),
id,
content: content.into(),
padding: [0.0, 10.0].into(),
clip: true,
class: Theme::default(),
status: None,
}
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
#[must_use]
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into();
self
}
#[must_use]
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
pub(crate) fn is_hovered(&self) -> bool {
self.status
.is_some_and(|status| matches!(status, Status::Hovered))
}
pub(crate) fn is_pressed(&self) -> bool {
self.status
.is_some_and(|status| matches!(status, Status::Pressed))
}
}
#[derive(Debug, Default)]
struct State {
is_pressed: bool,
}
impl<'a, Id, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Item<'a, Id, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: iced_core::Renderer,
{
fn tag(&self) -> Tag {
Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
Size::new(self.width, self.height)
}
fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
layout::padded(limits, self.width, self.height, self.padding, |limits| {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
})
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
operation,
);
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout.children().next().unwrap(),
cursor,
renderer,
clipboard,
shell,
viewport,
);
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if cursor.is_over(layout.bounds()) {
let state = tree.state.downcast_mut::<State>();
state.is_pressed = true;
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
let state = tree.state.downcast_mut::<State>();
if state.is_pressed {
state.is_pressed = false;
shell.capture_event();
}
}
_ => {}
}
let current_status = if cursor.is_over(layout.bounds()) {
let state = tree.state.downcast_mut::<State>();
if state.is_pressed {
Status::Pressed
} else {
Status::Hovered
}
} else {
Status::Active
};
if let Event::Window(window::Event::RedrawRequested(_)) = event {
self.status = Some(current_status);
} else if self.status.is_some_and(|status| status != current_status) {
shell.request_redraw();
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout.child(0),
cursor,
viewport,
renderer,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced_core::renderer::Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
let bounds = if self.clip {
layout.bounds().intersection(viewport).unwrap_or(*viewport)
} else {
*viewport
};
let item_style = theme.style(&self.class, self.status.unwrap_or_default());
renderer.fill_quad(
Quad {
bounds,
border: item_style.border,
shadow: item_style.shadow,
snap: item_style.snap,
},
item_style.background.unwrap_or(Color::TRANSPARENT.into()),
);
let child_layout = layout.children().next().unwrap();
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
child_layout,
cursor,
viewport,
);
if let Some(status) = &self.status {
let style = item_style.pending_indicator;
let bounds = child_layout.bounds();
match status {
Status::Active => {}
Status::Hovered | Status::Pressed => {
renderer.fill_quad(
Quad {
bounds: Rectangle {
x: bounds.x,
y: bounds.y + bounds.height - INDICATOR_HEIGHT,
width: bounds.width,
height: INDICATOR_HEIGHT,
},
border: style.border,
shadow: style.shadow,
snap: style.snap,
},
style.background.unwrap_or(Color::TRANSPARENT.into()),
);
}
}
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout.child(0),
renderer,
viewport,
translation,
)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Status {
#[default]
Active,
Hovered,
Pressed,
}
#[derive(Debug)]
pub struct Style {
pub background: Option<Background>,
pub border: Border,
pub shadow: Shadow,
pub snap: bool,
pub active_indicator: Indicator,
pub pending_indicator: Indicator,
}
#[derive(Debug)]
pub struct Indicator {
pub background: Option<Background>,
pub border: Border,
pub shadow: Shadow,
pub snap: bool,
}
pub trait Catalog {
type Class<'a>;
fn default<'a>() -> Self::Class<'a>;
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
}
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(default)
}
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
class(self, status)
}
}
pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let active_color = match status {
Status::Active => palette.primary.base.color,
Status::Hovered => palette.primary.base.color,
Status::Pressed => palette.primary.weak.color,
};
let pending_color = match status {
Status::Active => palette.secondary.base.color,
Status::Hovered => palette.secondary.base.color,
Status::Pressed => palette.secondary.weak.color,
};
let border = Border::default();
Style {
background: Some(palette.background.base.color.into()),
border,
shadow: Shadow::default(),
snap: true,
active_indicator: Indicator {
background: Some(active_color.into()),
border,
shadow: Shadow::default(),
snap: true,
},
pending_indicator: Indicator {
background: Some(pending_color.into()),
border,
shadow: Shadow::default(),
snap: true,
},
}
}