1use super::{Widget, element::Element, scroll_core};
30use alloc::{boxed::Box, vec::Vec};
31use core::marker::PhantomData;
32use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
33use zest_core::{
34 Constraints, Length, RenderError, Renderer, ScrollDirection, ScrollMsg, ScrollState, TouchPhase,
35};
36use zest_theme::Theme;
37
38pub struct Tileview<'a, C: PixelColor, M: Clone> {
40 rect: Rectangle,
41 tiles: Vec<Element<'a, C, M>>,
43 state: ScrollState,
45 dir: ScrollDirection,
47 on_scroll: Option<Box<dyn Fn(ScrollMsg) -> M + 'a>>,
49 on_change: Option<Box<dyn Fn(usize) -> M + 'a>>,
51 width: Length,
52 height: Length,
53 content: Size,
55 _color: PhantomData<C>,
56}
57
58impl<'a, C: PixelColor + 'a, M: Clone + 'a> Tileview<'a, C, M> {
59 pub fn new() -> Self {
64 Self {
65 rect: Rectangle::zero(),
66 tiles: Vec::new(),
67 state: ScrollState::new(),
68 dir: ScrollDirection::Horizontal,
69 on_scroll: None,
70 on_change: None,
71 width: Length::Fill,
72 height: Length::Fill,
73 content: Size::zero(),
74 _color: PhantomData,
75 }
76 }
77
78 #[must_use]
82 pub fn direction(mut self, dir: ScrollDirection) -> Self {
83 self.dir = match dir {
84 ScrollDirection::Vertical => ScrollDirection::Vertical,
85 _ => ScrollDirection::Horizontal,
86 };
87 self
88 }
89
90 #[must_use]
92 pub fn push<W>(mut self, tile: W) -> Self
93 where
94 W: Widget<C, M> + 'a,
95 {
96 self.tiles.push(Element::new(tile));
97 self
98 }
99
100 #[must_use]
102 pub fn scroll_state(mut self, state: &ScrollState) -> Self {
103 self.state = *state;
104 self
105 }
106
107 #[must_use]
109 pub fn width(mut self, width: impl Into<Length>) -> Self {
110 self.width = width.into();
111 self
112 }
113
114 #[must_use]
116 pub fn height(mut self, height: impl Into<Length>) -> Self {
117 self.height = height.into();
118 self
119 }
120
121 #[must_use]
124 pub fn on_scroll<F>(mut self, f: F) -> Self
125 where
126 F: Fn(ScrollMsg) -> M + 'a,
127 {
128 self.on_scroll = Some(Box::new(f));
129 self
130 }
131
132 #[must_use]
136 pub fn on_change<F>(mut self, f: F) -> Self
137 where
138 F: Fn(usize) -> M + 'a,
139 {
140 self.on_change = Some(Box::new(f));
141 self
142 }
143
144 fn snap_lines(&self) -> Vec<i32> {
147 let extent = if self.dir == ScrollDirection::Vertical {
148 self.rect.size.height as i32
149 } else {
150 self.rect.size.width as i32
151 };
152 (0..self.tiles.len() as i32).map(|i| i * extent).collect()
153 }
154
155 fn current_index(&self) -> usize {
157 Self::current_for(&self.state, self.rect.size, self.dir, self.tiles.len())
158 }
159
160 #[must_use]
165 pub fn current_for(
166 state: &ScrollState,
167 viewport: Size,
168 dir: ScrollDirection,
169 count: usize,
170 ) -> usize {
171 if count == 0 {
172 return 0;
173 }
174 let off = scroll_core::render_offset(*state, dir);
175 let (pos, extent) = if dir == ScrollDirection::Vertical {
176 (off.y, viewport.height as i32)
177 } else {
178 (off.x, viewport.width as i32)
179 };
180 let extent = extent.max(1);
181 let idx = (pos + extent / 2).div_euclid(extent);
182 idx.clamp(0, count as i32 - 1) as usize
183 }
184
185 #[must_use]
190 pub fn change_msg(&self, previous: usize) -> Option<M> {
191 let now = self.current_index();
192 if now != previous {
193 self.on_change.as_ref().map(|f| f(now))
194 } else {
195 None
196 }
197 }
198}
199
200impl<'a, C: PixelColor + 'a, M: Clone + 'a> Default for Tileview<'a, C, M> {
201 fn default() -> Self {
202 Self::new()
203 }
204}
205
206impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for Tileview<'a, C, M> {
207 fn measure(&mut self, constraints: Constraints) -> Size {
208 let w = self
209 .width
210 .resolve(constraints.max.width, constraints.max.width);
211 let h = self
212 .height
213 .resolve(constraints.max.height, constraints.max.height);
214 constraints.clamp(Size::new(w, h))
215 }
216
217 fn preferred_size(&self) -> (Length, Length) {
218 (self.width, self.height)
219 }
220
221 fn arrange(&mut self, rect: Rectangle) {
222 self.rect = rect;
223 let n = self.tiles.len() as u32;
224 let vertical = self.dir == ScrollDirection::Vertical;
225 self.content = if vertical {
226 Size::new(rect.size.width, rect.size.height.saturating_mul(n))
227 } else {
228 Size::new(rect.size.width.saturating_mul(n), rect.size.height)
229 };
230
231 let off = scroll_core::render_offset(self.state, self.dir);
234 let base = rect.top_left - off;
235 for (i, tile) in self.tiles.iter_mut().enumerate() {
236 let origin = if vertical {
237 Point::new(base.x, base.y + i as i32 * rect.size.height as i32)
238 } else {
239 Point::new(base.x + i as i32 * rect.size.width as i32, base.y)
240 };
241 tile.arrange(Rectangle::new(origin, rect.size));
242 }
243 }
244
245 fn rect(&self) -> Rectangle {
246 self.rect
247 }
248
249 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
250 if self.tiles.is_empty() {
251 return None;
252 }
253 let viewport = self.rect;
254 let content = self.content;
255 let dir = self.dir;
256 let lines = self.snap_lines();
257 let on_scroll = self.on_scroll.as_deref();
258 let tiles = &mut self.tiles;
259 scroll_core::route_touch(
260 self.state,
261 dir,
262 viewport,
263 content,
264 point,
265 phase,
266 &lines,
267 on_scroll,
268 |p, ph| {
269 for tile in tiles.iter_mut().rev() {
272 if scroll_core::rect_contains(tile.rect(), p) {
273 if let Some(msg) = tile.handle_touch(p, ph) {
274 return Some(msg);
275 }
276 }
277 }
278 None
279 },
280 )
281 }
282
283 fn mark_pressed(&mut self, point: Point) {
284 if matches!(
287 self.state.phase,
288 zest_core::GesturePhase::Dragging
289 | zest_core::GesturePhase::Flinging
290 | zest_core::GesturePhase::Springing
291 ) {
292 return;
293 }
294 for tile in &mut self.tiles {
295 tile.mark_pressed(point);
296 }
297 }
298
299 fn draw<'t>(
300 &self,
301 renderer: &mut dyn Renderer<C>,
302 theme: &Theme<'t, C>,
303 ) -> Result<(), RenderError> {
304 let viewport = self.rect;
305 renderer.push_clip(viewport);
306 for tile in &self.tiles {
307 if rects_overlap(tile.rect(), viewport) {
310 tile.draw(renderer, theme)?;
311 }
312 }
313 renderer.pop_clip();
314 Ok(())
315 }
316}
317
318fn rects_overlap(a: Rectangle, b: Rectangle) -> bool {
320 let a_br = a.top_left + Point::new(a.size.width as i32, a.size.height as i32);
321 let b_br = b.top_left + Point::new(b.size.width as i32, b.size.height as i32);
322 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
323}