1extern crate alloc;
21
22use alloc::string::String;
23use alloc::vec::Vec;
24
25use rlvgl_core::draw::draw_widget_bg;
26use rlvgl_core::event::Event;
27use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr};
28use rlvgl_core::renderer::Renderer;
29use rlvgl_core::style::Style;
30use rlvgl_core::widget::{Color, Rect, Widget};
31
32const DEFAULT_BAR_THICKNESS: i32 = 24;
38const TAB_PAD_X: i32 = 4;
40const TAB_NONE_VALUE: u16 = u16::MAX;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub struct TabId(pub u16);
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62pub enum TabBarPos {
63 #[default]
65 Top,
66 Bottom,
68 Left,
70 Right,
72}
73
74struct TabDesc {
79 name: String,
80}
81
82pub struct Tabview {
91 bounds: Rect,
93 tabs: Vec<TabDesc>,
95 active_idx: u16,
97 bar_pos: TabBarPos,
99 bar_thickness: i32,
101 next_id: u16,
103 pub style: Style,
105 pub tab_color: Color,
107 pub active_tab_color: Color,
109 pub tab_text_color: Color,
111 pub active_tab_text_color: Color,
113 font: WidgetFont,
116}
117
118impl Tabview {
119 pub const TAB_NONE: TabId = TabId(TAB_NONE_VALUE);
121
122 pub fn new(bounds: Rect, bar_pos: TabBarPos) -> Self {
127 Self {
128 bounds,
129 tabs: Vec::new(),
130 active_idx: TAB_NONE_VALUE,
131 bar_pos,
132 bar_thickness: DEFAULT_BAR_THICKNESS,
133 next_id: 0,
134 style: Style::default(),
135 tab_color: Color(140, 140, 140, 255),
136 active_tab_color: Color(60, 120, 200, 255),
137 tab_text_color: Color(200, 200, 200, 255),
138 active_tab_text_color: Color(255, 255, 255, 255),
139 font: WidgetFont::new(),
140 }
141 }
142
143 pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
146 self.font.set(font);
147 }
148
149 pub fn add_tab(&mut self, name: &str) -> TabId {
153 let id = TabId(self.next_id);
154 self.next_id = self.next_id.saturating_add(1);
155 let idx = self.tabs.len() as u16;
156 self.tabs.push(TabDesc {
157 name: String::from(name),
158 });
159 if self.active_idx == TAB_NONE_VALUE {
160 self.active_idx = idx;
161 }
162 id
163 }
164
165 pub fn rename_tab(&mut self, id: TabId, name: &str) {
169 if let Some(tab) = self.tabs.get_mut(id.0 as usize) {
170 tab.name = String::from(name);
171 }
172 }
173
174 pub fn set_active(&mut self, id: TabId) {
178 if id.0 < self.tabs.len() as u16 {
179 self.active_idx = id.0;
180 }
181 }
182
183 pub fn active_tab(&self) -> TabId {
185 if self.active_idx == TAB_NONE_VALUE {
186 Self::TAB_NONE
187 } else {
188 TabId(self.active_idx)
189 }
190 }
191
192 pub fn tab_count(&self) -> usize {
194 self.tabs.len()
195 }
196
197 pub fn tab_content_bounds(&self, id: TabId) -> Rect {
202 if id.0 as usize >= self.tabs.len() {
203 return Rect {
204 x: 0,
205 y: 0,
206 width: 0,
207 height: 0,
208 };
209 }
210 self.content_rect()
211 }
212
213 pub fn bar_pos(&self) -> TabBarPos {
215 self.bar_pos
216 }
217
218 pub fn set_bar_pos(&mut self, pos: TabBarPos) {
220 self.bar_pos = pos;
221 }
222
223 pub fn navigate_next_tab(&mut self) {
230 let n = self.tabs.len() as u16;
231 if n < 2 {
232 return;
233 }
234 self.active_idx = (self.active_idx.saturating_add(1)) % n;
235 }
236
237 pub fn navigate_prev_tab(&mut self) {
243 let n = self.tabs.len() as u16;
244 if n < 2 {
245 return;
246 }
247 self.active_idx = if self.active_idx == 0 {
248 n - 1
249 } else {
250 self.active_idx - 1
251 };
252 }
253
254 fn bar_rect(&self) -> Rect {
260 let t = self.bar_thickness;
261 match self.bar_pos {
262 TabBarPos::Top => Rect {
263 x: self.bounds.x,
264 y: self.bounds.y,
265 width: self.bounds.width,
266 height: t,
267 },
268 TabBarPos::Bottom => Rect {
269 x: self.bounds.x,
270 y: self.bounds.y + self.bounds.height - t,
271 width: self.bounds.width,
272 height: t,
273 },
274 TabBarPos::Left => Rect {
275 x: self.bounds.x,
276 y: self.bounds.y,
277 width: t,
278 height: self.bounds.height,
279 },
280 TabBarPos::Right => Rect {
281 x: self.bounds.x + self.bounds.width - t,
282 y: self.bounds.y,
283 width: t,
284 height: self.bounds.height,
285 },
286 }
287 }
288
289 fn content_rect(&self) -> Rect {
291 let t = self.bar_thickness;
292 match self.bar_pos {
293 TabBarPos::Top => Rect {
294 x: self.bounds.x,
295 y: self.bounds.y + t,
296 width: self.bounds.width,
297 height: (self.bounds.height - t).max(0),
298 },
299 TabBarPos::Bottom => Rect {
300 x: self.bounds.x,
301 y: self.bounds.y,
302 width: self.bounds.width,
303 height: (self.bounds.height - t).max(0),
304 },
305 TabBarPos::Left => Rect {
306 x: self.bounds.x + t,
307 y: self.bounds.y,
308 width: (self.bounds.width - t).max(0),
309 height: self.bounds.height,
310 },
311 TabBarPos::Right => Rect {
312 x: self.bounds.x,
313 y: self.bounds.y,
314 width: (self.bounds.width - t).max(0),
315 height: self.bounds.height,
316 },
317 }
318 }
319
320 fn draw_bar(&self, renderer: &mut dyn Renderer) {
322 let bar = self.bar_rect();
323 let n = self.tabs.len();
324 if n == 0 {
325 return;
326 }
327
328 let (is_horiz, tab_w, tab_h) = match self.bar_pos {
329 TabBarPos::Top | TabBarPos::Bottom => {
330 let w = bar.width / n as i32;
331 (true, w, bar.height)
332 }
333 TabBarPos::Left | TabBarPos::Right => {
334 let h = bar.height / n as i32;
335 (false, bar.width, h)
336 }
337 };
338
339 for (i, tab) in self.tabs.iter().enumerate() {
340 let (tx, ty) = if is_horiz {
341 (bar.x + i as i32 * tab_w, bar.y)
342 } else {
343 (bar.x, bar.y + i as i32 * tab_h)
344 };
345 let tab_rect = Rect {
346 x: tx,
347 y: ty,
348 width: tab_w,
349 height: tab_h,
350 };
351 let is_active = i as u16 == self.active_idx;
352 let bg = if is_active {
353 self.active_tab_color
354 } else {
355 self.tab_color
356 };
357 if bg.3 > 0 {
358 renderer.fill_rect(tab_rect, bg);
359 }
360 let text_color = if is_active {
362 self.active_tab_text_color
363 } else {
364 self.tab_text_color
365 };
366 if text_color.3 > 0 {
367 let font = self.font.resolve();
368 let metrics = rlvgl_core::font::FontMetrics::line_metrics(font);
369 let baseline =
370 ty + metrics.ascent as i32 + (tab_h - metrics.line_height as i32) / 2;
371 let shaped = shape_text_ltr(font, &tab.name, (tx + TAB_PAD_X, baseline), 0);
372 renderer.draw_text_shaped(&shaped, (0, 0), text_color);
373 }
374 }
375 }
376}
377
378impl Widget for Tabview {
379 fn bounds(&self) -> Rect {
380 self.bounds
381 }
382
383 fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
384 Some(&mut self.font)
385 }
386
387 fn set_bounds(&mut self, bounds: Rect) {
388 self.bounds = bounds;
389 }
391
392 fn draw(&self, renderer: &mut dyn Renderer) {
393 if self.bounds.width <= 0 || self.bounds.height <= 0 {
394 return;
395 }
396 draw_widget_bg(renderer, self.bounds, &self.style);
398 self.draw_bar(renderer);
400 let content = self.content_rect();
403 if content.width > 0 && content.height > 0 && self.style.bg_color.3 > 0 {
404 renderer.fill_rect(content, self.style.bg_color);
405 }
406 }
407
408 fn handle_event(&mut self, _event: &Event) -> bool {
409 false
410 }
411}
412
413#[cfg(test)]
418mod tests {
419 use super::*;
420
421 fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
422 Rect {
423 x,
424 y,
425 width: w,
426 height: h,
427 }
428 }
429
430 struct NullRenderer;
431 impl rlvgl_core::renderer::Renderer for NullRenderer {
432 fn fill_rect(&mut self, _r: Rect, _c: Color) {}
433 fn draw_text(&mut self, _pos: (i32, i32), _t: &str, _c: Color) {}
434 }
435
436 #[test]
437 fn new_has_no_tabs_and_none_active() {
438 let tv = Tabview::new(rect(0, 0, 200, 120), TabBarPos::Top);
439 assert_eq!(tv.tab_count(), 0);
440 assert_eq!(tv.active_tab(), Tabview::TAB_NONE);
441 }
442
443 #[test]
444 fn first_add_makes_active_and_returns_id() {
445 let mut tv = Tabview::new(rect(0, 0, 200, 120), TabBarPos::Top);
446 let id = tv.add_tab("Home");
447 assert_eq!(tv.tab_count(), 1);
448 assert_eq!(tv.active_tab(), id);
449 }
450
451 #[test]
452 fn set_active_changes_active_tab() {
453 let mut tv = Tabview::new(rect(0, 0, 200, 120), TabBarPos::Top);
454 let a = tv.add_tab("A");
455 let b = tv.add_tab("B");
456 tv.set_active(b);
457 assert_eq!(tv.active_tab(), b);
458 tv.set_active(a);
459 assert_eq!(tv.active_tab(), a);
460 }
461
462 #[test]
463 fn navigate_next_wraps_to_first() {
464 let mut tv = Tabview::new(rect(0, 0, 200, 120), TabBarPos::Top);
465 let a = tv.add_tab("A");
466 let b = tv.add_tab("B");
467 tv.set_active(b);
468 tv.navigate_next_tab();
469 assert_eq!(tv.active_tab(), a); }
471
472 #[test]
473 fn navigate_prev_wraps_to_last() {
474 let mut tv = Tabview::new(rect(0, 0, 200, 120), TabBarPos::Top);
475 let a = tv.add_tab("A");
476 let b = tv.add_tab("B");
477 tv.set_active(a);
478 tv.navigate_prev_tab();
479 assert_eq!(tv.active_tab(), b); }
481
482 #[test]
483 fn content_bounds_excludes_bar_for_top_position() {
484 let mut tv = Tabview::new(rect(0, 0, 200, 120), TabBarPos::Top);
485 let id = tv.add_tab("Tab");
486 let content = tv.tab_content_bounds(id);
487 assert_eq!(content.y, DEFAULT_BAR_THICKNESS);
488 assert_eq!(content.height, 120 - DEFAULT_BAR_THICKNESS);
489 }
490
491 #[test]
492 fn content_bounds_excludes_bar_for_bottom_position() {
493 let mut tv = Tabview::new(rect(0, 0, 200, 120), TabBarPos::Bottom);
494 let id = tv.add_tab("Tab");
495 let content = tv.tab_content_bounds(id);
496 assert_eq!(content.y, 0);
497 assert_eq!(content.height, 120 - DEFAULT_BAR_THICKNESS);
498 }
499
500 #[test]
501 fn rename_tab_changes_name() {
502 let mut tv = Tabview::new(rect(0, 0, 200, 120), TabBarPos::Top);
503 let id = tv.add_tab("OldName");
504 tv.rename_tab(id, "NewName");
505 assert_eq!(tv.tabs[0].name, "NewName");
506 }
507
508 #[test]
509 fn set_bounds_updates_geometry() {
510 let mut tv = Tabview::new(rect(0, 0, 200, 120), TabBarPos::Top);
511 tv.set_bounds(rect(10, 10, 300, 200));
512 assert_eq!(tv.bounds(), rect(10, 10, 300, 200));
513 }
514
515 #[test]
516 fn draw_does_not_panic_with_no_tabs() {
517 let tv = Tabview::new(rect(0, 0, 200, 120), TabBarPos::Top);
518 let mut r = NullRenderer;
519 tv.draw(&mut r); }
521}