1use iced_core::{
2 Background, Border, Clipboard, Color, Element, Event, Layout, Length, Padding, Rectangle,
3 Shadow, Shell, Size, Theme, Vector, Widget,
4 layout::{self, Limits, Node},
5 mouse::{self, Cursor, Interaction},
6 overlay,
7 renderer::Quad,
8 touch,
9 widget::{
10 Operation, Tree,
11 tree::{self, Tag},
12 },
13 window,
14};
15
16const INDICATOR_HEIGHT: f32 = 2.0;
17
18pub struct Item<'a, Id, Message, Theme = iced_core::Theme, Renderer = iced_widget::Renderer>
19where
20 Theme: Catalog,
21{
22 width: Length,
23 height: Length,
24 pub(super) id: Id,
25 content: Element<'a, Message, Theme, Renderer>,
26 pub(super) padding: Padding,
27 clip: bool,
28 pub(super) class: Theme::Class<'a>,
29 pub(super) status: Option<Status>,
30}
31
32impl<'a, Id, Message, Theme, Renderer> Item<'a, Id, Message, Theme, Renderer>
33where
34 Theme: Catalog,
35{
36 pub fn new(id: Id, content: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
38 Self {
39 width: Length::Shrink,
40 height: 32.into(),
41 id,
42 content: content.into(),
43 padding: [0.0, 10.0].into(),
44 clip: true,
45 class: Theme::default(),
46 status: None,
47 }
48 }
49
50 #[must_use]
52 pub fn width(mut self, width: impl Into<Length>) -> Self {
53 self.width = width.into();
54 self
55 }
56
57 #[must_use]
59 pub fn height(mut self, height: impl Into<Length>) -> Self {
60 self.height = height.into();
61 self
62 }
63
64 #[must_use]
66 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
67 self.padding = padding.into();
68 self
69 }
70
71 #[must_use]
74 pub fn clip(mut self, clip: bool) -> Self {
75 self.clip = clip;
76 self
77 }
78
79 pub(crate) fn is_hovered(&self) -> bool {
80 self.status
81 .is_some_and(|status| matches!(status, Status::Hovered))
82 }
83
84 pub(crate) fn is_pressed(&self) -> bool {
85 self.status
86 .is_some_and(|status| matches!(status, Status::Pressed))
87 }
88}
89
90#[derive(Debug, Default)]
91struct State {
92 is_pressed: bool,
93}
94
95impl<'a, Id, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
96 for Item<'a, Id, Message, Theme, Renderer>
97where
98 Theme: Catalog,
99 Renderer: iced_core::Renderer,
100{
101 fn tag(&self) -> Tag {
102 Tag::of::<State>()
103 }
104
105 fn state(&self) -> tree::State {
106 tree::State::new(State::default())
107 }
108
109 fn children(&self) -> Vec<Tree> {
110 vec![Tree::new(&self.content)]
111 }
112
113 fn diff(&self, tree: &mut Tree) {
114 tree.diff_children(std::slice::from_ref(&self.content));
115 }
116
117 fn size(&self) -> Size<Length> {
118 Size::new(self.width, self.height)
119 }
120
121 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
122 layout::padded(limits, self.width, self.height, self.padding, |limits| {
123 self.content
124 .as_widget_mut()
125 .layout(&mut tree.children[0], renderer, limits)
126 })
127 }
128
129 fn operate(
130 &mut self,
131 tree: &mut Tree,
132 layout: Layout<'_>,
133 renderer: &Renderer,
134 operation: &mut dyn Operation,
135 ) {
136 operation.container(None, layout.bounds());
137
138 operation.traverse(&mut |operation| {
139 self.content.as_widget_mut().operate(
140 &mut tree.children[0],
141 layout.children().next().unwrap(),
142 renderer,
143 operation,
144 );
145 });
146 }
147
148 fn update(
149 &mut self,
150 tree: &mut Tree,
151 event: &Event,
152 layout: Layout<'_>,
153 cursor: Cursor,
154 renderer: &Renderer,
155 clipboard: &mut dyn Clipboard,
156 shell: &mut Shell<'_, Message>,
157 viewport: &Rectangle,
158 ) {
159 self.content.as_widget_mut().update(
160 &mut tree.children[0],
161 event,
162 layout.children().next().unwrap(),
163 cursor,
164 renderer,
165 clipboard,
166 shell,
167 viewport,
168 );
169
170 match event {
171 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
172 | Event::Touch(touch::Event::FingerPressed { .. }) => {
173 if cursor.is_over(layout.bounds()) {
174 let state = tree.state.downcast_mut::<State>();
175 state.is_pressed = true;
176 shell.capture_event();
177 }
178 }
179 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
180 | Event::Touch(touch::Event::FingerLifted { .. }) => {
181 let state = tree.state.downcast_mut::<State>();
182
183 if state.is_pressed {
184 state.is_pressed = false;
185 shell.capture_event();
186 }
187 }
188 _ => {}
189 }
190
191 let current_status = if cursor.is_over(layout.bounds()) {
192 let state = tree.state.downcast_mut::<State>();
193
194 if state.is_pressed {
195 Status::Pressed
196 } else {
197 Status::Hovered
198 }
199 } else {
200 Status::Active
201 };
202
203 if let Event::Window(window::Event::RedrawRequested(_)) = event {
204 self.status = Some(current_status);
205 } else if self.status.is_some_and(|status| status != current_status) {
206 shell.request_redraw();
207 }
208 }
209
210 fn mouse_interaction(
211 &self,
212 tree: &Tree,
213 layout: Layout<'_>,
214 cursor: Cursor,
215 viewport: &Rectangle,
216 renderer: &Renderer,
217 ) -> Interaction {
218 self.content.as_widget().mouse_interaction(
219 &tree.children[0],
220 layout.child(0),
221 cursor,
222 viewport,
223 renderer,
224 )
225 }
226
227 fn draw(
228 &self,
229 tree: &Tree,
230 renderer: &mut Renderer,
231 theme: &Theme,
232 style: &iced_core::renderer::Style,
233 layout: Layout<'_>,
234 cursor: Cursor,
235 viewport: &Rectangle,
236 ) {
237 let bounds = if self.clip {
238 layout.bounds().intersection(viewport).unwrap_or(*viewport)
239 } else {
240 *viewport
241 };
242
243 let item_style = theme.style(&self.class, self.status.unwrap_or_default());
244
245 renderer.fill_quad(
247 Quad {
248 bounds,
249 border: item_style.border,
250 shadow: item_style.shadow,
251 snap: item_style.snap,
252 },
253 item_style.background.unwrap_or(Color::TRANSPARENT.into()),
254 );
255
256 let child_layout = layout.children().next().unwrap();
258
259 self.content.as_widget().draw(
260 &tree.children[0],
261 renderer,
262 theme,
263 style,
264 child_layout,
265 cursor,
266 viewport,
267 );
268
269 if let Some(status) = &self.status {
271 let style = item_style.pending_indicator;
272 let bounds = child_layout.bounds();
273
274 match status {
275 Status::Active => {}
276 Status::Hovered | Status::Pressed => {
277 renderer.fill_quad(
278 Quad {
279 bounds: Rectangle {
280 x: bounds.x,
281 y: bounds.y + bounds.height - INDICATOR_HEIGHT,
282 width: bounds.width,
283 height: INDICATOR_HEIGHT,
284 },
285 border: style.border,
286 shadow: style.shadow,
287 snap: style.snap,
288 },
289 style.background.unwrap_or(Color::TRANSPARENT.into()),
290 );
291 }
292 }
293 }
294 }
295
296 fn overlay<'b>(
297 &'b mut self,
298 tree: &'b mut Tree,
299 layout: Layout<'b>,
300 renderer: &Renderer,
301 viewport: &Rectangle,
302 translation: Vector,
303 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
304 self.content.as_widget_mut().overlay(
305 &mut tree.children[0],
306 layout.child(0),
307 renderer,
308 viewport,
309 translation,
310 )
311 }
312}
313
314#[derive(Clone, Copy, Debug, Default, PartialEq)]
315pub enum Status {
316 #[default]
317 Active,
318 Hovered,
319 Pressed,
320}
321
322#[derive(Debug)]
323pub struct Style {
324 pub background: Option<Background>,
325 pub border: Border,
326 pub shadow: Shadow,
327 pub snap: bool,
328 pub active_indicator: Indicator,
329 pub pending_indicator: Indicator,
330}
331
332#[derive(Debug)]
333pub struct Indicator {
334 pub background: Option<Background>,
335 pub border: Border,
336 pub shadow: Shadow,
337 pub snap: bool,
338}
339
340pub trait Catalog {
341 type Class<'a>;
342
343 fn default<'a>() -> Self::Class<'a>;
344 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
345}
346
347pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
348
349impl Catalog for Theme {
350 type Class<'a> = StyleFn<'a, Self>;
351
352 fn default<'a>() -> Self::Class<'a> {
353 Box::new(default)
354 }
355
356 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
357 class(self, status)
358 }
359}
360
361pub fn default(theme: &Theme, status: Status) -> Style {
362 let palette = theme.extended_palette();
363
364 let active_color = match status {
365 Status::Active => palette.primary.base.color,
366 Status::Hovered => palette.primary.base.color,
367 Status::Pressed => palette.primary.weak.color,
368 };
369
370 let pending_color = match status {
371 Status::Active => palette.secondary.base.color,
372 Status::Hovered => palette.secondary.base.color,
373 Status::Pressed => palette.secondary.weak.color,
374 };
375
376 let border = Border::default();
377
378 Style {
379 background: Some(palette.background.base.color.into()),
380 border,
381 shadow: Shadow::default(),
382 snap: true,
383 active_indicator: Indicator {
384 background: Some(active_color.into()),
385 border,
386 shadow: Shadow::default(),
387 snap: true,
388 },
389 pending_indicator: Indicator {
390 background: Some(pending_color.into()),
391 border,
392 shadow: Shadow::default(),
393 snap: true,
394 },
395 }
396}