use super::{Widget, column::Column};
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
use core::marker::PhantomData;
use embedded_graphics::{
pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
};
use zest_core::{
Constraints, Length, RenderError, Renderer, ScrollDirection, ScrollMsg, ScrollState,
ScrollbarMode, SnapMode, TouchPhase, UiAction, WidgetId,
};
use zest_theme::Theme;
pub const ROW_HEIGHT: u32 = 44;
pub const ROW_PADDING_X: u32 = 12;
pub const ROW_GAP: u32 = 8;
pub struct ListRow<'a, C: PixelColor, M: Clone> {
rect: Rectangle,
id: Option<WidgetId>,
index: usize,
leading: Option<String>,
label: String,
trailing: Option<String>,
on_select: Option<Rc<dyn Fn(usize) -> M + 'a>>,
divider: bool,
selected: bool,
focused: bool,
pressed: bool,
width: Length,
height: Length,
_color: PhantomData<C>,
}
impl<'a, C: PixelColor, M: Clone> ListRow<'a, C, M> {
fn new(index: usize, label: impl Into<String>) -> Self {
Self {
rect: Rectangle::zero(),
id: None,
index,
leading: None,
label: label.into(),
trailing: None,
on_select: None,
divider: false,
selected: false,
focused: false,
pressed: false,
width: Length::Fill,
height: Length::Fixed(ROW_HEIGHT),
_color: PhantomData,
}
}
fn is_enabled(&self) -> bool {
self.on_select.is_some()
}
fn hit_test(&self, point: Point) -> bool {
let tl = self.rect.top_left;
let br = tl + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
point.x >= tl.x && point.x < br.x && point.y >= tl.y && point.y < br.y
}
}
impl<'a, C: PixelColor, M: Clone> Widget<C, M> for ListRow<'a, C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
let w = self
.width
.resolve(constraints.max.width, constraints.max.width);
let h = self.height.resolve(ROW_HEIGHT, constraints.max.height);
constraints.clamp(Size::new(w, h))
}
fn preferred_size(&self) -> (Length, Length) {
(self.width, self.height)
}
fn arrange(&mut self, rect: Rectangle) {
self.rect = rect;
}
fn rect(&self) -> Rectangle {
self.rect
}
fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
if !self.is_enabled() || !self.hit_test(point) {
if matches!(phase, TouchPhase::Up | TouchPhase::Moved) {
self.pressed = false;
}
return None;
}
match phase {
TouchPhase::Down => {
self.pressed = true;
None
}
TouchPhase::Up => {
if self.pressed {
self.pressed = false;
self.on_select.as_ref().map(|cb| cb(self.index))
} else {
None
}
}
TouchPhase::Moved => None,
}
}
fn mark_pressed(&mut self, point: Point) {
if self.is_enabled() && self.hit_test(point) {
self.pressed = true;
}
}
fn widget_id(&self) -> Option<WidgetId> {
self.id
}
fn is_focusable(&self) -> bool {
self.id.is_some() && self.is_enabled()
}
fn handle_action(&mut self, action: UiAction) -> Option<M> {
if !self.is_enabled() {
return None;
}
match action {
UiAction::Activate => self.on_select.as_ref().map(|cb| cb(self.index)),
_ => None,
}
}
fn sync_focus(&mut self, focused: Option<WidgetId>) {
self.focused = self.id.is_some() && self.id == focused;
}
fn focus_at(&self, point: Point) -> Option<WidgetId> {
if self.is_focusable() && self.hit_test(point) {
self.id
} else {
None
}
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
let font = theme.default_font();
if self.pressed {
renderer.fill_rect(self.rect, theme.accent.pressed)?;
} else if self.selected {
renderer.fill_rect(self.rect, theme.accent.base)?;
} else {
renderer.fill_rect(self.rect, theme.primary.base)?;
}
let border = if self.focused {
theme.accent.base
} else {
theme.primary.divider
};
renderer.stroke_rect(self.rect, border)?;
let text_color = if self.pressed || self.selected {
theme.accent.on_base
} else {
theme.primary.on_base
};
let glyph_h = font.character_size.height as i32;
let baseline_y = self.rect.top_left.y + self.rect.size.height as i32 / 2 + glyph_h / 3;
let left_x = self.rect.top_left.x + ROW_PADDING_X as i32;
let right_x = self.rect.top_left.x + self.rect.size.width as i32 - ROW_PADDING_X as i32;
let mut label_x = left_x;
if let Some(leading) = &self.leading {
renderer.draw_text(
leading,
Point::new(left_x, baseline_y),
font,
text_color,
Alignment::Left,
)?;
let advance = font.character_size.width as i32 * leading.chars().count() as i32;
label_x = left_x + advance + ROW_GAP as i32;
}
renderer.draw_text(
&self.label,
Point::new(label_x, baseline_y),
font,
text_color,
Alignment::Left,
)?;
if let Some(trailing) = &self.trailing {
renderer.draw_text(
trailing,
Point::new(right_x, baseline_y),
font,
text_color,
Alignment::Right,
)?;
}
if self.divider {
let y = self.rect.top_left.y + self.rect.size.height as i32 - 1;
let divider = Rectangle::new(
Point::new(self.rect.top_left.x, y),
Size::new(self.rect.size.width, 1),
);
renderer.fill_rect(divider, theme.primary.divider)?;
}
Ok(())
}
}
pub struct List<'a, C: PixelColor, M: Clone> {
id: Option<WidgetId>,
rows: Vec<ListRow<'a, C, M>>,
on_select: Option<Rc<dyn Fn(usize) -> M + 'a>>,
selected: Option<usize>,
dividers: bool,
spacing: u32,
width: Length,
height: Length,
scroll_dir: Option<ScrollDirection>,
scroll_state: Option<ScrollState>,
scrollbar: Option<ScrollbarMode>,
snap: Option<SnapMode>,
on_scroll: Option<Box<dyn Fn(ScrollMsg) -> M + 'a>>,
inner: Option<Column<'a, C, M>>,
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> List<'a, C, M> {
pub fn new() -> Self {
Self {
id: None,
rows: Vec::new(),
on_select: None,
selected: None,
dividers: false,
spacing: 0,
width: Length::Fill,
height: Length::Fill,
scroll_dir: None,
scroll_state: None,
scrollbar: None,
snap: None,
on_scroll: None,
inner: 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 id(mut self, id: WidgetId) -> Self {
self.id = Some(id);
self
}
#[must_use]
pub fn spacing(mut self, spacing: u32) -> Self {
self.spacing = spacing;
self
}
#[must_use]
pub fn dividers(mut self, on: bool) -> Self {
self.dividers = on;
self
}
#[must_use]
pub fn selected(mut self, index: usize) -> Self {
self.selected = Some(index);
self
}
#[must_use]
pub fn on_select<F>(mut self, f: F) -> Self
where
F: Fn(usize) -> M + 'a,
{
self.on_select = Some(Rc::new(f));
self
}
#[must_use]
pub fn item(mut self, label: impl Into<String>) -> Self {
let index = self.rows.len();
self.rows.push(ListRow::new(index, label));
self
}
#[must_use]
pub fn item_with(
mut self,
leading: Option<impl Into<String>>,
label: impl Into<String>,
trailing: Option<impl Into<String>>,
) -> Self {
let index = self.rows.len();
let mut row = ListRow::new(index, label);
row.leading = leading.map(Into::into);
row.trailing = trailing.map(Into::into);
self.rows.push(row);
self
}
#[must_use]
pub fn push(mut self, mut row: ListRow<'a, C, M>) -> Self {
row.index = self.rows.len();
self.rows.push(row);
self
}
#[must_use]
pub fn scrollable(mut self, dir: ScrollDirection) -> Self {
self.scroll_dir = Some(dir);
self
}
#[must_use]
pub fn scroll_state(mut self, state: &ScrollState) -> Self {
self.scroll_state = Some(*state);
if self.scroll_dir.is_none() {
self.scroll_dir = Some(ScrollDirection::Vertical);
}
self
}
#[must_use]
pub fn scrollbar(mut self, mode: ScrollbarMode) -> Self {
self.scrollbar = Some(mode);
if self.scroll_dir.is_none() {
self.scroll_dir = Some(ScrollDirection::Vertical);
}
self
}
#[must_use]
pub fn snap(mut self, mode: SnapMode) -> Self {
self.snap = Some(mode);
if self.scroll_dir.is_none() {
self.scroll_dir = Some(ScrollDirection::Vertical);
}
self
}
#[must_use]
pub fn on_scroll<F>(mut self, f: F) -> Self
where
F: Fn(ScrollMsg) -> M + 'a,
{
self.on_scroll = Some(Box::new(f));
if self.scroll_dir.is_none() {
self.scroll_dir = Some(ScrollDirection::Vertical);
}
self
}
fn build_inner(&mut self) -> Column<'a, C, M> {
let mut col = Column::new()
.width(self.width)
.height(self.height)
.spacing(self.spacing);
if let Some(dir) = self.scroll_dir {
col = col.scrollable(dir);
if let Some(state) = self.scroll_state.as_ref() {
col = col.scroll_state(state);
}
if let Some(bar) = self.scrollbar {
col = col.scrollbar(bar);
}
if let Some(snap) = self.snap {
col = col.snap(snap);
}
if let Some(on_scroll) = self.on_scroll.take() {
col = col.on_scroll(move |sm| on_scroll(sm));
}
}
let dividers = self.dividers;
let selected = self.selected;
let id = self.id;
let on_select = self.on_select.clone();
for mut row in core::mem::take(&mut self.rows) {
row.id = id.map(|base| WidgetId::new(base.raw().wrapping_add(row.index as u64 + 1)));
row.divider = dividers;
row.selected = Some(row.index) == selected;
row.on_select = on_select.clone();
col = col.push(row);
}
col
}
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Default for List<'a, C, M> {
fn default() -> Self {
Self::new()
}
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for List<'a, C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
let w = self
.width
.resolve(constraints.max.width, constraints.max.width);
let h = self
.height
.resolve(constraints.max.height, constraints.max.height);
constraints.clamp(Size::new(w, h))
}
fn preferred_size(&self) -> (Length, Length) {
(self.width, self.height)
}
fn arrange(&mut self, rect: Rectangle) {
let mut col = self.build_inner();
col.arrange(rect);
self.inner = Some(col);
}
fn rect(&self) -> Rectangle {
self.inner.as_ref().map_or(Rectangle::zero(), Widget::rect)
}
fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
self.inner
.as_mut()
.and_then(|col| col.handle_touch(point, phase))
}
fn mark_pressed(&mut self, point: Point) {
if let Some(col) = self.inner.as_mut() {
col.mark_pressed(point);
}
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
if let Some(col) = self.inner.as_ref() {
col.draw(renderer, theme)?;
}
Ok(())
}
}