//! Page navigation minimap showing a 3x2 grid of tiles. use crate::theme; use ratatui::layout::{Alignment, Rect}; use ratatui::style::Style; use ratatui::widgets::{Clear, Paragraph}; use ratatui::Frame; const TILE_W: u16 = 12; const TILE_H: u16 = 3; const GAP: u16 = 1; const PAD: u16 = 2; const GRID_COLS: u16 = 3; const GRID_ROWS: u16 = 2; /// Compute the centered minimap area for a 3x2 grid. pub fn minimap_area(term: Rect) -> Rect { let content_w = TILE_W * GRID_COLS + GAP * (GRID_COLS - 1); let content_h = TILE_H * GRID_ROWS + GAP * (GRID_ROWS - 1); let modal_w = content_w + PAD * 2; let modal_h = content_h + PAD * 2; let x = term.x + (term.width.saturating_sub(modal_w)) / 2; let y = term.y + (term.height.saturating_sub(modal_h)) / 2; Rect::new(x, y, modal_w, modal_h) } /// Hit-test: returns `(grid_col, grid_row)` if the click lands on a tile. pub fn hit_test_tile(col: u16, row: u16, term: Rect) -> Option<(i8, i8)> { let area = minimap_area(term); let inner_x = area.x + PAD; let inner_y = area.y + PAD; for grid_row in 0..GRID_ROWS { for grid_col in 0..GRID_COLS { let tx = inner_x + grid_col * (TILE_W + GAP); let ty = inner_y + grid_row * (TILE_H + GAP); if col >= tx && col < tx + TILE_W && row >= ty && row < ty + TILE_H { return Some((grid_col as i8, grid_row as i8)); } } } None } /// A tile in the navigation grid pub struct NavTile { pub col: i8, pub row: i8, pub name: &'static str, } /// Navigation minimap widget that renders a grid of page tiles pub struct NavMinimap<'a> { tiles: &'a [NavTile], selected: (i8, i8), } impl<'a> NavMinimap<'a> { pub fn new(tiles: &'a [NavTile], selected: (i8, i8)) -> Self { Self { tiles, selected } } pub fn render_centered(self, frame: &mut Frame, term: Rect) { if self.tiles.is_empty() { return; } let area = minimap_area(term); frame.render_widget(Clear, area); // Fill background with theme color let t = theme::get(); let bg_fill = " ".repeat(area.width as usize); for row in 0..area.height { let line_area = Rect::new(area.x, area.y + row, area.width, 1); frame.render_widget( Paragraph::new(bg_fill.clone()).style(Style::new().bg(t.ui.bg)), line_area, ); } let inner_x = area.x + PAD; let inner_y = area.y + PAD; for tile in self.tiles { let tile_x = inner_x + (tile.col as u16) * (TILE_W + GAP); let tile_y = inner_y + (tile.row as u16) * (TILE_H + GAP); let tile_area = Rect::new(tile_x, tile_y, TILE_W, TILE_H); let is_selected = (tile.col, tile.row) == self.selected; self.render_tile(frame, tile_area, tile.name, is_selected); } } fn render_tile(&self, frame: &mut Frame, area: Rect, label: &str, is_selected: bool) { let t = theme::get(); let (bg, fg) = if is_selected { (t.nav.selected_bg, t.nav.selected_fg) } else { (t.nav.unselected_bg, t.nav.unselected_fg) }; // Fill background for row in 0..area.height { let line_area = Rect::new(area.x, area.y + row, area.width, 1); let fill = " ".repeat(area.width as usize); frame.render_widget(Paragraph::new(fill).style(Style::new().bg(bg)), line_area); } // Center text vertically let text_y = area.y + area.height / 2; let text_area = Rect::new(area.x, text_y, area.width, 1); let paragraph = Paragraph::new(label) .style(Style::new().bg(bg).fg(fg)) .alignment(Alignment::Center); frame.render_widget(paragraph, text_area); } }