typst_library/math/matrix.rs
1use smallvec::{SmallVec, smallvec};
2use typst_syntax::Spanned;
3use typst_utils::{Numeric, default_math_class};
4use unicode_math_class::MathClass;
5
6use crate::diag::{At, HintedStrResult, StrResult, bail};
7use crate::foundations::{
8 Array, Content, Dict, Fold, NoneValue, Resolve, Smart, StyleChain, Symbol, Value,
9 array, cast, dict, elem,
10};
11use crate::layout::{Abs, Em, HAlignment, Length, Rel};
12use crate::math::Mathy;
13use crate::visualize::Stroke;
14
15const DEFAULT_ROW_GAP: Em = Em::new(0.2);
16const DEFAULT_COL_GAP: Em = Em::new(0.5);
17
18/// A column vector.
19///
20/// Content in the vector's elements can be aligned with the
21/// @math.vec.align[`align`] parameter, or the `&` symbol.
22///
23/// This function is for typesetting vector components. To typeset a symbol that
24/// represents a vector, @math.accent[`arrow`] and @math.bold[`bold`] are
25/// commonly used.
26///
27/// = Example <example>
28/// ```example
29/// $ vec(a, b, c) dot vec(1, 2, 3)
30/// = a + 2b + 3c $
31/// ```
32#[elem(title = "Vector", Mathy)]
33pub struct VecElem {
34 /// The delimiter to use.
35 ///
36 /// Can be a single character specifying the left delimiter, in which case
37 /// the right delimiter is inferred. Otherwise, can be an array containing a
38 /// left and a right delimiter.
39 ///
40 /// ```example
41 /// #set math.vec(delim: "[")
42 /// $ vec(1, 2) $
43 /// ```
44 #[default(DelimiterPair::PAREN)]
45 pub delim: DelimiterPair,
46
47 /// The horizontal alignment that each element should have.
48 ///
49 /// ```example
50 /// #set math.vec(align: right)
51 /// $ vec(-1, 1, -1) $
52 /// ```
53 #[default(HAlignment::Center)]
54 pub align: HAlignment,
55
56 /// The gap between elements.
57 ///
58 /// ```example
59 /// #set math.vec(gap: 1em)
60 /// $ vec(1, 2) $
61 /// ```
62 #[default(DEFAULT_ROW_GAP.into())]
63 pub gap: Rel<Length>,
64
65 /// The elements of the vector.
66 #[variadic]
67 pub children: Vec<Content>,
68}
69
70/// A matrix.
71///
72/// The elements of a row should be separated by commas, while the rows
73/// themselves should be separated by semicolons. The semicolon syntax merges
74/// preceding arguments separated by commas into an array. You can also use this
75/// special syntax of math function calls to define custom functions that take
76/// 2D data.
77///
78/// Content in cells can be aligned with the @math.mat.align[`align`] parameter,
79/// or content in cells that are in the same row can be aligned with the `&`
80/// symbol.
81///
82/// = Example <example>
83/// ```example
84/// $ mat(
85/// 1, 2, ..., 10;
86/// 2, 2, ..., 10;
87/// dots.v, dots.v, dots.down, dots.v;
88/// 10, 10, ..., 10;
89/// ) $
90/// ```
91#[elem(title = "Matrix", Mathy)]
92pub struct MatElem {
93 /// The delimiter to use.
94 ///
95 /// Can be a single character specifying the left delimiter, in which case
96 /// the right delimiter is inferred. Otherwise, can be an array containing a
97 /// left and a right delimiter.
98 ///
99 /// ```example
100 /// #set math.mat(delim: "[")
101 /// $ mat(1, 2; 3, 4) $
102 /// ```
103 #[default(DelimiterPair::PAREN)]
104 pub delim: DelimiterPair,
105
106 /// The horizontal alignment that each cell should have.
107 ///
108 /// ```example
109 /// #set math.mat(align: right)
110 /// $ mat(-1, 1, 1; 1, -1, 1; 1, 1, -1) $
111 /// ```
112 #[default(HAlignment::Center)]
113 pub align: HAlignment,
114
115 /// Draws augmentation lines in a matrix.
116 ///
117 /// - `{none}`: No lines are drawn.
118 /// - A single number: A vertical augmentation line is drawn after the
119 /// specified column number. Negative numbers start from the end.
120 /// - A dictionary: With a dictionary, multiple augmentation lines can be
121 /// drawn both horizontally and vertically. Additionally, the style of the
122 /// lines can be set. The dictionary can contain the following keys:
123 /// - `hline`: The offsets at which horizontal lines should be drawn. For
124 /// example, an offset of `2` would result in a horizontal line being
125 /// drawn after the second row of the matrix. Accepts either an integer
126 /// for a single line, or an array of integers for multiple lines. Like
127 /// for a single number, negative numbers start from the end.
128 /// - `vline`: The offsets at which vertical lines should be drawn. For
129 /// example, an offset of `2` would result in a vertical line being
130 /// drawn after the second column of the matrix. Accepts either an
131 /// integer for a single line, or an array of integers for multiple
132 /// lines. Like for a single number, negative numbers start from the
133 /// end.
134 /// - `stroke`: How to @stroke[stroke] the line. If set to `{auto}`, takes
135 /// on a thickness of 0.05 em and square line caps.
136 ///
137 /// #example(
138 /// title: "Basic usage",
139 /// ```
140 /// $ mat(1, 0, 1; 0, 1, 2; augment: #2) $
141 /// // Equivalent to:
142 /// $ mat(1, 0, 1; 0, 1, 2; augment: #(-1)) $
143 /// ```
144 /// )
145 ///
146 /// #example(
147 /// title: "Customizing the augmentation line",
148 /// ```
149 /// $ mat(0, 0, 0; 1, 1, 1; augment: #(hline: 1, stroke: 2pt + green)) $
150 /// ```
151 /// )
152 #[fold]
153 pub augment: Option<Augment>,
154
155 /// The gap between rows and columns.
156 ///
157 /// This is a shorthand to set `row-gap` and `column-gap` to the same value.
158 ///
159 /// ```example
160 /// #set math.mat(gap: 1em)
161 /// $ mat(1, 2; 3, 4) $
162 /// ```
163 #[external]
164 pub gap: Rel<Length>,
165
166 /// The gap between rows.
167 ///
168 /// ```example
169 /// #set math.mat(row-gap: 1em)
170 /// $ mat(1, 2; 3, 4) $
171 /// ```
172 #[parse(
173 let gap = args.named("gap")?;
174 args.named("row-gap")?.or(gap)
175 )]
176 #[default(DEFAULT_ROW_GAP.into())]
177 pub row_gap: Rel<Length>,
178
179 /// The gap between columns.
180 ///
181 /// ```example
182 /// #set math.mat(column-gap: 1em)
183 /// $ mat(1, 2; 3, 4) $
184 /// ```
185 #[parse(args.named("column-gap")?.or(gap))]
186 #[default(DEFAULT_COL_GAP.into())]
187 pub column_gap: Rel<Length>,
188
189 /// An array of arrays with the rows of the matrix.
190 ///
191 /// ```example
192 /// #let data = ((1, 2, 3), (4, 5, 6))
193 /// #let matrix = math.mat(..data)
194 /// $ v := matrix $
195 /// ```
196 #[variadic]
197 #[parse(
198 let mut rows = vec![];
199 let mut width = 0;
200
201 let values = args.all::<Spanned<Value>>()?;
202 if values.iter().any(|spanned| matches!(spanned.v, Value::Array(_))) {
203 for Spanned { v, span } in values {
204 let array = v.cast::<Array>().at(span)?;
205 let row: Vec<_> = array.into_iter().map(Value::display).collect();
206 width = width.max(row.len());
207 rows.push(row);
208 }
209 } else {
210 rows = vec![values.into_iter().map(|spanned| spanned.v.display()).collect()];
211 }
212
213 for row in &mut rows {
214 if row.len() < width {
215 row.resize(width, Content::empty());
216 }
217 }
218
219 rows
220 )]
221 pub rows: Vec<Vec<Content>>,
222}
223
224/// A case distinction.
225///
226/// Content across different branches can be aligned with the `&` symbol.
227///
228/// = Example <example>
229/// ```example
230/// $ f(x, y) := cases(
231/// 1 "if" (x dot y)/2 <= 0,
232/// 2 "if" x "is even",
233/// 3 "if" x in NN,
234/// 4 "else",
235/// ) $
236/// ```
237#[elem(Mathy)]
238pub struct CasesElem {
239 /// The delimiter to use.
240 ///
241 /// Can be a single character specifying the left delimiter, in which case
242 /// the right delimiter is inferred. Otherwise, can be an array containing a
243 /// left and a right delimiter.
244 ///
245 /// ```example
246 /// #set math.cases(delim: "[")
247 /// $ x = cases(1, 2) $
248 /// ```
249 #[default(DelimiterPair::BRACE)]
250 pub delim: DelimiterPair,
251
252 /// Whether the direction of cases should be reversed.
253 ///
254 /// ```example
255 /// #set math.cases(reverse: true)
256 /// $ cases(1, 2) = x $
257 /// ```
258 #[default(false)]
259 pub reverse: bool,
260
261 /// The gap between branches.
262 ///
263 /// ```example
264 /// #set math.cases(gap: 1em)
265 /// $ x = cases(1, 2) $
266 /// ```
267 #[default(DEFAULT_ROW_GAP.into())]
268 pub gap: Rel<Length>,
269
270 /// The branches of the case distinction.
271 #[variadic]
272 pub children: Vec<Content>,
273}
274
275/// A delimiter is a single character that is used to delimit a matrix, vector
276/// or cases. The character has to be a Unicode codepoint tagged as a math
277/// "opening", "closing" or "fence".
278///
279/// Typically, the delimiter is stretched to fit the height of whatever it
280/// delimits.
281#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
282pub struct Delimiter(Option<char>);
283
284cast! {
285 Delimiter,
286 self => self.0.into_value(),
287 _: NoneValue => Self::none(),
288 v: Symbol => Self::char(v.get().parse::<char>().map_err(|_| "expected a single-codepoint symbol")?)?,
289 v: char => Self::char(v)?,
290}
291
292impl Delimiter {
293 pub fn none() -> Self {
294 Self(None)
295 }
296
297 pub fn char(c: char) -> StrResult<Self> {
298 if !matches!(
299 default_math_class(c),
300 Some(MathClass::Opening | MathClass::Closing | MathClass::Fence),
301 ) {
302 bail!("invalid delimiter: \"{c}\"")
303 }
304 Ok(Self(Some(c)))
305 }
306
307 pub fn get(self) -> Option<char> {
308 self.0
309 }
310
311 pub fn find_matching(self) -> Self {
312 match self.0 {
313 None => Self::none(),
314 Some('[') => Self(Some(']')),
315 Some(']') => Self(Some('[')),
316 Some('{') => Self(Some('}')),
317 Some('}') => Self(Some('{')),
318 Some(c) => match default_math_class(c) {
319 Some(MathClass::Opening) => Self(char::from_u32(c as u32 + 1)),
320 Some(MathClass::Closing) => Self(char::from_u32(c as u32 - 1)),
321 _ => Self(Some(c)),
322 },
323 }
324 }
325}
326
327/// A pair of delimiters (one closing, one opening) used for matrices, vectors
328/// and cases.
329#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
330pub struct DelimiterPair {
331 open: Delimiter,
332 close: Delimiter,
333}
334
335cast! {
336 DelimiterPair,
337
338 self => array![self.open, self.close].into_value(),
339
340 v: Array => match v.as_slice() {
341 [open, close] => Self {
342 open: open.clone().cast()?,
343 close: close.clone().cast()?,
344 },
345 _ => bail!("expected 2 delimiters, found {}", v.len())
346 },
347 v: Delimiter => Self { open: v, close: v.find_matching() }
348}
349
350impl DelimiterPair {
351 const PAREN: Self = Self {
352 open: Delimiter(Some('(')),
353 close: Delimiter(Some(')')),
354 };
355 const BRACE: Self = Self {
356 open: Delimiter(Some('{')),
357 close: Delimiter(Some('}')),
358 };
359
360 /// The delimiter's opening character.
361 pub fn open(self) -> Option<char> {
362 self.open.get()
363 }
364
365 /// The delimiter's closing character.
366 pub fn close(self) -> Option<char> {
367 self.close.get()
368 }
369}
370
371/// Parameters specifying how augmentation lines
372/// should be drawn on a matrix.
373#[derive(Debug, Default, Clone, PartialEq, Hash)]
374pub struct Augment<T: Numeric = Length> {
375 pub hline: AugmentOffsets,
376 pub vline: AugmentOffsets,
377 pub stroke: Smart<Stroke<T>>,
378}
379
380impl<T: Numeric + Fold> Fold for Augment<T> {
381 fn fold(self, outer: Self) -> Self {
382 Self {
383 stroke: match (self.stroke, outer.stroke) {
384 (Smart::Custom(inner), Smart::Custom(outer)) => {
385 Smart::Custom(inner.fold(outer))
386 }
387 // Usually, folding an inner `auto` with an `outer` prefers
388 // the explicit `auto`. However, here `auto` means unspecified
389 // and thus we want `outer`.
390 (inner, outer) => inner.or(outer),
391 },
392 ..self
393 }
394 }
395}
396
397impl Resolve for Augment {
398 type Output = Augment<Abs>;
399
400 fn resolve(self, styles: StyleChain) -> Self::Output {
401 Augment {
402 hline: self.hline,
403 vline: self.vline,
404 stroke: self.stroke.resolve(styles),
405 }
406 }
407}
408
409cast! {
410 Augment,
411 self => {
412 // if the stroke is auto and there is only one vertical line,
413 if self.stroke.is_auto() && self.hline.0.is_empty() && self.vline.0.len() == 1 {
414 return self.vline.0[0].into_value();
415 }
416
417 dict! {
418 "hline" => self.hline,
419 "vline" => self.vline,
420 "stroke" => self.stroke,
421 }.into_value()
422 },
423 v: isize => Augment {
424 hline: AugmentOffsets::default(),
425 vline: AugmentOffsets(smallvec![v]),
426 stroke: Smart::Auto,
427 },
428 mut dict: Dict => {
429 let mut take = |key| dict.take(key).ok().map(AugmentOffsets::from_value).transpose();
430 let hline = take("hline")?.unwrap_or_default();
431 let vline = take("vline")?.unwrap_or_default();
432 let stroke = dict.take("stroke")
433 .ok()
434 .map(Stroke::from_value)
435 .transpose()?
436 .map(Smart::Custom)
437 .unwrap_or(Smart::Auto);
438 Augment { hline, vline, stroke }
439 },
440}
441
442cast! {
443 Augment<Abs>,
444 self => self.into_value(),
445}
446
447/// The offsets at which augmentation lines should be drawn on a matrix.
448#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
449pub struct AugmentOffsets(pub SmallVec<[isize; 1]>);
450
451cast! {
452 AugmentOffsets,
453 self => self.0.into_value(),
454 v: isize => Self(smallvec![v]),
455 v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
456}