use crate::event::Rect;
use crate::primitives::scrollbar::Scrollbar;
use crate::types::{Color, WidgetId};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Minimap {
pub id: WidgetId,
pub lines: Vec<MinimapLine>,
pub syntax_spans: Vec<MinimapSpan>,
pub visible_row_start: usize,
pub visible_row_count: usize,
pub total_buffer_lines: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MinimapLine {
pub text: String,
pub line_idx: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct MinimapSpan {
pub line_idx: usize,
pub start_col: usize,
pub end_col: usize,
pub color: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MinimapGrid {
pub rows: usize,
pub cols: usize,
pub lines_per_row: usize,
pub cols_per_cell: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VisibleMinimapLine {
pub start_line_idx: usize,
pub bounds: Rect,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MinimapLayout {
pub bounds: Rect,
pub visible_lines: Vec<VisibleMinimapLine>,
pub viewport_highlight: Rect,
pub scrollbar: Option<Scrollbar>,
pub cols_per_cell: usize,
}
impl Default for MinimapLayout {
fn default() -> Self {
Self {
bounds: Rect::default(),
visible_lines: Vec::new(),
viewport_highlight: Rect::default(),
scrollbar: None,
cols_per_cell: 1,
}
}
}
pub const MAX_ROW_PITCH: f32 = 8.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MinimapSizing {
Fill,
FixedPitch(f32),
RepeatToFill(f32),
VsCodeParity {
target_cols: f32,
fraction: f32,
min: f32,
max: f32,
},
}
impl MinimapSizing {
pub fn resolve_width(&self, pane_width: f32, char_width: f32) -> Option<f32> {
let MinimapSizing::VsCodeParity {
target_cols,
fraction,
min,
max,
} = *self
else {
return None;
};
let cw = if char_width > 0.0 { char_width } else { 1.0 };
let pane_width_cols = pane_width / cw;
let want_cols = target_cols.min(pane_width_cols * fraction).clamp(min, max);
Some(want_cols * cw)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MinimapHit {
Seek { fraction: f32 },
None,
}
impl MinimapLayout {
pub fn hit_test(&self, x: f32, y: f32) -> MinimapHit {
let b = &self.bounds;
if b.height <= 0.0 || x < b.x || x >= b.x + b.width || y < b.y || y >= b.y + b.height {
return MinimapHit::None;
}
let fraction = ((y - b.y) / b.height).clamp(0.0, 1.0);
MinimapHit::Seek { fraction }
}
}
impl Minimap {
pub fn layout_with_sizing(
&self,
bounds: Rect,
lines_per_row: usize,
sizing: MinimapSizing,
) -> MinimapLayout {
let lines_per_row = lines_per_row.max(1);
if self.lines.is_empty() || bounds.width <= 0.0 || bounds.height <= 0.0 {
return MinimapLayout {
bounds,
visible_lines: Vec::new(),
viewport_highlight: Rect::new(bounds.x, bounds.y, 0.0, 0.0),
scrollbar: None,
cols_per_cell: 1,
};
}
let row_count = self.lines.len().div_ceil(lines_per_row);
let (row_heights, window_start_row, rows_shown): (Vec<f32>, usize, usize) = match sizing {
MinimapSizing::Fill => {
let row_h = (bounds.height / row_count as f32).min(MAX_ROW_PITCH);
(vec![row_h; row_count], 0usize, row_count)
}
MinimapSizing::FixedPitch(px) => {
let row_h = px.max(f32::MIN_POSITIVE);
let rows_that_fit = (bounds.height / row_h).floor() as usize;
let rows_shown = rows_that_fit.min(row_count);
let window_start_row = self.slide_window_start_row(row_count, rows_shown);
(vec![row_h; rows_shown], window_start_row, rows_shown)
}
MinimapSizing::RepeatToFill(base_pitch) => {
let base_pitch = base_pitch.max(f32::MIN_POSITIVE);
if row_count == 0 || base_pitch * row_count as f32 > bounds.height {
let row_h = base_pitch;
let rows_that_fit = (bounds.height / row_h).floor() as usize;
let rows_shown = rows_that_fit.min(row_count);
let window_start_row = self.slide_window_start_row(row_count, rows_shown);
(vec![row_h; rows_shown], window_start_row, rows_shown)
} else {
let total_k = (bounds.height / base_pitch).floor() as usize;
let base_k = total_k / row_count;
let remainder = total_k % row_count;
let row_heights: Vec<f32> = (0..row_count)
.map(|i| {
let k = if i < remainder { base_k + 1 } else { base_k };
base_pitch * k.max(1) as f32
})
.collect();
(row_heights, 0usize, row_count)
}
}
MinimapSizing::VsCodeParity { .. } => {
let row_h = (bounds.height / row_count as f32).min(MAX_ROW_PITCH);
(vec![row_h; row_count], 0usize, row_count)
}
};
let mut row_offsets = Vec::with_capacity(rows_shown + 1);
let mut acc = 0.0f32;
row_offsets.push(acc);
for h in &row_heights {
acc += *h;
row_offsets.push(acc);
}
let visible_lines: Vec<VisibleMinimapLine> = (0..rows_shown)
.map(|i| {
let r = window_start_row + i;
VisibleMinimapLine {
start_line_idx: r * lines_per_row,
bounds: Rect::new(
bounds.x,
bounds.y + row_offsets[i],
bounds.width,
row_heights[i],
),
}
})
.collect();
let start_row_abs = (self.visible_row_start / lines_per_row) as f32;
let end_line = (self.visible_row_start + self.visible_row_count).min(self.lines.len());
let end_row_abs = if end_line == 0 {
0.0
} else {
((end_line - 1) / lines_per_row) as f32 + 1.0
};
let window_lo = window_start_row as f32;
let window_hi = (window_start_row + rows_shown) as f32;
let clipped_start = start_row_abs.clamp(window_lo, window_hi);
let clipped_end = end_row_abs.clamp(window_lo, window_hi);
let whole_file_visible = start_row_abs <= 0.0 && end_row_abs >= row_count as f32;
let last_offset = row_offsets.len() - 1;
let start_idx = ((clipped_start - window_lo).round() as usize).min(last_offset);
let end_idx = ((clipped_end - window_lo).round() as usize).min(last_offset);
let highlight_h = if whole_file_visible {
0.0
} else {
(row_offsets[end_idx] - row_offsets[start_idx]).max(0.0)
};
let viewport_highlight = Rect::new(
bounds.x,
bounds.y + row_offsets[start_idx],
bounds.width,
highlight_h,
);
let scrollbar = self.scroll_thumb();
MinimapLayout {
bounds,
visible_lines,
viewport_highlight,
scrollbar,
cols_per_cell: 1,
}
}
fn slide_window_start_row(&self, row_count: usize, rows_shown: usize) -> usize {
let start_buffer_line = self
.lines
.get(self.visible_row_start)
.map(|l| l.line_idx)
.unwrap_or_else(|| self.total_buffer_lines.saturating_sub(1));
slide_start(
row_count,
rows_shown,
start_buffer_line,
self.total_buffer_lines,
)
}
fn scroll_thumb(&self) -> Option<Scrollbar> {
if self.total_buffer_lines == 0 || self.lines.is_empty() {
return None;
}
let start_buffer_line = self
.lines
.get(self.visible_row_start)
.map(|l| l.line_idx)
.unwrap_or(0);
let end_idx = (self.visible_row_start + self.visible_row_count).min(self.lines.len());
let end_buffer_line = if end_idx == 0 {
start_buffer_line
} else {
self.lines
.get(end_idx - 1)
.map(|l| l.line_idx + 1)
.unwrap_or(self.total_buffer_lines)
};
let visible = end_buffer_line.saturating_sub(start_buffer_line).max(1) as f32;
let total = self.total_buffer_lines as f32;
if total <= visible {
return None;
}
Some(Scrollbar::vertical(
format!("{}-scrollbar", self.id),
Rect::new(0.0, 0.0, 0.0, 0.0),
start_buffer_line as f32,
total,
visible,
1.0,
))
}
}
pub const BLOCK_LINE_SAMPLE_CAP: usize = 8;
pub fn block_bounds(total_lines: usize, target_rows: usize) -> Vec<usize> {
if total_lines == 0 || target_rows == 0 {
return vec![0];
}
if total_lines <= target_rows {
return (0..=total_lines).collect();
}
let stride = total_lines as f64 / target_rows as f64;
let mut bounds = Vec::with_capacity(target_rows + 1);
for r in 0..target_rows {
bounds.push(((r as f64 * stride) as usize).min(total_lines));
}
bounds.push(total_lines);
bounds
}
pub fn block_sample_indices(start: usize, end: usize, cap: usize) -> Vec<usize> {
let len = end.saturating_sub(start);
if len == 0 {
return Vec::new();
}
let cap = cap.max(1);
if len <= cap {
return (start..end).collect();
}
let step = len as f64 / cap as f64;
(0..cap)
.map(|i| (start + (i as f64 * step) as usize).min(end - 1))
.collect()
}
pub const BAYER4: [[u8; 4]; 4] = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
pub fn dither_threshold_met(covered: usize, total: usize, row: usize, col: usize) -> bool {
if total == 0 {
return false;
}
let threshold = BAYER4[row & 3][col & 3] as usize;
covered * 16 > total * threshold
}
pub fn sample_blocks<F: FnMut(usize) -> String>(
total_lines: usize,
target_rows: usize,
mut line_at: F,
) -> Vec<MinimapLine> {
if total_lines == 0 || target_rows == 0 {
return Vec::new();
}
let bounds = block_bounds(total_lines, target_rows);
(0..bounds.len() - 1)
.map(|r| {
let indices = block_sample_indices(bounds[r], bounds[r + 1], BLOCK_LINE_SAMPLE_CAP);
MinimapLine {
text: aggregate_block_text(&indices, r, &mut line_at),
line_idx: bounds[r],
}
})
.collect()
}
fn slide_start(total: usize, window: usize, position: usize, total_at_position: usize) -> usize {
if window == 0 || total <= window {
return 0;
}
let max_start = total - window;
if total_at_position <= 1 {
return 0;
}
let denom = (total_at_position - 1) as f32;
let fraction = (position.min(total_at_position - 1) as f32 / denom).clamp(0.0, 1.0);
((fraction * max_start as f32).round() as usize).min(max_start)
}
pub fn window_start_line(
total_lines: usize,
window_lines: usize,
visible_row_start: usize,
total_buffer_lines: usize,
) -> usize {
let window_lines = window_lines.min(total_lines);
slide_start(
total_lines,
window_lines,
visible_row_start,
total_buffer_lines,
)
}
pub fn sample_window<F: FnMut(usize) -> String>(
total_lines: usize,
window_lines: usize,
target_rows: usize,
visible_row_start: usize,
mut line_at: F,
) -> Vec<MinimapLine> {
if total_lines == 0 || target_rows == 0 {
return Vec::new();
}
let window_lines = window_lines.clamp(1, total_lines);
let start = window_start_line(total_lines, window_lines, visible_row_start, total_lines);
let mut lines = sample_blocks(window_lines, target_rows, |i| line_at(start + i));
for line in &mut lines {
line.line_idx += start;
}
lines
}
fn aggregate_block_text<F: FnMut(usize) -> String>(
indices: &[usize],
block_row: usize,
line_at: &mut F,
) -> String {
if indices.is_empty() {
return String::new();
}
if let [only] = indices {
return truncate_to_columns(&line_at(*only), COLUMN_CAPACITY).to_string();
}
let char_rows: Vec<Vec<char>> = indices
.iter()
.map(|&i| {
truncate_to_columns(&line_at(i), COLUMN_CAPACITY)
.chars()
.collect()
})
.collect();
let max_len = char_rows.iter().map(Vec::len).max().unwrap_or(0);
let total = char_rows.len();
(0..max_len)
.map(|c| {
let covered = char_rows
.iter()
.filter(|row| row.get(c).is_some_and(|ch| !ch.is_whitespace()))
.count();
if dither_threshold_met(covered, total, block_row, c) {
'x'
} else {
' '
}
})
.collect()
}
pub fn aggregate_spans(spans: &[MinimapSpan], grid: MinimapGrid) -> Vec<MinimapSpan> {
if grid.rows == 0 || grid.cols == 0 || grid.lines_per_row == 0 || grid.cols_per_cell == 0 {
return Vec::new();
}
let mut hist: std::collections::HashMap<(usize, usize), Vec<(Color, usize)>> =
std::collections::HashMap::new();
for span in spans {
if span.end_col <= span.start_col {
continue;
}
let row = span.line_idx / grid.lines_per_row;
if row >= grid.rows {
continue;
}
let weight = span.end_col - span.start_col;
let col_start = span.start_col / grid.cols_per_cell;
let col_end = (span.end_col - 1) / grid.cols_per_cell;
for col in col_start..=col_end {
if col >= grid.cols {
break;
}
let entry = hist.entry((row, col)).or_default();
match entry.iter_mut().find(|(c, _)| *c == span.color) {
Some((_, w)) => *w += weight,
None => entry.push((span.color, weight)),
}
}
}
let mut out: Vec<MinimapSpan> = hist
.into_iter()
.filter_map(|((row, col), colors)| {
colors
.into_iter()
.max_by_key(|(_, w)| *w)
.map(|(color, _)| MinimapSpan {
line_idx: row * grid.lines_per_row,
start_col: col * grid.cols_per_cell,
end_col: (col + 1) * grid.cols_per_cell,
color,
})
})
.collect();
out.sort_by_key(|s| (s.line_idx, s.start_col));
out
}
pub fn reserved_width(cols_or_px: f32, has_minimap: bool) -> f32 {
if has_minimap {
cols_or_px
} else {
0.0
}
}
pub const LEGIBILITY_FLOOR_PX: f64 = 4.0;
pub const ROW_PITCH_PX: f64 = 2.0;
pub const COLUMN_CAPACITY: usize = 120;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MinimapRenderMode {
Characters,
ColumnBlocks,
}
pub fn is_legible(line_px: f64) -> bool {
line_px >= LEGIBILITY_FLOOR_PX
}
pub fn render_mode(line_px: f64) -> MinimapRenderMode {
if is_legible(line_px) {
MinimapRenderMode::Characters
} else {
MinimapRenderMode::ColumnBlocks
}
}
pub fn minimap_font_px(line_px: f64) -> f64 {
line_px.clamp(1.0, 64.0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MinimapScale {
#[default]
One,
Two,
}
impl MinimapScale {
pub fn row_pitch_px(self) -> f64 {
match self {
MinimapScale::One => ROW_PITCH_PX,
MinimapScale::Two => ROW_PITCH_PX * 2.0,
}
}
pub fn cell_w_px(self) -> f64 {
match self {
MinimapScale::One => 1.0,
MinimapScale::Two => 2.0,
}
}
}
pub const ATLAS_FIRST_CHAR: u32 = 0x20;
pub const ATLAS_LAST_CHAR: u32 = 0x7E;
pub const ATLAS_CHAR_COUNT: usize = (ATLAS_LAST_CHAR - ATLAS_FIRST_CHAR + 1) as usize;
#[derive(Debug, Clone, PartialEq)]
pub struct MinimapCharAtlas {
tile_w: usize,
tile_h: usize,
data: Vec<u8>,
fallback: Vec<u8>,
}
impl MinimapCharAtlas {
pub fn tile_w(&self) -> usize {
self.tile_w
}
pub fn tile_h(&self) -> usize {
self.tile_h
}
pub fn filled(tile_w: usize, tile_h: usize) -> Self {
let tile_w = tile_w.max(1);
let tile_h = tile_h.max(1);
let tile_len = tile_w * tile_h;
Self {
tile_w,
tile_h,
data: vec![255u8; tile_len * ATLAS_CHAR_COUNT],
fallback: vec![255u8; tile_len],
}
}
pub fn from_alpha_sheet(
alpha: &[u8],
sheet_w: usize,
sheet_h: usize,
cell_w: usize,
tile_w: usize,
tile_h: usize,
) -> Self {
let tile_w = tile_w.max(1);
let tile_h = tile_h.max(1);
if cell_w == 0
|| sheet_h == 0
|| alpha.len() != sheet_w * sheet_h
|| sheet_w < cell_w * ATLAS_CHAR_COUNT
{
return Self::filled(tile_w, tile_h);
}
let tile_len = tile_w * tile_h;
let mut raw = vec![0f32; tile_len * ATLAS_CHAR_COUNT];
for i in 0..ATLAS_CHAR_COUNT {
let cell_x0 = i * cell_w;
let cell =
box_downsample_cell(alpha, sheet_w, sheet_h, cell_x0, cell_w, tile_w, tile_h);
raw[i * tile_len..(i + 1) * tile_len].copy_from_slice(&cell);
}
Self {
tile_w,
tile_h,
data: normalise_max_to_255(&raw),
fallback: vec![255u8; tile_len],
}
}
pub fn tile(&self, ch: char) -> &[u8] {
match ascii_tile_index(ch) {
Some(i) => {
let tile_len = self.tile_w * self.tile_h;
&self.data[i * tile_len..(i + 1) * tile_len]
}
None => &self.fallback,
}
}
}
fn ascii_tile_index(ch: char) -> Option<usize> {
let c = ch as u32;
if (ATLAS_FIRST_CHAR..=ATLAS_LAST_CHAR).contains(&c) {
Some((c - ATLAS_FIRST_CHAR) as usize)
} else {
None
}
}
fn box_downsample_cell(
alpha: &[u8],
sheet_w: usize,
sheet_h: usize,
cell_x0: usize,
cell_w: usize,
tile_w: usize,
tile_h: usize,
) -> Vec<f32> {
let scale_x = cell_w as f64 / tile_w as f64;
let scale_y = sheet_h as f64 / tile_h as f64;
let mut out = vec![0f32; tile_w * tile_h];
for ty in 0..tile_h {
let y0 = ty as f64 * scale_y;
let y1 = (((ty + 1) as f64) * scale_y).min(sheet_h as f64);
let iy0 = y0.floor() as usize;
let iy1 = (y1.ceil() as usize).min(sheet_h);
for tx in 0..tile_w {
let x0 = tx as f64 * scale_x;
let x1 = (((tx + 1) as f64) * scale_x).min(cell_w as f64);
let ix0 = x0.floor() as usize;
let ix1 = (x1.ceil() as usize).min(cell_w);
let mut sum = 0f64;
for sy in iy0..iy1 {
let wy = overlap(sy as f64, sy as f64 + 1.0, y0, y1);
if wy <= 0.0 {
continue;
}
for local_sx in ix0..ix1 {
let wx = overlap(local_sx as f64, local_sx as f64 + 1.0, x0, x1);
if wx <= 0.0 {
continue;
}
let sx = cell_x0 + local_sx;
if sx >= sheet_w {
continue;
}
sum += alpha[sy * sheet_w + sx] as f64 * wx * wy;
}
}
let area = scale_x * scale_y;
out[ty * tile_w + tx] = if area > 0.0 { (sum / area) as f32 } else { 0.0 };
}
}
out
}
fn overlap(a0: f64, a1: f64, b0: f64, b1: f64) -> f64 {
(a1.min(b1) - a0.max(b0)).max(0.0)
}
fn normalise_max_to_255(raw: &[f32]) -> Vec<u8> {
let max = raw.iter().cloned().fold(0f32, f32::max);
if max <= 0.0 {
return vec![0u8; raw.len()];
}
let scale = 255.0 / max;
raw.iter()
.map(|&v| (v * scale).round().clamp(0.0, 255.0) as u8)
.collect()
}
#[derive(Debug, Clone)]
pub struct MinimapAtlasCache {
key: Option<(String, u32)>,
atlas: Option<MinimapCharAtlas>,
}
impl MinimapAtlasCache {
pub fn new() -> Self {
Self {
key: None,
atlas: None,
}
}
pub fn get_or_build(
&mut self,
family: &str,
scale: f32,
build: impl FnOnce() -> MinimapCharAtlas,
) -> &MinimapCharAtlas {
let scale_key = (scale.max(0.0) * 1000.0).round() as u32;
let hit = self
.key
.as_ref()
.is_some_and(|(f, s)| f == family && *s == scale_key);
if !hit {
self.atlas = Some(build());
self.key = Some((family.to_string(), scale_key));
}
self.atlas
.as_ref()
.expect("populated unconditionally above")
}
}
impl Default for MinimapAtlasCache {
fn default() -> Self {
Self::new()
}
}
pub fn truncate_to_columns(text: &str, n: usize) -> &str {
match text.char_indices().nth(n) {
Some((byte_idx, _)) => &text[..byte_idx],
None => text,
}
}
pub fn color_at_column(row_spans: &[MinimapSpan], col: usize, default_fg: Color) -> Color {
row_spans
.iter()
.find(|s| col >= s.start_col && col < s.end_col)
.map(|s| s.color)
.unwrap_or(default_fg)
}
pub struct SpanCursor<'a> {
spans: &'a [MinimapSpan],
pos: usize,
}
impl<'a> SpanCursor<'a> {
pub fn new(spans: &'a [MinimapSpan]) -> Self {
Self { spans, pos: 0 }
}
pub fn row_spans(&mut self, line_idx: usize) -> &'a [MinimapSpan] {
while self.pos < self.spans.len() && self.spans[self.pos].line_idx < line_idx {
self.pos += 1;
}
let start = self.pos;
let mut end = start;
while end < self.spans.len() && self.spans[end].line_idx == line_idx {
end += 1;
}
&self.spans[start..end]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lines(n: usize) -> Vec<MinimapLine> {
(0..n)
.map(|i| MinimapLine {
text: format!("line{i}"),
line_idx: i,
})
.collect()
}
fn minimap(n: usize, visible_row_start: usize, visible_row_count: usize) -> Minimap {
Minimap {
id: WidgetId::new("mm"),
lines: lines(n),
syntax_spans: Vec::new(),
visible_row_start,
visible_row_count,
total_buffer_lines: n,
}
}
#[test]
fn layout_tiles_rows_evenly_across_bounds() {
let mm = minimap(8, 2, 3);
let bounds = Rect::new(0.0, 0.0, 10.0, 16.0);
let layout = mm.layout_with_sizing(bounds, 2, MinimapSizing::Fill); assert_eq!(layout.visible_lines.len(), 4);
assert_eq!(
layout.visible_lines[0].bounds,
Rect::new(0.0, 0.0, 10.0, 4.0)
);
assert_eq!(
layout.visible_lines[1].bounds,
Rect::new(0.0, 4.0, 10.0, 4.0)
);
assert_eq!(
layout.visible_lines[3].bounds,
Rect::new(0.0, 12.0, 10.0, 4.0)
);
assert_eq!(layout.visible_lines[3].start_line_idx, 6);
}
#[test]
fn layout_viewport_highlight_spans_the_editor_viewport() {
let mm = minimap(8, 2, 3);
let bounds = Rect::new(0.0, 0.0, 10.0, 16.0); let layout = mm.layout_with_sizing(bounds, 2, MinimapSizing::Fill);
assert_eq!(layout.viewport_highlight, Rect::new(0.0, 4.0, 10.0, 8.0));
}
#[test]
fn layout_viewport_highlight_is_empty_when_viewport_covers_the_whole_file() {
let mm = minimap(1, 0, 1);
let bounds = Rect::new(0.0, 0.0, 10.0, 16.0);
for sizing in [MinimapSizing::Fill, MinimapSizing::FixedPitch(2.0)] {
let layout = mm.layout_with_sizing(bounds, 1, sizing);
assert_eq!(
layout.viewport_highlight.height, 0.0,
"a fully-visible one-line file must not highlight any viewport band \
(sizing {sizing:?})"
);
}
let mut mm_overshoot = minimap(3, 0, 3);
mm_overshoot.visible_row_count = 50;
let layout = mm_overshoot.layout_with_sizing(bounds, 1, MinimapSizing::FixedPitch(2.0));
assert_eq!(layout.viewport_highlight.height, 0.0);
}
#[test]
fn layout_caps_row_pitch_and_top_aligns_short_files() {
let mm = minimap(3, 0, 3);
let bounds = Rect::new(0.0, 0.0, 40.0, 200.0);
let layout = mm.layout_with_sizing(bounds, 1, MinimapSizing::Fill);
assert_eq!(layout.visible_lines.len(), 3);
for vline in &layout.visible_lines {
assert!(
vline.bounds.height <= MAX_ROW_PITCH,
"row pitch {} exceeds the MAX_ROW_PITCH ceiling",
vline.bounds.height
);
}
assert_eq!(
layout.visible_lines[0].bounds.y, bounds.y,
"rows must top-align to the strip"
);
let last = layout.visible_lines.last().unwrap();
let occupied = last.bounds.y + last.bounds.height;
assert!(
occupied < bounds.height,
"a short file must not stretch its rows to fill the whole strip \
(occupied {occupied}, strip height {})",
bounds.height
);
}
#[test]
fn layout_empty_lines_is_a_no_op() {
let mm = minimap(0, 0, 0);
let layout = mm.layout_with_sizing(Rect::new(0.0, 0.0, 10.0, 40.0), 4, MinimapSizing::Fill);
assert!(layout.visible_lines.is_empty());
assert_eq!(layout.viewport_highlight.height, 0.0);
}
#[test]
fn layout_zero_size_bounds_is_a_no_op() {
let mm = minimap(8, 0, 4);
let layout = mm.layout_with_sizing(Rect::new(0.0, 0.0, 0.0, 0.0), 4, MinimapSizing::Fill);
assert!(layout.visible_lines.is_empty());
}
#[test]
fn fixed_pitch_row_height_is_independent_of_file_length() {
let bounds = Rect::new(0.0, 0.0, 40.0, 400.0);
let short = minimap(3, 0, 3);
let long = minimap(10_000, 0, 10);
let short_layout = short.layout_with_sizing(bounds, 1, MinimapSizing::FixedPitch(2.0));
let long_layout = long.layout_with_sizing(bounds, 1, MinimapSizing::FixedPitch(2.0));
assert_eq!(short_layout.visible_lines[0].bounds.height, 2.0);
assert_eq!(long_layout.visible_lines[0].bounds.height, 2.0);
assert_eq!(
short_layout.visible_lines[0].bounds.height,
long_layout.visible_lines[0].bounds.height
);
}
#[test]
fn fixed_pitch_short_file_top_aligns_without_stretching() {
let mm = minimap(3, 0, 3);
let bounds = Rect::new(0.0, 0.0, 40.0, 400.0);
let layout = mm.layout_with_sizing(bounds, 1, MinimapSizing::FixedPitch(2.0));
assert_eq!(layout.visible_lines.len(), 3);
let last = layout.visible_lines.last().unwrap();
assert!(last.bounds.y + last.bounds.height < bounds.height);
}
#[test]
fn fixed_pitch_slides_monotonically_and_reaches_both_ends() {
let bounds = Rect::new(0.0, 0.0, 40.0, 200.0);
let n = 1000;
let mut starts = Vec::new();
for visible_row_start in [0, 100, 300, 500, 700, 900, 999] {
let mm = minimap(n, visible_row_start, 1);
let layout = mm.layout_with_sizing(bounds, 1, MinimapSizing::FixedPitch(2.0));
let first_line_idx = layout.visible_lines.first().unwrap().start_line_idx;
starts.push(first_line_idx);
}
assert!(
starts.windows(2).all(|w| w[0] <= w[1]),
"window start must advance monotonically as visible_row_start advances: {starts:?}"
);
assert_eq!(
starts[0], 0,
"scrolled to the top, the window starts at row 0"
);
assert_eq!(
*starts.last().unwrap(),
n - 100,
"scrolled to the bottom, the window's last row must reach the file's end"
);
}
#[test]
fn fixed_pitch_out_of_range_visible_row_start_slides_to_the_end() {
let bounds = Rect::new(0.0, 0.0, 40.0, 200.0);
let n = 1000;
let mut mm = minimap(n, 0, 1);
mm.visible_row_start = n;
let layout = mm.layout_with_sizing(bounds, 1, MinimapSizing::FixedPitch(2.0));
let first_line_idx = layout.visible_lines.first().unwrap().start_line_idx;
assert_eq!(
first_line_idx,
n - 100,
"out-of-range visible_row_start must slide to the bottom of the file, not the top"
);
}
#[test]
fn fixed_pitch_whole_file_fits_needs_no_slide() {
let mm = minimap(100, 50, 1);
let bounds = Rect::new(0.0, 0.0, 40.0, 200.0);
let layout = mm.layout_with_sizing(bounds, 1, MinimapSizing::FixedPitch(2.0));
assert_eq!(layout.visible_lines.len(), 100);
assert_eq!(layout.visible_lines[0].start_line_idx, 0);
}
#[test]
fn repeat_to_fill_stretches_a_short_file_at_a_whole_number_pitch() {
let mm = minimap(4, 0, 4);
let bounds = Rect::new(0.0, 0.0, 2.0, 20.0);
let layout = mm.layout_with_sizing(bounds, 1, MinimapSizing::RepeatToFill(1.0));
assert_eq!(layout.visible_lines.len(), 4);
for vline in &layout.visible_lines {
assert_eq!(
vline.bounds.height, 5.0,
"row pitch must be a whole multiple of the base pitch"
);
}
let last = layout.visible_lines.last().unwrap();
assert_eq!(
last.bounds.y + last.bounds.height,
bounds.height,
"a whole-number repeat factor fills the strip exactly, with no leftover gap"
);
}
#[test]
fn repeat_to_fill_falls_back_to_fixed_pitch_when_the_file_does_not_fit() {
let bounds = Rect::new(0.0, 0.0, 2.0, 200.0);
let n = 1000;
let mm = minimap(n, 500, 1);
let repeat_layout = mm.layout_with_sizing(bounds, 1, MinimapSizing::RepeatToFill(1.0));
let fixed_layout = mm.layout_with_sizing(bounds, 1, MinimapSizing::FixedPitch(1.0));
assert_eq!(
repeat_layout.visible_lines.len(),
fixed_layout.visible_lines.len()
);
assert_eq!(
repeat_layout.visible_lines[0].bounds.height,
fixed_layout.visible_lines[0].bounds.height
);
assert_eq!(
repeat_layout.visible_lines[0].start_line_idx,
fixed_layout.visible_lines[0].start_line_idx
);
}
#[test]
fn repeat_to_fill_whole_file_needs_no_slide_even_when_stretched() {
let mm = minimap(3, 0, 3);
let bounds = Rect::new(0.0, 0.0, 2.0, 30.0);
let layout = mm.layout_with_sizing(bounds, 1, MinimapSizing::RepeatToFill(1.0));
assert_eq!(layout.visible_lines.len(), 3);
assert_eq!(layout.visible_lines[0].start_line_idx, 0);
}
#[test]
fn repeat_to_fill_splits_the_remainder_instead_of_leaving_a_gap() {
let mm = minimap(5, 0, 5);
let bounds = Rect::new(0.0, 0.0, 2.0, 12.0);
let layout = mm.layout_with_sizing(bounds, 1, MinimapSizing::RepeatToFill(1.0));
assert_eq!(layout.visible_lines.len(), 5);
let heights: Vec<f32> = layout
.visible_lines
.iter()
.map(|v| v.bounds.height)
.collect();
assert_eq!(heights, vec![3.0, 3.0, 2.0, 2.0, 2.0]);
let last = layout.visible_lines.last().unwrap();
assert_eq!(
last.bounds.y + last.bounds.height,
bounds.height,
"splitting the remainder across rows fills the strip exactly, with no leftover gap"
);
}
#[test]
fn hit_test_top_middle_bottom_of_track() {
let layout = MinimapLayout {
bounds: Rect::new(0.0, 0.0, 10.0, 10.0),
visible_lines: Vec::new(),
viewport_highlight: Rect::default(),
scrollbar: None,
..Default::default()
};
assert_eq!(
layout.hit_test(5.0, 0.0),
MinimapHit::Seek { fraction: 0.0 }
);
assert_eq!(
layout.hit_test(5.0, 5.0),
MinimapHit::Seek { fraction: 0.5 }
);
assert_eq!(
layout.hit_test(5.0, 9.0),
MinimapHit::Seek { fraction: 0.9 }
);
}
#[test]
fn hit_test_outside_bounds_is_none() {
let layout = MinimapLayout {
bounds: Rect::new(10.0, 10.0, 10.0, 10.0),
visible_lines: Vec::new(),
viewport_highlight: Rect::default(),
scrollbar: None,
..Default::default()
};
assert_eq!(layout.hit_test(0.0, 0.0), MinimapHit::None);
assert_eq!(layout.hit_test(25.0, 15.0), MinimapHit::None);
}
#[test]
fn block_bounds_never_upscales() {
assert_eq!(block_bounds(3, 10), vec![0, 1, 2, 3]);
}
#[test]
fn block_bounds_downsamples_with_the_stride_formula() {
assert_eq!(
block_bounds(100, 10),
vec![0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
);
}
#[test]
fn block_bounds_degenerate_inputs_do_not_panic() {
assert_eq!(block_bounds(0, 10), vec![0]);
assert_eq!(block_bounds(10, 0), vec![0]);
}
#[test]
fn block_sample_indices_reads_every_line_under_the_cap() {
assert_eq!(block_sample_indices(10, 15, 8), vec![10, 11, 12, 13, 14]);
}
#[test]
fn block_sample_indices_spreads_evenly_when_capped() {
let idx = block_sample_indices(0, 1000, 8);
assert_eq!(idx.len(), 8);
assert!(idx.windows(2).all(|w| w[0] < w[1]));
assert_eq!(idx[0], 0);
assert!(*idx.last().unwrap() < 1000);
}
#[test]
fn block_sample_indices_empty_span_is_empty() {
assert!(block_sample_indices(5, 5, 8).is_empty());
}
#[test]
fn dither_threshold_met_zero_total_never_fires() {
assert!(!dither_threshold_met(0, 0, 0, 0));
}
#[test]
fn dither_threshold_met_full_coverage_always_fires() {
for row in 0..4 {
for col in 0..4 {
assert!(dither_threshold_met(6, 6, row, col));
}
}
}
#[test]
fn dither_threshold_met_zero_coverage_never_fires() {
for row in 0..4 {
for col in 0..4 {
assert!(!dither_threshold_met(0, 6, row, col));
}
}
}
#[test]
fn sample_blocks_empty_or_zero_target_is_empty() {
assert!(sample_blocks(0, 5, |_| String::new()).is_empty());
assert!(sample_blocks(5, 0, |_| String::new()).is_empty());
}
#[test]
fn sample_blocks_never_upscales_and_keeps_real_text_verbatim() {
let lines = ["fn main() {", " body();", "}"];
let out = sample_blocks(lines.len(), 10, |i| lines[i].to_string());
assert_eq!(out.len(), 3);
for (i, line) in out.iter().enumerate() {
assert_eq!(line.line_idx, i);
assert_eq!(
line.text, lines[i],
"single-line block must not be dithered"
);
}
}
#[test]
fn sample_blocks_no_line_in_a_block_is_ever_fully_discarded() {
let total = 1000;
let lines: Vec<String> = (0..total)
.map(|i| match i {
0 => String::new(),
125 => " ".repeat(4) + "x",
250 => " ".repeat(8) + "x",
375 => " ".repeat(12) + "x",
500 => " ".repeat(16) + "x",
625 => " ".repeat(20) + "x",
750 => " ".repeat(24) + "x",
875 => " ".repeat(28) + "x",
_ => String::new(),
})
.collect();
let out = sample_blocks(total, 1, |i| lines[i].clone());
assert_eq!(out.len(), 1);
assert!(
out[0].text.contains('x'),
"aggregated block text must retain real content from non-boundary lines, not just \
the single line a point sampler would have picked"
);
}
#[test]
fn sample_blocks_respects_the_read_cap_per_block() {
use std::cell::Cell;
let calls = Cell::new(0usize);
let total = 10_000;
let out = sample_blocks(total, 1, |i| {
calls.set(calls.get() + 1);
format!("line{i}")
});
assert_eq!(out.len(), 1);
assert!(
calls.get() <= BLOCK_LINE_SAMPLE_CAP,
"expected at most {BLOCK_LINE_SAMPLE_CAP} accessor calls, got {}",
calls.get()
);
}
#[test]
fn sample_blocks_line_idx_is_each_blocks_start_line() {
let total = 100;
let out = sample_blocks(total, 10, |i| format!("l{i}"));
assert_eq!(out.len(), 10);
let expected_bounds = block_bounds(total, 10);
for (row, line) in out.iter().enumerate() {
assert_eq!(line.line_idx, expected_bounds[row]);
}
}
#[test]
fn window_start_line_whole_buffer_fits_needs_no_slide() {
assert_eq!(window_start_line(100, 100, 50, 100), 0);
assert_eq!(window_start_line(100, 200, 99, 100), 0);
}
#[test]
fn window_start_line_slides_monotonically_and_reaches_both_ends() {
let total = 1000;
let window = 100;
let mut starts = Vec::new();
for visible_row_start in [0, 100, 300, 500, 700, 900, 999] {
starts.push(window_start_line(total, window, visible_row_start, total));
}
assert!(
starts.windows(2).all(|w| w[0] <= w[1]),
"window start must advance monotonically: {starts:?}"
);
assert_eq!(starts[0], 0, "scrolled to the top, window starts at 0");
assert_eq!(
*starts.last().unwrap(),
total - window,
"scrolled to the bottom, the window's last line must reach the file's end"
);
}
#[test]
fn window_start_line_zero_or_one_line_buffer_does_not_panic() {
assert_eq!(window_start_line(0, 10, 0, 0), 0);
assert_eq!(window_start_line(1, 10, 0, 1), 0);
}
#[test]
fn sample_window_reads_only_the_windows_own_lines() {
let total = 1000;
let expected_start = window_start_line(total, 100, 900, total);
let calls = std::cell::RefCell::new(Vec::new());
let out = sample_window(total, 100, 10, 900, |i| {
calls.borrow_mut().push(i);
format!("l{i}")
});
assert_eq!(out.len(), 10);
let seen = calls.borrow();
assert!(seen
.iter()
.all(|&i| i >= expected_start && i < expected_start + 100));
}
#[test]
fn sample_window_line_idx_is_a_real_buffer_line_no_caller_shift_needed() {
let total = 1000;
let out = sample_window(total, 100, 10, 900, |i| format!("l{i}"));
let expected_start = window_start_line(total, 100, 900, total);
assert_eq!(out.first().unwrap().line_idx, expected_start);
assert!(out.last().unwrap().line_idx < total);
}
#[test]
fn sample_window_uncompressed_scale_matches_sample_blocks_over_the_window() {
let total = 1000;
let start = window_start_line(total, 50, 700, total);
let direct = sample_blocks(50, 50, |i| format!("l{}", i + start));
let via_window = sample_window(total, 50, 50, 700, |i| format!("l{i}"));
assert_eq!(direct.len(), via_window.len());
for (d, w) in direct.iter().zip(via_window.iter()) {
assert_eq!(d.text, w.text);
assert_eq!(d.line_idx + start, w.line_idx);
}
}
#[test]
fn sample_window_empty_inputs_are_empty() {
assert!(sample_window(0, 10, 10, 0, |i| format!("l{i}")).is_empty());
assert!(sample_window(100, 10, 0, 0, |i| format!("l{i}")).is_empty());
}
fn red() -> Color {
Color::rgb(255, 0, 0)
}
fn blue() -> Color {
Color::rgb(0, 0, 255)
}
fn sample_spans() -> Vec<MinimapSpan> {
vec![
MinimapSpan {
line_idx: 0,
start_col: 0,
end_col: 2,
color: red(),
},
MinimapSpan {
line_idx: 0,
start_col: 2,
end_col: 4,
color: blue(),
},
MinimapSpan {
line_idx: 1,
start_col: 0,
end_col: 4,
color: red(),
},
MinimapSpan {
line_idx: 2,
start_col: 0,
end_col: 4,
color: blue(),
},
MinimapSpan {
line_idx: 3,
start_col: 0,
end_col: 4,
color: blue(),
},
]
}
#[test]
fn aggregate_spans_gtk_grid_is_per_line_no_reduction() {
let grid = MinimapGrid {
rows: 4,
cols: 4,
lines_per_row: 1,
cols_per_cell: 1,
};
let out = aggregate_spans(&sample_spans(), grid);
assert!(out.contains(&MinimapSpan {
line_idx: 0,
start_col: 0,
end_col: 1,
color: red()
}));
assert!(out.contains(&MinimapSpan {
line_idx: 0,
start_col: 2,
end_col: 3,
color: blue()
}));
assert!(out.contains(&MinimapSpan {
line_idx: 1,
start_col: 0,
end_col: 1,
color: red()
}));
}
#[test]
fn aggregate_spans_tui_grid_folds_four_lines_into_one_cell() {
let grid = MinimapGrid {
rows: 1,
cols: 2,
lines_per_row: 4,
cols_per_cell: 2,
};
let out = aggregate_spans(&sample_spans(), grid);
assert_eq!(out.len(), 2);
assert_eq!(
out[0],
MinimapSpan {
line_idx: 0,
start_col: 0,
end_col: 2,
color: blue()
}
);
assert_eq!(
out[1],
MinimapSpan {
line_idx: 0,
start_col: 2,
end_col: 4,
color: blue()
}
);
}
#[test]
fn aggregate_spans_empty_grid_dims_return_empty() {
let grid = MinimapGrid {
rows: 0,
cols: 4,
lines_per_row: 1,
cols_per_cell: 1,
};
assert!(aggregate_spans(&sample_spans(), grid).is_empty());
}
#[test]
fn reserved_width_is_zero_without_a_minimap() {
assert_eq!(reserved_width(20.0, false), 0.0);
}
#[test]
fn reserved_width_returns_the_width_with_a_minimap() {
assert_eq!(reserved_width(20.0, true), 20.0);
}
#[test]
fn resolve_width_returns_none_for_fill_and_fixed_pitch() {
assert_eq!(MinimapSizing::Fill.resolve_width(800.0, 8.0), None);
assert_eq!(
MinimapSizing::FixedPitch(2.0).resolve_width(800.0, 8.0),
None
);
}
#[test]
fn resolve_width_holds_steady_at_target_cols_for_a_wide_pane() {
let sizing = MinimapSizing::VsCodeParity {
target_cols: 15.0,
fraction: 0.15,
min: 6.0,
max: 30.0,
};
let got = sizing.resolve_width(1600.0, 8.0).unwrap();
assert_eq!(got, 15.0 * 8.0);
}
#[test]
fn resolve_width_shrinks_with_a_narrow_pane_down_to_the_floor() {
let sizing = MinimapSizing::VsCodeParity {
target_cols: 15.0,
fraction: 0.15,
min: 6.0,
max: 30.0,
};
let got = sizing.resolve_width(20.0, 1.0).unwrap();
assert_eq!(got, 6.0);
}
#[test]
fn resolve_width_is_the_same_formula_across_native_units() {
let sizing = MinimapSizing::VsCodeParity {
target_cols: 15.0,
fraction: 0.2,
min: 6.0,
max: 30.0,
};
let cols_native = sizing.resolve_width(50.0, 1.0).unwrap();
let px_native = sizing.resolve_width(50.0 * 8.0, 8.0).unwrap();
assert_eq!(cols_native * 8.0, px_native);
}
#[test]
fn resolve_width_treats_non_positive_char_width_as_one() {
let sizing = MinimapSizing::VsCodeParity {
target_cols: 15.0,
fraction: 0.15,
min: 6.0,
max: 30.0,
};
assert_eq!(
sizing.resolve_width(200.0, 0.0),
sizing.resolve_width(200.0, 1.0)
);
}
#[test]
fn vscode_parity_in_row_layout_falls_back_to_fill_behavior() {
let mm = Minimap {
id: WidgetId::new("mm"),
lines: (0..8)
.map(|i| MinimapLine {
text: String::new(),
line_idx: i,
})
.collect(),
syntax_spans: Vec::new(),
visible_row_start: 0,
visible_row_count: 8,
total_buffer_lines: 8,
};
let bounds = Rect::new(0.0, 0.0, 10.0, 40.0);
let via_fill = mm.layout_with_sizing(bounds, 2, MinimapSizing::Fill);
let via_vscode_parity = mm.layout_with_sizing(
bounds,
2,
MinimapSizing::VsCodeParity {
target_cols: 15.0,
fraction: 0.15,
min: 6.0,
max: 30.0,
},
);
assert_eq!(via_fill, via_vscode_parity);
}
#[test]
fn legibility_floor_switches_render_mode_on_both_sides() {
assert_eq!(
render_mode(LEGIBILITY_FLOOR_PX - 0.1),
MinimapRenderMode::ColumnBlocks
);
assert_eq!(
render_mode(LEGIBILITY_FLOOR_PX),
MinimapRenderMode::Characters
);
assert_eq!(
render_mode(LEGIBILITY_FLOOR_PX + 4.0),
MinimapRenderMode::Characters
);
}
#[test]
fn default_row_pitch_stays_below_the_legibility_floor() {
assert!(ROW_PITCH_PX < LEGIBILITY_FLOOR_PX);
assert_eq!(render_mode(ROW_PITCH_PX), MinimapRenderMode::ColumnBlocks);
}
#[test]
fn minimap_font_px_clamps_to_a_sane_band() {
assert_eq!(minimap_font_px(0.0), 1.0);
assert_eq!(minimap_font_px(1000.0), 64.0);
assert_eq!(minimap_font_px(10.0), 10.0);
}
#[test]
fn minimap_scale_default_is_one_and_matches_row_pitch_px() {
assert_eq!(MinimapScale::default(), MinimapScale::One);
assert_eq!(MinimapScale::One.row_pitch_px(), ROW_PITCH_PX);
assert_eq!(MinimapScale::One.cell_w_px(), 1.0);
}
#[test]
fn minimap_scale_two_is_a_2x4_cell_and_clears_the_legibility_floor() {
assert_eq!(MinimapScale::Two.cell_w_px(), 2.0);
assert_eq!(MinimapScale::Two.row_pitch_px(), 4.0);
assert!(is_legible(MinimapScale::Two.row_pitch_px()));
assert_eq!(
render_mode(MinimapScale::Two.row_pitch_px()),
MinimapRenderMode::Characters
);
}
#[test]
fn minimap_scale_one_stays_below_the_legibility_floor() {
assert!(!is_legible(MinimapScale::One.row_pitch_px()));
assert_eq!(
render_mode(MinimapScale::One.row_pitch_px()),
MinimapRenderMode::ColumnBlocks
);
}
#[test]
fn truncate_to_columns_cuts_on_a_char_boundary() {
assert_eq!(truncate_to_columns("café", 3), "caf");
assert_eq!(truncate_to_columns("ab", 10), "ab");
assert_eq!(truncate_to_columns("", 5), "");
}
#[test]
fn color_at_column_picks_the_span_covering_that_column() {
let spans = vec![
MinimapSpan {
line_idx: 0,
start_col: 0,
end_col: 1,
color: red(),
},
MinimapSpan {
line_idx: 0,
start_col: 1,
end_col: 6,
color: blue(),
},
];
assert_eq!(color_at_column(&spans, 0, Color::rgb(1, 1, 1)), red());
assert_eq!(color_at_column(&spans, 3, Color::rgb(1, 1, 1)), blue());
}
#[test]
fn color_at_column_falls_back_to_default_outside_any_span() {
let fallback = Color::rgb(9, 9, 9);
assert_eq!(color_at_column(&[], 2, fallback), fallback);
}
#[test]
fn span_cursor_matches_a_full_linear_scan_per_row() {
let green = Color::rgb(0, 255, 0);
let spans = vec![
MinimapSpan {
line_idx: 0,
start_col: 0,
end_col: 2,
color: red(),
},
MinimapSpan {
line_idx: 0,
start_col: 2,
end_col: 4,
color: blue(),
},
MinimapSpan {
line_idx: 2,
start_col: 0,
end_col: 3,
color: green,
},
MinimapSpan {
line_idx: 5,
start_col: 1,
end_col: 2,
color: red(),
},
];
let mut cursor = SpanCursor::new(&spans);
for line_idx in 0..6 {
let via_cursor = cursor.row_spans(line_idx).to_vec();
let via_linear: Vec<MinimapSpan> = spans
.iter()
.filter(|s| s.line_idx == line_idx)
.cloned()
.collect();
assert_eq!(
via_cursor, via_linear,
"row {line_idx} spans must match a full linear scan"
);
}
}
fn sheet_with_one_cell(target: char, cell_w: usize, sheet_h: usize, pattern: &[u8]) -> Vec<u8> {
let sheet_w = cell_w * ATLAS_CHAR_COUNT;
let mut sheet = vec![0u8; sheet_w * sheet_h];
let idx = ascii_tile_index(target).expect("target must be in the sampled ASCII range");
let cell_x0 = idx * cell_w;
for (i, &v) in pattern.iter().enumerate().take(cell_w * sheet_h) {
let row = i / cell_w;
let col = i % cell_w;
sheet[row * sheet_w + cell_x0 + col] = v;
}
sheet
}
#[test]
fn atlas_blank_cell_downsamples_to_all_zero() {
let cell_w = 10;
let sheet_h = 16;
let sheet = sheet_with_one_cell('!', cell_w, sheet_h, &vec![200u8; cell_w * sheet_h]);
let atlas = MinimapCharAtlas::from_alpha_sheet(
&sheet,
cell_w * ATLAS_CHAR_COUNT,
sheet_h,
cell_w,
2,
4,
);
assert!(
atlas.tile(' ').iter().all(|&b| b == 0),
"a blank glyph cell must downsample to an all-zero tile"
);
}
#[test]
fn atlas_all_blank_sheet_stays_all_zero() {
let cell_w = 10;
let sheet_h = 16;
let sheet = vec![0u8; cell_w * ATLAS_CHAR_COUNT * sheet_h];
let atlas = MinimapCharAtlas::from_alpha_sheet(
&sheet,
cell_w * ATLAS_CHAR_COUNT,
sheet_h,
cell_w,
2,
4,
);
assert!(atlas.tile('a').iter().all(|&b| b == 0));
}
#[test]
fn atlas_normalisation_drives_the_brightest_tile_to_255() {
let cell_w = 10;
let sheet_h = 16;
let sheet = sheet_with_one_cell('x', cell_w, sheet_h, &vec![100u8; cell_w * sheet_h]);
let atlas = MinimapCharAtlas::from_alpha_sheet(
&sheet,
cell_w * ATLAS_CHAR_COUNT,
sheet_h,
cell_w,
2,
4,
);
assert!(
atlas.tile('x').iter().any(|&b| b == 255),
"the brightest sampled tile must be rescaled to full opacity, got {:?}",
atlas.tile('x')
);
}
#[test]
fn atlas_tile_count_and_stride_are_exact() {
let cell_w = 10;
let sheet_h = 16;
let (tile_w, tile_h) = (2, 4);
let sheet_w = cell_w * ATLAS_CHAR_COUNT;
let mut sheet = vec![0u8; sheet_w * sheet_h];
for i in 0..ATLAS_CHAR_COUNT {
let v = ((i + 1) % 256) as u8;
for row in 0..sheet_h {
for col in 0..cell_w {
sheet[row * sheet_w + i * cell_w + col] = v;
}
}
}
let atlas =
MinimapCharAtlas::from_alpha_sheet(&sheet, sheet_w, sheet_h, cell_w, tile_w, tile_h);
for ch in ATLAS_FIRST_CHAR..=ATLAS_LAST_CHAR {
let c = char::from_u32(ch).unwrap();
let tile = atlas.tile(c);
assert_eq!(
tile.len(),
tile_w * tile_h,
"tile for {c:?} must be exactly tile_w * tile_h bytes"
);
}
assert_ne!(
atlas.tile(' '),
atlas.tile('!'),
"adjacent cells must not alias each other's tile data"
);
}
#[test]
fn atlas_fractional_edge_box_filter_matches_hand_computed_values() {
let cell_w = 3;
let sheet_h = 1;
let sheet = sheet_with_one_cell('Q', cell_w, sheet_h, &[50, 90, 255]);
let atlas = MinimapCharAtlas::from_alpha_sheet(
&sheet,
cell_w * ATLAS_CHAR_COUNT,
sheet_h,
cell_w,
2,
1,
);
assert_eq!(atlas.tile('Q'), &[81, 255]);
}
#[test]
fn atlas_malformed_sheet_falls_back_to_filled() {
let atlas = MinimapCharAtlas::from_alpha_sheet(&[0u8; 4], 4, 1, 10, 2, 4);
assert_eq!(atlas.tile(' '), &[255, 255, 255, 255, 255, 255, 255, 255]);
assert_eq!(atlas.tile('~'), &[255, 255, 255, 255, 255, 255, 255, 255]);
}
#[test]
fn atlas_tile_falls_back_to_filled_block_outside_ascii_range() {
let cell_w = 10;
let sheet_h = 16;
let sheet = sheet_with_one_cell('x', cell_w, sheet_h, &vec![10u8; cell_w * sheet_h]);
let atlas = MinimapCharAtlas::from_alpha_sheet(
&sheet,
cell_w * ATLAS_CHAR_COUNT,
sheet_h,
cell_w,
2,
4,
);
assert_eq!(atlas.tile('字'), &[255; 8]);
}
#[test]
fn atlas_cache_builds_once_for_repeated_same_key_lookups() {
let mut cache = MinimapAtlasCache::new();
let calls = std::cell::Cell::new(0);
for _ in 0..5 {
cache.get_or_build("Monospace", 1.0, || {
calls.set(calls.get() + 1);
MinimapCharAtlas::filled(1, 2)
});
}
assert_eq!(
calls.get(),
1,
"the build closure must run once across N paints at the same (family, scale) -- no per-frame shaping"
);
}
#[test]
fn atlas_cache_rebuilds_when_family_changes() {
let mut cache = MinimapAtlasCache::new();
let calls = std::cell::Cell::new(0);
let build = |calls: &std::cell::Cell<i32>| {
calls.set(calls.get() + 1);
MinimapCharAtlas::filled(1, 2)
};
cache.get_or_build("Monospace", 1.0, || build(&calls));
cache.get_or_build("Fira Code", 1.0, || build(&calls));
assert_eq!(calls.get(), 2, "a different font family must rebuild");
}
#[test]
fn atlas_cache_rebuilds_when_scale_changes() {
let mut cache = MinimapAtlasCache::new();
let calls = std::cell::Cell::new(0);
let build = |calls: &std::cell::Cell<i32>| {
calls.set(calls.get() + 1);
MinimapCharAtlas::filled(1, 2)
};
cache.get_or_build("Monospace", 1.0, || build(&calls));
cache.get_or_build("Monospace", 2.0, || build(&calls));
assert_eq!(calls.get(), 2, "a different scale must rebuild");
}
}