use crate::canvas::Canvas;
use crate::scene::{Point, Transform};
use crate::tex::{self, Cat, Meaning, Token};
use crate::Error;
use super::nodes::{NodeSpec, OnPath};
use super::{tikra_tokens, Engine};
#[derive(Debug, Clone, Copy, Default)]
struct Extent {
left: f64,
right: f64,
up: f64,
down: f64,
empty: bool,
}
struct Body {
rows: Vec<Vec<Vec<Token>>>,
cell_seps: Vec<Vec<Vec<Token>>>,
row_seps: Vec<Vec<Token>>,
}
struct Sep {
distance: f64,
origins: bool,
}
impl Engine {
fn split_body(&self, body: &[Token], ampersand: Option<&str>) -> Body {
let next_cell = self.tex.meaning(&Token::cs("pgfmatrixnextcell"));
let end_row = self.tex.meaning(&Token::cs("pgfmatrixendrow"));
let mut out = Body { rows: vec![vec![Vec::new()]], cell_seps: vec![vec![Vec::new()]], row_seps: vec![Vec::new()] };
let mut depth = 0usize;
let mut i = 0;
let spec = |i: usize| -> (Vec<Token>, usize) {
let mut j = i + 1;
while j < body.len() && body[j].is_space() {
j += 1;
}
if !body.get(j).is_some_and(|x| x.is_char('[')) {
return (Vec::new(), i + 1);
}
let mut k = j + 1;
let mut v = Vec::new();
while k < body.len() && !body[k].is_char(']') {
v.push(body[k].clone());
k += 1;
}
(v, k + 1)
};
while i < body.len() {
let t = &body[i];
let cs = |name: &str| matches!(t, Token::Cs(n) if &**n == name);
let is_cell =
matches!(t, Token::Char(_, Cat::Align)) || matches!(t, Token::Cs(n) if Some(&**n) == ampersand) || (matches!(t, Token::Cs(_)) && self.tex.meaning(t) == next_cell);
let is_row = cs("\\") || (matches!(t, Token::Cs(_)) && self.tex.meaning(t) == end_row);
match t {
Token::Char(_, Cat::Begin) => depth += 1,
Token::Char(_, Cat::End) => depth = depth.saturating_sub(1),
_ if depth == 0 && is_cell => {
let (sep, next) = spec(i);
out.rows.last_mut().expect("a row").push(Vec::new());
out.cell_seps.last_mut().expect("a row").push(sep);
i = next;
continue;
}
_ if depth == 0 && is_row => {
let (sep, next) = spec(i);
*out.row_seps.last_mut().expect("a row") = sep;
out.rows.push(vec![Vec::new()]);
out.cell_seps.push(vec![Vec::new()]);
out.row_seps.push(Vec::new());
i = next;
continue;
}
_ => {}
}
out.rows.last_mut().expect("a row").last_mut().expect("a cell").push(t.clone());
i += 1;
}
while out.rows.len() > 1 && out.rows.last().is_some_and(|r| r.iter().all(|c| crate::keys::trim(c).is_empty())) {
out.rows.pop();
out.cell_seps.pop();
out.row_seps.pop();
}
out
}
fn sep(&mut self, default: &[Token], extra: &[Token]) -> Sep {
let mut sep = Sep { distance: 0.0, origins: false };
for list in [default, extra] {
for item in crate::keys::split_list(list) {
match item.key.as_str() {
"between origins" => sep.origins = true,
"between borders" => sep.origins = false,
"" => {}
k => match self.length(k) {
Ok(d) => sep.distance += d,
Err(_) => self.warn(format!("matrix separation '{k}' not understood: ignored")),
},
}
}
}
sep
}
pub fn pgf_matrix(&mut self) -> Result<(), Error> {
let shape = self.argument_string()?;
let anchor = self.argument_string()?;
let name = self.argument_string()?;
let usage = self.argument_string()?;
let shift = self.point_arg()?;
let pre = self.argument()?;
let body = self.argument()?;
self.begin_group();
let r = (|| {
self.run_tokens(pre)?;
let mut options = format!("shape={shape},anchor={anchor},draw=none,fill=none");
for (word, key) in [("stroke", "draw"), ("draw", "draw"), ("fill", "fill")] {
if usage.contains(word) {
options.push_str(&format!(",{key}"));
}
}
let at = self.st.transform.apply(shift);
let body = self.expand_body_macros(body);
let name = Some(name.trim().to_string()).filter(|n| !n.is_empty());
self.matrix_in_group(tikra_tokens(&options), name, Some(at), body)
})();
self.end_group();
r
}
pub fn matrix(&mut self) -> Result<(), Error> {
let mut options = Vec::new();
let mut name = None;
let mut at = None;
loop {
match self.tex.next_nonspace() {
Some(t) if t.is_char('[') => {
if !options.is_empty() {
options.push(Token::other(','));
}
options.extend(self.tex.read_until_char(']', false)?);
}
Some(t) if t.is_char('(') => {
let n = super::coords::read_coordinate_tokens(self)?;
name = Some(self.tex.expand_to_string(n)?.trim().to_string());
}
Some(Token::Char('a', Cat::Letter)) => {
let _ = self.tex.get_token();
match self.tex.next_nonspace() {
Some(t) if t.is_char('(') => {
let c = super::coords::read_coordinate_tokens(self)?;
let text = self.math_string(c)?;
at = Some(self.coordinate(&text)?.p);
}
_ => return Err(Error::Figure("\\matrix: at without a point".into())),
}
}
Some(Token::Char(_, Cat::Begin)) => break,
Some(t) => return Err(Error::Figure(format!("\\matrix: unexpected {}", tex::show_token(&t)))),
None => return Err(Error::Figure("\\matrix without a body".into())),
}
}
let body = self.tex.read_group_body()?;
let body = self.expand_body_macros(body);
if let Some(t) = self.tex.next_nonspace() {
if !t.is_char(';') {
self.tex.back(t);
}
}
self.begin_group();
let r = self.matrix_in_group(options, name, at, body);
self.end_group();
r
}
fn matrix_in_group(&mut self, options: Vec<Token>, name: Option<String>, at: Option<Point>, body: Vec<Token>) -> Result<(), Error> {
let every_node = Token::cs(&crate::keys::macro_name("/tikz/every node/.@cmd"));
let outer_every_node = self.tex.meaning(&every_node);
let outer = std::mem::take(&mut self.st.matrix);
self.st.matrix = MatrixSettings { ampersand: outer.ampersand, column_sep: outer.column_sep, row_sep: outer.row_sep, ..Default::default() };
let saved_mode = self.st.mode.clone();
self.process_keys(tikra_tokens("every matrix/.try"), Some("/tikz"))?;
self.process_keys(options.clone(), Some("/tikz"))?;
self.st.mode = saved_mode;
for (code, m) in [("pgfmatrixemptycode", 0), ("pgfmatrixbegincode", 1), ("pgfmatrixendcode", 2)] {
let target = match m {
0 => &mut self.st.matrix.at_empty_cell,
1 => &mut self.st.matrix.at_begin_cell,
_ => &mut self.st.matrix.at_end_cell,
};
if target.is_empty() {
if let Meaning::Macro(m) = self.tex.meaning_of(code) {
*target = tex::substitute(&m.body, &[]);
}
}
}
let settings = self.st.matrix.clone();
let name = name.or_else(|| self.st.node.name.clone());
let at = at.or(self.st.node.at).unwrap_or_else(|| self.st.transform.apply(Point::default()));
let Body { rows, cell_seps, row_seps } = self.split_body(&body, settings.ampersand.as_deref());
let nrows = rows.len();
let ncols = rows.iter().map(Vec::len).max().unwrap_or(0);
let anchor = self.key_text("/tikz/anchor");
let inner = settings.anchor.as_deref().or(Some(anchor.as_str())).and_then(|a| a.split_once('.')).map(|(n, a)| (n.trim().to_string(), a.trim().to_string()));
if let Some((n, _)) = &inner {
self.nodes.remove(n);
}
let mut inner_at: Option<(usize, usize, Point)> = None;
let mut extents = vec![vec![Extent { empty: true, ..Default::default() }; ncols]; nrows];
for (r, row) in rows.iter().enumerate() {
for (c, cell) in row.iter().enumerate() {
let saved = std::mem::take(&mut self.canvas);
let frozen = self.canvas_frozen;
self.canvas_frozen = false;
let clipped: Vec<bool> = self.scopes.iter_mut().map(|s| std::mem::take(&mut s.clipped)).collect();
let res = self.draw_cell(name.as_deref(), &settings, r + 1, c + 1, cell, Transform::default());
let drawn = std::mem::replace(&mut self.canvas, saved);
self.canvas_frozen = frozen;
for (s, c) in self.scopes.iter_mut().zip(clipped) {
s.clipped = c;
}
res?;
if let (Some((n, a)), None) = (&inner, inner_at) {
if let Some(p) = self.nodes.get(n).and_then(|i| i.anchor(a)) {
inner_at = Some((r, c, p));
}
}
if let (Some(lo), Some(hi)) = (drawn.min, drawn.max) {
extents[r][c] = Extent { left: (-lo.x).max(0.0), right: hi.x.max(0.0), up: hi.y.max(0.0), down: (-lo.y).max(0.0), empty: false };
}
}
}
let left: Vec<f64> = (0..ncols).map(|c| extents.iter().map(|row| if row[c].empty { 0.0 } else { row[c].left }).fold(0.0, f64::max)).collect();
let right: Vec<f64> = (0..ncols).map(|c| extents.iter().map(|row| if row[c].empty { 0.0 } else { row[c].right }).fold(0.0, f64::max)).collect();
let up: Vec<f64> = extents.iter().map(|row| row.iter().filter(|e| !e.empty).map(|e| e.up).fold(0.0, f64::max)).collect();
let down: Vec<f64> = extents.iter().map(|row| row.iter().filter(|e| !e.empty).map(|e| e.down).fold(0.0, f64::max)).collect();
let mut xs = Vec::with_capacity(ncols);
for c in 0..ncols {
if c == 0 {
xs.push(left[0]);
continue;
}
let extra = rows.iter().zip(&cell_seps).find(|(row, _)| row.len() > c).and_then(|(_, seps)| seps.get(c).cloned()).unwrap_or_default();
let sep = self.sep(&settings.column_sep, &extra);
let step = if sep.origins { sep.distance } else { right[c - 1] + sep.distance + left[c] };
xs.push(xs[c - 1] + step);
}
let mut ys = Vec::with_capacity(nrows);
for r in 0..nrows {
if r == 0 {
ys.push(0.0);
continue;
}
let sep = self.sep(&settings.row_sep, &row_seps[r - 1]);
let step = if sep.origins { sep.distance } else { down[r - 1] + sep.distance + up[r] };
ys.push(ys[r - 1] - step);
}
if let Some(&last) = ys.last() {
for y in &mut ys {
*y -= last;
}
}
let width = if ncols == 0 { 0.0 } else { xs[ncols - 1] + right[ncols - 1] };
let height = up.first().map_or(0.0, |u| u + ys[0]);
let depth = down.last().copied().unwrap_or(0.0);
self.forced_box = Some((width, height, depth));
let cells_every_node = self.tex.meaning(&every_node);
self.tex.set_meaning(&every_node, outer_every_node, false);
let mut options = options;
let mut at = at;
match (&settings.anchor, inner_at) {
(_, Some((r, c, p))) if inner.is_some() => {
let center = Point::new(width / 2.0, (height - depth) / 2.0);
at = at.plus(center).minus(Point::new(xs[c], ys[r])).minus(p);
options.extend(tikra_tokens(",anchor=center"));
}
(Some(a), _) if inner.is_none() => options.extend(tikra_tokens(&format!(",anchor={a}"))),
_ => {}
}
let items =
self.node(NodeSpec { options, name: name.clone(), at: Some(at), text: Vec::new(), place: OnPath::Point(at), point: at, coordinate: false, default_pos: None })?;
self.forced_box = None;
let after = std::mem::take(&mut self.after_node_path);
let matrix_node = self.last_node.clone();
self.tex.set_meaning(&every_node, cells_every_node, false);
self.canvas.items.extend(items);
let placed = match &name {
Some(n) => self.nodes.get(n).map(|i| i.transform),
None => self.last_node.as_ref().map(|i| i.transform),
}
.unwrap_or_default();
for (r, row) in rows.iter().enumerate() {
for (c, cell) in row.iter().enumerate() {
let t = placed.compose(&Transform::translation(xs[c], ys[r]));
self.draw_cell(name.as_deref(), &settings, r + 1, c + 1, cell, t)?;
}
}
if !after.is_empty() {
let node = match &name {
Some(n) => n.clone(),
None => {
let n = format!("tikra@fig@{}", self.steps);
if let Some(info) = matrix_node {
self.nodes.insert(n.clone(), info);
}
n
}
};
self.tex.define_string("tikzlastnode", &node, false);
let mut path = tikra_tokens("\\path ");
path.extend(after);
path.extend(tikra_tokens(";"));
self.run_tokens(path)?;
}
Ok(())
}
fn expand_body_macros(&self, body: Vec<Token>) -> Vec<Token> {
let mut out = Vec::new();
let mut todo: Vec<Token> = body.into_iter().rev().collect();
let mut budget = 1000;
let separators = [self.tex.meaning(&Token::cs("pgfmatrixnextcell")), self.tex.meaning(&Token::cs("pgfmatrixendrow"))];
while let Some(t) = todo.pop() {
if let (Token::Cs(_), tex::Meaning::Macro(m)) = (&t, self.tex.meaning(&t)) {
if !separators.contains(&tex::Meaning::Macro(m.clone())) && m.params.is_empty() && m.optional.is_none() && budget > 0 {
budget -= 1;
for b in m.body.iter().rev() {
if let tex::Body::Tok(x) = b {
todo.push(x.clone());
}
}
continue;
}
}
out.push(t);
}
out
}
fn draw_cell(&mut self, matrix: Option<&str>, settings: &MatrixSettings, r: usize, c: usize, cell: &[Token], place: Transform) -> Result<(), Error> {
let mut cell = crate::keys::trim(cell).to_vec();
let empty = cell.is_empty();
let empty_node = empty && settings.of_nodes && settings.nodes_in_empty_cells && settings.at_empty_cell.is_empty();
if empty && !empty_node {
if settings.at_empty_cell.is_empty() {
return Ok(());
}
cell = settings.at_empty_cell.clone();
}
let empty = empty && !empty_node;
self.begin_group();
self.begin_scope();
let res = (|| -> Result<(), Error> {
self.st.transform = place;
self.st.pending = Transform::default();
self.tex.define_string("pgfmatrixcurrentrow", &r.to_string(), false);
self.tex.define_string("pgfmatrixcurrentcolumn", &c.to_string(), false);
let parity = |n: usize| if n.is_multiple_of(2) { "even" } else { "odd" };
if !settings.nodes_style.is_empty() {
let mut v = tikra_tokens("every node/.append style=");
v.push(Token::Char('{', Cat::Begin));
v.extend(settings.nodes_style.clone());
v.push(Token::Char('}', Cat::End));
self.process_keys(v, Some("/tikz"))?;
}
let mut styles = if settings.of_nodes { "anchor=base,".to_string() } else { String::new() };
styles.push_str(&format!("every cell/.try,every {} row/.try,every {} column/.try,row {r}/.try,column {c}/.try,row {r} column {c}/.try", parity(r), parity(c)));
self.process_keys(tikra_tokens(&styles), Some("/tikz"))?;
self.st.apply_pending();
const PATHS: &[&str] = &["node", "draw", "path", "fill", "filldraw", "shade", "shadedraw", "clip", "coordinate", "pic", "matrix", "useasboundingbox"];
let plain = cell.first().is_some_and(|t| PATHS.iter().any(|p| t.is_cs(p)));
if settings.of_nodes && !empty && !plain {
let (opts, text) = cell_options(&cell);
let mut statement = tikra_tokens("\\node[");
if let Some(m) = matrix {
statement.extend(tikra_tokens(&format!(",name={m}-{r}-{c}")));
}
statement.push(Token::other(','));
statement.extend(opts);
statement.extend(tikra_tokens("] at (0,0) "));
statement.push(Token::Char('{', Cat::Begin));
if settings.math {
statement.push(Token::Char('$', Cat::Math));
}
statement.extend(text);
if settings.math {
statement.push(Token::Char('$', Cat::Math));
}
statement.push(Token::Char('}', Cat::End));
statement.extend(tikra_tokens(";"));
self.run_tokens(statement)
} else if empty {
self.run_tokens(cell)
} else {
let mut v = settings.at_begin_cell.clone();
v.extend(cell);
v.extend(settings.at_end_cell.clone());
self.run_tokens(v)
}
})();
self.end_scope();
self.end_group();
res
}
}
fn cell_options(cell: &[Token]) -> (Vec<Token>, Vec<Token>) {
let t = crate::keys::trim(cell);
if t.first().is_some_and(|x| x.is_char('|')) {
if let Some(end) = t.iter().skip(1).position(|x| x.is_char('|')) {
let inner = crate::keys::trim(&t[1..end + 1]);
let mut options = Vec::new();
let mut i = 0;
while i < inner.len() {
let close = if inner[i].is_char('[') {
']'
} else if inner[i].is_char('(') {
')'
} else {
i += 1;
continue;
};
let Some(len) = inner[i + 1..].iter().position(|x| x.is_char(close)) else { break };
if !options.is_empty() {
options.push(Token::other(','));
}
if close == ')' {
options.extend(tikra_tokens("name="));
options.push(Token::Char('{', Cat::Begin));
options.extend(inner[i + 1..i + 1 + len].iter().cloned());
options.push(Token::Char('}', Cat::End));
} else {
options.extend(inner[i + 1..i + 1 + len].iter().cloned());
}
i += len + 2;
}
return (options, crate::keys::trim(&t[end + 2..]).to_vec());
}
}
(Vec::new(), t.to_vec())
}
#[derive(Debug, Clone, Default)]
pub struct MatrixSettings {
pub column_sep: Vec<Token>,
pub row_sep: Vec<Token>,
pub of_nodes: bool,
pub math: bool,
pub nodes_style: Vec<Token>,
pub at_begin_cell: Vec<Token>,
pub at_end_cell: Vec<Token>,
pub at_empty_cell: Vec<Token>,
pub ampersand: Option<String>,
pub nodes_in_empty_cells: bool,
pub anchor: Option<String>,
}
#[allow(dead_code)]
fn unused(_: Canvas) {}