use crate::preview::mermaid::flowchart::{Shape, Stroke};
use crate::preview::mermaid::gitgraph::{Commit, CommitKind, Direction, GitGraph};
use crate::preview::mermaid::layout::Point;
use crate::preview::mermaid::text_metrics;
use super::band;
use super::chart::label_node;
use super::{
normalise, shapes, svg, Curve, Diagram, Glyph, Label, PlacedEdge, PlacedNode, RenderError,
Size, Theme, Tip,
};
pub const COMMIT_STEP: f64 = 46.0;
pub const LANE_STEP: f64 = 44.0;
pub const NAME_GAP: f64 = 10.0;
pub const CAPTION_GAP: f64 = 6.0;
pub const TAG_GAP: f64 = 5.0;
pub const TAG_STACK: f64 = 3.0;
pub const LABEL_GAP: f64 = 12.0;
struct Marks {
caption: Label,
tags: Vec<(Label, Size)>,
}
impl Marks {
fn of(commit: &Commit) -> Marks {
let caption = Label::measure(if commit.explicit_id { &commit.id } else { "" });
let tags = commit
.tags
.iter()
.map(|tag| {
let label = Label::measure(tag);
let size = shapes::size(Glyph::Tag, Size::new(label.width, label.height));
(label, size)
})
.collect();
Marks { caption, tags }
}
fn caption_box(&self) -> Size {
if self.caption.is_blank() {
Size::new(0.0, 0.0)
} else {
Size::new(self.caption.width, self.caption.height)
}
}
}
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let graph = crate::preview::mermaid::gitgraph::parse(code)?;
let diagram = lay_out(&graph)?;
Ok(svg::emit(&diagram, &Theme::named(theme)))
}
pub fn lay_out(graph: &GitGraph) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if graph.commits.is_empty() {
return Err(RenderError::NothingToDraw);
}
let lanes: Vec<&str> = graph.lanes().iter().map(|b| b.name.as_str()).collect();
let lane_of = |branch: &str| lanes.iter().position(|b| *b == branch).unwrap_or(0);
let names: Vec<Label> = lanes.iter().map(|b| Label::measure(b)).collect();
let widest = names.iter().map(|l| l.width).fold(0.0_f64, f64::max);
let x0 = widest + NAME_GAP;
let lr = matches!(graph.direction, Direction::LeftToRight);
let along_of = move |s: Size| if lr { s.w } else { s.h };
let across_of = move |s: Size| if lr { s.h } else { s.w };
let marks: Vec<Marks> = graph.commits.iter().map(Marks::of).collect();
let caption_reach = |m: &Marks| {
if m.caption.is_blank() {
0.0
} else {
shapes::COMMIT_RADIUS + CAPTION_GAP + across_of(m.caption_box())
}
};
let tags_reach = |m: &Marks| {
if m.tags.is_empty() {
0.0
} else {
shapes::COMMIT_RADIUS
+ TAG_GAP
+ m.tags.iter().map(|(_, s)| across_of(*s)).sum::<f64>()
+ (m.tags.len() - 1) as f64 * TAG_STACK
}
};
let caption_half = |m: &Marks| along_of(m.caption_box()) / 2.0;
let tags_half = |m: &Marks| {
m.tags
.iter()
.map(|(_, s)| along_of(*s) / 2.0)
.fold(0.0_f64, f64::max)
};
let mut captions_out = vec![0.0_f64; lanes.len()];
let mut tags_out = vec![0.0_f64; lanes.len()];
for (commit, m) in graph.commits.iter().zip(&marks) {
let k = lane_of(&commit.branch);
captions_out[k] = captions_out[k].max(caption_reach(m));
tags_out[k] = tags_out[k].max(tags_reach(m));
}
if lr {
for (k, name) in names.iter().enumerate() {
let half = across_of(Size::new(name.width, name.height)) / 2.0;
captions_out[k] = captions_out[k].max(half);
tags_out[k] = tags_out[k].max(half);
}
}
let mut lane_step = LANE_STEP;
if !lr {
lane_step = lane_step.max(widest + NAME_GAP);
}
for k in 0..lanes.len() {
if k + 1 < lanes.len() {
lane_step = lane_step.max(captions_out[k] * 2.0 + LABEL_GAP);
}
if k > 0 {
lane_step = lane_step.max(tags_out[k] * 2.0 + LABEL_GAP);
}
}
let ext = |m: &Marks| caption_half(m).max(tags_half(m));
let mut at = vec![0.0_f64; graph.commits.len()];
for i in 1..graph.commits.len() {
let need = ext(&marks[i - 1]) + ext(&marks[i]) + LABEL_GAP;
at[i] = at[i - 1] + need.max(COMMIT_STEP);
}
let along = |seq: usize| x0 + at[seq] + shapes::COMMIT_RADIUS;
let across = move |lane: usize| lane as f64 * lane_step + shapes::COMMIT_RADIUS;
let axes = |along_v: f64, across_v: f64| match graph.direction {
Direction::LeftToRight => Point::new(along_v, across_v),
Direction::TopToBottom => Point::new(across_v, along_v),
Direction::BottomToTop => Point::new(across_v, -along_v),
};
let place = |seq: usize, lane: usize| -> Point { axes(along(seq), across(lane)) };
let corridor = |seq: usize| {
let opens = along(seq - 1) + ext(&marks[seq - 1]);
let closes = along(seq) - ext(&marks[seq]);
(opens + closes) / 2.0
};
let beside = |center: &Point, out: f64| match graph.direction {
Direction::LeftToRight => Point::new(center.x, center.y + out),
_ => Point::new(center.x + out, center.y),
};
let mut nodes: Vec<PlacedNode> = Vec::new();
let mut edges: Vec<PlacedEdge> = Vec::new();
let d = shapes::COMMIT_RADIUS * 2.0;
for (i, name) in names.iter().enumerate() {
let at = match graph.direction {
Direction::LeftToRight => Point::new(widest - name.width / 2.0, across(i)),
_ => Point::new(across(i), -NAME_GAP - name.height / 2.0),
};
if let Some(n) = label_node(format!("branch#{}", lanes[i]), name.clone(), at, None) {
nodes.push(n);
}
}
for (i, commit) in graph.commits.iter().enumerate() {
let lane = lane_of(&commit.branch);
let center = place(commit.seq, lane);
let m = &marks[i];
let drawn = commit.custom_type.unwrap_or(commit.kind);
let (glyph, size) = match drawn {
CommitKind::Reverse => (Glyph::Reverted, Size::new(d, d)),
CommitKind::Highlight => (Glyph::Flow(Shape::Rect), Size::new(d, d)),
CommitKind::Merge => (Glyph::Flow(Shape::DoubleCircle), Size::new(d, d)),
CommitKind::CherryPick | CommitKind::Normal => {
(Glyph::Flow(Shape::Circle), Size::new(d, d))
}
};
nodes.push(PlacedNode {
id: commit.id.clone(),
shape: glyph,
center: center.clone(),
size,
label: Label::measure(""),
panel: None,
series: Some(lane),
mark: None,
style: None,
});
let at = beside(
¢er,
shapes::COMMIT_RADIUS + CAPTION_GAP + across_of(m.caption_box()) / 2.0,
);
if let Some(n) = label_node(
format!("{}#caption", commit.id),
m.caption.clone(),
at,
None,
) {
nodes.push(n);
}
let mut out = shapes::COMMIT_RADIUS + TAG_GAP;
for (k, (label, size)) in m.tags.iter().enumerate() {
nodes.push(PlacedNode {
id: format!("{}#tag{k}", commit.id),
shape: Glyph::Tag,
center: beside(¢er, -(out + across_of(*size) / 2.0)),
size: *size,
label: label.clone(),
panel: None,
series: Some(lane),
mark: None,
style: None,
});
out += across_of(*size) + TAG_STACK;
}
}
for commit in &graph.commits {
let lane = lane_of(&commit.branch);
let head = place(commit.seq, lane);
for parent_id in &commit.parents {
let Some(parent) = graph.commit(parent_id) else {
continue;
};
let plane = lane_of(&parent.branch);
let tail = place(parent.seq, plane);
let mut points = vec![tail.clone()];
if plane != lane {
let cross = corridor(commit.seq);
points.push(axes(cross, across(plane)));
points.push(axes(cross, across(lane)));
}
points.push(head.clone());
edges.push(PlacedEdge {
from: parent.id.clone(),
to: commit.id.clone(),
points,
gaps: Vec::new(),
tip_start: Tip::None,
tip_end: Tip::None,
stroke: Stroke::Normal,
label: None,
start_label: None,
end_label: None,
badge: None,
series: Some(plane),
straight: true,
overlay: false,
style: None,
curve: Curve::Basis,
tip_matches_line: false,
});
}
}
let mut diagram = Diagram {
nodes,
edges,
..Diagram::default()
};
band::add_title(&mut diagram, graph.preamble.title.as_deref());
normalise(&mut diagram);
Ok(diagram)
}