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#[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#[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 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#[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 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
91fn 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#[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 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#[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 let pod = Region::new(size, Axes::splat(false));
195 let frame = crate::layout_frame(engine, body, locator.relayout(), styles, pod)?;
196
197 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 let ts = Transform::translate(x, y)
204 .pre_concat(transform)
205 .pre_concat(Transform::translate(-x, -y));
206
207 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 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 let ts = Transform::translate(x, y)
220 .pre_concat(transform)
221 .pre_concat(Transform::translate(-x, -y));
222
223 frame.transform(ts);
225 Ok(frame)
226 }
227}
228
229fn 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 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 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}