1use super::{Widget, column::Column};
25use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
26use core::marker::PhantomData;
27use embedded_graphics::{
28 pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
29};
30use zest_core::{
31 Constraints, Length, RenderError, Renderer, ScrollDirection, ScrollMsg, ScrollState,
32 ScrollbarMode, SnapMode, TouchPhase, UiAction, WidgetId,
33};
34use zest_theme::Theme;
35
36pub const ROW_HEIGHT: u32 = 44;
38pub const ROW_PADDING_X: u32 = 12;
40pub const ROW_GAP: u32 = 8;
42
43pub struct ListRow<'a, C: PixelColor, M: Clone> {
51 rect: Rectangle,
52 id: Option<WidgetId>,
53 index: usize,
54 leading: Option<String>,
55 label: String,
56 trailing: Option<String>,
57 on_select: Option<Rc<dyn Fn(usize) -> M + 'a>>,
59 divider: bool,
61 selected: bool,
63 focused: bool,
64 pressed: bool,
65 width: Length,
66 height: Length,
67 _color: PhantomData<C>,
68}
69
70impl<'a, C: PixelColor, M: Clone> ListRow<'a, C, M> {
71 fn new(index: usize, label: impl Into<String>) -> Self {
72 Self {
73 rect: Rectangle::zero(),
74 id: None,
75 index,
76 leading: None,
77 label: label.into(),
78 trailing: None,
79 on_select: None,
80 divider: false,
81 selected: false,
82 focused: false,
83 pressed: false,
84 width: Length::Fill,
85 height: Length::Fixed(ROW_HEIGHT),
86 _color: PhantomData,
87 }
88 }
89
90 fn is_enabled(&self) -> bool {
92 self.on_select.is_some()
93 }
94
95 fn hit_test(&self, point: Point) -> bool {
96 let tl = self.rect.top_left;
97 let br = tl + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
98 point.x >= tl.x && point.x < br.x && point.y >= tl.y && point.y < br.y
99 }
100}
101
102impl<'a, C: PixelColor, M: Clone> Widget<C, M> for ListRow<'a, C, M> {
103 fn measure(&mut self, constraints: Constraints) -> Size {
104 let w = self
105 .width
106 .resolve(constraints.max.width, constraints.max.width);
107 let h = self.height.resolve(ROW_HEIGHT, constraints.max.height);
108 constraints.clamp(Size::new(w, h))
109 }
110
111 fn preferred_size(&self) -> (Length, Length) {
112 (self.width, self.height)
113 }
114
115 fn arrange(&mut self, rect: Rectangle) {
116 self.rect = rect;
117 }
118
119 fn rect(&self) -> Rectangle {
120 self.rect
121 }
122
123 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
124 if !self.is_enabled() || !self.hit_test(point) {
125 if matches!(phase, TouchPhase::Up | TouchPhase::Moved) {
126 self.pressed = false;
127 }
128 return None;
129 }
130 match phase {
131 TouchPhase::Down => {
132 self.pressed = true;
133 None
134 }
135 TouchPhase::Up => {
136 if self.pressed {
137 self.pressed = false;
138 self.on_select.as_ref().map(|cb| cb(self.index))
139 } else {
140 None
141 }
142 }
143 TouchPhase::Moved => None,
144 }
145 }
146
147 fn mark_pressed(&mut self, point: Point) {
148 if self.is_enabled() && self.hit_test(point) {
149 self.pressed = true;
150 }
151 }
152
153 fn widget_id(&self) -> Option<WidgetId> {
154 self.id
155 }
156
157 fn is_focusable(&self) -> bool {
158 self.id.is_some() && self.is_enabled()
159 }
160
161 fn handle_action(&mut self, action: UiAction) -> Option<M> {
162 if !self.is_enabled() {
163 return None;
164 }
165
166 match action {
167 UiAction::Activate => self.on_select.as_ref().map(|cb| cb(self.index)),
168 _ => None,
169 }
170 }
171
172 fn sync_focus(&mut self, focused: Option<WidgetId>) {
173 self.focused = self.id.is_some() && self.id == focused;
174 }
175
176 fn focus_at(&self, point: Point) -> Option<WidgetId> {
177 if self.is_focusable() && self.hit_test(point) {
178 self.id
179 } else {
180 None
181 }
182 }
183
184 fn draw<'t>(
185 &self,
186 renderer: &mut dyn Renderer<C>,
187 theme: &Theme<'t, C>,
188 ) -> Result<(), RenderError> {
189 let font = theme.default_font();
190 if self.pressed {
192 renderer.fill_rect(self.rect, theme.accent.pressed)?;
193 } else if self.selected {
194 renderer.fill_rect(self.rect, theme.accent.base)?;
195 } else {
196 renderer.fill_rect(self.rect, theme.primary.base)?;
197 }
198 let border = if self.focused {
199 theme.accent.base
200 } else {
201 theme.primary.divider
202 };
203 renderer.stroke_rect(self.rect, border)?;
204
205 let text_color = if self.pressed || self.selected {
206 theme.accent.on_base
207 } else {
208 theme.primary.on_base
209 };
210
211 let glyph_h = font.character_size.height as i32;
212 let baseline_y = self.rect.top_left.y + self.rect.size.height as i32 / 2 + glyph_h / 3;
213 let left_x = self.rect.top_left.x + ROW_PADDING_X as i32;
214 let right_x = self.rect.top_left.x + self.rect.size.width as i32 - ROW_PADDING_X as i32;
215
216 let mut label_x = left_x;
218 if let Some(leading) = &self.leading {
219 renderer.draw_text(
220 leading,
221 Point::new(left_x, baseline_y),
222 font,
223 text_color,
224 Alignment::Left,
225 )?;
226 let advance = font.character_size.width as i32 * leading.chars().count() as i32;
227 label_x = left_x + advance + ROW_GAP as i32;
228 }
229
230 renderer.draw_text(
232 &self.label,
233 Point::new(label_x, baseline_y),
234 font,
235 text_color,
236 Alignment::Left,
237 )?;
238
239 if let Some(trailing) = &self.trailing {
241 renderer.draw_text(
242 trailing,
243 Point::new(right_x, baseline_y),
244 font,
245 text_color,
246 Alignment::Right,
247 )?;
248 }
249
250 if self.divider {
252 let y = self.rect.top_left.y + self.rect.size.height as i32 - 1;
253 let divider = Rectangle::new(
254 Point::new(self.rect.top_left.x, y),
255 Size::new(self.rect.size.width, 1),
256 );
257 renderer.fill_rect(divider, theme.primary.divider)?;
258 }
259
260 Ok(())
261 }
262}
263
264pub struct List<'a, C: PixelColor, M: Clone> {
271 id: Option<WidgetId>,
272 rows: Vec<ListRow<'a, C, M>>,
274 on_select: Option<Rc<dyn Fn(usize) -> M + 'a>>,
276 selected: Option<usize>,
278 dividers: bool,
280 spacing: u32,
281 width: Length,
282 height: Length,
283 scroll_dir: Option<ScrollDirection>,
285 scroll_state: Option<ScrollState>,
286 scrollbar: Option<ScrollbarMode>,
287 snap: Option<SnapMode>,
288 on_scroll: Option<Box<dyn Fn(ScrollMsg) -> M + 'a>>,
289 inner: Option<Column<'a, C, M>>,
291}
292
293impl<'a, C: PixelColor + 'a, M: Clone + 'a> List<'a, C, M> {
294 pub fn new() -> Self {
298 Self {
299 id: None,
300 rows: Vec::new(),
301 on_select: None,
302 selected: None,
303 dividers: false,
304 spacing: 0,
305 width: Length::Fill,
306 height: Length::Fill,
307 scroll_dir: None,
308 scroll_state: None,
309 scrollbar: None,
310 snap: None,
311 on_scroll: None,
312 inner: None,
313 }
314 }
315
316 #[must_use]
318 pub fn width(mut self, width: impl Into<Length>) -> Self {
319 self.width = width.into();
320 self
321 }
322
323 #[must_use]
325 pub fn height(mut self, height: impl Into<Length>) -> Self {
326 self.height = height.into();
327 self
328 }
329
330 #[must_use]
332 pub fn id(mut self, id: WidgetId) -> Self {
333 self.id = Some(id);
334 self
335 }
336
337 #[must_use]
340 pub fn spacing(mut self, spacing: u32) -> Self {
341 self.spacing = spacing;
342 self
343 }
344
345 #[must_use]
347 pub fn dividers(mut self, on: bool) -> Self {
348 self.dividers = on;
349 self
350 }
351
352 #[must_use]
355 pub fn selected(mut self, index: usize) -> Self {
356 self.selected = Some(index);
357 self
358 }
359
360 #[must_use]
363 pub fn on_select<F>(mut self, f: F) -> Self
364 where
365 F: Fn(usize) -> M + 'a,
366 {
367 self.on_select = Some(Rc::new(f));
368 self
369 }
370
371 #[must_use]
373 pub fn item(mut self, label: impl Into<String>) -> Self {
374 let index = self.rows.len();
375 self.rows.push(ListRow::new(index, label));
376 self
377 }
378
379 #[must_use]
384 pub fn item_with(
385 mut self,
386 leading: Option<impl Into<String>>,
387 label: impl Into<String>,
388 trailing: Option<impl Into<String>>,
389 ) -> Self {
390 let index = self.rows.len();
391 let mut row = ListRow::new(index, label);
392 row.leading = leading.map(Into::into);
393 row.trailing = trailing.map(Into::into);
394 self.rows.push(row);
395 self
396 }
397
398 #[must_use]
402 pub fn push(mut self, mut row: ListRow<'a, C, M>) -> Self {
403 row.index = self.rows.len();
404 self.rows.push(row);
405 self
406 }
407
408 #[must_use]
412 pub fn scrollable(mut self, dir: ScrollDirection) -> Self {
413 self.scroll_dir = Some(dir);
414 self
415 }
416
417 #[must_use]
421 pub fn scroll_state(mut self, state: &ScrollState) -> Self {
422 self.scroll_state = Some(*state);
423 if self.scroll_dir.is_none() {
424 self.scroll_dir = Some(ScrollDirection::Vertical);
425 }
426 self
427 }
428
429 #[must_use]
431 pub fn scrollbar(mut self, mode: ScrollbarMode) -> Self {
432 self.scrollbar = Some(mode);
433 if self.scroll_dir.is_none() {
434 self.scroll_dir = Some(ScrollDirection::Vertical);
435 }
436 self
437 }
438
439 #[must_use]
441 pub fn snap(mut self, mode: SnapMode) -> Self {
442 self.snap = Some(mode);
443 if self.scroll_dir.is_none() {
444 self.scroll_dir = Some(ScrollDirection::Vertical);
445 }
446 self
447 }
448
449 #[must_use]
452 pub fn on_scroll<F>(mut self, f: F) -> Self
453 where
454 F: Fn(ScrollMsg) -> M + 'a,
455 {
456 self.on_scroll = Some(Box::new(f));
457 if self.scroll_dir.is_none() {
458 self.scroll_dir = Some(ScrollDirection::Vertical);
459 }
460 self
461 }
462
463 fn build_inner(&mut self) -> Column<'a, C, M> {
467 let mut col = Column::new()
468 .width(self.width)
469 .height(self.height)
470 .spacing(self.spacing);
471
472 if let Some(dir) = self.scroll_dir {
474 col = col.scrollable(dir);
475 if let Some(state) = self.scroll_state.as_ref() {
476 col = col.scroll_state(state);
477 }
478 if let Some(bar) = self.scrollbar {
479 col = col.scrollbar(bar);
480 }
481 if let Some(snap) = self.snap {
482 col = col.snap(snap);
483 }
484 if let Some(on_scroll) = self.on_scroll.take() {
485 col = col.on_scroll(move |sm| on_scroll(sm));
486 }
487 }
488
489 let dividers = self.dividers;
490 let selected = self.selected;
491 let id = self.id;
492 let on_select = self.on_select.clone();
493 for mut row in core::mem::take(&mut self.rows) {
494 row.id = id.map(|base| WidgetId::new(base.raw().wrapping_add(row.index as u64 + 1)));
495 row.divider = dividers;
496 row.selected = Some(row.index) == selected;
497 row.on_select = on_select.clone();
498 col = col.push(row);
499 }
500 col
501 }
502}
503
504impl<'a, C: PixelColor + 'a, M: Clone + 'a> Default for List<'a, C, M> {
505 fn default() -> Self {
506 Self::new()
507 }
508}
509
510impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for List<'a, C, M> {
511 fn measure(&mut self, constraints: Constraints) -> Size {
512 let w = self
513 .width
514 .resolve(constraints.max.width, constraints.max.width);
515 let h = self
516 .height
517 .resolve(constraints.max.height, constraints.max.height);
518 constraints.clamp(Size::new(w, h))
519 }
520
521 fn preferred_size(&self) -> (Length, Length) {
522 (self.width, self.height)
523 }
524
525 fn arrange(&mut self, rect: Rectangle) {
526 let mut col = self.build_inner();
529 col.arrange(rect);
530 self.inner = Some(col);
531 }
532
533 fn rect(&self) -> Rectangle {
534 self.inner.as_ref().map_or(Rectangle::zero(), Widget::rect)
535 }
536
537 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
538 self.inner
539 .as_mut()
540 .and_then(|col| col.handle_touch(point, phase))
541 }
542
543 fn mark_pressed(&mut self, point: Point) {
544 if let Some(col) = self.inner.as_mut() {
545 col.mark_pressed(point);
546 }
547 }
548
549 fn draw<'t>(
550 &self,
551 renderer: &mut dyn Renderer<C>,
552 theme: &Theme<'t, C>,
553 ) -> Result<(), RenderError> {
554 if let Some(col) = self.inner.as_ref() {
555 col.draw(renderer, theme)?;
556 }
557 Ok(())
558 }
559}