typst_library/layout/
transform.rs

1use crate::diag::SourceResult;
2use crate::engine::Engine;
3use crate::foundations::{
4    cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain,
5};
6use crate::layout::{
7    Abs, Alignment, Angle, BlockElem, HAlignment, Length, Ratio, Rel, VAlignment,
8};
9
10/// Moves content without affecting layout.
11///
12/// The `move` function allows you to move content while the layout still 'sees'
13/// it at the original positions. Containers will still be sized as if the
14/// content was not moved.
15///
16/// # Example
17/// ```example
18/// #rect(inset: 0pt, move(
19///   dx: 6pt, dy: 6pt,
20///   rect(
21///     inset: 8pt,
22///     fill: white,
23///     stroke: black,
24///     [Abra cadabra]
25///   )
26/// ))
27/// ```
28#[elem(Show)]
29pub struct MoveElem {
30    /// The horizontal displacement of the content.
31    pub dx: Rel<Length>,
32
33    /// The vertical displacement of the content.
34    pub dy: Rel<Length>,
35
36    /// The content to move.
37    #[required]
38    pub body: Content,
39}
40
41impl Show for Packed<MoveElem> {
42    fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
43        Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_move)
44            .pack()
45            .spanned(self.span()))
46    }
47}
48
49/// Rotates content without affecting layout.
50///
51/// Rotates an element by a given angle. The layout will act as if the element
52/// was not rotated unless you specify `{reflow: true}`.
53///
54/// # Example
55/// ```example
56/// #stack(
57///   dir: ltr,
58///   spacing: 1fr,
59///   ..range(16)
60///     .map(i => rotate(24deg * i)[X]),
61/// )
62/// ```
63#[elem(Show)]
64pub struct RotateElem {
65    /// The amount of rotation.
66    ///
67    /// ```example
68    /// #rotate(-1.571rad)[Space!]
69    /// ```
70    ///
71    #[positional]
72    pub angle: Angle,
73
74    /// The origin of the rotation.
75    ///
76    /// If, for instance, you wanted the bottom left corner of the rotated
77    /// element to stay aligned with the baseline, you would set it to `bottom +
78    /// left` instead.
79    ///
80    /// ```example
81    /// #set text(spacing: 8pt)
82    /// #let square = square.with(width: 8pt)
83    ///
84    /// #box(square())
85    /// #box(rotate(30deg, origin: center, square()))
86    /// #box(rotate(30deg, origin: top + left, square()))
87    /// #box(rotate(30deg, origin: bottom + right, square()))
88    /// ```
89    #[fold]
90    #[default(HAlignment::Center + VAlignment::Horizon)]
91    pub origin: Alignment,
92
93    /// Whether the rotation impacts the layout.
94    ///
95    /// If set to `{false}`, the rotated content will retain the bounding box of
96    /// the original content. If set to `{true}`, the bounding box will take the
97    /// rotation of the content into account and adjust the layout accordingly.
98    ///
99    /// ```example
100    /// Hello #rotate(90deg, reflow: true)[World]!
101    /// ```
102    #[default(false)]
103    pub reflow: bool,
104
105    /// The content to rotate.
106    #[required]
107    pub body: Content,
108}
109
110impl Show for Packed<RotateElem> {
111    fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
112        Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rotate)
113            .pack()
114            .spanned(self.span()))
115    }
116}
117
118/// Scales content without affecting layout.
119///
120/// Lets you mirror content by specifying a negative scale on a single axis.
121///
122/// # Example
123/// ```example
124/// #set align(center)
125/// #scale(x: -100%)[This is mirrored.]
126/// #scale(x: -100%, reflow: true)[This is mirrored.]
127/// ```
128#[elem(Show)]
129pub struct ScaleElem {
130    /// The scaling factor for both axes, as a positional argument. This is just
131    /// an optional shorthand notation for setting `x` and `y` to the same
132    /// value.
133    #[external]
134    #[positional]
135    #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
136    pub factor: Smart<ScaleAmount>,
137
138    /// The horizontal scaling factor.
139    ///
140    /// The body will be mirrored horizontally if the parameter is negative.
141    #[parse(
142        let all = args.find()?;
143        args.named("x")?.or(all)
144    )]
145    #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
146    pub x: Smart<ScaleAmount>,
147
148    /// The vertical scaling factor.
149    ///
150    /// The body will be mirrored vertically if the parameter is negative.
151    #[parse(args.named("y")?.or(all))]
152    #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
153    pub y: Smart<ScaleAmount>,
154
155    /// The origin of the transformation.
156    ///
157    /// ```example
158    /// A#box(scale(75%)[A])A \
159    /// B#box(scale(75%, origin: bottom + left)[B])B
160    /// ```
161    #[fold]
162    #[default(HAlignment::Center + VAlignment::Horizon)]
163    pub origin: Alignment,
164
165    /// Whether the scaling impacts the layout.
166    ///
167    /// If set to `{false}`, the scaled content will be allowed to overlap
168    /// other content. If set to `{true}`, it will compute the new size of
169    /// the scaled content and adjust the layout accordingly.
170    ///
171    /// ```example
172    /// Hello #scale(x: 20%, y: 40%, reflow: true)[World]!
173    /// ```
174    #[default(false)]
175    pub reflow: bool,
176
177    /// The content to scale.
178    #[required]
179    pub body: Content,
180}
181
182impl Show for Packed<ScaleElem> {
183    fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
184        Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_scale)
185            .pack()
186            .spanned(self.span()))
187    }
188}
189
190/// To what size something shall be scaled.
191#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
192pub enum ScaleAmount {
193    Ratio(Ratio),
194    Length(Length),
195}
196
197cast! {
198    ScaleAmount,
199    self => match self {
200        ScaleAmount::Ratio(ratio) => ratio.into_value(),
201        ScaleAmount::Length(length) => length.into_value(),
202    },
203    ratio: Ratio => ScaleAmount::Ratio(ratio),
204    length: Length => ScaleAmount::Length(length),
205}
206
207/// Skews content.
208///
209/// Skews an element in horizontal and/or vertical direction. The layout will
210/// act as if the element was not skewed unless you specify `{reflow: true}`.
211///
212/// # Example
213/// ```example
214/// #skew(ax: -12deg)[
215///   This is some fake italic text.
216/// ]
217/// ```
218#[elem(Show)]
219pub struct SkewElem {
220    /// The horizontal skewing angle.
221    ///
222    /// ```example
223    /// #skew(ax: 30deg)[Skewed]
224    /// ```
225    ///
226    #[default(Angle::zero())]
227    pub ax: Angle,
228
229    /// The vertical skewing angle.
230    ///
231    /// ```example
232    /// #skew(ay: 30deg)[Skewed]
233    /// ```
234    ///
235    #[default(Angle::zero())]
236    pub ay: Angle,
237
238    /// The origin of the skew transformation.
239    ///
240    /// The origin will stay fixed during the operation.
241    ///
242    /// ```example
243    /// X #box(skew(ax: -30deg, origin: center + horizon)[X]) X \
244    /// X #box(skew(ax: -30deg, origin: bottom + left)[X]) X \
245    /// X #box(skew(ax: -30deg, origin: top + right)[X]) X
246    /// ```
247    #[fold]
248    #[default(HAlignment::Center + VAlignment::Horizon)]
249    pub origin: Alignment,
250
251    /// Whether the skew transformation impacts the layout.
252    ///
253    /// If set to `{false}`, the skewed content will retain the bounding box of
254    /// the original content. If set to `{true}`, the bounding box will take the
255    /// transformation of the content into account and adjust the layout accordingly.
256    ///
257    /// ```example
258    /// Hello #skew(ay: 30deg, reflow: true, "World")!
259    /// ```
260    #[default(false)]
261    pub reflow: bool,
262
263    /// The content to skew.
264    #[required]
265    pub body: Content,
266}
267
268impl Show for Packed<SkewElem> {
269    fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
270        Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_skew)
271            .pack()
272            .spanned(self.span()))
273    }
274}
275
276/// A scale-skew-translate transformation.
277#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
278pub struct Transform {
279    pub sx: Ratio,
280    pub ky: Ratio,
281    pub kx: Ratio,
282    pub sy: Ratio,
283    pub tx: Abs,
284    pub ty: Abs,
285}
286
287impl Transform {
288    /// The identity transformation.
289    pub const fn identity() -> Self {
290        Self {
291            sx: Ratio::one(),
292            ky: Ratio::zero(),
293            kx: Ratio::zero(),
294            sy: Ratio::one(),
295            tx: Abs::zero(),
296            ty: Abs::zero(),
297        }
298    }
299
300    /// A translate transform.
301    pub const fn translate(tx: Abs, ty: Abs) -> Self {
302        Self { tx, ty, ..Self::identity() }
303    }
304
305    /// A scale transform.
306    pub const fn scale(sx: Ratio, sy: Ratio) -> Self {
307        Self { sx, sy, ..Self::identity() }
308    }
309
310    /// A rotate transform.
311    pub fn rotate(angle: Angle) -> Self {
312        let cos = Ratio::new(angle.cos());
313        let sin = Ratio::new(angle.sin());
314        Self {
315            sx: cos,
316            ky: sin,
317            kx: -sin,
318            sy: cos,
319            ..Self::default()
320        }
321    }
322
323    /// A skew transform.
324    pub fn skew(ax: Angle, ay: Angle) -> Self {
325        Self {
326            kx: Ratio::new(ax.tan()),
327            ky: Ratio::new(ay.tan()),
328            ..Self::identity()
329        }
330    }
331
332    /// Whether this is the identity transformation.
333    pub fn is_identity(self) -> bool {
334        self == Self::identity()
335    }
336
337    /// Pre-concatenate another transformation.
338    pub fn pre_concat(self, prev: Self) -> Self {
339        Transform {
340            sx: self.sx * prev.sx + self.kx * prev.ky,
341            ky: self.ky * prev.sx + self.sy * prev.ky,
342            kx: self.sx * prev.kx + self.kx * prev.sy,
343            sy: self.ky * prev.kx + self.sy * prev.sy,
344            tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx,
345            ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty,
346        }
347    }
348
349    /// Post-concatenate another transformation.
350    pub fn post_concat(self, next: Self) -> Self {
351        next.pre_concat(self)
352    }
353
354    /// Inverts the transformation.
355    ///
356    /// Returns `None` if the determinant of the matrix is zero.
357    pub fn invert(self) -> Option<Self> {
358        // Allow the trivial case to be inlined.
359        if self.is_identity() {
360            return Some(self);
361        }
362
363        // Fast path for scale-translate-only transforms.
364        if self.kx.is_zero() && self.ky.is_zero() {
365            if self.sx.is_zero() || self.sy.is_zero() {
366                return Some(Self::translate(-self.tx, -self.ty));
367            }
368
369            let inv_x = 1.0 / self.sx;
370            let inv_y = 1.0 / self.sy;
371            return Some(Self {
372                sx: Ratio::new(inv_x),
373                ky: Ratio::zero(),
374                kx: Ratio::zero(),
375                sy: Ratio::new(inv_y),
376                tx: -self.tx * inv_x,
377                ty: -self.ty * inv_y,
378            });
379        }
380
381        let det = self.sx * self.sy - self.kx * self.ky;
382        if det.get().abs() < 1e-12 {
383            return None;
384        }
385
386        let inv_det = 1.0 / det;
387        Some(Self {
388            sx: (self.sy * inv_det),
389            ky: (-self.ky * inv_det),
390            kx: (-self.kx * inv_det),
391            sy: (self.sx * inv_det),
392            tx: Abs::pt(
393                (self.kx.get() * self.ty.to_pt() - self.sy.get() * self.tx.to_pt())
394                    * inv_det,
395            ),
396            ty: Abs::pt(
397                (self.ky.get() * self.tx.to_pt() - self.sx.get() * self.ty.to_pt())
398                    * inv_det,
399            ),
400        })
401    }
402}
403
404impl Default for Transform {
405    fn default() -> Self {
406        Self::identity()
407    }
408}