1use std::sync::Arc;
14
15use serde::{Deserialize, Serialize};
16
17use crate::layout_context::LayoutContext;
18use crate::layout_geometry::{layout_error, validate_anchor_graph};
19
20use crate::{
21 AssetResolver, CollisionPolicy, DocumentFingerprint, DocumentIr, ElementIr, FontManager,
22 LayoutMode, OperationControl, ProgressPhase, Rect, ResolvedScene, ResourceLimits, Result,
23 SceneCache, Transform, Unit, ENGINE_VERSION,
24};
25
26#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
28pub struct LayoutOptions {
29 pub collision: CollisionPolicy,
31 pub logical_unit: Unit,
33 pub minimum_size: Unit,
35 pub collision_gap: Unit,
37}
38
39impl Default for LayoutOptions {
40 fn default() -> Self {
41 Self {
42 collision: CollisionPolicy::default(),
43 logical_unit: Unit::from_raw(Unit::PER_POINT),
44 minimum_size: Unit::from_raw(Unit::PER_POINT),
45 collision_gap: Unit::ZERO,
46 }
47 }
48}
49
50pub struct LayoutEngine<'a> {
52 pub(crate) limits: &'a ResourceLimits,
53 pub(crate) fonts: &'a FontManager,
54 pub(crate) options: LayoutOptions,
55 pub(crate) control: OperationControl,
56 pub(crate) assets: Option<&'a dyn AssetResolver>,
57}
58
59#[derive(Clone)]
60pub(crate) struct ElementPlacement {
61 pub(crate) initial_page: usize,
62 pub(crate) page_index: usize,
63 pub(crate) proposed: Rect,
64 pub(crate) rect: Rect,
65 pub(crate) policy: CollisionPolicy,
66 pub(crate) transform: Transform,
67}
68
69impl<'a> LayoutEngine<'a> {
70 pub fn new(
72 limits: &'a ResourceLimits,
73 fonts: &'a FontManager,
74 options: LayoutOptions,
75 ) -> Result<Self> {
76 Self::new_controlled(limits, fonts, options, OperationControl::default())
77 }
78
79 pub fn new_controlled(
81 limits: &'a ResourceLimits,
82 fonts: &'a FontManager,
83 options: LayoutOptions,
84 control: OperationControl,
85 ) -> Result<Self> {
86 limits.validate()?;
87 if options.logical_unit <= Unit::ZERO
88 || options.minimum_size <= Unit::ZERO
89 || options.collision_gap < Unit::ZERO
90 {
91 return Err(layout_error("layout options contain invalid dimensions"));
92 }
93 Ok(Self {
94 limits,
95 fonts,
96 options,
97 control,
98 assets: None,
99 })
100 }
101
102 #[must_use]
104 pub fn with_assets(mut self, assets: &'a dyn AssetResolver) -> Self {
105 self.assets = Some(assets);
106 self
107 }
108
109 pub fn resolve(&self, document: &DocumentIr) -> Result<ResolvedScene> {
111 self.control.checkpoint(ProgressPhase::Layout, 0, None)?;
112 let page_size = document
113 .page_size
114 .ok_or_else(|| layout_error("document/canvas requires an explicit page size"))?;
115 validate_anchor_graph(&document.elements)?;
116 let exclusions = crate::layout_exclusion::resolve_exclusions(
117 document,
118 page_size,
119 self.options.logical_unit,
120 )?;
121 let mut context = LayoutContext::new(
122 page_size,
123 document.page_template.clone(),
124 exclusions,
125 self.limits.max_pages,
126 self.limits.max_elements,
127 );
128 let page_rect = document.page_template.as_ref().map_or_else(
129 || Rect::new(Unit::ZERO, Unit::ZERO, page_size.width, page_size.height),
130 crate::PageTemplate::content_bounds,
131 )?;
132 let document_collision = document
133 .collision
134 .as_ref()
135 .unwrap_or(&self.options.collision);
136 let page_collision = document
137 .page_collision
138 .as_ref()
139 .unwrap_or(document_collision);
140 let regions =
141 crate::layout_region::resolve_regions(document, page_rect, self.options.logical_unit)?;
142 context.regions = regions;
143 self.layout_list(
144 &document.elements,
145 document,
146 page_rect,
147 0,
148 LayoutMode::Absolute,
149 crate::Distribution::Start,
150 Unit::ZERO,
151 page_collision,
152 Transform::IDENTITY,
153 &mut context,
154 )?;
155 context.remove_leading_empty_pages();
156 crate::layout_page::resolve_page_layers(self, document, page_collision, &mut context)?;
157 for page in &mut context.pages {
158 page.elements.sort_by(|left, right| {
159 (&left.layer, left.z_index, left.sequence).cmp(&(
160 &right.layer,
161 right.z_index,
162 right.sequence,
163 ))
164 });
165 }
166 self.control.checkpoint(
167 ProgressPhase::Layout,
168 u64::try_from(context.sequence).unwrap_or(u64::MAX),
169 Some(u64::try_from(self.limits.max_elements).unwrap_or(u64::MAX)),
170 )?;
171 Ok(ResolvedScene {
172 template_id: document.template_id.clone(),
173 pages: context.pages,
174 engine_version: ENGINE_VERSION.to_owned(),
175 })
176 }
177
178 pub fn resolve_cached(
183 &self,
184 document: &DocumentIr,
185 fingerprint: DocumentFingerprint,
186 cache: &mut SceneCache,
187 ) -> Result<Arc<ResolvedScene>> {
188 cache.get_or_try_insert_with(fingerprint, self.limits, || self.resolve(document))
189 }
190
191 #[allow(clippy::too_many_arguments)]
192 pub(crate) fn layout_list(
193 &self,
194 elements: &[ElementIr],
195 document: &DocumentIr,
196 container: Rect,
197 mut page_index: usize,
198 parent_layout: LayoutMode,
199 distribution: crate::Distribution,
200 gap: Unit,
201 inherited_collision: &CollisionPolicy,
202 parent_transform: Transform,
203 context: &mut LayoutContext,
204 ) -> Result<()> {
205 let flow = crate::layout_flow::plan_flow(
206 elements,
207 container,
208 parent_layout,
209 distribution,
210 gap,
211 self.options.logical_unit,
212 )?;
213 let mut flow_x = flow.x;
214 let mut flow_y = flow.y;
215 let effective_gap = flow.gap;
216 let visible: Vec<_> = elements
217 .iter()
218 .filter(|element| element.page_placement.is_none())
219 .filter(|element| !element.hidden)
220 .collect();
221 let mut index = 0;
222 while index < visible.len() {
223 let element = visible[index];
224 context.checkpoint(&self.control, self.limits.max_elements)?;
225 crate::layout_keep::validate_flow(parent_layout, element)?;
226 if element.keep_with_next {
227 (page_index, flow_y) = self.defer_kept_block(
228 &visible,
229 index,
230 container,
231 flow_x,
232 flow_y,
233 effective_gap,
234 page_index,
235 document,
236 context,
237 )?;
238 }
239 page_index = self.layout_element_at_flow(
240 element,
241 document,
242 container,
243 page_index,
244 parent_layout,
245 &mut flow_x,
246 &mut flow_y,
247 effective_gap,
248 inherited_collision,
249 parent_transform,
250 context,
251 )?;
252 index += 1;
253 }
254 Ok(())
255 }
256
257 #[allow(clippy::too_many_arguments)]
258 fn defer_kept_block(
259 &self,
260 visible: &[&ElementIr],
261 index: usize,
262 container: Rect,
263 flow_x: Unit,
264 flow_y: Unit,
265 gap: Unit,
266 page_index: usize,
267 document: &DocumentIr,
268 context: &LayoutContext,
269 ) -> Result<(usize, Unit)> {
270 let mut end = index + 1;
271 while end < visible.len() && visible[end - 1].keep_with_next {
272 end += 1;
273 }
274 if end == index + 1 || visible[end - 1].keep_with_next {
275 return Err(
276 layout_error("keep_with_next must be followed by another visible sibling")
277 .at(visible[index].id.as_str()),
278 );
279 }
280 let height = crate::layout_keep::block_height(
281 &visible[index..end],
282 container,
283 flow_x,
284 flow_y,
285 gap,
286 self.options.logical_unit,
287 &context.positions,
288 &document.guides,
289 self.fonts,
290 )?;
291 if crate::layout_keep::should_defer(height, flow_y, container)? {
292 Ok((
293 crate::layout_keep::next_page(page_index)?,
294 container.origin.y,
295 ))
296 } else {
297 Ok((page_index, flow_y))
298 }
299 }
300
301 #[allow(clippy::too_many_arguments)]
302 fn layout_element_at_flow(
303 &self,
304 element: &ElementIr,
305 document: &DocumentIr,
306 container: Rect,
307 page_index: usize,
308 parent_layout: LayoutMode,
309 flow_x: &mut Unit,
310 flow_y: &mut Unit,
311 gap: Unit,
312 inherited_collision: &CollisionPolicy,
313 parent_transform: Transform,
314 context: &mut LayoutContext,
315 ) -> Result<usize> {
316 let flow_origin = crate::Point {
317 x: *flow_x,
318 y: *flow_y,
319 };
320 if let Some(last) = self.commit_expanded_text_fragments(
321 element,
322 document,
323 container,
324 page_index,
325 parent_layout,
326 flow_origin,
327 inherited_collision,
328 parent_transform,
329 context,
330 )? {
331 self.record_flow_end(element, &last, parent_layout, gap, flow_x, flow_y, context)?;
332 return Ok(last.page_index);
333 }
334 let placement_page = crate::layout_geometry::anchor_page(
335 element,
336 &context.positions,
337 self.options.logical_unit,
338 )?
339 .unwrap_or(page_index);
340 let placement = self.place_element(
341 element,
342 document,
343 container,
344 placement_page,
345 parent_layout,
346 flow_origin,
347 inherited_collision,
348 parent_transform,
349 context,
350 )?;
351 if element.kind == crate::ElementKind::Table {
352 let last = self.commit_table_fragments(
353 element,
354 document,
355 container,
356 parent_layout,
357 flow_origin,
358 inherited_collision,
359 parent_transform,
360 placement,
361 context,
362 )?;
363 self.record_flow_end(element, &last, parent_layout, gap, flow_x, flow_y, context)?;
364 return Ok(last.page_index);
365 }
366 self.commit_regular_element(
367 element,
368 document,
369 placement,
370 parent_layout,
371 gap,
372 flow_x,
373 flow_y,
374 context,
375 )
376 }
377
378 #[allow(clippy::too_many_arguments)]
379 fn record_flow_end(
380 &self,
381 element: &ElementIr,
382 last: &ElementPlacement,
383 parent_layout: LayoutMode,
384 gap: Unit,
385 flow_x: &mut Unit,
386 flow_y: &mut Unit,
387 context: &mut LayoutContext,
388 ) -> Result<()> {
389 context
390 .positions
391 .insert(element.id.as_str().to_owned(), (last.page_index, last.rect));
392 advance_flow(parent_layout, last.rect, gap, flow_x, flow_y)?;
393 Ok(())
394 }
395
396 #[allow(clippy::too_many_arguments)]
397 fn commit_regular_element(
398 &self,
399 element: &ElementIr,
400 document: &DocumentIr,
401 placement: ElementPlacement,
402 parent_layout: LayoutMode,
403 gap: Unit,
404 flow_x: &mut Unit,
405 flow_y: &mut Unit,
406 context: &mut LayoutContext,
407 ) -> Result<usize> {
408 let resolved = self.build_resolved_element(element, &placement, context)?;
409 context.commit(placement.page_index, resolved, placement.policy.clone())?;
410 context.positions.insert(
411 element.id.as_str().to_owned(),
412 (placement.page_index, placement.rect),
413 );
414 advance_flow(parent_layout, placement.rect, gap, flow_x, flow_y)?;
415 let child_gap = element
416 .gap
417 .resolve(placement.rect.size.width, self.options.logical_unit)?
418 .unwrap_or(Unit::ZERO);
419 self.layout_list(
420 &element.children,
421 document,
422 placement.rect,
423 placement.page_index,
424 element.layout,
425 element.distribute,
426 child_gap,
427 &placement.policy,
428 placement.transform,
429 context,
430 )?;
431 Ok(placement.page_index)
432 }
433}
434
435fn advance_flow(
436 layout: LayoutMode,
437 rect: Rect,
438 gap: Unit,
439 flow_x: &mut Unit,
440 flow_y: &mut Unit,
441) -> Result<()> {
442 match layout {
443 LayoutMode::FlowVertical => *flow_y = rect.bottom()?.checked_add(gap)?,
444 LayoutMode::FlowHorizontal => *flow_x = rect.right()?.checked_add(gap)?,
445 LayoutMode::Absolute => {}
446 }
447 Ok(())
448}