#![forbid(unsafe_code)]
use crate::canvas::{Mode, Painter};
use crate::mermaid_layout::{DiagramLayout, LayoutPoint, LayoutRect};
use ftui_core::geometry::Rect;
use ftui_render::buffer::Buffer;
use ftui_render::cell::{Cell, PackedRgba};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MinimapCorner {
#[default]
BottomRight,
BottomLeft,
TopRight,
TopLeft,
}
#[derive(Debug, Clone, Copy)]
pub struct MinimapConfig {
pub max_width: u16,
pub max_height: u16,
pub corner: MinimapCorner,
pub margin: u16,
pub node_color: PackedRgba,
pub edge_color: PackedRgba,
pub viewport_color: PackedRgba,
pub highlight_color: PackedRgba,
pub bg_color: PackedRgba,
pub border_color: PackedRgba,
}
impl Default for MinimapConfig {
fn default() -> Self {
Self {
max_width: 30,
max_height: 15,
corner: MinimapCorner::BottomRight,
margin: 1,
node_color: PackedRgba::rgb(80, 180, 255),
edge_color: PackedRgba::rgb(100, 100, 100),
viewport_color: PackedRgba::rgb(255, 220, 60),
highlight_color: PackedRgba::rgb(255, 100, 80),
bg_color: PackedRgba::rgb(20, 20, 30),
border_color: PackedRgba::rgb(60, 60, 80),
}
}
}
#[derive(Debug)]
pub struct Minimap {
painter: Painter,
bounding_box: LayoutRect,
content_cells: (u16, u16),
config: MinimapConfig,
}
impl Minimap {
#[must_use]
pub fn new(layout: &DiagramLayout, config: MinimapConfig) -> Self {
let bb = &layout.bounding_box;
let (content_w, content_h) = fit_aspect_ratio(
bb.width,
bb.height,
config.max_width.saturating_sub(2), config.max_height.saturating_sub(2),
);
let px_w = content_w * Mode::Braille.cols_per_cell();
let px_h = content_h * Mode::Braille.rows_per_cell();
let mut painter = Painter::new(px_w, px_h, Mode::Braille);
for edge in &layout.edges {
if edge.waypoints.len() >= 2 {
for pair in edge.waypoints.windows(2) {
let (x0, y0) = layout_to_px(pair[0], bb, px_w, px_h);
let (x1, y1) = layout_to_px(pair[1], bb, px_w, px_h);
painter.line_colored(x0, y0, x1, y1, Some(config.edge_color));
}
}
}
for node in &layout.nodes {
let (nx, ny) = layout_to_px(
LayoutPoint {
x: node.rect.x,
y: node.rect.y,
},
bb,
px_w,
px_h,
);
let (nx2, ny2) = layout_to_px(
LayoutPoint {
x: node.rect.x + node.rect.width,
y: node.rect.y + node.rect.height,
},
bb,
px_w,
px_h,
);
let nw = (nx2 - nx).max(1);
let nh = (ny2 - ny).max(1);
if nw <= 2 && nh <= 2 {
painter.point_colored(nx, ny, config.node_color);
} else {
painter.rect_filled(nx, ny, nw, nh);
for x in nx..nx + nw {
painter.point_colored(x, ny, config.node_color);
painter.point_colored(x, ny + nh - 1, config.node_color);
}
for y in ny..ny + nh {
painter.point_colored(nx, y, config.node_color);
painter.point_colored(nx + nw - 1, y, config.node_color);
}
}
}
Self {
painter,
bounding_box: *bb,
content_cells: (content_w, content_h),
config,
}
}
#[must_use]
pub fn placement(&self, area: Rect) -> Rect {
let (cw, ch) = self.content_cells;
let total_w = cw + 2;
let total_h = ch + 2;
if total_w > area.width || total_h > area.height {
return Rect::new(0, 0, 0, 0);
}
let m = self.config.margin;
let x = match self.config.corner {
MinimapCorner::TopLeft | MinimapCorner::BottomLeft => area.x + m,
MinimapCorner::TopRight | MinimapCorner::BottomRight => {
area.x + area.width - total_w - m
}
};
let y = match self.config.corner {
MinimapCorner::TopLeft | MinimapCorner::TopRight => area.y + m,
MinimapCorner::BottomLeft | MinimapCorner::BottomRight => {
area.y + area.height - total_h - m
}
};
Rect::new(x, y, total_w, total_h)
}
pub fn render(
&self,
area: Rect,
buf: &mut Buffer,
viewport: Option<&LayoutRect>,
selected_node: Option<usize>,
) {
let minimap_rect = self.placement(area);
if minimap_rect.is_empty() {
return;
}
let bg_cell = Cell::from_char(' ').with_bg(self.config.bg_color);
for y in minimap_rect.y..minimap_rect.y + minimap_rect.height {
for x in minimap_rect.x..minimap_rect.x + minimap_rect.width {
buf.set_fast(x, y, bg_cell);
}
}
self.draw_border(minimap_rect, buf);
let content_area = Rect::new(
minimap_rect.x + 1,
minimap_rect.y + 1,
self.content_cells.0,
self.content_cells.1,
);
let style = ftui_style::Style::new().fg(self.config.node_color);
self.painter.render_to_buffer(content_area, buf, style);
if let Some(vp) = viewport {
self.draw_viewport_indicator(content_area, buf, vp);
}
if let Some(_node_idx) = selected_node {
}
}
fn draw_border(&self, rect: Rect, buf: &mut Buffer) {
let x1 = rect.x;
let y1 = rect.y;
let x2 = rect.x + rect.width.saturating_sub(1);
let y2 = rect.y + rect.height.saturating_sub(1);
let bc = self.config.border_color;
buf.set_fast(x1, y1, Cell::from_char('\u{250C}').with_fg(bc));
buf.set_fast(x2, y1, Cell::from_char('\u{2510}').with_fg(bc));
buf.set_fast(x1, y2, Cell::from_char('\u{2514}').with_fg(bc));
buf.set_fast(x2, y2, Cell::from_char('\u{2518}').with_fg(bc));
for x in (x1 + 1)..x2 {
buf.set_fast(x, y1, Cell::from_char('\u{2500}').with_fg(bc));
buf.set_fast(x, y2, Cell::from_char('\u{2500}').with_fg(bc));
}
for y in (y1 + 1)..y2 {
buf.set_fast(x1, y, Cell::from_char('\u{2502}').with_fg(bc));
buf.set_fast(x2, y, Cell::from_char('\u{2502}').with_fg(bc));
}
}
fn draw_viewport_indicator(&self, content_area: Rect, buf: &mut Buffer, viewport: &LayoutRect) {
let bb = &self.bounding_box;
let (cw, ch) = self.content_cells;
if bb.width <= 0.0 || bb.height <= 0.0 || cw == 0 || ch == 0 {
return;
}
let scale_x = f64::from(cw) / bb.width;
let scale_y = f64::from(ch) / bb.height;
let vx1 = ((viewport.x - bb.x) * scale_x).round() as i32;
let vy1 = ((viewport.y - bb.y) * scale_y).round() as i32;
let vx2 = ((viewport.x + viewport.width - bb.x) * scale_x).round() as i32;
let vy2 = ((viewport.y + viewport.height - bb.y) * scale_y).round() as i32;
let cx = content_area.x as i32;
let cy = content_area.y as i32;
let cw_i = cw as i32;
let ch_i = ch as i32;
let left = vx1.max(0).min(cw_i - 1) + cx;
let top = vy1.max(0).min(ch_i - 1) + cy;
let right = vx2.max(0).min(cw_i) + cx;
let bottom = vy2.max(0).min(ch_i) + cy;
let vc = self.config.viewport_color;
for x in left..right {
recolor_cell(buf, x as u16, top as u16, vc);
recolor_cell(buf, x as u16, (bottom - 1).max(top) as u16, vc);
}
for y in top..bottom {
recolor_cell(buf, left as u16, y as u16, vc);
recolor_cell(buf, (right - 1).max(left) as u16, y as u16, vc);
}
}
#[must_use]
pub fn total_size(&self) -> (u16, u16) {
(self.content_cells.0 + 2, self.content_cells.1 + 2)
}
#[must_use]
pub fn is_trivial(&self) -> bool {
self.content_cells.0 < 3 || self.content_cells.1 < 2
}
}
fn fit_aspect_ratio(diagram_w: f64, diagram_h: f64, max_w: u16, max_h: u16) -> (u16, u16) {
if diagram_w <= 0.0 || diagram_h <= 0.0 || max_w == 0 || max_h == 0 {
return (max_w.max(1), max_h.max(1));
}
let aspect = diagram_w / diagram_h;
let cell_aspect = aspect * 0.5;
let w_from_h = (f64::from(max_h) * cell_aspect).round() as u16;
let h_from_w = (f64::from(max_w) / cell_aspect).round() as u16;
if w_from_h <= max_w {
(w_from_h.max(3), max_h)
} else {
(max_w, h_from_w.min(max_h).max(2))
}
}
fn layout_to_px(p: LayoutPoint, bb: &LayoutRect, px_w: u16, px_h: u16) -> (i32, i32) {
if bb.width <= 0.0 || bb.height <= 0.0 {
return (0, 0);
}
let x = ((p.x - bb.x) / bb.width * f64::from(px_w.saturating_sub(1))).round() as i32;
let y = ((p.y - bb.y) / bb.height * f64::from(px_h.saturating_sub(1))).round() as i32;
(x.max(0), y.max(0))
}
fn recolor_cell(buf: &mut Buffer, x: u16, y: u16, color: PackedRgba) {
if let Some(existing) = buf.get(x, y) {
let mut cell = *existing;
cell.fg = color;
buf.set_fast(x, y, cell);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mermaid_layout::{
DiagramLayout, LayoutEdgePath, LayoutNodeBox, LayoutPoint, LayoutRect, LayoutStats,
};
fn simple_layout() -> DiagramLayout {
DiagramLayout {
nodes: vec![
LayoutNodeBox {
node_idx: 0,
rect: LayoutRect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 5.0,
},
label_rect: None,
rank: 0,
order: 0,
},
LayoutNodeBox {
node_idx: 1,
rect: LayoutRect {
x: 30.0,
y: 20.0,
width: 10.0,
height: 5.0,
},
label_rect: None,
rank: 1,
order: 0,
},
],
clusters: vec![],
edges: vec![LayoutEdgePath {
edge_idx: 0,
waypoints: vec![
LayoutPoint { x: 5.0, y: 5.0 },
LayoutPoint { x: 35.0, y: 20.0 },
],
bundle_count: 1,
bundle_members: Vec::new(),
}],
bounding_box: LayoutRect {
x: 0.0,
y: 0.0,
width: 40.0,
height: 25.0,
},
stats: LayoutStats {
iterations_used: 1,
max_iterations: 10,
budget_exceeded: false,
crossings: 0,
ranks: 2,
max_rank_width: 1,
total_bends: 0,
position_variance: 0.0,
},
degradation: None,
}
}
#[test]
fn minimap_creation() {
let layout = simple_layout();
let config = MinimapConfig::default();
let minimap = Minimap::new(&layout, config);
assert!(!minimap.is_trivial());
let (w, h) = minimap.total_size();
assert!(w > 2);
assert!(h > 2);
assert!(w <= config.max_width + 2);
assert!(h <= config.max_height + 2);
}
#[test]
fn placement_bottom_right() {
let layout = simple_layout();
let config = MinimapConfig {
corner: MinimapCorner::BottomRight,
margin: 1,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let area = Rect::new(0, 0, 120, 40);
let rect = minimap.placement(area);
assert!(rect.x + rect.width <= area.x + area.width);
assert!(rect.y + rect.height <= area.y + area.height);
assert!(rect.x > area.x + area.width / 2); assert!(rect.y > area.y + area.height / 2); }
#[test]
fn placement_top_left() {
let layout = simple_layout();
let config = MinimapConfig {
corner: MinimapCorner::TopLeft,
margin: 1,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let area = Rect::new(0, 0, 120, 40);
let rect = minimap.placement(area);
assert_eq!(rect.x, area.x + 1); assert_eq!(rect.y, area.y + 1);
}
#[test]
fn placement_too_small_returns_zero() {
let layout = simple_layout();
let config = MinimapConfig::default();
let minimap = Minimap::new(&layout, config);
let area = Rect::new(0, 0, 3, 3);
let rect = minimap.placement(area);
assert!(rect.is_empty());
}
#[test]
fn render_does_not_panic() {
let layout = simple_layout();
let config = MinimapConfig::default();
let minimap = Minimap::new(&layout, config);
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
let viewport = LayoutRect {
x: 5.0,
y: 5.0,
width: 20.0,
height: 15.0,
};
minimap.render(area, &mut buf, Some(&viewport), None);
minimap.render(area, &mut buf, None, None);
minimap.render(area, &mut buf, Some(&viewport), Some(0));
}
#[test]
fn fit_aspect_ratio_wide() {
let (w, h) = fit_aspect_ratio(100.0, 20.0, 30, 15);
assert!(w <= 30);
assert!(h <= 15);
assert!(w >= 3);
assert!(h >= 2);
}
#[test]
fn fit_aspect_ratio_tall() {
let (w, h) = fit_aspect_ratio(20.0, 100.0, 30, 15);
assert!(w <= 30);
assert!(h <= 15);
}
#[test]
fn fit_aspect_ratio_zero() {
let (w, h) = fit_aspect_ratio(0.0, 0.0, 30, 15);
assert!(w >= 1);
assert!(h >= 1);
}
#[test]
fn layout_to_px_maps_corners() {
let bb = LayoutRect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 50.0,
};
let (x, y) = layout_to_px(LayoutPoint { x: 0.0, y: 0.0 }, &bb, 60, 40);
assert_eq!((x, y), (0, 0));
let (x, y) = layout_to_px(LayoutPoint { x: 100.0, y: 50.0 }, &bb, 60, 40);
assert_eq!((x, y), (59, 39));
}
#[test]
fn render_with_empty_layout() {
let layout = DiagramLayout {
nodes: vec![],
clusters: vec![],
edges: vec![],
bounding_box: LayoutRect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
},
stats: LayoutStats {
iterations_used: 0,
max_iterations: 0,
budget_exceeded: false,
crossings: 0,
ranks: 0,
max_rank_width: 0,
total_bends: 0,
position_variance: 0.0,
},
degradation: None,
};
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(80, 24);
minimap.render(Rect::new(0, 0, 80, 24), &mut buf, None, None);
}
#[test]
fn border_draws_box_chars() {
let layout = simple_layout();
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
minimap.render(area, &mut buf, None, None);
let rect = minimap.placement(area);
if !rect.is_empty() {
if let Some(cell) = buf.get(rect.x, rect.y) {
assert_eq!(cell.content.as_char(), Some('\u{250C}'));
}
if let Some(cell) = buf.get(rect.x + rect.width - 1, rect.y) {
assert_eq!(cell.content.as_char(), Some('\u{2510}'));
}
}
}
#[test]
fn minimap_corner_default_is_bottom_right() {
let corner = MinimapCorner::default();
assert_eq!(corner, MinimapCorner::BottomRight);
}
#[test]
fn config_default_fields() {
let config = MinimapConfig::default();
assert_eq!(config.max_width, 30);
assert_eq!(config.max_height, 15);
assert_eq!(config.margin, 1);
assert_eq!(config.corner, MinimapCorner::BottomRight);
}
#[test]
fn total_size_includes_border() {
let layout = simple_layout();
let minimap = Minimap::new(&layout, MinimapConfig::default());
let (w, h) = minimap.total_size();
let (cw, ch) = minimap.content_cells;
assert_eq!(w, cw + 2);
assert_eq!(h, ch + 2);
}
#[test]
fn placement_bottom_left() {
let layout = simple_layout();
let config = MinimapConfig {
corner: MinimapCorner::BottomLeft,
margin: 1,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let area = Rect::new(0, 0, 120, 40);
let rect = minimap.placement(area);
assert_eq!(rect.x, area.x + 1);
assert!(rect.y > area.y + area.height / 2);
}
#[test]
fn placement_top_right() {
let layout = simple_layout();
let config = MinimapConfig {
corner: MinimapCorner::TopRight,
margin: 1,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let area = Rect::new(0, 0, 120, 40);
let rect = minimap.placement(area);
assert!(rect.x > area.x + area.width / 2);
assert_eq!(rect.y, area.y + 1);
}
#[test]
fn layout_to_px_zero_bounding_box() {
let bb = LayoutRect {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
};
let (x, y) = layout_to_px(LayoutPoint { x: 5.0, y: 5.0 }, &bb, 60, 40);
assert_eq!((x, y), (0, 0));
}
#[test]
fn is_trivial_for_tiny_config() {
let layout = simple_layout();
let config = MinimapConfig {
max_width: 4,
max_height: 3,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
assert!(minimap.is_trivial());
}
#[test]
fn render_with_viewport_and_selected_node() {
let layout = simple_layout();
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
let viewport = LayoutRect {
x: 0.0,
y: 0.0,
width: 40.0,
height: 25.0,
};
minimap.render(area, &mut buf, Some(&viewport), Some(0));
minimap.render(area, &mut buf, Some(&viewport), Some(1));
}
#[test]
fn fit_aspect_ratio_negative_diagram() {
let (w, h) = fit_aspect_ratio(-10.0, -5.0, 30, 15);
assert!(w >= 1);
assert!(h >= 1);
}
#[test]
fn fit_aspect_ratio_zero_max_w_only() {
let (w, h) = fit_aspect_ratio(100.0, 50.0, 0, 15);
assert!(w >= 1);
assert!(h >= 1);
}
#[test]
fn fit_aspect_ratio_zero_max_h_only() {
let (w, h) = fit_aspect_ratio(100.0, 50.0, 30, 0);
assert!(w >= 1);
assert!(h >= 1);
}
#[test]
fn fit_aspect_ratio_square_diagram() {
let (w, h) = fit_aspect_ratio(50.0, 50.0, 30, 15);
assert!(w <= 30);
assert!(h <= 15);
assert!(w >= 3);
assert!(h >= 2);
}
#[test]
fn fit_aspect_ratio_extreme_wide() {
let (w, h) = fit_aspect_ratio(10_000.0, 1.0, 30, 15);
assert!(w <= 30);
assert!(h <= 15);
assert!(w >= 3);
assert!(h >= 2);
}
#[test]
fn fit_aspect_ratio_extreme_tall() {
let (w, h) = fit_aspect_ratio(1.0, 10_000.0, 30, 15);
assert!(w <= 30);
assert!(h <= 15);
}
#[test]
fn fit_aspect_ratio_one_by_one_max() {
let (w, h) = fit_aspect_ratio(50.0, 50.0, 1, 1);
assert!(w >= 1);
assert!(h >= 1);
}
#[test]
fn fit_aspect_ratio_diagram_zero_height() {
let (w, h) = fit_aspect_ratio(50.0, 0.0, 30, 15);
assert!(w >= 1);
assert!(h >= 1);
}
#[test]
fn fit_aspect_ratio_diagram_zero_width() {
let (w, h) = fit_aspect_ratio(0.0, 50.0, 30, 15);
assert!(w >= 1);
assert!(h >= 1);
}
#[test]
fn layout_to_px_negative_coordinates() {
let bb = LayoutRect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 50.0,
};
let (x, y) = layout_to_px(LayoutPoint { x: -10.0, y: -5.0 }, &bb, 60, 40);
assert_eq!(x, 0);
assert_eq!(y, 0);
}
#[test]
fn layout_to_px_center_point() {
let bb = LayoutRect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let (x, y) = layout_to_px(LayoutPoint { x: 50.0, y: 50.0 }, &bb, 101, 101);
assert_eq!(x, 50);
assert_eq!(y, 50);
}
#[test]
fn layout_to_px_offset_bounding_box() {
let bb = LayoutRect {
x: 100.0,
y: 200.0,
width: 50.0,
height: 50.0,
};
let (x, y) = layout_to_px(LayoutPoint { x: 100.0, y: 200.0 }, &bb, 60, 40);
assert_eq!((x, y), (0, 0));
let (x, y) = layout_to_px(LayoutPoint { x: 150.0, y: 250.0 }, &bb, 60, 40);
assert_eq!((x, y), (59, 39));
}
#[test]
fn layout_to_px_min_pixel_dimensions() {
let bb = LayoutRect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let (x, y) = layout_to_px(LayoutPoint { x: 50.0, y: 50.0 }, &bb, 1, 1);
assert_eq!((x, y), (0, 0));
}
#[test]
fn layout_to_px_zero_pixel_width() {
let bb = LayoutRect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let (x, y) = layout_to_px(LayoutPoint { x: 50.0, y: 50.0 }, &bb, 0, 40);
assert!(x >= 0);
assert!(y >= 0);
}
#[test]
fn recolor_cell_valid() {
let mut buf = Buffer::new(10, 10);
let orig_char = Cell::from_char('A');
buf.set_fast(3, 3, orig_char);
let new_color = PackedRgba::rgb(255, 0, 0);
recolor_cell(&mut buf, 3, 3, new_color);
let cell = buf.get(3, 3).unwrap();
assert_eq!(cell.fg, new_color);
assert_eq!(cell.content.as_char(), Some('A'));
}
#[test]
fn recolor_cell_out_of_bounds() {
let mut buf = Buffer::new(10, 10);
recolor_cell(&mut buf, 100, 100, PackedRgba::rgb(0, 0, 0));
}
#[test]
fn is_trivial_boundary_just_above() {
let layout = simple_layout();
let config = MinimapConfig {
max_width: 5,
max_height: 4,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let (cw, ch) = minimap.content_cells;
let expected_trivial = cw < 3 || ch < 2;
assert_eq!(minimap.is_trivial(), expected_trivial);
}
#[test]
fn is_trivial_large_config() {
let layout = simple_layout();
let config = MinimapConfig {
max_width: 60,
max_height: 30,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
assert!(!minimap.is_trivial());
}
#[test]
fn placement_with_offset_area() {
let layout = simple_layout();
let config = MinimapConfig {
corner: MinimapCorner::TopLeft,
margin: 2,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let area = Rect::new(10, 5, 120, 40);
let rect = minimap.placement(area);
assert_eq!(rect.x, area.x + 2); assert_eq!(rect.y, area.y + 2);
assert!(rect.x + rect.width <= area.x + area.width);
assert!(rect.y + rect.height <= area.y + area.height);
}
#[test]
fn placement_zero_margin() {
let layout = simple_layout();
let config = MinimapConfig {
corner: MinimapCorner::BottomRight,
margin: 0,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let area = Rect::new(0, 0, 120, 40);
let rect = minimap.placement(area);
assert_eq!(rect.x + rect.width, area.x + area.width);
assert_eq!(rect.y + rect.height, area.y + area.height);
}
#[test]
fn placement_area_exactly_fits() {
let layout = simple_layout();
let config = MinimapConfig {
margin: 0,
corner: MinimapCorner::TopLeft,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let (tw, th) = minimap.total_size();
let area = Rect::new(0, 0, tw, th);
let rect = minimap.placement(area);
assert_eq!(rect, Rect::new(0, 0, tw, th));
}
#[test]
fn placement_area_one_short_returns_empty() {
let layout = simple_layout();
let config = MinimapConfig {
margin: 0,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let (tw, th) = minimap.total_size();
let area = Rect::new(0, 0, tw - 1, th);
let rect = minimap.placement(area);
assert!(rect.is_empty());
}
#[test]
fn render_viewport_larger_than_diagram() {
let layout = simple_layout();
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
let viewport = LayoutRect {
x: -100.0,
y: -100.0,
width: 500.0,
height: 500.0,
};
minimap.render(area, &mut buf, Some(&viewport), None);
}
#[test]
fn render_viewport_outside_diagram() {
let layout = simple_layout();
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
let viewport = LayoutRect {
x: 1000.0,
y: 1000.0,
width: 10.0,
height: 10.0,
};
minimap.render(area, &mut buf, Some(&viewport), None);
}
#[test]
fn render_zero_size_viewport() {
let layout = simple_layout();
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
let viewport = LayoutRect {
x: 10.0,
y: 10.0,
width: 0.0,
height: 0.0,
};
minimap.render(area, &mut buf, Some(&viewport), None);
}
#[test]
fn render_edge_with_single_waypoint() {
let layout = DiagramLayout {
nodes: vec![],
clusters: vec![],
edges: vec![LayoutEdgePath {
edge_idx: 0,
waypoints: vec![LayoutPoint { x: 5.0, y: 5.0 }],
bundle_count: 1,
bundle_members: Vec::new(),
}],
bounding_box: LayoutRect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
},
stats: LayoutStats {
iterations_used: 0,
max_iterations: 0,
budget_exceeded: false,
crossings: 0,
ranks: 0,
max_rank_width: 0,
total_bends: 0,
position_variance: 0.0,
},
degradation: None,
};
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(80, 24);
minimap.render(Rect::new(0, 0, 80, 24), &mut buf, None, None);
}
#[test]
fn render_edge_with_many_waypoints() {
let layout = DiagramLayout {
nodes: vec![],
clusters: vec![],
edges: vec![LayoutEdgePath {
edge_idx: 0,
waypoints: vec![
LayoutPoint { x: 0.0, y: 0.0 },
LayoutPoint { x: 10.0, y: 5.0 },
LayoutPoint { x: 20.0, y: 0.0 },
LayoutPoint { x: 30.0, y: 10.0 },
LayoutPoint { x: 40.0, y: 5.0 },
],
bundle_count: 1,
bundle_members: Vec::new(),
}],
bounding_box: LayoutRect {
x: 0.0,
y: 0.0,
width: 40.0,
height: 10.0,
},
stats: LayoutStats {
iterations_used: 0,
max_iterations: 0,
budget_exceeded: false,
crossings: 0,
ranks: 0,
max_rank_width: 0,
total_bends: 0,
position_variance: 0.0,
},
degradation: None,
};
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(80, 24);
minimap.render(Rect::new(0, 0, 80, 24), &mut buf, None, None);
}
#[test]
fn render_tiny_node_draws_point() {
let layout = DiagramLayout {
nodes: vec![LayoutNodeBox {
node_idx: 0,
rect: LayoutRect {
x: 50.0,
y: 50.0,
width: 0.1,
height: 0.1,
},
label_rect: None,
rank: 0,
order: 0,
}],
clusters: vec![],
edges: vec![],
bounding_box: LayoutRect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
},
stats: LayoutStats {
iterations_used: 0,
max_iterations: 0,
budget_exceeded: false,
crossings: 0,
ranks: 0,
max_rank_width: 0,
total_bends: 0,
position_variance: 0.0,
},
degradation: None,
};
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(80, 24);
minimap.render(Rect::new(0, 0, 80, 24), &mut buf, None, None);
}
#[test]
fn render_area_too_small_is_noop() {
let layout = simple_layout();
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(5, 5);
minimap.render(Rect::new(0, 0, 5, 5), &mut buf, None, None);
}
#[test]
fn custom_colors_render_without_panic() {
let layout = simple_layout();
let config = MinimapConfig {
node_color: PackedRgba::rgb(0, 0, 0),
edge_color: PackedRgba::rgb(255, 255, 255),
viewport_color: PackedRgba::rgb(128, 128, 0),
highlight_color: PackedRgba::rgb(0, 255, 0),
bg_color: PackedRgba::rgb(255, 0, 255),
border_color: PackedRgba::rgb(0, 255, 255),
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
let viewport = LayoutRect {
x: 5.0,
y: 5.0,
width: 20.0,
height: 10.0,
};
minimap.render(area, &mut buf, Some(&viewport), Some(0));
}
#[test]
fn border_all_four_corners() {
let layout = simple_layout();
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
minimap.render(area, &mut buf, None, None);
let rect = minimap.placement(area);
if !rect.is_empty() {
let x1 = rect.x;
let y1 = rect.y;
let x2 = rect.x + rect.width - 1;
let y2 = rect.y + rect.height - 1;
assert_eq!(buf.get(x1, y1).unwrap().content.as_char(), Some('\u{250C}'));
assert_eq!(buf.get(x2, y1).unwrap().content.as_char(), Some('\u{2510}'));
assert_eq!(buf.get(x1, y2).unwrap().content.as_char(), Some('\u{2514}'));
assert_eq!(buf.get(x2, y2).unwrap().content.as_char(), Some('\u{2518}'));
}
}
#[test]
fn border_horizontal_edges() {
let layout = simple_layout();
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
minimap.render(area, &mut buf, None, None);
let rect = minimap.placement(area);
if !rect.is_empty() && rect.width > 2 {
let cell = buf.get(rect.x + 1, rect.y).unwrap();
assert_eq!(cell.content.as_char(), Some('\u{2500}'));
}
}
#[test]
fn border_vertical_edges() {
let layout = simple_layout();
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
minimap.render(area, &mut buf, None, None);
let rect = minimap.placement(area);
if !rect.is_empty() && rect.height > 2 {
let cell = buf.get(rect.x, rect.y + 1).unwrap();
assert_eq!(cell.content.as_char(), Some('\u{2502}'));
}
}
#[test]
fn render_fills_background_color() {
let layout = simple_layout();
let bg = PackedRgba::rgb(42, 42, 42);
let config = MinimapConfig {
bg_color: bg,
..MinimapConfig::default()
};
let minimap = Minimap::new(&layout, config);
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
minimap.render(area, &mut buf, None, None);
let rect = minimap.placement(area);
if !rect.is_empty() {
let cell = buf.get(rect.x + 1, rect.y + 1).unwrap();
assert_eq!(cell.bg, bg);
}
}
#[test]
fn render_zero_bounding_box_with_viewport() {
let layout = DiagramLayout {
nodes: vec![],
clusters: vec![],
edges: vec![],
bounding_box: LayoutRect {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
},
stats: LayoutStats {
iterations_used: 0,
max_iterations: 0,
budget_exceeded: false,
crossings: 0,
ranks: 0,
max_rank_width: 0,
total_bends: 0,
position_variance: 0.0,
},
degradation: None,
};
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(80, 24);
let viewport = LayoutRect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
};
minimap.render(Rect::new(0, 0, 80, 24), &mut buf, Some(&viewport), None);
}
#[test]
fn minimap_corner_all_variants_distinct() {
let corners = [
MinimapCorner::TopLeft,
MinimapCorner::TopRight,
MinimapCorner::BottomLeft,
MinimapCorner::BottomRight,
];
for (i, a) in corners.iter().enumerate() {
for (j, b) in corners.iter().enumerate() {
if i == j {
assert_eq!(a, b);
} else {
assert_ne!(a, b);
}
}
}
}
#[test]
fn minimap_corner_clone() {
let corner = MinimapCorner::TopLeft;
let cloned = corner;
assert_eq!(corner, cloned);
}
#[test]
fn config_clone_preserves_values() {
let config = MinimapConfig {
max_width: 50,
max_height: 25,
margin: 3,
corner: MinimapCorner::TopRight,
..MinimapConfig::default()
};
let cloned = config;
assert_eq!(cloned.max_width, 50);
assert_eq!(cloned.max_height, 25);
assert_eq!(cloned.margin, 3);
assert_eq!(cloned.corner, MinimapCorner::TopRight);
}
#[test]
fn render_dense_layout() {
let layout = DiagramLayout {
nodes: vec![
LayoutNodeBox {
node_idx: 0,
rect: LayoutRect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 10.0,
},
label_rect: None,
rank: 0,
order: 0,
},
LayoutNodeBox {
node_idx: 1,
rect: LayoutRect {
x: 40.0,
y: 0.0,
width: 20.0,
height: 10.0,
},
label_rect: None,
rank: 0,
order: 1,
},
LayoutNodeBox {
node_idx: 2,
rect: LayoutRect {
x: 20.0,
y: 30.0,
width: 20.0,
height: 10.0,
},
label_rect: None,
rank: 1,
order: 0,
},
],
clusters: vec![],
edges: vec![
LayoutEdgePath {
edge_idx: 0,
waypoints: vec![
LayoutPoint { x: 10.0, y: 10.0 },
LayoutPoint { x: 30.0, y: 30.0 },
],
bundle_count: 1,
bundle_members: Vec::new(),
},
LayoutEdgePath {
edge_idx: 1,
waypoints: vec![
LayoutPoint { x: 50.0, y: 10.0 },
LayoutPoint { x: 30.0, y: 30.0 },
],
bundle_count: 1,
bundle_members: Vec::new(),
},
],
bounding_box: LayoutRect {
x: 0.0,
y: 0.0,
width: 60.0,
height: 40.0,
},
stats: LayoutStats {
iterations_used: 5,
max_iterations: 50,
budget_exceeded: false,
crossings: 0,
ranks: 2,
max_rank_width: 2,
total_bends: 0,
position_variance: 1.0,
},
degradation: None,
};
let minimap = Minimap::new(&layout, MinimapConfig::default());
let mut buf = Buffer::new(120, 40);
let area = Rect::new(0, 0, 120, 40);
let viewport = LayoutRect {
x: 10.0,
y: 5.0,
width: 30.0,
height: 20.0,
};
minimap.render(area, &mut buf, Some(&viewport), Some(2));
assert!(!minimap.is_trivial());
}
}