typst_layout/
transforms.rs

1use std::cell::LazyCell;
2
3use typst_library::diag::{bail, SourceResult};
4use typst_library::engine::Engine;
5use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
6use typst_library::introspection::Locator;
7use typst_library::layout::{
8    Abs, Axes, FixedAlignment, Frame, MoveElem, Point, Ratio, Region, Rel, RotateElem,
9    ScaleAmount, ScaleElem, Size, SkewElem, Transform,
10};
11use typst_utils::Numeric;
12
13/// Layout the moved content.
14#[typst_macros::time(span = elem.span())]
15pub fn layout_move(
16    elem: &Packed<MoveElem>,
17    engine: &mut Engine,
18    locator: Locator,
19    styles: StyleChain,
20    region: Region,
21) -> SourceResult<Frame> {
22    let mut frame = crate::layout_frame(engine, &elem.body, locator, styles, region)?;
23    let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
24    let delta = delta.zip_map(region.size, Rel::relative_to);
25    frame.translate(delta.to_point());
26    Ok(frame)
27}
28
29/// Layout the rotated content.
30#[typst_macros::time(span = elem.span())]
31pub fn layout_rotate(
32    elem: &Packed<RotateElem>,
33    engine: &mut Engine,
34    locator: Locator,
35    styles: StyleChain,
36    region: Region,
37) -> SourceResult<Frame> {
38    let angle = elem.angle(styles);
39    let align = elem.origin(styles).resolve(styles);
40
41    // Compute the new region's approximate size.
42    let is_finite = region.size.is_finite();
43    let size = if is_finite {
44        compute_bounding_box(region.size, Transform::rotate(-angle)).1
45    } else {
46        Size::splat(Abs::inf())
47    };
48
49    measure_and_layout(
50        engine,
51        locator,
52        region,
53        size,
54        styles,
55        &elem.body,
56        Transform::rotate(angle),
57        align,
58        elem.reflow(styles),
59    )
60}
61
62/// Layout the scaled content.
63#[typst_macros::time(span = elem.span())]
64pub fn layout_scale(
65    elem: &Packed<ScaleElem>,
66    engine: &mut Engine,
67    locator: Locator,
68    styles: StyleChain,
69    region: Region,
70) -> SourceResult<Frame> {
71    // Compute the new region's approximate size.
72    let scale = resolve_scale(elem, engine, locator.relayout(), region.size, styles)?;
73    let size = region
74        .size
75        .zip_map(scale, |r, s| if r.is_finite() { Ratio::new(1.0 / s).of(r) } else { r })
76        .map(Abs::abs);
77
78    measure_and_layout(
79        engine,
80        locator,
81        region,
82        size,
83        styles,
84        &elem.body,
85        Transform::scale(scale.x, scale.y),
86        elem.origin(styles).resolve(styles),
87        elem.reflow(styles),
88    )
89}
90
91/// Resolves scale parameters, preserving aspect ratio if one of the scales
92/// is set to `auto`.
93fn resolve_scale(
94    elem: &Packed<ScaleElem>,
95    engine: &mut Engine,
96    locator: Locator,
97    container: Size,
98    styles: StyleChain,
99) -> SourceResult<Axes<Ratio>> {
100    fn resolve_axis(
101        axis: Smart<ScaleAmount>,
102        body: impl Fn() -> SourceResult<Abs>,
103        styles: StyleChain,
104    ) -> SourceResult<Smart<Ratio>> {
105        Ok(match axis {
106            Smart::Auto => Smart::Auto,
107            Smart::Custom(amt) => Smart::Custom(match amt {
108                ScaleAmount::Ratio(ratio) => ratio,
109                ScaleAmount::Length(length) => {
110                    let length = length.resolve(styles);
111                    Ratio::new(length / body()?)
112                }
113            }),
114        })
115    }
116
117    let size = LazyCell::new(|| {
118        let pod = Region::new(container, Axes::splat(false));
119        let frame = crate::layout_frame(engine, &elem.body, locator, styles, pod)?;
120        SourceResult::Ok(frame.size())
121    });
122
123    let x = resolve_axis(
124        elem.x(styles),
125        || size.as_ref().map(|size| size.x).map_err(Clone::clone),
126        styles,
127    )?;
128
129    let y = resolve_axis(
130        elem.y(styles),
131        || size.as_ref().map(|size| size.y).map_err(Clone::clone),
132        styles,
133    )?;
134
135    match (x, y) {
136        (Smart::Auto, Smart::Auto) => {
137            bail!(elem.span(), "x and y cannot both be auto")
138        }
139        (Smart::Custom(x), Smart::Custom(y)) => Ok(Axes::new(x, y)),
140        (Smart::Auto, Smart::Custom(v)) | (Smart::Custom(v), Smart::Auto) => {
141            Ok(Axes::splat(v))
142        }
143    }
144}
145
146/// Layout the skewed content.
147#[typst_macros::time(span = elem.span())]
148pub fn layout_skew(
149    elem: &Packed<SkewElem>,
150    engine: &mut Engine,
151    locator: Locator,
152    styles: StyleChain,
153    region: Region,
154) -> SourceResult<Frame> {
155    let ax = elem.ax(styles);
156    let ay = elem.ay(styles);
157    let align = elem.origin(styles).resolve(styles);
158
159    // Compute the new region's approximate size.
160    let size = if region.size.is_finite() {
161        compute_bounding_box(region.size, Transform::skew(ax, ay)).1
162    } else {
163        Size::splat(Abs::inf())
164    };
165
166    measure_and_layout(
167        engine,
168        locator,
169        region,
170        size,
171        styles,
172        &elem.body,
173        Transform::skew(ax, ay),
174        align,
175        elem.reflow(styles),
176    )
177}
178
179/// Applies a transformation to a frame, reflowing the layout if necessary.
180#[allow(clippy::too_many_arguments)]
181fn measure_and_layout(
182    engine: &mut Engine,
183    locator: Locator,
184    region: Region,
185    size: Size,
186    styles: StyleChain,
187    body: &Content,
188    transform: Transform,
189    align: Axes<FixedAlignment>,
190    reflow: bool,
191) -> SourceResult<Frame> {
192    if reflow {
193        // Measure the size of the body.
194        let pod = Region::new(size, Axes::splat(false));
195        let frame = crate::layout_frame(engine, body, locator.relayout(), styles, pod)?;
196
197        // Actually perform the layout.
198        let pod = Region::new(frame.size(), Axes::splat(true));
199        let mut frame = crate::layout_frame(engine, body, locator, styles, pod)?;
200        let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
201
202        // Compute the transform.
203        let ts = Transform::translate(x, y)
204            .pre_concat(transform)
205            .pre_concat(Transform::translate(-x, -y));
206
207        // Compute the bounding box and offset and wrap in a new frame.
208        let (offset, size) = compute_bounding_box(frame.size(), ts);
209        frame.transform(ts);
210        frame.translate(offset);
211        frame.set_size(size);
212        Ok(frame)
213    } else {
214        // Layout the body.
215        let mut frame = crate::layout_frame(engine, body, locator, styles, region)?;
216        let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
217
218        // Compute the transform.
219        let ts = Transform::translate(x, y)
220            .pre_concat(transform)
221            .pre_concat(Transform::translate(-x, -y));
222
223        // Apply the transform.
224        frame.transform(ts);
225        Ok(frame)
226    }
227}
228
229/// Computes the bounding box and offset of a transformed area.
230fn compute_bounding_box(size: Size, ts: Transform) -> (Point, Size) {
231    let top_left = Point::zero().transform_inf(ts);
232    let top_right = Point::with_x(size.x).transform_inf(ts);
233    let bottom_left = Point::with_y(size.y).transform_inf(ts);
234    let bottom_right = size.to_point().transform_inf(ts);
235
236    // We first compute the new bounding box of the rotated area.
237    let min_x = top_left.x.min(top_right.x).min(bottom_left.x).min(bottom_right.x);
238    let min_y = top_left.y.min(top_right.y).min(bottom_left.y).min(bottom_right.y);
239    let max_x = top_left.x.max(top_right.x).max(bottom_left.x).max(bottom_right.x);
240    let max_y = top_left.y.max(top_right.y).max(bottom_left.y).max(bottom_right.y);
241
242    // Then we compute the new size of the area.
243    let width = max_x - min_x;
244    let height = max_y - min_y;
245
246    (Point::new(-min_x, -min_y), Size::new(width.abs(), height.abs()))
247}