1use std::sync::Arc;
14
15use serde::{Deserialize, Serialize};
16
17use crate::layout_context::LayoutContext;
18use crate::layout_geometry::{
19 layout_error, propose_rect, resolve_layout_rect, resolve_transform, select_collision_bounds,
20 shape_for, validate_anchor_graph, visual_bounds,
21};
22use crate::layout_measure::{measure_content, resolve_image};
23
24use crate::{
25 AssetResolver, BoundsSet, CollisionPolicy, DocumentFingerprint, DocumentIr, ElementIr,
26 FontManager, LayoutMode, OperationControl, ProgressPhase, Rect, ResolvedElement, ResolvedScene,
27 ResourceLimits, Result, SceneCache, TextOverflow, Transform, Unit, ENGINE_VERSION,
28};
29
30#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
32pub struct LayoutOptions {
33 pub collision: CollisionPolicy,
35 pub logical_unit: Unit,
37 pub minimum_size: Unit,
39 pub collision_gap: Unit,
41}
42
43impl Default for LayoutOptions {
44 fn default() -> Self {
45 Self {
46 collision: CollisionPolicy::default(),
47 logical_unit: Unit::from_raw(Unit::PER_POINT),
48 minimum_size: Unit::from_raw(Unit::PER_POINT),
49 collision_gap: Unit::ZERO,
50 }
51 }
52}
53
54pub struct LayoutEngine<'a> {
56 pub(crate) limits: &'a ResourceLimits,
57 fonts: &'a FontManager,
58 pub(crate) options: LayoutOptions,
59 control: OperationControl,
60 assets: Option<&'a dyn AssetResolver>,
61}
62
63#[derive(Clone)]
64struct ElementPlacement {
65 initial_page: usize,
66 page_index: usize,
67 proposed: Rect,
68 rect: Rect,
69 policy: CollisionPolicy,
70 transform: Transform,
71}
72
73impl<'a> LayoutEngine<'a> {
74 pub fn new(
76 limits: &'a ResourceLimits,
77 fonts: &'a FontManager,
78 options: LayoutOptions,
79 ) -> Result<Self> {
80 Self::new_controlled(limits, fonts, options, OperationControl::default())
81 }
82
83 pub fn new_controlled(
85 limits: &'a ResourceLimits,
86 fonts: &'a FontManager,
87 options: LayoutOptions,
88 control: OperationControl,
89 ) -> Result<Self> {
90 limits.validate()?;
91 if options.logical_unit <= Unit::ZERO
92 || options.minimum_size <= Unit::ZERO
93 || options.collision_gap < Unit::ZERO
94 {
95 return Err(layout_error("layout options contain invalid dimensions"));
96 }
97 Ok(Self {
98 limits,
99 fonts,
100 options,
101 control,
102 assets: None,
103 })
104 }
105
106 #[must_use]
108 pub fn with_assets(mut self, assets: &'a dyn AssetResolver) -> Self {
109 self.assets = Some(assets);
110 self
111 }
112
113 pub fn resolve(&self, document: &DocumentIr) -> Result<ResolvedScene> {
115 self.control.checkpoint(ProgressPhase::Layout, 0, None)?;
116 let page_size = document
117 .page_size
118 .ok_or_else(|| layout_error("document/canvas requires an explicit page size"))?;
119 validate_anchor_graph(&document.elements)?;
120 let exclusions = crate::layout_exclusion::resolve_exclusions(
121 document,
122 page_size,
123 self.options.logical_unit,
124 )?;
125 let mut context = LayoutContext::new(
126 page_size,
127 document.page_template.clone(),
128 exclusions,
129 self.limits.max_pages,
130 self.limits.max_elements,
131 );
132 let page_rect = document.page_template.as_ref().map_or_else(
133 || Rect::new(Unit::ZERO, Unit::ZERO, page_size.width, page_size.height),
134 crate::PageTemplate::content_bounds,
135 )?;
136 let document_collision = document
137 .collision
138 .as_ref()
139 .unwrap_or(&self.options.collision);
140 let page_collision = document
141 .page_collision
142 .as_ref()
143 .unwrap_or(document_collision);
144 let regions =
145 crate::layout_region::resolve_regions(document, page_rect, self.options.logical_unit)?;
146 context.regions = regions;
147 self.layout_list(
148 &document.elements,
149 document,
150 page_rect,
151 0,
152 LayoutMode::Absolute,
153 crate::Distribution::Start,
154 Unit::ZERO,
155 page_collision,
156 Transform::IDENTITY,
157 &mut context,
158 )?;
159 crate::layout_page::resolve_page_layers(self, document, page_collision, &mut context)?;
160 for page in &mut context.pages {
161 page.elements.sort_by(|left, right| {
162 (&left.layer, left.z_index, left.sequence).cmp(&(
163 &right.layer,
164 right.z_index,
165 right.sequence,
166 ))
167 });
168 }
169 self.control.checkpoint(
170 ProgressPhase::Layout,
171 u64::try_from(context.sequence).unwrap_or(u64::MAX),
172 Some(u64::try_from(self.limits.max_elements).unwrap_or(u64::MAX)),
173 )?;
174 Ok(ResolvedScene {
175 template_id: document.template_id.clone(),
176 pages: context.pages,
177 engine_version: ENGINE_VERSION.to_owned(),
178 })
179 }
180
181 pub fn resolve_cached(
186 &self,
187 document: &DocumentIr,
188 fingerprint: DocumentFingerprint,
189 cache: &mut SceneCache,
190 ) -> Result<Arc<ResolvedScene>> {
191 cache.get_or_try_insert_with(fingerprint, self.limits, || self.resolve(document))
192 }
193
194 #[allow(clippy::too_many_arguments)]
195 pub(crate) fn layout_list(
196 &self,
197 elements: &[ElementIr],
198 document: &DocumentIr,
199 container: Rect,
200 mut page_index: usize,
201 parent_layout: LayoutMode,
202 distribution: crate::Distribution,
203 gap: Unit,
204 inherited_collision: &CollisionPolicy,
205 parent_transform: Transform,
206 context: &mut LayoutContext,
207 ) -> Result<()> {
208 let flow = crate::layout_flow::plan_flow(
209 elements,
210 container,
211 parent_layout,
212 distribution,
213 gap,
214 self.options.logical_unit,
215 )?;
216 let mut flow_x = flow.x;
217 let mut flow_y = flow.y;
218 let effective_gap = flow.gap;
219 for element in elements
220 .iter()
221 .filter(|element| element.page_placement.is_none())
222 {
223 context.checkpoint(&self.control, self.limits.max_elements)?;
224 if element.hidden {
225 continue;
226 }
227 let placement = self.place_element(
228 element,
229 document,
230 container,
231 page_index,
232 parent_layout,
233 crate::Point {
234 x: flow_x,
235 y: flow_y,
236 },
237 inherited_collision,
238 parent_transform,
239 context,
240 )?;
241 if element.kind == crate::ElementKind::Table {
242 let first = placement.clone();
243 let last = self.commit_table_fragments(
244 element,
245 document,
246 container,
247 parent_layout,
248 crate::Point {
249 x: flow_x,
250 y: flow_y,
251 },
252 inherited_collision,
253 parent_transform,
254 placement,
255 context,
256 )?;
257 page_index = last.page_index;
258 context.positions.insert(
259 element.id.as_str().to_owned(),
260 (first.page_index, first.rect),
261 );
262 match parent_layout {
263 LayoutMode::FlowVertical => {
264 flow_y = last.rect.bottom()?.checked_add(effective_gap)?;
265 }
266 LayoutMode::FlowHorizontal => {
267 flow_x = last.rect.right()?.checked_add(effective_gap)?;
268 }
269 LayoutMode::Absolute => {}
270 }
271 continue;
272 }
273 page_index = placement.page_index;
274 let resolved = self.build_resolved_element(element, &placement, context)?;
275 context.commit(placement.page_index, resolved, placement.policy.clone())?;
276 context.positions.insert(
277 element.id.as_str().to_owned(),
278 (placement.page_index, placement.rect),
279 );
280 match parent_layout {
281 LayoutMode::FlowVertical => {
282 flow_y = placement.rect.bottom()?.checked_add(effective_gap)?;
283 }
284 LayoutMode::FlowHorizontal => {
285 flow_x = placement.rect.right()?.checked_add(effective_gap)?;
286 }
287 LayoutMode::Absolute => {}
288 }
289 let child_gap = element
290 .gap
291 .resolve(placement.rect.size.width, self.options.logical_unit)?
292 .unwrap_or(Unit::ZERO);
293 self.layout_list(
294 &element.children,
295 document,
296 placement.rect,
297 placement.page_index,
298 element.layout,
299 element.distribute,
300 child_gap,
301 &placement.policy,
302 placement.transform,
303 context,
304 )?;
305 }
306 Ok(())
307 }
308
309 #[allow(clippy::too_many_arguments)]
310 fn place_element(
311 &self,
312 element: &ElementIr,
313 document: &DocumentIr,
314 container: Rect,
315 page_index: usize,
316 parent_layout: LayoutMode,
317 flow_origin: crate::Point,
318 inherited_collision: &CollisionPolicy,
319 parent_transform: Transform,
320 context: &mut LayoutContext,
321 ) -> Result<ElementPlacement> {
322 let initial_page = page_index;
323 let effective_container = crate::layout_region::resolve_region(
324 element,
325 document,
326 container,
327 self.options.logical_unit,
328 )?;
329 let mut proposed = propose_rect(
330 element,
331 effective_container,
332 parent_layout,
333 flow_origin,
334 &context.positions,
335 &document.guides,
336 self.options.logical_unit,
337 )?;
338 let policy = crate::layout_policy::effective_collision_policy(
339 element,
340 document,
341 inherited_collision,
342 );
343 crate::layout_policy::validate_shrink_policy(&policy)?;
344 let (mut initial_style, initial_text_layout, mut initial_intrinsic) =
345 measure_content(element, proposed, self.fonts)?;
346 if element.kind == crate::ElementKind::Text
347 && element.text_options.overflow == TextOverflow::Expand
348 {
349 let measured = initial_text_layout
350 .as_ref()
351 .ok_or_else(|| layout_error("expanded text has no measurement"))?
352 .measured;
353 proposed.size.width = proposed.size.width.max(measured.width);
354 proposed.size.height = proposed.size.height.max(measured.height);
355 (initial_style, _, initial_intrinsic) = measure_content(element, proposed, self.fonts)?;
356 }
357 let proposed_layout = proposed;
358 let proposed_transform = resolve_transform(element, proposed, self.options.logical_unit)?
359 .then(parent_transform)?;
360 crate::layout_policy::validate_shrink_transform(&policy, proposed_transform)?;
361 let initial_visual =
362 proposed_transform.bounds(visual_bounds(proposed, initial_style.stroke_width)?)?;
363 let collision_candidate = select_collision_bounds(
364 policy.bounds,
365 proposed_transform.bounds(proposed)?,
366 proposed_transform.bounds(initial_intrinsic)?,
367 initial_visual,
368 );
369 let (page_index, resolved_collision) = crate::layout_collision::resolve_candidate(
370 element,
371 page_index,
372 collision_candidate,
373 &policy,
374 context,
375 self.limits,
376 &self.options,
377 &self.control,
378 )?;
379 let rect = resolve_layout_rect(
380 policy.bounds,
381 proposed,
382 collision_candidate,
383 resolved_collision,
384 parent_transform,
385 )?;
386 let transform =
387 resolve_transform(element, rect, self.options.logical_unit)?.then(parent_transform)?;
388 Ok(ElementPlacement {
389 initial_page,
390 page_index,
391 proposed: proposed_layout,
392 rect,
393 policy,
394 transform,
395 })
396 }
397
398 fn build_resolved_element(
399 &self,
400 element: &ElementIr,
401 placement: &ElementPlacement,
402 context: &mut LayoutContext,
403 ) -> Result<ResolvedElement> {
404 let (style, text_layout, intrinsic) = measure_content(element, placement.rect, self.fonts)?;
405 let image_placement = resolve_image(element, placement.rect, self.assets, self.limits)?;
406 let transformed_layout = placement.transform.bounds(placement.rect)?;
407 let transformed_intrinsic = placement.transform.bounds(intrinsic)?;
408 let visual = placement
409 .transform
410 .bounds(visual_bounds(placement.rect, style.stroke_width)?)?;
411 let collision = select_collision_bounds(
412 placement.policy.bounds,
413 transformed_layout,
414 transformed_intrinsic,
415 visual,
416 );
417 let clip = text_layout.as_ref().and_then(|layout| {
418 layout
419 .diagnostics
420 .contains(&crate::TextDiagnostic::Clipped)
421 .then_some(placement.rect)
422 });
423 Ok(ResolvedElement {
424 id: element.id.clone(),
425 kind: element.kind,
426 bounds: BoundsSet {
427 intrinsic,
428 layout: placement.rect,
429 collision,
430 visual,
431 clip,
432 },
433 collidable: placement.policy.enabled,
434 shape: shape_for(element, placement.rect, self.options.logical_unit)?,
435 transform: placement.transform,
436 style,
437 text: element.text.clone(),
438 text_layout,
439 asset: element.asset.clone(),
440 image_placement,
441 table: None,
442 layer: element.layer.clone(),
443 z_index: element.z_index,
444 sequence: context.next_sequence(),
445 provenance: element.provenance.clone(),
446 layout_trace: crate::LayoutTrace {
447 geometry: element.geometry.clone(),
448 proposed: placement.proposed,
449 collision_policy: placement.policy.clone(),
450 initial_page: placement.initial_page,
451 reflowed: placement.initial_page != placement.page_index
452 || placement.proposed != placement.rect,
453 },
454 })
455 }
456
457 #[allow(clippy::too_many_arguments)]
458 fn commit_table_fragments(
459 &self,
460 element: &ElementIr,
461 document: &DocumentIr,
462 container: Rect,
463 parent_layout: LayoutMode,
464 flow_origin: crate::Point,
465 inherited_collision: &CollisionPolicy,
466 parent_transform: Transform,
467 first: ElementPlacement,
468 context: &mut LayoutContext,
469 ) -> Result<ElementPlacement> {
470 let fragments = crate::layout_table::resolve_table_fragments(
471 element,
472 first.rect,
473 self.fonts,
474 self.limits,
475 self.options.logical_unit,
476 )?;
477 let mut placement = first.clone();
478 for (index, mut fragment) in fragments.into_iter().enumerate() {
479 if index > 0 {
480 placement = self.place_element(
481 element,
482 document,
483 container,
484 placement
485 .page_index
486 .checked_add(1)
487 .ok_or_else(|| layout_error("table continuation page index overflow"))?,
488 parent_layout,
489 flow_origin,
490 inherited_collision,
491 parent_transform,
492 context,
493 )?;
494 }
495 crate::layout_table::translate_table_fragment(
496 &mut fragment,
497 first.rect.origin,
498 placement.rect.origin,
499 )?;
500 let mut resolved = self.build_resolved_element(element, &placement, context)?;
501 resolved.table = Some(fragment);
502 context.commit(placement.page_index, resolved, placement.policy.clone())?;
503 }
504 Ok(placement)
505 }
506}