use core_graphics::geometry::CGRect;
use core_graphics::sys::CGContextRef;
use core_text::font::CTFont;
use core_text::font_descriptor::kCTFontBoldTrait;
use super::headless::BitmapSurface;
use super::text::{draw_text, make_font};
use crate::event::Rect;
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_CHAR_COUNT, ATLAS_FIRST_CHAR, ATLAS_LAST_CHAR, COLUMN_CAPACITY,
};
use crate::theme::Theme;
use crate::types::Color;
pub fn mac_minimap_layout(minimap: &Minimap, rect: Rect) -> MinimapLayout {
mac_minimap_layout_scaled(minimap, rect, MinimapScale::One)
}
pub(crate) fn mac_minimap_layout_scaled(
minimap: &Minimap,
rect: Rect,
scale: MinimapScale,
) -> MinimapLayout {
pixel_minimap_layout_scaled(minimap, rect, scale)
}
pub unsafe fn draw_minimap(
ctx: CGContextRef,
font: &CTFont,
rect: Rect,
minimap: &Minimap,
theme: &Theme,
) -> MinimapLayout {
draw_minimap_scaled(ctx, font, rect, minimap, theme, MinimapScale::One)
}
pub(crate) unsafe fn draw_minimap_scaled(
ctx: CGContextRef,
font: &CTFont,
rect: Rect,
minimap: &Minimap,
theme: &Theme,
scale: MinimapScale,
) -> MinimapLayout {
paint_minimap_rows(
ctx,
rect,
minimap,
theme,
scale,
|ctx, vline, row_px, cell_w, text, row_spans| match render_mode(row_px) {
MinimapRenderMode::Characters => {
paint_row_glyphs(ctx, font, vline, text, row_spans, theme)
}
MinimapRenderMode::ColumnBlocks => {
paint_row_blocks(ctx, vline, text, row_spans, theme, cell_w)
}
},
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) unsafe fn draw_minimap_cached(
ctx: CGContextRef,
font: &CTFont,
rect: Rect,
minimap: &Minimap,
theme: &Theme,
scale: MinimapScale,
atlas_cache: &mut MinimapAtlasCache,
dpi_scale: f64,
) -> MinimapLayout {
let family = font.family_name();
let dpi_scale = dpi_scale.max(1.0);
let tile_w = (scale.cell_w_px() * dpi_scale).round().max(1.0) as usize;
let tile_h = (scale.row_pitch_px() * dpi_scale).round().max(1.0) as usize;
let family_key = format!("{family}#{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(
ctx,
rect,
minimap,
theme,
scale,
|ctx, vline, _row_px, cell_w, text, row_spans| {
if atlas.tile_w() == 0 || atlas.tile_h() == 0 {
paint_row_blocks(ctx, vline, text, row_spans, theme, cell_w);
} else {
paint_row_atlas(ctx, atlas, dpi_scale, vline, text, row_spans, theme, cell_w);
}
},
)
}
unsafe fn paint_minimap_rows(
ctx: CGContextRef,
rect: Rect,
minimap: &Minimap,
theme: &Theme,
scale: MinimapScale,
mut paint_row: impl FnMut(CGContextRef, &VisibleMinimapLine, f64, f64, &str, &[MinimapSpan]),
) -> MinimapLayout {
let layout = mac_minimap_layout_scaled(minimap, rect, scale);
if rect.width <= 0.0 || rect.height <= 0.0 || layout.visible_lines.is_empty() {
return layout;
}
fill_rect(
ctx,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
theme.background,
);
let hl = &layout.viewport_highlight;
if hl.height > 0.0 {
fill_rect_alpha(
ctx,
hl.x as f64,
hl.y as f64,
hl.width as f64,
hl.height as f64,
theme.accent_bg,
0.25,
);
}
CGContextSaveGState(ctx);
CGContextClipToRect(
ctx,
CGRect::new_xywh(
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
),
);
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(
ctx,
vline,
vline.bounds.height as f64,
scale.cell_w_px(),
&line.text,
row_spans,
);
}
CGContextRestoreGState(ctx);
layout
}
fn paint_row_glyphs(
ctx: CGContextRef,
font: &CTFont,
vline: &VisibleMinimapLine,
text: &str,
row_spans: &[MinimapSpan],
theme: &Theme,
) {
let _ = minimap_font_px(vline.bounds.height as f64);
let truncated = truncate_to_columns(text, COLUMN_CAPACITY);
let fg = row_spans
.first()
.map(|s| s.color)
.unwrap_or(theme.foreground);
unsafe {
draw_text(
ctx,
font,
truncated,
vline.bounds.x as f64,
vline.bounds.y as f64,
color_to_cg(fg),
);
}
}
fn paint_row_blocks(
ctx: CGContextRef,
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);
unsafe {
fill_rect(
ctx,
vline.bounds.x as f64 + col as f64 * cell_w,
vline.bounds.y as f64,
cell_w,
vline.bounds.height as f64,
color,
);
}
}
}
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) * 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 cell_w = CHAR_SAMPLE_CELL_W as u32;
let sheet_h = CHAR_SAMPLE_CELL_H as u32;
let sheet_w = cell_w * ATLAS_CHAR_COUNT as u32;
let base_font = make_font(family, CHAR_SAMPLE_CELL_H * 0.85)?;
let font = base_font
.clone_with_symbolic_traits(kCTFontBoldTrait, kCTFontBoldTrait)
.unwrap_or(base_font);
let surface = BitmapSurface::new(sheet_w, sheet_h);
let prev_sink = crate::testing::install_text_run_sink();
for (i, code) in (ATLAS_FIRST_CHAR..=ATLAS_LAST_CHAR).enumerate() {
let ch = code as u8 as char;
unsafe {
draw_text(
surface.context_ptr(),
&font,
&ch.to_string(),
i as f64 * CHAR_SAMPLE_CELL_W,
0.0,
(1.0, 1.0, 1.0, 1.0),
);
}
}
let _ = crate::testing::take_text_run_sink(prev_sink);
let mut alpha = vec![0u8; (sheet_w * sheet_h) as usize];
for y in 0..sheet_h {
for x in 0..sheet_w {
let (r, _g, _b, _a) = surface.pixel(x, y);
alpha[(y * sheet_w + x) as usize] = r;
}
}
Some(alpha)
}
#[allow(clippy::too_many_arguments)]
fn paint_row_atlas(
ctx: CGContextRef,
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);
unsafe {
blit_alpha_tile(
ctx,
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)]
unsafe fn blit_alpha_tile(
ctx: CGContextRef,
tile: &[u8],
tile_w: usize,
tile_h: usize,
x: f64,
y: f64,
dpi_scale: f64,
color: Color,
) {
if tile_w == 0 || tile_h == 0 {
return;
}
let (r, g, b, _) = color_to_cg(color);
let px = 1.0 / dpi_scale.max(f64::MIN_POSITIVE);
for row in 0..tile_h {
for col in 0..tile_w {
let coverage = tile[row * tile_w + col];
if coverage == 0 {
continue;
}
CGContextSetRGBFillColor(ctx, r, g, b, coverage as f64 / 255.0);
CGContextFillRect(
ctx,
CGRect::new_xywh(x + col as f64 * px, y + row as f64 * px, px, px),
);
}
}
}
fn color_to_cg(c: Color) -> (f64, f64, f64, f64) {
(
c.r as f64 / 255.0,
c.g as f64 / 255.0,
c.b as f64 / 255.0,
c.a as f64 / 255.0,
)
}
unsafe fn fill_rect(ctx: CGContextRef, x: f64, y: f64, w: f64, h: f64, c: Color) {
if w <= 0.0 || h <= 0.0 {
return;
}
let (r, g, b, a) = color_to_cg(c);
CGContextSetRGBFillColor(ctx, r, g, b, a);
CGContextFillRect(ctx, CGRect::new_xywh(x, y, w, h));
}
unsafe fn fill_rect_alpha(ctx: CGContextRef, x: f64, y: f64, w: f64, h: f64, c: Color, alpha: f64) {
if w <= 0.0 || h <= 0.0 {
return;
}
let (r, g, b, _) = color_to_cg(c);
CGContextSetRGBFillColor(ctx, r, g, b, alpha);
CGContextFillRect(ctx, CGRect::new_xywh(x, y, w, h));
}
trait CGRectExt {
fn new_xywh(x: f64, y: f64, w: f64, h: f64) -> Self;
}
impl CGRectExt for CGRect {
fn new_xywh(x: f64, y: f64, w: f64, h: f64) -> Self {
use core_graphics::geometry::{CGPoint, CGSize};
CGRect::new(&CGPoint::new(x, y), &CGSize::new(w, h))
}
}
extern "C" {
fn CGContextSaveGState(c: CGContextRef);
fn CGContextRestoreGState(c: CGContextRef);
fn CGContextClipToRect(c: CGContextRef, rect: CGRect);
fn CGContextSetRGBFillColor(
c: CGContextRef,
red: core_graphics::base::CGFloat,
green: core_graphics::base::CGFloat,
blue: core_graphics::base::CGFloat,
alpha: core_graphics::base::CGFloat,
);
fn CGContextFillRect(c: CGContextRef, rect: CGRect);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::minimap::{MinimapHit, MinimapLine, ROW_PITCH_PX};
use crate::types::WidgetId;
const W: f32 = 200.0;
const H: f32 = 200.0;
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 test_font() -> CTFont {
make_font("Menlo", 10.0).expect("Menlo should be installed on every macOS host")
}
#[test]
fn scale_two_reaches_the_characters_branch_with_a_taller_row_pitch() {
let surface = BitmapSurface::new(W as u32, H as u32);
surface.fill(1.0, 1.0, 1.0, 1.0);
let font = test_font();
let theme = Theme {
background: Color::rgb(255, 255, 255),
foreground: Color::rgb(0, 0, 0),
..Theme::default()
};
let mm = minimap_from(vec!["fn main() {}"; 4], 4);
let rect = Rect::new(0.0, 0.0, W, H);
let layout = unsafe {
draw_minimap_scaled(
surface.context_ptr(),
&font,
rect,
&mm,
&theme,
MinimapScale::Two,
)
};
assert_eq!(
layout.visible_lines[0].bounds.height,
MinimapScale::Two.row_pitch_px() as f32
);
let mut painted_any = false;
for x in 0..W as u32 {
for y in 0..(MinimapScale::Two.row_pitch_px() as u32 * 4).max(8) {
let (r, g, b, _) = surface.pixel(x, y);
if (r, g, b) != (255, 255, 255) {
painted_any = true;
}
}
}
assert!(
painted_any,
"expected draw_minimap_scaled at MinimapScale::Two to paint visible text"
);
}
#[test]
fn draw_minimap_paints_column_blocks_and_returns_layout() {
let surface = BitmapSurface::new(W as u32, H as u32);
surface.fill(1.0, 1.0, 1.0, 1.0);
let font = test_font();
let theme = Theme {
background: Color::rgb(255, 255, 255),
foreground: Color::rgb(0, 0, 0),
..Theme::default()
};
let mm = minimap_from(vec!["fn main() {}"; 8], 8);
let rect = Rect::new(0.0, 0.0, W, H);
let layout = unsafe { draw_minimap(surface.context_ptr(), &font, rect, &mm, &theme) };
assert!(!layout.visible_lines.is_empty());
let mut painted_any = false;
for x in 0..W as u32 {
for y in 0..(ROW_PITCH_PX as u32 * 8).max(8) {
let (r, g, b, _) = surface.pixel(x, y);
if (r, g, b) != (255, 255, 255) {
painted_any = true;
}
}
}
assert!(painted_any, "expected draw_minimap to paint visible blocks");
}
#[test]
fn paint_and_click_round_trip_at_nonzero_origin() {
let origin_x = 7.0_f32;
let origin_y = 13.0_f32;
let surface = BitmapSurface::new(W as u32, H as u32);
let font = test_font();
let theme = Theme::default();
let mm = minimap_from(vec!["x"; 8], 8);
let rect = Rect::new(origin_x, origin_y, 40.0, 100.0);
let layout = unsafe { draw_minimap(surface.context_ptr(), &font, rect, &mm, &theme) };
assert_eq!(
layout.hit_test(origin_x + 20.0, origin_y + 50.0),
MinimapHit::Seek { fraction: 0.5 }
);
assert_eq!(
layout.hit_test(origin_x + 20.0, origin_y),
MinimapHit::Seek { fraction: 0.0 }
);
}
#[test]
fn no_paint_layout_matches_paint_layout() {
let mm = minimap_from(vec!["fn main() {}"; 8], 8);
let rect = Rect::new(0.0, 0.0, W, H);
let font = test_font();
let surface = BitmapSurface::new(W as u32, H as u32);
let painted =
unsafe { draw_minimap(surface.context_ptr(), &font, rect, &mm, &Theme::default()) };
let no_paint = mac_minimap_layout(&mm, rect);
assert_eq!(painted, no_paint);
}
#[test]
fn zero_size_rect_is_a_no_op() {
let surface = BitmapSurface::new(W as u32, H as u32);
let font = test_font();
let theme = Theme {
background: Color::rgb(255, 255, 255),
..Theme::default()
};
let mm = minimap_from(vec!["fn main() {}"; 8], 8);
let rect = Rect::new(0.0, 0.0, 0.0, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
unsafe {
draw_minimap(surface.context_ptr(), &font, rect, &mm, &theme);
}
let (r, g, b, _) = surface.pixel(1, 1);
assert_eq!(
(r, g, b),
(255, 255, 255),
"a zero-width minimap should paint nothing at all",
);
}
#[test]
fn row_pitch_is_independent_of_strip_height_and_file_length() {
let short = 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 = minimap_from(long_refs, 10_000);
let tall = mac_minimap_layout(&short, Rect::new(0.0, 0.0, 40.0, 800.0));
let short_strip = mac_minimap_layout(&short, Rect::new(0.0, 0.0, 40.0, 16.0));
let long_layout = mac_minimap_layout(&long, Rect::new(0.0, 0.0, 40.0, 400.0));
assert_eq!(tall.visible_lines[0].bounds.height as f64, ROW_PITCH_PX);
assert_eq!(
short_strip.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 characters_branch_paints_a_truncated_line_directly() {
let surface = BitmapSurface::new(W as u32, H as u32);
surface.fill(1.0, 1.0, 1.0, 1.0);
let font = test_font();
let theme = Theme {
background: Color::rgb(255, 255, 255),
foreground: Color::rgb(0, 0, 0),
..Theme::default()
};
let long_line = "y".repeat(10_000);
let vline = VisibleMinimapLine {
start_line_idx: 0,
bounds: Rect::new(0.0, 0.0, W, 20.0),
};
paint_row_glyphs(
surface.context_ptr(),
&font,
&vline,
&long_line,
&[],
&theme,
);
let mut painted_any = false;
for x in 0..W as u32 {
for y in 0..20u32 {
let (r, g, b, _) = surface.pixel(x, y);
if (r, g, b) != (255, 255, 255) {
painted_any = true;
}
}
}
assert!(painted_any, "expected the Characters branch to paint text");
}
#[test]
fn atlas_mode_keeps_each_row_within_its_own_band() {
let code_line = "fn main()";
let mm = minimap_from(vec![code_line, ""], 2);
let scale = MinimapScale::Two;
let cell_w = scale.cell_w_px() as u32;
let row_h = scale.row_pitch_px() as u32;
let w = (code_line.chars().count() as u32) * cell_w;
let h = row_h * 2;
let theme = Theme {
background: Color::rgb(255, 255, 255),
foreground: Color::rgb(0, 0, 0),
..Theme::default()
};
let surface = BitmapSurface::new(w, h);
surface.fill(1.0, 1.0, 1.0, 1.0);
let font = test_font();
let mut cache = MinimapAtlasCache::new();
let rect = Rect::new(0.0, 0.0, w as f32, h as f32);
let layout = unsafe {
draw_minimap_cached(
surface.context_ptr(),
&font,
rect,
&mm,
&theme,
scale,
&mut cache,
1.0,
)
};
assert_eq!(
layout.visible_lines.len(),
2,
"expected exactly 2 visible rows"
);
for y in row_h..h {
for x in 0..w {
let (r, g, b, _) = surface.pixel(x, y);
assert_eq!(
(r, g, b),
(255, 255, 255),
"row 1 (blank) must stay fully background at ({x}, {y}) -- \
any ink here means row 0 bled past its own row band"
);
}
}
let mut painted_row0 = false;
for y in 0..row_h {
for x in 0..w {
let (r, g, b, _) = surface.pixel(x, y);
if (r, g, b) != (255, 255, 255) {
painted_row0 = true;
}
}
}
assert!(painted_row0, "expected row 0 to paint something");
}
#[test]
fn atlas_mode_paints_a_shape_not_a_solid_block() {
let line = "fn main()";
let mm = minimap_from(vec![line], 1);
let scale = MinimapScale::Two;
let cell_w = scale.cell_w_px() as u32;
let row_h = scale.row_pitch_px() as u32;
let w = line.chars().count() as u32 * cell_w;
let theme = Theme {
background: Color::rgb(255, 255, 255),
foreground: Color::rgb(0, 0, 0),
..Theme::default()
};
let surface = BitmapSurface::new(w, row_h);
surface.fill(1.0, 1.0, 1.0, 1.0);
let font = test_font();
let mut cache = MinimapAtlasCache::new();
let rect = Rect::new(0.0, 0.0, w as f32, row_h as f32);
unsafe {
draw_minimap_cached(
surface.context_ptr(),
&font,
rect,
&mm,
&theme,
scale,
&mut cache,
1.0,
);
}
let mut cell_pixels = Vec::new();
for dx in 0..cell_w {
for y in 0..row_h {
cell_pixels.push(surface.pixel(dx, y));
}
}
let distinct: std::collections::HashSet<_> = cell_pixels.iter().collect();
assert!(
distinct.len() > 1,
"expected the 'f' cell to show a shape (multiple distinct pixel values), got {cell_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("Menlo");
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:?}"
);
}
}