1extern crate alloc;
19
20use alloc::vec::Vec;
21
22use rlvgl_core::draw::draw_widget_bg;
23use rlvgl_core::event::Event;
24use rlvgl_core::renderer::Renderer;
25use rlvgl_core::style::Style;
26use rlvgl_core::widget::{Color, Rect, Widget};
27
28const TILE_NONE_VALUE: u16 = u16::MAX;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub struct TileId(pub u16);
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct TileDir(u8);
56
57impl TileDir {
58 pub const NONE: Self = Self(0);
60 pub const UP: Self = Self(1 << 0);
62 pub const DOWN: Self = Self(1 << 1);
64 pub const LEFT: Self = Self(1 << 2);
66 pub const RIGHT: Self = Self(1 << 3);
68 pub const ALL: Self = Self(0b0000_1111);
70
71 pub fn contains(self, other: TileDir) -> bool {
73 (self.0 & other.0) == other.0
74 }
75}
76
77impl core::ops::BitOr for TileDir {
78 type Output = Self;
79 fn bitor(self, rhs: Self) -> Self {
80 Self(self.0 | rhs.0)
81 }
82}
83
84#[derive(Clone)]
90struct TileDesc {
91 col: u8,
93 row: u8,
95 dir: TileDir,
97}
98
99pub struct Tileview {
108 bounds: Rect,
110 tiles: Vec<TileDesc>,
112 active_idx: u16,
114 next_id: u16,
116 offset_x: i32,
118 offset_y: i32,
120 pub style: Style,
122 pub tile_color: Color,
124}
125
126impl Tileview {
127 pub const TILE_NONE: TileId = TileId(TILE_NONE_VALUE);
129
130 pub fn new(bounds: Rect) -> Self {
132 Self {
133 bounds,
134 tiles: Vec::new(),
135 active_idx: TILE_NONE_VALUE,
136 next_id: 0,
137 offset_x: 0,
138 offset_y: 0,
139 style: Style::default(),
140 tile_color: Color(30, 30, 30, 255),
141 }
142 }
143
144 pub fn add_tile(&mut self, col: u8, row: u8, dir: TileDir) -> TileId {
149 let id = TileId(self.next_id);
150 self.next_id = self.next_id.saturating_add(1);
151 let idx = self.tiles.len() as u16;
152 self.tiles.push(TileDesc { col, row, dir });
153 if self.active_idx == TILE_NONE_VALUE {
154 self.active_idx = idx;
155 self.update_offset_from_active();
156 }
157 id
158 }
159
160 pub fn set_active(&mut self, id: TileId) {
165 if (id.0 as usize) < self.tiles.len() {
166 self.active_idx = id.0;
167 self.update_offset_from_active();
168 }
169 }
170
171 pub fn active_tile(&self) -> TileId {
174 if self.active_idx == TILE_NONE_VALUE {
175 Self::TILE_NONE
176 } else {
177 TileId(self.active_idx)
178 }
179 }
180
181 pub fn set_active_by_index(&mut self, col: u8, row: u8) {
185 if let Some(idx) = self.find_tile(col, row) {
186 self.active_idx = idx;
187 self.update_offset_from_active();
188 }
189 }
190
191 pub fn tile_bounds(&self, id: TileId) -> Rect {
196 let idx = id.0 as usize;
197 if idx >= self.tiles.len() {
198 return Rect {
199 x: 0,
200 y: 0,
201 width: 0,
202 height: 0,
203 };
204 }
205 let t = &self.tiles[idx];
206 Rect {
207 x: t.col as i32 * self.bounds.width,
208 y: t.row as i32 * self.bounds.height,
209 width: self.bounds.width,
210 height: self.bounds.height,
211 }
212 }
213
214 pub fn navigate_up(&mut self) {
219 self.navigate_direction(TileDir::UP, 0i8, -1i8);
220 }
221
222 pub fn navigate_down(&mut self) {
227 self.navigate_direction(TileDir::DOWN, 0i8, 1i8);
228 }
229
230 pub fn navigate_left(&mut self) {
235 self.navigate_direction(TileDir::LEFT, -1i8, 0i8);
236 }
237
238 pub fn navigate_right(&mut self) {
243 self.navigate_direction(TileDir::RIGHT, 1i8, 0i8);
244 }
245
246 fn navigate_direction(&mut self, required_dir: TileDir, dc: i8, dr: i8) {
252 if self.active_idx == TILE_NONE_VALUE {
253 return;
254 }
255 let current = &self.tiles[self.active_idx as usize];
256 if !current.dir.contains(required_dir) {
257 return;
258 }
259 let new_col = (current.col as i16 + dc as i16).clamp(0, 255) as u8;
260 let new_row = (current.row as i16 + dr as i16).clamp(0, 255) as u8;
261 if let Some(idx) = self.find_tile(new_col, new_row) {
262 self.active_idx = idx;
263 self.update_offset_from_active();
264 }
265 }
266
267 fn find_tile(&self, col: u8, row: u8) -> Option<u16> {
269 self.tiles
270 .iter()
271 .position(|t| t.col == col && t.row == row)
272 .map(|i| i as u16)
273 }
274
275 fn update_offset_from_active(&mut self) {
277 if self.active_idx == TILE_NONE_VALUE {
278 return;
279 }
280 let t = &self.tiles[self.active_idx as usize];
281 self.offset_x = t.col as i32 * self.bounds.width;
282 self.offset_y = t.row as i32 * self.bounds.height;
283 }
284}
285
286impl Widget for Tileview {
287 fn bounds(&self) -> Rect {
288 self.bounds
289 }
290
291 fn set_bounds(&mut self, bounds: Rect) {
292 self.bounds = bounds;
293 self.update_offset_from_active();
295 }
296
297 fn draw(&self, renderer: &mut dyn Renderer) {
298 if self.bounds.width <= 0 || self.bounds.height <= 0 {
299 return;
300 }
301 draw_widget_bg(renderer, self.bounds, &self.style);
303 if self.active_idx != TILE_NONE_VALUE {
306 let color = self.tile_color;
307 if color.3 > 0 {
308 renderer.fill_rect(self.bounds, color);
309 }
310 }
311 }
312
313 fn handle_event(&mut self, _event: &Event) -> bool {
314 false
315 }
316}
317
318#[cfg(test)]
323mod tests {
324 use super::*;
325
326 fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
327 Rect {
328 x,
329 y,
330 width: w,
331 height: h,
332 }
333 }
334
335 struct NullRenderer;
336 impl rlvgl_core::renderer::Renderer for NullRenderer {
337 fn fill_rect(&mut self, _r: Rect, _c: Color) {}
338 fn draw_text(&mut self, _pos: (i32, i32), _t: &str, _c: Color) {}
339 }
340
341 #[test]
342 fn new_is_empty_with_no_active_tile() {
343 let tv = Tileview::new(rect(0, 0, 100, 80));
344 assert_eq!(tv.active_tile(), Tileview::TILE_NONE);
345 }
346
347 #[test]
348 fn first_add_becomes_active() {
349 let mut tv = Tileview::new(rect(0, 0, 100, 80));
350 let id = tv.add_tile(0, 0, TileDir::ALL);
351 assert_eq!(tv.active_tile(), id);
352 }
353
354 #[test]
355 fn tile_bounds_correct_for_grid_position() {
356 let mut tv = Tileview::new(rect(0, 0, 100, 80));
357 let _ = tv.add_tile(0, 0, TileDir::ALL);
358 let id = tv.add_tile(2, 3, TileDir::ALL);
359 let b = tv.tile_bounds(id);
360 assert_eq!(b.x, 200); assert_eq!(b.y, 240); assert_eq!(b.width, 100);
363 assert_eq!(b.height, 80);
364 }
365
366 #[test]
367 fn navigate_right_moves_to_adjacent_tile() {
368 let mut tv = Tileview::new(rect(0, 0, 100, 80));
369 let a = tv.add_tile(0, 0, TileDir::RIGHT);
370 let b = tv.add_tile(1, 0, TileDir::LEFT);
371 tv.set_active(a);
372 tv.navigate_right();
373 assert_eq!(tv.active_tile(), b);
374 }
375
376 #[test]
377 fn navigate_blocked_by_missing_direction_flag() {
378 let mut tv = Tileview::new(rect(0, 0, 100, 80));
379 let a = tv.add_tile(0, 0, TileDir::NONE); let _b = tv.add_tile(1, 0, TileDir::LEFT);
381 tv.set_active(a);
382 tv.navigate_right(); assert_eq!(tv.active_tile(), a); }
385
386 #[test]
387 fn navigate_no_op_when_no_target_tile() {
388 let mut tv = Tileview::new(rect(0, 0, 100, 80));
389 let a = tv.add_tile(0, 0, TileDir::RIGHT); tv.set_active(a);
391 tv.navigate_right(); assert_eq!(tv.active_tile(), a);
393 }
394
395 #[test]
396 fn set_active_by_index_finds_tile() {
397 let mut tv = Tileview::new(rect(0, 0, 100, 80));
398 let a = tv.add_tile(0, 0, TileDir::ALL);
399 let b = tv.add_tile(0, 1, TileDir::ALL);
400 tv.set_active_by_index(0, 1);
401 assert_eq!(tv.active_tile(), b);
402 tv.set_active_by_index(0, 0);
403 assert_eq!(tv.active_tile(), a);
404 }
405
406 #[test]
407 fn offset_updates_to_exact_tile_position() {
408 let mut tv = Tileview::new(rect(0, 0, 100, 80));
409 tv.add_tile(0, 0, TileDir::RIGHT | TileDir::DOWN);
410 tv.add_tile(1, 0, TileDir::LEFT);
411 tv.add_tile(0, 1, TileDir::UP);
412 tv.navigate_right();
413 assert_eq!(tv.offset_x, 100);
414 assert_eq!(tv.offset_y, 0);
415 tv.navigate_left();
416 assert_eq!(tv.offset_x, 0);
417 tv.navigate_down();
418 assert_eq!(tv.offset_y, 80);
419 }
420
421 #[test]
422 fn tile_dir_contains_check() {
423 let d = TileDir::UP | TileDir::DOWN;
424 assert!(d.contains(TileDir::UP));
425 assert!(d.contains(TileDir::DOWN));
426 assert!(!d.contains(TileDir::LEFT));
427 }
428
429 #[test]
430 fn draw_does_not_panic() {
431 let mut tv = Tileview::new(rect(0, 0, 100, 80));
432 tv.add_tile(0, 0, TileDir::ALL);
433 let mut r = NullRenderer;
434 tv.draw(&mut r);
435 }
436}