use crate::preview::mermaid::flowchart::Shape;
use crate::preview::mermaid::kanban::{Card, Kanban};
use crate::preview::mermaid::layout::Point;
use crate::preview::mermaid::text_metrics;
use super::band;
use super::panel::{class_panel, Compartment};
use super::{
normalise, shapes, svg, Diagram, Glyph, Label, PlacedCluster, PlacedNode, RenderError, Size,
Theme,
};
pub const COLUMN_GAP: f64 = 26.0;
pub const CARD_GAP: f64 = 8.0;
pub const COLUMN_PAD: f64 = 8.0;
pub const MIN_COLUMN: f64 = 120.0;
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let board = crate::preview::mermaid::kanban::parse(code)?;
let diagram = lay_out(&board)?;
Ok(svg::emit(&diagram, &Theme::named(theme)))
}
pub fn lay_out(board: &Kanban) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if board.columns.is_empty() {
return Err(RenderError::NothingToDraw);
}
let panels: Vec<Vec<super::Panel>> = board
.columns
.iter()
.map(|c| c.cards.iter().map(card_panel).collect())
.collect();
let widest = panels
.iter()
.flatten()
.map(|p| p.size.w)
.fold(MIN_COLUMN, f64::max);
let mut nodes: Vec<PlacedNode> = Vec::new();
let mut clusters: Vec<PlacedCluster> = Vec::new();
let mut x = 0.0_f64;
let mut n = 0usize;
for (ci, column) in board.columns.iter().enumerate() {
let mut y = 0.0_f64;
for (k, panel) in panels[ci].iter().enumerate() {
let size = Size::new(widest, panel.size.h);
let mut panel = panel.clone();
panel.size = size;
nodes.push(PlacedNode {
id: format!("card#{ci}#{k}"),
shape: Glyph::Flow(Shape::RoundedRect),
center: Point::new(x + widest / 2.0, y + size.h / 2.0),
size,
label: Label::measure(""),
panel: Some(panel),
series: Some(ci),
mark: None,
});
y += size.h + CARD_GAP;
n += 1;
}
let bottom = if column.cards.is_empty() {
shapes::PADDING * 4.0
} else {
y - CARD_GAP
};
clusters.push(band::frame(
format!("column#{ci}"),
&column_title(column.label.as_str(), column.ticket.as_deref()),
x - COLUMN_PAD,
-COLUMN_PAD,
x + widest + COLUMN_PAD,
bottom + COLUMN_PAD,
));
x += widest + COLUMN_GAP;
}
if n == 0 && clusters.is_empty() {
return Err(RenderError::NothingToDraw);
}
let deepest = clusters
.iter()
.map(|c| c.bounds().3)
.fold(f64::NEG_INFINITY, f64::max);
for c in &mut clusters {
let (l, t, r, b) = c.bounds();
if deepest > b {
c.size = Size::new(r - l, deepest - t);
c.center = Point::new((l + r) / 2.0, (t + deepest) / 2.0);
}
}
let mut diagram = Diagram {
nodes,
clusters,
..Diagram::default()
};
normalise(&mut diagram);
Ok(diagram)
}
fn card_panel(card: &Card) -> super::Panel {
let mut meta: Vec<Label> = Vec::new();
for (key, value) in [
("assigned", &card.assigned),
("ticket", &card.ticket),
("priority", &card.priority),
] {
if let Some(v) = value.as_deref().filter(|v| !v.trim().is_empty()) {
meta.push(Label::measure(&format!("{key}: {v}")));
}
}
let mut compartments = vec![Compartment {
lines: vec![Label::measure(&card.label)],
centered: false,
}];
if !meta.is_empty() {
compartments.push(Compartment {
lines: meta,
centered: false,
});
}
class_panel(&compartments)
}
fn column_title(label: &str, ticket: Option<&str>) -> String {
match ticket.filter(|t| !t.trim().is_empty()) {
Some(t) => format!("{label} ({t})"),
None => label.to_string(),
}
}