use crate::figure::Figure;
use crate::svg;
#[derive(Debug, Clone)]
pub struct PanelGridOpts {
pub columns: Option<usize>,
pub rows: Option<usize>,
pub cell_width: Option<f64>,
pub cell_height: Option<f64>,
pub gap: f64,
pub padding: f64,
pub title: Option<String>,
pub background: &'static str,
}
impl Default for PanelGridOpts {
fn default() -> Self {
Self {
columns: None,
rows: None,
cell_width: None,
cell_height: None,
gap: 16.0,
padding: 20.0,
title: None,
background: "white",
}
}
}
impl PanelGridOpts {
pub fn columns(mut self, n: usize) -> Self {
self.columns = Some(n);
self
}
pub fn rows(mut self, n: usize) -> Self {
self.rows = Some(n);
self
}
pub fn cell_size(mut self, w: f64, h: f64) -> Self {
self.cell_width = Some(w);
self.cell_height = Some(h);
self
}
pub fn gap(mut self, g: f64) -> Self {
self.gap = g;
self
}
pub fn padding(mut self, p: f64) -> Self {
self.padding = p;
self
}
pub fn title(mut self, t: impl Into<String>) -> Self {
self.title = Some(t.into());
self
}
}
pub fn multi_panel(figures: &[Figure], opts: PanelGridOpts) -> String {
if figures.is_empty() {
return String::new();
}
let first = &figures[0];
let cell_w = opts.cell_width.unwrap_or(first.width);
let cell_h = opts.cell_height.unwrap_or(first.height);
let n = figures.len();
let cols = opts.columns.unwrap_or_else(|| match opts.rows {
Some(r) if r > 0 => (n + r - 1) / r,
_ => n,
});
let cols = cols.max(1);
let rows = opts.rows.unwrap_or_else(|| (n + cols - 1) / cols);
let title_h = if opts.title.is_some() { 36.0 } else { 0.0 };
let total_w = opts.padding * 2.0 + cols as f64 * cell_w + (cols as f64 - 1.0) * opts.gap;
let total_h =
opts.padding * 2.0 + title_h + rows as f64 * cell_h + (rows as f64 - 1.0) * opts.gap;
let mut buf = String::with_capacity(8 * 1024);
svg::document_open(&mut buf, total_w, total_h);
let bg = format!(" fill=\"{}\"", opts.background);
svg::rect(&mut buf, 0.0, 0.0, total_w, total_h, &bg);
if let Some(title) = &opts.title {
let attrs = svg::Attrs::new()
.num("font-size", 16.0)
.str("font-family", "Times, 'Liberation Serif', serif")
.str("text-anchor", "middle")
.str("letter-spacing", "0.05em")
.str("fill", "black")
.into_string();
svg::text(
&mut buf,
total_w / 2.0,
opts.padding + 22.0,
&svg::escape(title),
&attrs,
);
}
for (i, fig) in figures.iter().enumerate() {
let row = i / cols;
let col = i % cols;
let x = opts.padding + col as f64 * (cell_w + opts.gap);
let y = opts.padding + title_h + row as f64 * (cell_h + opts.gap);
let inner = strip_outer_svg(&fig.to_svg());
buf.push_str("<svg x=\"");
svg::num_into(&mut buf, x);
buf.push_str("\" y=\"");
svg::num_into(&mut buf, y);
buf.push_str("\" width=\"");
svg::num_into(&mut buf, cell_w);
buf.push_str("\" height=\"");
svg::num_into(&mut buf, cell_h);
buf.push_str("\" viewBox=\"0 0 ");
svg::num_into(&mut buf, fig.width);
buf.push(' ');
svg::num_into(&mut buf, fig.height);
buf.push_str("\" preserveAspectRatio=\"xMidYMid meet\">");
buf.push_str(&inner);
buf.push_str("</svg>");
}
svg::document_close(&mut buf);
buf
}
fn strip_outer_svg(svg: &str) -> String {
let mut s = svg;
if let Some(idx) = s.find("?>") {
s = &s[idx + 2..];
}
s = s.trim_start();
if let Some(start) = s.find("<svg") {
if let Some(end) = s[start..].find('>') {
s = &s[start + end + 1..];
}
}
let s = s.trim_end();
let s = s.strip_suffix("</svg>").unwrap_or(s);
s.to_string()
}