use gtk4::cairo::{Context, Format, ImageSurface};
use gtk4::pango;
use super::{cairo_rgb, set_source};
use crate::event::Rect as QRect;
use crate::primitives::layout_metrics::pixel_minimap_layout_scaled;
use crate::primitives::minimap::{
color_at_column, minimap_font_px, render_mode, truncate_to_columns, Minimap, MinimapAtlasCache,
MinimapCharAtlas, MinimapLayout, MinimapRenderMode, MinimapScale, MinimapSpan, SpanCursor,
VisibleMinimapLine, ATLAS_FIRST_CHAR, ATLAS_LAST_CHAR, COLUMN_CAPACITY,
};
use crate::theme::Theme;
pub fn gtk_minimap_layout(minimap: &Minimap, x: f64, y: f64, w: f64, h: f64) -> MinimapLayout {
gtk_minimap_layout_scaled(minimap, x, y, w, h, MinimapScale::One)
}
pub(crate) fn gtk_minimap_layout_scaled(
minimap: &Minimap,
x: f64,
y: f64,
w: f64,
h: f64,
scale: MinimapScale,
) -> MinimapLayout {
pixel_minimap_layout_scaled(
minimap,
QRect::new(x as f32, y as f32, w as f32, h as f32),
scale,
)
}
#[allow(clippy::too_many_arguments)]
fn paint_minimap_rows(
cr: &Context,
x: f64,
y: f64,
w: f64,
h: f64,
minimap: &Minimap,
theme: &Theme,
scale: MinimapScale,
mut paint_row: impl FnMut(&Context, &VisibleMinimapLine, f64, f64, &str, &[MinimapSpan]),
) -> MinimapLayout {
let layout = gtk_minimap_layout_scaled(minimap, x, y, w, h, scale);
if layout.visible_lines.is_empty() {
return layout;
}
set_source(cr, theme.background);
cr.rectangle(x, y, w, h);
cr.fill().ok();
let hl = &layout.viewport_highlight;
if hl.height > 0.0 {
let (r, g, b) = cairo_rgb(theme.accent_bg);
cr.set_source_rgba(r, g, b, 0.25);
cr.rectangle(hl.x as f64, hl.y as f64, hl.width as f64, hl.height as f64);
cr.fill().ok();
}
cr.save().ok();
cr.rectangle(x, y, w, h);
cr.clip();
let mut spans = SpanCursor::new(&minimap.syntax_spans);
for vline in &layout.visible_lines {
let Some(line) = minimap.lines.get(vline.start_line_idx) else {
continue;
};
let row_spans = spans.row_spans(vline.start_line_idx);
paint_row(
cr,
vline,
vline.bounds.height as f64,
scale.cell_w_px(),
&line.text,
row_spans,
);
}
cr.restore().ok();
layout
}
#[allow(clippy::too_many_arguments)]
pub fn draw_minimap(
cr: &Context,
pango_layout: &pango::Layout,
x: f64,
y: f64,
w: f64,
h: f64,
minimap: &Minimap,
theme: &Theme,
) -> MinimapLayout {
let saved_font = pango_layout.font_description();
let layout = paint_minimap_rows(
cr,
x,
y,
w,
h,
minimap,
theme,
MinimapScale::One,
|cr, vline, row_px, cell_w, text, row_spans| match render_mode(row_px) {
MinimapRenderMode::Characters => paint_row_glyphs(
cr,
pango_layout,
saved_font.as_ref(),
vline,
row_px,
text,
row_spans,
theme,
),
MinimapRenderMode::ColumnBlocks => {
paint_row_blocks(cr, vline, text, row_spans, theme, cell_w)
}
},
);
pango_layout.set_attributes(None);
pango_layout.set_font_description(saved_font.as_ref());
pango_layout.set_width(-1);
pango_layout.set_ellipsize(pango::EllipsizeMode::None);
layout
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_minimap_cached(
cr: &Context,
pango_layout: &pango::Layout,
x: f64,
y: f64,
w: f64,
h: f64,
minimap: &Minimap,
theme: &Theme,
atlas_cache: &mut MinimapAtlasCache,
dpi_scale: f64,
mm_scale: MinimapScale,
) -> MinimapLayout {
let family = pango_layout
.font_description()
.and_then(|f| f.family())
.map(|g| g.to_string())
.unwrap_or_else(|| "Monospace".to_string());
let dpi_scale = dpi_scale.max(1.0);
let tile_w = (mm_scale.cell_w_px() * dpi_scale).round().max(1.0) as usize;
let tile_h = (mm_scale.row_pitch_px() * dpi_scale).round().max(1.0) as usize;
let family_key = format!("{family}#{mm_scale:?}");
let atlas = atlas_cache.get_or_build(&family_key, dpi_scale as f32, || {
build_char_atlas(&family, tile_w, tile_h)
});
paint_minimap_rows(
cr,
x,
y,
w,
h,
minimap,
theme,
mm_scale,
|cr, vline, _row_px, cell_w, text, row_spans| {
if atlas.tile_w() == 0 || atlas.tile_h() == 0 {
paint_row_blocks(cr, vline, text, row_spans, theme, cell_w);
} else {
paint_row_atlas(cr, atlas, dpi_scale, vline, text, row_spans, theme, cell_w);
}
},
)
}
const CHAR_SAMPLE_CELL_W: f64 = 10.0;
const CHAR_SAMPLE_CELL_H: f64 = 16.0;
fn build_char_atlas(family: &str, tile_w: usize, tile_h: usize) -> MinimapCharAtlas {
match render_char_sample_sheet(family) {
Some(alpha) => MinimapCharAtlas::from_alpha_sheet(
&alpha,
(CHAR_SAMPLE_CELL_W as usize) * (crate::primitives::minimap::ATLAS_CHAR_COUNT),
CHAR_SAMPLE_CELL_H as usize,
CHAR_SAMPLE_CELL_W as usize,
tile_w,
tile_h,
),
None => MinimapCharAtlas::filled(tile_w, tile_h),
}
}
fn render_char_sample_sheet(family: &str) -> Option<Vec<u8>> {
let char_count = crate::primitives::minimap::ATLAS_CHAR_COUNT;
let cell_w = CHAR_SAMPLE_CELL_W as i32;
let sheet_h = CHAR_SAMPLE_CELL_H as i32;
let sheet_w = cell_w * char_count as i32;
let surface = ImageSurface::create(Format::ARgb32, sheet_w, sheet_h).ok()?;
let cr = Context::new(&surface).ok()?;
let pango_layout = pangocairo::functions::create_layout(&cr);
let mut font = pango::FontDescription::new();
font.set_family(family);
font.set_weight(pango::Weight::Bold);
font.set_absolute_size(CHAR_SAMPLE_CELL_H * 0.85 * pango::SCALE as f64);
pango_layout.set_font_description(Some(&font));
cr.set_source_rgb(1.0, 1.0, 1.0);
for (i, code) in (ATLAS_FIRST_CHAR..=ATLAS_LAST_CHAR).enumerate() {
let ch = char::from_u32(code)?;
pango_layout.set_text(&ch.to_string());
cr.move_to(i as f64 * CHAR_SAMPLE_CELL_W, 0.0);
pangocairo::functions::show_layout(&cr, &pango_layout);
}
drop(pango_layout);
drop(cr);
surface.flush();
let stride = surface.stride() as usize;
let mut surface = surface;
let data = surface.data().ok()?;
let (sheet_w, sheet_h) = (sheet_w as usize, sheet_h as usize);
let mut alpha = vec![0u8; sheet_w * sheet_h];
for row in 0..sheet_h {
for col in 0..sheet_w {
alpha[row * sheet_w + col] = data[row * stride + col * 4 + 3];
}
}
Some(alpha)
}
#[allow(clippy::too_many_arguments)]
fn paint_row_atlas(
cr: &Context,
atlas: &MinimapCharAtlas,
dpi_scale: f64,
vline: &VisibleMinimapLine,
text: &str,
row_spans: &[MinimapSpan],
theme: &Theme,
cell_w: f64,
) {
for (col, ch) in text.chars().enumerate().take(COLUMN_CAPACITY) {
if ch.is_whitespace() {
continue;
}
let color = color_at_column(row_spans, col, theme.foreground);
blit_alpha_tile(
cr,
atlas.tile(ch),
atlas.tile_w(),
atlas.tile_h(),
vline.bounds.x as f64 + col as f64 * cell_w,
vline.bounds.y as f64,
dpi_scale,
color,
);
}
}
#[allow(clippy::too_many_arguments)]
fn blit_alpha_tile(
cr: &Context,
tile: &[u8],
tile_w: usize,
tile_h: usize,
x: f64,
y: f64,
dpi_scale: f64,
color: crate::types::Color,
) {
if tile_w == 0 || tile_h == 0 {
return;
}
let Ok(mut surface) = ImageSurface::create(Format::A8, tile_w as i32, tile_h as i32) else {
return;
};
{
let stride = surface.stride() as usize;
let Ok(mut data) = surface.data() else {
return;
};
for row in 0..tile_h {
let src = &tile[row * tile_w..(row + 1) * tile_w];
let dst_off = row * stride;
data[dst_off..dst_off + tile_w].copy_from_slice(src);
}
}
surface.flush();
let (r, g, b) = cairo_rgb(color);
cr.save().ok();
cr.translate(x, y);
let inv_scale = 1.0 / dpi_scale.max(f64::MIN_POSITIVE);
cr.scale(inv_scale, inv_scale);
cr.set_source_rgb(r, g, b);
cr.mask_surface(&surface, 0.0, 0.0).ok();
cr.restore().ok();
}
#[allow(clippy::too_many_arguments)]
fn paint_row_glyphs(
cr: &Context,
pango_layout: &pango::Layout,
saved_font: Option<&pango::FontDescription>,
vline: &VisibleMinimapLine,
row_px: f64,
text: &str,
row_spans: &[MinimapSpan],
theme: &Theme,
) {
let mut font = saved_font.cloned().unwrap_or_default();
font.set_absolute_size(minimap_font_px(row_px) * pango::SCALE as f64);
pango_layout.set_font_description(Some(&font));
pango_layout.set_width((vline.bounds.width * pango::SCALE as f32).round() as i32);
pango_layout.set_ellipsize(pango::EllipsizeMode::End);
let truncated = truncate_to_columns(text, COLUMN_CAPACITY);
pango_layout.set_text(truncated);
if row_spans.is_empty() {
pango_layout.set_attributes(None);
} else {
let to_u16 = |c: u8| -> u16 { ((c as u16) << 8) | c as u16 };
let attrs = pango::AttrList::new();
for span in row_spans {
let (start, end) = char_range_to_byte_range(truncated, span.start_col, span.end_col);
let mut a = pango::AttrColor::new_foreground(
to_u16(span.color.r),
to_u16(span.color.g),
to_u16(span.color.b),
);
a.set_start_index(start);
a.set_end_index(end);
attrs.insert(a);
}
pango_layout.set_attributes(Some(&attrs));
}
set_source(cr, theme.foreground);
cr.move_to(vline.bounds.x as f64, vline.bounds.y as f64);
super::painted_text::show_layout(cr, pango_layout);
}
fn paint_row_blocks(
cr: &Context,
vline: &VisibleMinimapLine,
text: &str,
row_spans: &[MinimapSpan],
theme: &Theme,
cell_w: f64,
) {
for (col, ch) in text.chars().enumerate().take(COLUMN_CAPACITY) {
if ch.is_whitespace() {
continue;
}
let color = color_at_column(row_spans, col, theme.foreground);
set_source(cr, color);
cr.rectangle(
vline.bounds.x as f64 + col as f64 * cell_w,
vline.bounds.y as f64,
cell_w,
vline.bounds.height as f64,
);
cr.fill().ok();
}
}
fn char_range_to_byte_range(text: &str, start_col: usize, end_col: usize) -> (u32, u32) {
let mut start_byte = text.len();
let mut end_byte = text.len();
let mut char_idx = 0usize;
for (byte_idx, _) in text.char_indices() {
if char_idx == start_col {
start_byte = start_byte.min(byte_idx);
}
if char_idx == end_col {
end_byte = byte_idx;
}
char_idx += 1;
}
if start_col == 0 {
start_byte = 0;
}
if end_col >= char_idx {
end_byte = text.len();
}
(
start_byte.min(text.len()) as u32,
end_byte.min(text.len()) as u32,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::minimap::{MinimapHit, MinimapLine, ROW_PITCH_PX};
use crate::types::{Color, WidgetId};
use pangocairo::cairo::{Context as CairoContext, Format, ImageSurface};
fn minimap_from(lines: Vec<&str>, total_buffer_lines: usize) -> Minimap {
Minimap {
id: WidgetId::new("mm"),
lines: lines
.into_iter()
.enumerate()
.map(|(i, t)| MinimapLine {
text: t.into(),
line_idx: i,
})
.collect(),
syntax_spans: Vec::new(),
visible_row_start: 0,
visible_row_count: 0,
total_buffer_lines,
}
}
fn surface_and_layout() -> (CairoContext, pango::Layout) {
let surface = ImageSurface::create(Format::ARgb32, 200, 200).expect("create ImageSurface");
let cr = CairoContext::new(&surface).expect("Context::new");
let layout = pangocairo::functions::create_layout(&cr);
(cr, layout)
}
#[test]
fn row_pitch_is_independent_of_the_strip_height() {
let lines: Vec<&str> = vec!["fn main() {}"; 8];
let mm = minimap_from(lines, 8);
let tall = gtk_minimap_layout(&mm, 0.0, 0.0, 40.0, 800.0);
let short = gtk_minimap_layout(&mm, 0.0, 0.0, 40.0, 16.0);
let tall_px = tall.visible_lines[0].bounds.height as f64;
let short_px = short.visible_lines[0].bounds.height as f64;
assert_eq!(tall_px, ROW_PITCH_PX);
assert_eq!(short_px, ROW_PITCH_PX);
}
#[test]
fn row_pitch_is_independent_of_the_file_length() {
let short_mm = minimap_from(vec!["fn main() {}"; 3], 3);
let long_lines: Vec<String> = (0..10_000).map(|i| format!("line {i}")).collect();
let long_refs: Vec<&str> = long_lines.iter().map(String::as_str).collect();
let long_mm = minimap_from(long_refs, 10_000);
let short_layout = gtk_minimap_layout(&short_mm, 0.0, 0.0, 40.0, 400.0);
let long_layout = gtk_minimap_layout(&long_mm, 0.0, 0.0, 40.0, 400.0);
assert_eq!(
short_layout.visible_lines[0].bounds.height as f64,
ROW_PITCH_PX
);
assert_eq!(
long_layout.visible_lines[0].bounds.height as f64,
ROW_PITCH_PX
);
}
#[test]
fn short_file_in_a_tall_strip_does_not_stretch() {
let mm = minimap_from(vec!["//! Placeholder module"; 3], 3);
let layout = gtk_minimap_layout(&mm, 0.0, 0.0, 200.0, 200.0);
assert_eq!(layout.visible_lines.len(), 3);
let last = layout.visible_lines.last().unwrap();
assert!(
(last.bounds.y + last.bounds.height) < 200.0,
"a short file must not stretch its rows to fill the whole strip"
);
}
#[test]
fn default_fixed_pitch_stays_below_the_floor_and_never_shapes_text() {
let mm = minimap_from(vec!["fn main() {}"; 4], 4);
let (cr, layout) = surface_and_layout();
let _ = draw_minimap(&cr, &layout, 0.0, 0.0, 40.0, 4.0, &mm, &Theme::default());
assert_eq!(layout.text(), "");
let _ = draw_minimap(&cr, &layout, 0.0, 0.0, 40.0, 400.0, &mm, &Theme::default());
assert_eq!(
layout.text(),
"",
"the fixed pitch stays below the legibility floor regardless of strip height"
);
}
#[test]
fn characters_branch_truncates_to_the_column_capacity_before_shaping() {
let long_line = "y".repeat(10_000);
let (cr, pango_layout) = surface_and_layout();
let vline = VisibleMinimapLine {
start_line_idx: 0,
bounds: QRect::new(0.0, 0.0, 400.0, 20.0),
};
paint_row_glyphs(
&cr,
&pango_layout,
None,
&vline,
20.0,
&long_line,
&[],
&Theme::default(),
);
assert_eq!(
pango_layout.text().chars().count(),
COLUMN_CAPACITY,
"must shape no more than COLUMN_CAPACITY characters even for a 10,000-char line"
);
}
fn paint_cached_pixels(mm: &Minimap, w: i32, h: i32) -> Vec<u8> {
paint_cached_pixels_at_scale(mm, w, h, MinimapScale::One, &Theme::default())
}
fn paint_cached_pixels_at_scale(
mm: &Minimap,
w: i32,
h: i32,
mm_scale: MinimapScale,
theme: &Theme,
) -> Vec<u8> {
let mut surface = ImageSurface::create(Format::ARgb32, w, h).expect("create surface");
{
let cr = CairoContext::new(&surface).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
let mut cache = MinimapAtlasCache::new();
draw_minimap_cached(
&cr,
&pango_layout,
0.0,
0.0,
w as f64,
h as f64,
mm,
theme,
&mut cache,
1.0,
mm_scale,
);
}
surface.flush();
let pixels = surface.data().expect("surface data").to_vec();
pixels
}
#[test]
fn atlas_mode_differentiates_content_column_blocks_would_make_identical() {
let code_line = "fn main() {";
let comment_line = "///////////";
assert_eq!(
code_line.chars().count(),
comment_line.chars().count(),
"both lines must be the same length for this to be a fair comparison"
);
let code_pixels = paint_cached_pixels(&minimap_from(vec![code_line], 1), 40, 4);
let comment_pixels = paint_cached_pixels(&minimap_from(vec![comment_line], 1), 40, 4);
assert_ne!(
code_pixels, comment_pixels,
"Characters-mode (atlas) rows for different same-length, all-non-blank \
content must paint different pixels"
);
}
#[test]
fn render_char_sample_sheet_does_not_pollute_the_text_run_sink() {
let previous = crate::testing::install_text_run_sink();
let _ = render_char_sample_sheet("sans-serif");
let recorded = crate::testing::take_text_run_sink(previous);
assert!(
recorded.is_empty(),
"render_char_sample_sheet must never record into the text-run sink, got {recorded:?}"
);
}
#[test]
fn atlas_mode_paints_something_for_a_non_blank_line() {
let bg = ImageSurface::create(Format::ARgb32, 40, 4)
.and_then(|mut s| {
let cr = CairoContext::new(&s)?;
cr.set_source_rgb(0.0, 0.0, 0.0);
cr.paint()?;
drop(cr); s.flush();
Ok(s.data().expect("surface data").to_vec())
})
.expect("blank surface");
let painted = paint_cached_pixels(&minimap_from(vec!["fn main() {"], 1), 40, 4);
assert_ne!(
painted, bg,
"a non-blank line must paint something other than a uniform background"
);
}
#[test]
fn atlas_mode_falls_back_to_column_blocks_visuals_when_atlas_is_degenerate() {
let mm = minimap_from(vec![" x y"], 1);
let theme = Theme {
background: Color::rgb(10, 10, 10),
foreground: Color::rgb(200, 200, 200),
..Theme::default()
};
let mut surface = ImageSurface::create(Format::ARgb32, 20, 4).expect("create surface");
let layout = {
let cr = CairoContext::new(&surface).expect("Context::new");
let vline = layout_first_vline(&mm, 20.0, 4.0);
let row_spans: &[MinimapSpan] = &[];
paint_row_blocks(&cr, &vline, &mm.lines[0].text, row_spans, &theme, 1.0);
vline
};
surface.flush();
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
let fg = (theme.foreground.r, theme.foreground.g, theme.foreground.b);
assert_eq!(pixel(&data, stride, layout.bounds.x as i32 + 4, 0), fg);
}
#[test]
fn scale_two_atlas_mode_paints_a_shape_and_leaves_blank_columns_empty() {
let line = "fn main()";
let mm = minimap_from(vec![line], 1);
let cell_w = MinimapScale::Two.cell_w_px() as i32;
let row_h = MinimapScale::Two.row_pitch_px() as i32;
let w = line.chars().count() as i32 * cell_w;
let theme = Theme {
background: Color::rgb(255, 255, 255),
foreground: Color::rgb(0, 0, 0),
..Theme::default()
};
let pixels = paint_cached_pixels_at_scale(&mm, w, row_h, MinimapScale::Two, &theme);
let stride = ImageSurface::create(Format::ARgb32, w, row_h)
.expect("create surface")
.stride() as usize;
let bg = (theme.background.r, theme.background.g, theme.background.b);
let space_col = 2i32;
for dx in 0..cell_w {
for y in 0..row_h {
let x = space_col * cell_w + dx;
assert_eq!(
pixel(&pixels, stride, x, y),
bg,
"a blank column must stay fully background at ({x}, {y})"
);
}
}
let mut cell_pixels = Vec::new();
for dx in 0..cell_w {
for y in 0..row_h {
cell_pixels.push(pixel(&pixels, stride, dx, y));
}
}
assert!(
cell_pixels.windows(2).any(|w| w[0] != w[1]),
"expected non-uniform alpha (a glyph shape) across the 'f' cell, got {cell_pixels:?}"
);
}
#[test]
fn atlas_cache_key_includes_minimap_scale_so_a_scale_change_rebuilds() {
let mm = minimap_from(vec!["fn main() {}"], 1);
let mut cache = MinimapAtlasCache::new();
let theme = Theme::default();
let bg = (theme.background.r, theme.background.g, theme.background.b);
let w = 40;
let two_h = MinimapScale::Two.row_pitch_px() as i32;
let surface_one =
ImageSurface::create(Format::ARgb32, w, MinimapScale::One.row_pitch_px() as i32)
.expect("create surface");
{
let cr = CairoContext::new(&surface_one).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
draw_minimap_cached(
&cr,
&pango_layout,
0.0,
0.0,
w as f64,
MinimapScale::One.row_pitch_px(),
&mm,
&theme,
&mut cache,
1.0,
MinimapScale::One,
);
}
let mut surface_two =
ImageSurface::create(Format::ARgb32, w, two_h).expect("create surface");
{
let cr = CairoContext::new(&surface_two).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
draw_minimap_cached(
&cr,
&pango_layout,
0.0,
0.0,
w as f64,
two_h as f64,
&mm,
&theme,
&mut cache,
1.0,
MinimapScale::Two,
);
}
surface_two.flush();
let stride = surface_two.stride() as usize;
let data = surface_two.data().expect("surface data");
let mut painted_below_row_2 = false;
for y in 2..two_h {
for x in 0..w {
if pixel(&data, stride, x, y) != bg {
painted_below_row_2 = true;
}
}
}
assert!(
painted_below_row_2,
"MinimapScale::Two's own 4px-tall cell must paint below row 2 -- a stale \
1x2 tile carried over from MinimapScale::One would not"
);
}
fn layout_first_vline(mm: &Minimap, w: f64, h: f64) -> VisibleMinimapLine {
gtk_minimap_layout(mm, 0.0, 0.0, w, h).visible_lines[0]
}
#[test]
fn column_blocks_skip_indentation_and_internal_gaps() {
let mm = minimap_from(vec![" x y"], 1);
let theme = Theme {
background: Color::rgb(10, 10, 10),
foreground: Color::rgb(200, 200, 200),
..Theme::default()
};
let mut surface = ImageSurface::create(Format::ARgb32, 20, 4).expect("create surface");
{
let cr = CairoContext::new(&surface).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
draw_minimap(&cr, &pango_layout, 0.0, 0.0, 20.0, 4.0, &mm, &theme);
}
surface.flush();
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
let bg = (theme.background.r, theme.background.g, theme.background.b);
let fg = (theme.foreground.r, theme.foreground.g, theme.foreground.b);
for col in 0..4 {
assert_eq!(
pixel(&data, stride, col, 0),
bg,
"indent column {col} must stay blank"
);
}
assert_eq!(
pixel(&data, stride, 4, 0),
fg,
"'x' column must paint a block"
);
for col in 5..7 {
assert_eq!(
pixel(&data, stride, col, 0),
bg,
"gap column {col} must stay blank"
);
}
assert_eq!(
pixel(&data, stride, 7, 0),
fg,
"'y' column must paint a block"
);
}
#[test]
fn column_blocks_stop_at_the_column_capacity() {
let long_line = "x".repeat(10_000);
let mm = minimap_from(vec![&long_line], 1);
let theme = Theme {
background: Color::rgb(1, 1, 1),
foreground: Color::rgb(250, 250, 250),
..Theme::default()
};
let strip_w = (COLUMN_CAPACITY + 50) as i32;
let mut surface = ImageSurface::create(Format::ARgb32, strip_w, 4).expect("create surface");
{
let cr = CairoContext::new(&surface).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
draw_minimap(
&cr,
&pango_layout,
0.0,
0.0,
strip_w as f64,
4.0,
&mm,
&theme,
);
}
surface.flush();
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
let fg = (theme.foreground.r, theme.foreground.g, theme.foreground.b);
let bg = (theme.background.r, theme.background.g, theme.background.b);
assert_eq!(
pixel(&data, stride, (COLUMN_CAPACITY - 1) as i32, 0),
fg,
"the last in-capacity column must still paint"
);
assert_eq!(
pixel(&data, stride, COLUMN_CAPACITY as i32, 0),
bg,
"the walk must stop at COLUMN_CAPACITY and paint no further"
);
}
fn paint_and_click_round_trip_at(x: f64, y: f64) {
let mm = minimap_from(vec!["x"; 8], 8);
let (cr, pango_layout) = surface_and_layout();
let layout = draw_minimap(
&cr,
&pango_layout,
x,
y,
40.0,
100.0,
&mm,
&Theme::default(),
);
assert_eq!(
layout.hit_test((x + 20.0) as f32, (y + 50.0) as f32),
MinimapHit::Seek { fraction: 0.5 }
);
assert_eq!(
layout.hit_test((x + 20.0) as f32, y as f32),
MinimapHit::Seek { fraction: 0.0 }
);
}
#[test]
fn paint_and_click_round_trip_returns_seek_for_the_clicked_fraction() {
paint_and_click_round_trip_at(0.0, 0.0);
}
#[test]
fn paint_and_click_round_trip_returns_seek_for_the_clicked_fraction_at_nonzero_origin() {
paint_and_click_round_trip_at(7.0, 13.0);
}
#[test]
fn char_range_to_byte_range_handles_multibyte_text() {
let (start, end) = char_range_to_byte_range("café", 3, 4);
assert_eq!(start, 3);
assert_eq!(end, 5);
}
#[test]
fn empty_minimap_is_a_no_op() {
let mm = minimap_from(vec![], 0);
let (cr, pango_layout) = surface_and_layout();
let layout = draw_minimap(
&cr,
&pango_layout,
0.0,
0.0,
40.0,
100.0,
&mm,
&Theme::default(),
);
assert!(layout.visible_lines.is_empty());
}
fn pixel(data: &[u8], stride: usize, x: i32, y: i32) -> (u8, u8, u8) {
let off = y as usize * stride + x as usize * 4;
(data[off + 2], data[off + 1], data[off])
}
#[test]
fn wide_line_blocks_never_paint_right_of_bounds() {
const STRIP_W: i32 = 40;
const CANVAS_W: i32 = 200;
const H: i32 = 40;
let long_line = "x".repeat(500);
let mm = minimap_from(vec![&long_line], 1);
let mut surface =
ImageSurface::create(Format::ARgb32, CANVAS_W, H).expect("create surface");
{
let cr = CairoContext::new(&surface).expect("Context::new");
cr.set_source_rgb(1.0, 1.0, 1.0); cr.paint().ok();
let pango_layout = pangocairo::functions::create_layout(&cr);
let theme = Theme {
background: Color::rgb(20, 20, 20),
foreground: Color::rgb(255, 255, 255),
..Theme::default()
};
draw_minimap(
&cr,
&pango_layout,
0.0,
0.0,
STRIP_W as f64,
H as f64,
&mm,
&theme,
);
}
surface.flush();
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
for y in 0..H {
for x in STRIP_W..CANVAS_W {
assert_eq!(
pixel(&data, stride, x, y),
(255, 255, 255),
"pixel ({x},{y}) is right of bounds.x + bounds.width and must be untouched"
);
}
}
}
}