use super::{Widget, element::Element, scroll_core};
use alloc::{boxed::Box, vec::Vec};
use core::marker::PhantomData;
use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
use zest_core::{
Constraints, Length, RenderError, Renderer, ScrollDirection, ScrollMsg, ScrollState, TouchPhase,
};
use zest_theme::Theme;
pub struct Tileview<'a, C: PixelColor, M: Clone> {
rect: Rectangle,
tiles: Vec<Element<'a, C, M>>,
state: ScrollState,
dir: ScrollDirection,
on_scroll: Option<Box<dyn Fn(ScrollMsg) -> M + 'a>>,
on_change: Option<Box<dyn Fn(usize) -> M + 'a>>,
width: Length,
height: Length,
content: Size,
_color: PhantomData<C>,
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Tileview<'a, C, M> {
pub fn new() -> Self {
Self {
rect: Rectangle::zero(),
tiles: Vec::new(),
state: ScrollState::new(),
dir: ScrollDirection::Horizontal,
on_scroll: None,
on_change: None,
width: Length::Fill,
height: Length::Fill,
content: Size::zero(),
_color: PhantomData,
}
}
#[must_use]
pub fn direction(mut self, dir: ScrollDirection) -> Self {
self.dir = match dir {
ScrollDirection::Vertical => ScrollDirection::Vertical,
_ => ScrollDirection::Horizontal,
};
self
}
#[must_use]
pub fn push<W>(mut self, tile: W) -> Self
where
W: Widget<C, M> + 'a,
{
self.tiles.push(Element::new(tile));
self
}
#[must_use]
pub fn scroll_state(mut self, state: &ScrollState) -> Self {
self.state = *state;
self
}
#[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 on_scroll<F>(mut self, f: F) -> Self
where
F: Fn(ScrollMsg) -> M + 'a,
{
self.on_scroll = Some(Box::new(f));
self
}
#[must_use]
pub fn on_change<F>(mut self, f: F) -> Self
where
F: Fn(usize) -> M + 'a,
{
self.on_change = Some(Box::new(f));
self
}
fn snap_lines(&self) -> Vec<i32> {
let extent = if self.dir == ScrollDirection::Vertical {
self.rect.size.height as i32
} else {
self.rect.size.width as i32
};
(0..self.tiles.len() as i32).map(|i| i * extent).collect()
}
fn current_index(&self) -> usize {
Self::current_for(&self.state, self.rect.size, self.dir, self.tiles.len())
}
#[must_use]
pub fn current_for(
state: &ScrollState,
viewport: Size,
dir: ScrollDirection,
count: usize,
) -> usize {
if count == 0 {
return 0;
}
let off = scroll_core::render_offset(*state, dir);
let (pos, extent) = if dir == ScrollDirection::Vertical {
(off.y, viewport.height as i32)
} else {
(off.x, viewport.width as i32)
};
let extent = extent.max(1);
let idx = (pos + extent / 2).div_euclid(extent);
idx.clamp(0, count as i32 - 1) as usize
}
#[must_use]
pub fn change_msg(&self, previous: usize) -> Option<M> {
let now = self.current_index();
if now != previous {
self.on_change.as_ref().map(|f| f(now))
} else {
None
}
}
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Default for Tileview<'a, C, M> {
fn default() -> Self {
Self::new()
}
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for Tileview<'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) {
self.rect = rect;
let n = self.tiles.len() as u32;
let vertical = self.dir == ScrollDirection::Vertical;
self.content = if vertical {
Size::new(rect.size.width, rect.size.height.saturating_mul(n))
} else {
Size::new(rect.size.width.saturating_mul(n), rect.size.height)
};
let off = scroll_core::render_offset(self.state, self.dir);
let base = rect.top_left - off;
for (i, tile) in self.tiles.iter_mut().enumerate() {
let origin = if vertical {
Point::new(base.x, base.y + i as i32 * rect.size.height as i32)
} else {
Point::new(base.x + i as i32 * rect.size.width as i32, base.y)
};
tile.arrange(Rectangle::new(origin, rect.size));
}
}
fn rect(&self) -> Rectangle {
self.rect
}
fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
if self.tiles.is_empty() {
return None;
}
let viewport = self.rect;
let content = self.content;
let dir = self.dir;
let lines = self.snap_lines();
let on_scroll = self.on_scroll.as_deref();
let tiles = &mut self.tiles;
scroll_core::route_touch(
self.state,
dir,
viewport,
content,
point,
phase,
&lines,
on_scroll,
|p, ph| {
for tile in tiles.iter_mut().rev() {
if scroll_core::rect_contains(tile.rect(), p) {
if let Some(msg) = tile.handle_touch(p, ph) {
return Some(msg);
}
}
}
None
},
)
}
fn mark_pressed(&mut self, point: Point) {
if matches!(
self.state.phase,
zest_core::GesturePhase::Dragging
| zest_core::GesturePhase::Flinging
| zest_core::GesturePhase::Springing
) {
return;
}
for tile in &mut self.tiles {
tile.mark_pressed(point);
}
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
let viewport = self.rect;
renderer.push_clip(viewport);
for tile in &self.tiles {
if rects_overlap(tile.rect(), viewport) {
tile.draw(renderer, theme)?;
}
}
renderer.pop_clip();
Ok(())
}
}
fn rects_overlap(a: Rectangle, b: Rectangle) -> bool {
let a_br = a.top_left + Point::new(a.size.width as i32, a.size.height as i32);
let b_br = b.top_left + Point::new(b.size.width as i32, b.size.height as i32);
a.top_left.x < b_br.x && b.top_left.x < a_br.x && a.top_left.y < b_br.y && b.top_left.y < a_br.y
}