use ratatui::{Frame, layout::Rect, style::Color};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CanvasGridProjection {
pub world_left: f64,
pub world_right: f64,
pub world_top: f64,
pub world_bottom: f64,
pub origin_col: f64,
pub origin_row: f64,
pub cols_per_world_x: f64,
pub rows_per_world_y: f64,
}
impl CanvasGridProjection {
fn is_valid(self) -> bool {
[
self.world_left,
self.world_right,
self.world_top,
self.world_bottom,
self.origin_col,
self.origin_row,
self.cols_per_world_x,
self.rows_per_world_y,
]
.iter()
.all(|v| v.is_finite())
&& self.cols_per_world_x != 0.0
&& self.rows_per_world_y != 0.0
}
}
pub fn draw_canvas_grid(
frame: &mut Frame,
area: Rect,
visible: bool,
projection: CanvasGridProjection,
muted: Color,
zoom: f64,
) {
if !visible || area.is_empty() || !projection.is_valid() || !zoom.is_finite() || zoom <= 0.0 {
return;
}
let min_x = projection.world_left.min(projection.world_right);
let max_x = projection.world_left.max(projection.world_right);
let min_y = projection.world_top.min(projection.world_bottom);
let max_y = projection.world_top.max(projection.world_bottom);
let mut grid_step_x: f64 = 100.0;
let mut grid_step_y: f64 = 100.0;
while grid_step_y * zoom < 6.0 {
grid_step_x *= 2.0;
grid_step_y *= 2.0;
}
grid_step_y *= projection.cols_per_world_x.abs() / (2.0 * projection.rows_per_world_y.abs());
let step_x = grid_step_x;
let step_y = grid_step_y;
if !step_x.is_finite() || !step_y.is_finite() || step_x == 0.0 || step_y == 0.0 {
return;
}
let Some(start_x) = grid_index(min_x, step_x, f64::floor) else {
return;
};
let Some(end_x) = grid_index(max_x, step_x, f64::ceil) else {
return;
};
let Some(start_y) = grid_index(min_y, step_y, f64::floor) else {
return;
};
let Some(end_y) = grid_index(max_y, step_y, f64::ceil) else {
return;
};
let width = i64::from(area.width);
let height = i64::from(area.height);
let max_dots = width.saturating_mul(height).saturating_mul(4).max(1);
let x_count = end_x.saturating_sub(start_x).saturating_add(1);
let y_count = end_y.saturating_sub(start_y).saturating_add(1);
if x_count.saturating_mul(y_count) > max_dots {
return;
}
let left = f64::from(area.left());
let right = f64::from(area.right());
let top = f64::from(area.top());
let bottom = f64::from(area.bottom());
let buffer = frame.buffer_mut();
for x_index in start_x..=end_x {
let world_x = x_index as f64 * step_x;
let col = projection.origin_col + world_x * projection.cols_per_world_x;
if !col.is_finite() {
continue;
}
let col = col.round();
if col < left || col >= right {
continue;
}
for y_index in start_y..=end_y {
let world_y = y_index as f64 * step_y;
let row = projection.origin_row + world_y * projection.rows_per_world_y;
if !row.is_finite() {
continue;
}
let row = row.round();
if row < top || row >= bottom {
continue;
}
if let Some(cell) = buffer.cell_mut((col as u16, row as u16))
&& (cell.symbol() == " " || cell.symbol() == "")
{
cell.set_char('·').set_fg(muted);
}
}
}
}
fn grid_index(value: f64, step: f64, round: fn(f64) -> f64) -> Option<i64> {
let index = round(value / step);
(index >= i64::MIN as f64 && index <= i64::MAX as f64).then_some(index as i64)
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::{Terminal, backend::TestBackend};
fn projection(
cols_per_world_x: f64,
rows_per_world_y: f64,
step_x: f64,
step_y: f64,
) -> CanvasGridProjection {
let (world_top, world_bottom) = if rows_per_world_y.is_sign_negative() {
(-step_y, 0.0)
} else {
(0.0, step_y)
};
CanvasGridProjection {
world_left: 0.0,
world_right: step_x,
world_top,
world_bottom,
origin_col: 0.0,
origin_row: 0.0,
cols_per_world_x,
rows_per_world_y,
}
}
fn render_grid(
area: Rect,
visible: bool,
projection: CanvasGridProjection,
zoom: f64,
) -> ratatui::buffer::Buffer {
let mut terminal = Terminal::new(TestBackend::new(24, 16)).unwrap();
terminal
.draw(|frame| draw_canvas_grid(frame, area, visible, projection, Color::DarkGray, zoom))
.unwrap();
terminal.backend().buffer().clone()
}
#[test]
fn grid_dots_appear_on_empty_cells_only() {
let p = projection(1.0, 1.0, 1.0, 1.0);
let buf = render_grid(Rect::new(0, 0, 24, 16), true, p, 1.0);
let dots = buf.content.iter().filter(|c| c.symbol() == "·").count();
assert!(dots > 0, "expected grid dots");
}
#[test]
fn hidden_grid_draws_nothing() {
let p = projection(1.0, 1.0, 1.0, 1.0);
let buf = render_grid(Rect::new(0, 0, 24, 16), false, p, 1.0);
assert!(buf.content.iter().all(|c| c.symbol() == " "));
}
#[test]
fn degenerate_projection_is_noop() {
let p = CanvasGridProjection {
world_left: 0.0,
world_right: 1.0,
world_top: 0.0,
world_bottom: 1.0,
origin_col: f64::NAN,
origin_row: 0.0,
cols_per_world_x: 1.0,
rows_per_world_y: 1.0,
};
let buf = render_grid(Rect::new(0, 0, 24, 16), true, p, 1.0);
assert!(buf.content.iter().all(|c| c.symbol() == " "));
}
}