120 lines
3.8 KiB
Rust
120 lines
3.8 KiB
Rust
//! 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);
|
|
}
|
|
}
|