use unicode_width::UnicodeWidthStr;
use super::{
canvas::{Canvas, STY_DOT, STY_SOLID, STY_THICK},
drawing::*,
model::{Dir, Graph, LineKind},
painter::{
MermaidArt, MermaidStyles, Oversize, MAX_CANVAS_CELLS, MAX_LABEL, MAX_LINES, PAD,
WRAP_WIDTH,
},
};
mod class;
mod groups;
mod ordering;
mod placement;
const MIN_FLOW_WRAP_WIDTH: usize = 12;
const FLOW_WRAP_STEP: usize = 4;
use class::draw_class_box;
pub(super) use class::render_class;
use groups::draw_frame;
use ordering::order_ranks;
use placement::{place_lr, place_td};
pub(super) struct Placed {
pub(super) x: usize,
pub(super) y: usize,
pub(super) w: usize,
pub(super) h: usize,
pub(super) cx: usize,
pub(super) cy: usize,
pub(super) rank: usize,
}
pub(super) struct NodeSizes {
box_w: Vec<usize>,
box_h: Vec<usize>,
lay_w: Vec<usize>,
lay_h: Vec<usize>,
extra_h: Vec<usize>,
self_label_w: Vec<usize>,
}
pub(super) fn layout_flow(
graph: &Graph,
styles: &MermaidStyles,
max_width: Option<usize>,
) -> Result<MermaidArt, Oversize> {
for wrap_width in (MIN_FLOW_WRAP_WIDTH..=WRAP_WIDTH)
.rev()
.step_by(FLOW_WRAP_STEP)
{
if !flow_labels_fit(graph, wrap_width) {
continue;
}
let result = if graph.groups.is_empty() {
layout_plain_flow(graph, styles, max_width, wrap_width)
} else {
groups::render_grouped(graph, styles, max_width, wrap_width)
};
match result {
Ok(art) => return Ok(art),
Err(Oversize::Width) => continue,
Err(error) => return Err(error),
}
}
Err(Oversize::Width)
}
fn layout_plain_flow(
graph: &Graph,
styles: &MermaidStyles,
max_width: Option<usize>,
wrap_width: usize,
) -> Result<MermaidArt, Oversize> {
let extras: Vec<NodeExtra> = (0..graph.nodes.len()).map(|_| NodeExtra::Plain).collect();
let mut canvas = layout_canvas(graph, &extras, max_width, wrap_width)?;
match graph.dir {
Dir::Up => canvas.flip_vertical(),
Dir::Left => canvas.flip_horizontal(),
Dir::Down | Dir::Right => {}
}
let (styled_lines, plain_lines) = canvas.to_lines(styles);
Ok(MermaidArt {
styled_lines,
plain_lines,
})
}
fn flow_labels_fit(graph: &Graph, wrap_width: usize) -> bool {
graph
.nodes
.iter()
.all(|node| wrap_label(&node.label, wrap_width, usize::MAX).len() <= MAX_LINES)
}
pub(super) enum NodeExtra {
Plain,
Frame(Canvas),
Compartments(Vec<Vec<String>>),
}
pub(super) fn layout_canvas(
graph: &Graph,
extras: &[NodeExtra],
max_width: Option<usize>,
wrap_width: usize,
) -> Result<Canvas, Oversize> {
let n = graph.nodes.len();
if n == 0 {
return Err(Oversize::Cells);
}
let ranks = compute_ranks(graph);
let max_rank = *ranks.iter().max().unwrap_or(&0);
let mut by_rank: Vec<Vec<usize>> = vec![Vec::new(); max_rank + 1];
for (idx, &r) in ranks.iter().enumerate() {
by_rank[r].push(idx);
}
order_ranks(&mut by_rank, &graph.edges, &ranks);
let wrapped: Vec<Vec<String>> = graph
.nodes
.iter()
.map(|node| wrap_label(&node.label, wrap_width, MAX_LINES))
.collect();
let mut box_w: Vec<usize> = (0..n)
.map(|i| match &extras[i] {
NodeExtra::Frame(sub) => {
let title_w = fit_label(&graph.nodes[i].label, wrap_width).width();
(sub.w + 2).max(title_w + 4)
}
NodeExtra::Compartments(sections) => {
sections
.iter()
.flatten()
.map(|l| l.width())
.max()
.unwrap_or(1)
.max(1)
+ 2 * PAD
+ 2
}
NodeExtra::Plain => {
wrapped[i]
.iter()
.map(|l| l.width())
.max()
.unwrap_or(1)
.max(1)
+ 2 * PAD
+ 2
}
})
.collect();
let box_h: Vec<usize> = (0..n)
.map(|i| match &extras[i] {
NodeExtra::Frame(sub) => sub.h + 2,
NodeExtra::Compartments(sections) => {
let filled = sections.iter().filter(|s| !s.is_empty()).count();
sections.iter().map(|s| s.len()).sum::<usize>() + filled.saturating_sub(1) + 2
}
NodeExtra::Plain => wrapped[i].len() + 2,
})
.collect();
let mut extra_h = vec![0usize; n];
let mut self_label_w = vec![0usize; n];
for e in &graph.edges {
if e.from == e.to {
extra_h[e.from] = 2;
if let Some(l) = &e.label {
self_label_w[e.from] = self_label_w[e.from].max(l.width().min(MAX_LABEL));
}
}
}
for i in 0..n {
if extra_h[i] > 0 {
box_w[i] = box_w[i].max(7);
}
}
let lay_w: Vec<usize> = (0..n)
.map(|i| {
box_w[i]
+ if self_label_w[i] > 0 {
2 * (self_label_w[i] + 3)
} else {
0
}
})
.collect();
let lay_h: Vec<usize> = (0..n).map(|i| box_h[i] + extra_h[i]).collect();
let sizes = NodeSizes {
box_w,
box_h,
lay_w,
lay_h,
extra_h,
self_label_w,
};
let mut placed: Vec<Placed> = (0..n)
.map(|_| Placed {
x: 0,
y: 0,
w: 0,
h: 0,
cx: 0,
cy: 0,
rank: 0,
})
.collect();
let vertical = matches!(graph.dir, Dir::Down | Dir::Up);
let plan = if vertical {
place_td(&ranks, max_rank, &by_rank, &sizes, graph, &mut placed)
} else {
place_lr(&ranks, max_rank, &by_rank, &sizes, graph, &mut placed)
};
let (canvas_w, canvas_h) = plan.canvas;
if max_width.is_some_and(|max_width| canvas_w > max_width) {
return Err(Oversize::Width);
}
if canvas_w.saturating_mul(canvas_h) > MAX_CANVAS_CELLS {
return Err(Oversize::Cells);
}
let mut canvas = Canvas::new(canvas_w, canvas_h);
for idx in 0..n {
match &extras[idx] {
NodeExtra::Frame(sub) => {
draw_frame(&mut canvas, &placed[idx], &graph.nodes[idx].label, sub)
}
NodeExtra::Compartments(sections) => {
draw_class_box(&mut canvas, &placed[idx], sections)
}
NodeExtra::Plain => draw_box(
&mut canvas,
&placed[idx],
&wrapped[idx],
graph.nodes[idx].shape,
),
}
}
for (i, edge) in graph.edges.iter().enumerate() {
canvas.cur_style = match edge.line {
LineKind::Solid => STY_SOLID,
LineKind::Dotted => STY_DOT,
LineKind::Thick => STY_THICK,
};
if edge.from == edge.to {
route_self(&mut canvas, &placed[edge.from], edge);
continue;
}
let (from, to) = (&placed[edge.from], &placed[edge.to]);
let adjacent = to.rank == from.rank + 1;
let bus = plan.band_end[from.rank] + plan.edge_bus[i];
let lane = plan.lane_base + plan.edge_lane[i];
match (vertical, adjacent) {
(true, true) => route_forward(
&mut canvas,
from,
to,
edge,
bus,
plan.source_anchors[edge.from],
),
(true, false) => route_back(&mut canvas, from, to, edge, lane),
(false, true) => route_forward_lr(
&mut canvas,
from,
to,
edge,
bus,
plan.source_anchors[edge.from],
),
(false, false) => route_back_lr(&mut canvas, from, to, edge, lane),
}
}
canvas.finalize_mask();
Ok(canvas)
}