typst_library/math/
matrix.rs

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