use ratatui::{ buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, widgets::Widget, }; use unicode_width::UnicodeWidthStr; use crate::model::Model; use crate::ui::app::AppMode; use crate::view::Axis; pub struct TileBar<'a> { pub model: &'a Model, pub mode: &'a AppMode, pub tile_cat_idx: usize, } impl<'a> TileBar<'a> { pub fn new(model: &'a Model, mode: &'a AppMode, tile_cat_idx: usize) -> Self { Self { model, mode, tile_cat_idx, } } fn axis_display(axis: Axis) -> (&'static str, Color) { match axis { Axis::Row => ("Row", Color::Green), Axis::Column => ("Col", Color::Blue), Axis::Page => ("Pag", Color::Magenta), Axis::None => ("·", Color::DarkGray), } } } impl<'a> Widget for TileBar<'a> { fn render(self, area: Rect, buf: &mut Buffer) { // Clear the line to avoid stale characters from previous renders buf.set_string( area.x, area.y, " ".repeat(area.width as usize), Style::default(), ); let view = self.model.active_view(); let selected_cat_idx = if matches!(self.mode, AppMode::TileSelect) { Some(self.tile_cat_idx) } else { None }; let prefix = " Tiles: "; let prefix_w = prefix.width() as u16; buf.set_string(area.x, area.y, prefix, Style::default().fg(Color::Gray)); let cat_names: Vec<&str> = self.model.category_names(); // Compute label widths for all tiles let labels: Vec = cat_names .iter() .map(|cat_name| { let (axis_symbol, _) = TileBar::axis_display(view.axis_of(cat_name)); format!(" [{cat_name} {axis_symbol}] ") }) .collect(); let widths: Vec = labels.iter().map(|l| l.width() as u16).collect(); // Available space for tiles (after prefix) let avail = area.width.saturating_sub(prefix_w); // Find the minimal starting index so the selected tile is fully visible. // We scroll by whole tiles: find the first tile to draw such that the // selected tile fits within the available width. let sel = selected_cat_idx.unwrap_or(0); let mut start = 0; loop { // Check if selected tile is visible when starting from `start` let mut used: u16 = 0; let mut sel_visible = false; for i in start..labels.len() { if used + widths[i] > avail { break; } used += widths[i]; if i == sel { sel_visible = true; } } if sel_visible || start >= sel { break; } start += 1; } // Draw an overflow indicator if we scrolled past the beginning let mut x = area.x + prefix_w + 1; if start > 0 { buf.set_string( area.x + prefix_w, area.y, "◀", Style::default().fg(Color::DarkGray), ); x += 1; } // Render tiles from `start` let mut last_drawn = start; for i in start..labels.len() { let label_w = widths[i]; if x + label_w > area.x + area.width { break; } let (_, axis_color) = TileBar::axis_display(view.axis_of(cat_names[i])); let is_selected = selected_cat_idx == Some(i); let style = if is_selected { Style::default() .fg(Color::Black) .bg(Color::Cyan) .add_modifier(Modifier::BOLD) } else { Style::default().fg(axis_color) }; buf.set_string(x, area.y, &labels[i], style); x += label_w; last_drawn = i; } // Draw overflow indicator if tiles remain after the visible area if last_drawn + 1 < labels.len() && x < area.x + area.width { buf.set_string(x, area.y, "▶", Style::default().fg(Color::DarkGray)); x += 1; } // Hint if matches!(self.mode, AppMode::TileSelect) { let hint = " [Enter] cycle axis [r/c/p] set axis [←→] select [Esc] cancel"; if x + hint.width() as u16 <= area.x + area.width { buf.set_string(x, area.y, hint, Style::default().fg(Color::DarkGray)); } } else { let hint = " Ctrl+↑↓←→ to move tiles"; if x + hint.width() as u16 <= area.x + area.width { buf.set_string(x, area.y, hint, Style::default().fg(Color::DarkGray)); } } } }