1pub mod flex;
35pub mod grid;
36pub mod page_break;
37
38use std::cell::RefCell;
39use std::collections::HashMap;
40
41use serde::Serialize;
42
43use crate::font::FontContext;
44use crate::model::*;
45use crate::style::*;
46use crate::text::bidi;
47use crate::text::shaping;
48use crate::text::{BrokenLine, RunBrokenLine, StyledChar, TextLayout};
49
50#[derive(Debug, Clone, Serialize)]
52#[serde(rename_all = "camelCase")]
53pub struct BookmarkEntry {
54 pub title: String,
55 pub page_index: usize,
56 pub y: f64,
57}
58
59#[derive(Debug, Clone, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct LayoutInfo {
65 pub pages: Vec<PageInfo>,
66}
67
68#[derive(Debug, Clone, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct PageInfo {
72 pub width: f64,
73 pub height: f64,
74 pub content_x: f64,
75 pub content_y: f64,
76 pub content_width: f64,
77 pub content_height: f64,
78 pub elements: Vec<ElementInfo>,
79}
80
81#[derive(Debug, Clone, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct ElementStyleInfo {
85 pub margin: Edges,
87 pub padding: Edges,
88 pub border_width: Edges,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub width: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub height: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub min_width: Option<f64>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub min_height: Option<f64>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub max_width: Option<f64>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub max_height: Option<f64>,
101 pub flex_direction: FlexDirection,
103 pub justify_content: JustifyContent,
104 pub align_items: AlignItems,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub align_self: Option<AlignItems>,
107 pub flex_wrap: FlexWrap,
108 pub align_content: AlignContent,
109 pub flex_grow: f64,
110 pub flex_shrink: f64,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub flex_basis: Option<String>,
113 pub gap: f64,
114 pub row_gap: f64,
115 pub column_gap: f64,
116 pub font_family: String,
118 pub font_size: f64,
119 pub font_weight: u32,
120 pub font_style: FontStyle,
121 pub line_height: f64,
122 pub text_align: TextAlign,
123 pub letter_spacing: f64,
124 pub text_decoration: TextDecoration,
125 pub text_transform: TextTransform,
126 pub color: Color,
128 pub background_color: Option<Color>,
129 pub border_color: EdgeValues<Color>,
130 pub border_radius: CornerValues,
131 pub opacity: f64,
132 pub position: Position,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub top: Option<f64>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub right: Option<f64>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub bottom: Option<f64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub left: Option<f64>,
142 pub overflow: Overflow,
144 pub breakable: bool,
146 pub break_before: bool,
147 pub min_widow_lines: u32,
148 pub min_orphan_lines: u32,
149}
150
151fn size_constraint_to_str(sc: &SizeConstraint) -> Option<String> {
152 match sc {
153 SizeConstraint::Auto => None,
154 SizeConstraint::Fixed(v) => Some(format!("{v}")),
155 }
156}
157
158impl ElementStyleInfo {
159 fn from_resolved(style: &ResolvedStyle) -> Self {
160 ElementStyleInfo {
161 margin: style.margin.to_edges(),
162 padding: style.padding,
163 border_width: style.border_width,
164 width: size_constraint_to_str(&style.width),
165 height: size_constraint_to_str(&style.height),
166 min_width: if style.min_width > 0.0 {
167 Some(style.min_width)
168 } else {
169 None
170 },
171 min_height: if style.min_height > 0.0 {
172 Some(style.min_height)
173 } else {
174 None
175 },
176 max_width: if style.max_width.is_finite() {
177 Some(style.max_width)
178 } else {
179 None
180 },
181 max_height: if style.max_height.is_finite() {
182 Some(style.max_height)
183 } else {
184 None
185 },
186 flex_direction: style.flex_direction,
187 justify_content: style.justify_content,
188 align_items: style.align_items,
189 align_self: style.align_self,
190 flex_wrap: style.flex_wrap,
191 align_content: style.align_content,
192 flex_grow: style.flex_grow,
193 flex_shrink: style.flex_shrink,
194 flex_basis: size_constraint_to_str(&style.flex_basis),
195 gap: style.gap,
196 row_gap: style.row_gap,
197 column_gap: style.column_gap,
198 font_family: style.font_family.clone(),
199 font_size: style.font_size,
200 font_weight: style.font_weight,
201 font_style: style.font_style,
202 line_height: style.line_height,
203 text_align: style.text_align,
204 letter_spacing: style.letter_spacing,
205 text_decoration: style.text_decoration,
206 text_transform: style.text_transform,
207 color: style.color,
208 background_color: style.background_color,
209 border_color: style.border_color,
210 border_radius: style.border_radius,
211 opacity: style.opacity,
212 position: style.position,
213 top: style.top,
214 right: style.right,
215 bottom: style.bottom,
216 left: style.left,
217 overflow: style.overflow,
218 breakable: style.breakable,
219 break_before: style.break_before,
220 min_widow_lines: style.min_widow_lines,
221 min_orphan_lines: style.min_orphan_lines,
222 }
223 }
224}
225
226impl Default for ElementStyleInfo {
227 fn default() -> Self {
228 ElementStyleInfo {
229 margin: Edges::default(),
230 padding: Edges::default(),
231 border_width: Edges::default(),
232 width: None,
233 height: None,
234 min_width: None,
235 min_height: None,
236 max_width: None,
237 max_height: None,
238 flex_direction: FlexDirection::default(),
239 justify_content: JustifyContent::default(),
240 align_items: AlignItems::default(),
241 align_self: None,
242 flex_wrap: FlexWrap::default(),
243 align_content: AlignContent::default(),
244 flex_grow: 0.0,
245 flex_shrink: 1.0,
246 flex_basis: None,
247 gap: 0.0,
248 row_gap: 0.0,
249 column_gap: 0.0,
250 font_family: "Helvetica".to_string(),
251 font_size: 12.0,
252 font_weight: 400,
253 font_style: FontStyle::default(),
254 line_height: 1.4,
255 text_align: TextAlign::default(),
256 letter_spacing: 0.0,
257 text_decoration: TextDecoration::None,
258 text_transform: TextTransform::None,
259 color: Color::BLACK,
260 background_color: None,
261 border_color: EdgeValues::uniform(Color::BLACK),
262 border_radius: CornerValues::uniform(0.0),
263 opacity: 1.0,
264 position: Position::default(),
265 top: None,
266 right: None,
267 bottom: None,
268 left: None,
269 overflow: Overflow::default(),
270 breakable: false,
271 break_before: false,
272 min_widow_lines: 2,
273 min_orphan_lines: 2,
274 }
275 }
276}
277
278#[derive(Debug, Clone, Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct ElementInfo {
282 pub x: f64,
283 pub y: f64,
284 pub width: f64,
285 pub height: f64,
286 pub kind: String,
288 pub node_type: String,
290 pub style: ElementStyleInfo,
292 pub children: Vec<ElementInfo>,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub source_location: Option<SourceLocation>,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub text_content: Option<String>,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub href: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub bookmark: Option<String>,
306}
307
308impl LayoutInfo {
309 pub fn from_pages(pages: &[LayoutPage]) -> Self {
311 LayoutInfo {
312 pages: pages
313 .iter()
314 .map(|page| {
315 let (page_w, page_h) = page.config.size.dimensions();
316 let content_x = page.config.margin.left;
317 let content_y = page.config.margin.top;
318 let content_width = page_w - page.config.margin.horizontal();
319 let content_height = page_h - page.config.margin.vertical();
320
321 let elements = Self::build_element_tree(&page.elements);
322
323 PageInfo {
324 width: page_w,
325 height: page_h,
326 content_x,
327 content_y,
328 content_width,
329 content_height,
330 elements,
331 }
332 })
333 .collect(),
334 }
335 }
336
337 fn build_element_tree(elems: &[LayoutElement]) -> Vec<ElementInfo> {
338 elems
339 .iter()
340 .map(|elem| {
341 let kind = match &elem.draw {
342 DrawCommand::None => "None",
343 DrawCommand::Rect { .. } => "Rect",
344 DrawCommand::Text { .. } => "Text",
345 DrawCommand::Image { .. } => "Image",
346 DrawCommand::ImagePlaceholder => "ImagePlaceholder",
347 DrawCommand::Svg { .. } => "Svg",
348 DrawCommand::Barcode { .. } => "Barcode",
349 DrawCommand::QrCode { .. } => "QrCode",
350 DrawCommand::Chart { .. } => "Chart",
351 DrawCommand::Watermark { .. } => "Watermark",
352 DrawCommand::FormField { .. } => "FormField",
353 };
354 let text_content = match &elem.draw {
355 DrawCommand::Text { lines, .. } => {
356 let text: String = lines
357 .iter()
358 .flat_map(|line| {
359 line.glyphs.iter().flat_map(|g| {
360 g.cluster_text.as_deref().unwrap_or("").chars().chain(
362 if g.cluster_text.is_none() {
363 Some(g.char_value)
364 } else {
365 None
366 },
367 )
368 })
369 })
370 .collect();
371 if text.is_empty() {
372 None
373 } else {
374 Some(text)
375 }
376 }
377 _ => None,
378 };
379 let node_type = elem.node_type.clone().unwrap_or_else(|| kind.to_string());
380 let style = elem
381 .resolved_style
382 .as_ref()
383 .map(ElementStyleInfo::from_resolved)
384 .unwrap_or_default();
385 ElementInfo {
386 x: elem.x,
387 y: elem.y,
388 width: elem.width,
389 height: elem.height,
390 kind: kind.to_string(),
391 node_type,
392 style,
393 children: Self::build_element_tree(&elem.children),
394 source_location: elem.source_location.clone(),
395 text_content,
396 href: elem.href.clone(),
397 bookmark: elem.bookmark.clone(),
398 }
399 })
400 .collect()
401 }
402}
403
404#[derive(Debug, Clone)]
406pub struct LayoutPage {
407 pub width: f64,
408 pub height: f64,
409 pub elements: Vec<LayoutElement>,
410 pub(crate) fixed_header: Vec<(Node, f64)>,
412 pub(crate) fixed_footer: Vec<(Node, f64)>,
414 pub(crate) watermarks: Vec<Node>,
416 pub(crate) config: PageConfig,
418}
419
420#[derive(Debug, Clone)]
422pub struct LayoutElement {
423 pub x: f64,
425 pub y: f64,
426 pub width: f64,
428 pub height: f64,
429 pub draw: DrawCommand,
431 pub children: Vec<LayoutElement>,
433 pub node_type: Option<String>,
435 pub resolved_style: Option<ResolvedStyle>,
437 pub source_location: Option<SourceLocation>,
439 pub href: Option<String>,
441 pub bookmark: Option<String>,
443 pub alt: Option<String>,
445 pub is_header_row: bool,
447 pub overflow: Overflow,
449 pub opacity: f64,
454}
455
456fn node_kind_name(kind: &NodeKind) -> &'static str {
458 match kind {
459 NodeKind::View => "View",
460 NodeKind::Text { .. } => "Text",
461 NodeKind::Heading { level: 1, .. } => "H1",
462 NodeKind::Heading { level: 2, .. } => "H2",
463 NodeKind::Heading { level: 3, .. } => "H3",
464 NodeKind::Heading { level: 4, .. } => "H4",
465 NodeKind::Heading { level: 5, .. } => "H5",
466 NodeKind::Heading { .. } => "H6",
470 NodeKind::List { .. } => "List",
471 NodeKind::ListItem => "ListItem",
472 NodeKind::Image { .. } => "Image",
473 NodeKind::Table { .. } => "Table",
474 NodeKind::TableRow { .. } => "TableRow",
475 NodeKind::TableCell { .. } => "TableCell",
476 NodeKind::Fixed {
477 position: FixedPosition::Header,
478 } => "FixedHeader",
479 NodeKind::Fixed {
480 position: FixedPosition::Footer,
481 } => "FixedFooter",
482 NodeKind::Page { .. } => "Page",
483 NodeKind::PageBreak => "PageBreak",
484 NodeKind::Svg { .. } => "Svg",
485 NodeKind::Canvas { .. } => "Canvas",
486 NodeKind::Barcode { .. } => "Barcode",
487 NodeKind::QrCode { .. } => "QrCode",
488 NodeKind::BarChart { .. } => "BarChart",
489 NodeKind::LineChart { .. } => "LineChart",
490 NodeKind::PieChart { .. } => "PieChart",
491 NodeKind::AreaChart { .. } => "AreaChart",
492 NodeKind::DotPlot { .. } => "DotPlot",
493 NodeKind::Watermark { .. } => "Watermark",
494 NodeKind::TextField { .. } => "TextField",
495 NodeKind::Checkbox { .. } => "Checkbox",
496 NodeKind::Dropdown { .. } => "Dropdown",
497 NodeKind::RadioButton { .. } => "RadioButton",
498 }
499}
500
501fn format_marker(idx: u32, ordered: bool, marker_type: ListMarkerType) -> String {
508 if !ordered {
509 return match marker_type {
513 ListMarkerType::None => String::new(),
514 _ => "•".to_string(),
515 };
516 }
517 let body = match marker_type {
518 ListMarkerType::LowerAlpha => to_alpha(idx, false),
519 ListMarkerType::UpperAlpha => to_alpha(idx, true),
520 ListMarkerType::LowerRoman => to_roman(idx, false),
521 ListMarkerType::UpperRoman => to_roman(idx, true),
522 ListMarkerType::None => return String::new(),
523 _ => idx.to_string(),
526 };
527 format!("{body}.")
528}
529
530fn to_alpha(mut n: u32, upper: bool) -> String {
533 if n == 0 {
534 return String::new();
535 }
536 let base = if upper { b'A' } else { b'a' };
537 let mut bytes: Vec<u8> = Vec::new();
538 while n > 0 {
539 let rem = ((n - 1) % 26) as u8;
540 bytes.push(base + rem);
541 n = (n - 1) / 26;
542 }
543 bytes.reverse();
544 String::from_utf8(bytes).unwrap_or_default()
545}
546
547fn to_roman(n: u32, upper: bool) -> String {
551 if n == 0 || n > 3999 {
552 return n.to_string();
553 }
554 const UPPER: &[(&str, u32)] = &[
555 ("M", 1000),
556 ("CM", 900),
557 ("D", 500),
558 ("CD", 400),
559 ("C", 100),
560 ("XC", 90),
561 ("L", 50),
562 ("XL", 40),
563 ("X", 10),
564 ("IX", 9),
565 ("V", 5),
566 ("IV", 4),
567 ("I", 1),
568 ];
569 const LOWER: &[(&str, u32)] = &[
570 ("m", 1000),
571 ("cm", 900),
572 ("d", 500),
573 ("cd", 400),
574 ("c", 100),
575 ("xc", 90),
576 ("l", 50),
577 ("xl", 40),
578 ("x", 10),
579 ("ix", 9),
580 ("v", 5),
581 ("iv", 4),
582 ("i", 1),
583 ];
584 let table = if upper { UPPER } else { LOWER };
585 let mut out = String::new();
586 let mut remaining = n;
587 for &(sym, val) in table {
588 while remaining >= val {
589 out.push_str(sym);
590 remaining -= val;
591 }
592 }
593 out
594}
595
596fn compute_marker_gutter_width(
603 ordered: bool,
604 marker_type: ListMarkerType,
605 start: u32,
606 n_items: u32,
607 style: &ResolvedStyle,
608) -> f64 {
609 if matches!(marker_type, ListMarkerType::None) {
610 return 0.0;
611 }
612 let approx_char_w = style.font_size * 0.6;
614 let gap = 6.0_f64;
615 if !ordered {
616 return approx_char_w + gap;
618 }
619 let last_idx = start + n_items.saturating_sub(1);
621 let widest = match marker_type {
622 ListMarkerType::Decimal => format_marker(last_idx, true, ListMarkerType::Decimal),
623 ListMarkerType::LowerAlpha => format_marker(last_idx, true, ListMarkerType::LowerAlpha),
624 ListMarkerType::UpperAlpha => format_marker(last_idx, true, ListMarkerType::UpperAlpha),
625 ListMarkerType::LowerRoman => format_marker(last_idx, true, ListMarkerType::LowerRoman),
626 ListMarkerType::UpperRoman => format_marker(last_idx, true, ListMarkerType::UpperRoman),
627 _ => format_marker(last_idx, true, ListMarkerType::Decimal),
628 };
629 widest.chars().count() as f64 * approx_char_w + gap
630}
631
632#[derive(Debug, Clone)]
634pub enum FormFieldType {
635 TextField {
636 value: Option<String>,
637 placeholder: Option<String>,
638 multiline: bool,
639 password: bool,
640 read_only: bool,
641 max_length: Option<u32>,
642 font_size: f64,
643 },
644 Checkbox {
645 checked: bool,
646 read_only: bool,
647 },
648 Dropdown {
649 options: Vec<String>,
650 value: Option<String>,
651 read_only: bool,
652 font_size: f64,
653 },
654 RadioButton {
655 value: String,
656 checked: bool,
657 read_only: bool,
658 },
659}
660
661#[derive(Debug, Clone)]
663pub enum DrawCommand {
664 None,
666 Rect {
668 background: Option<Color>,
669 border_width: Edges,
670 border_color: EdgeValues<Color>,
671 border_radius: CornerValues,
672 opacity: f64,
673 box_shadow: Option<Box<crate::style::BoxShadow>>,
676 background_gradient: Option<Box<crate::style::Background>>,
680 },
681 Text {
683 lines: Vec<TextLine>,
684 color: Color,
685 text_decoration: TextDecoration,
686 opacity: f64,
687 },
688 Image {
690 image_data: crate::image_loader::LoadedImage,
691 },
692 ImagePlaceholder,
694 Svg {
696 commands: Vec<crate::svg::SvgCommand>,
697 width: f64,
699 height: f64,
701 viewbox_min_x: f64,
705 viewbox_min_y: f64,
706 viewbox_width: f64,
707 viewbox_height: f64,
708 clip: bool,
710 },
711 Barcode {
713 bars: Vec<u8>,
714 bar_width: f64,
715 height: f64,
716 color: Color,
717 },
718 QrCode {
720 modules: Vec<Vec<bool>>,
721 module_size: f64,
722 color: Color,
723 },
724 Chart {
726 primitives: Vec<crate::chart::ChartPrimitive>,
727 },
728 Watermark {
730 lines: Vec<TextLine>,
731 color: Color,
732 opacity: f64,
733 angle_rad: f64,
734 font_family: String,
736 },
737 FormField {
739 field_type: FormFieldType,
740 name: String,
741 },
742}
743
744#[derive(Debug, Clone)]
745pub struct TextLine {
746 pub x: f64,
747 pub y: f64,
748 pub glyphs: Vec<PositionedGlyph>,
749 pub width: f64,
750 pub height: f64,
751 pub word_spacing: f64,
753}
754
755#[derive(Debug, Clone)]
756pub struct PositionedGlyph {
757 pub glyph_id: u16,
760 pub x_offset: f64,
762 pub y_offset: f64,
764 pub x_advance: f64,
766 pub font_size: f64,
767 pub font_family: String,
768 pub font_weight: u32,
769 pub font_style: FontStyle,
770 pub char_value: char,
772 pub color: Option<Color>,
774 pub href: Option<String>,
776 pub text_decoration: TextDecoration,
778 pub letter_spacing: f64,
780 pub cluster_text: Option<String>,
783}
784
785fn offset_element_y(el: &mut LayoutElement, dy: f64) {
788 el.y += dy;
789 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
790 for line in lines.iter_mut() {
791 line.y += dy;
792 }
793 }
794 for child in &mut el.children {
795 offset_element_y(child, dy);
796 }
797}
798
799#[allow(dead_code)]
801fn offset_element_x(el: &mut LayoutElement, dx: f64) {
802 el.x += dx;
803 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
804 for line in lines.iter_mut() {
805 line.x += dx;
806 }
807 }
808 for child in &mut el.children {
809 offset_element_x(child, dx);
810 }
811}
812
813fn reapply_justify_content(elem: &mut LayoutElement) {
817 let style = match elem.resolved_style {
818 Some(ref s) => s,
819 None => return,
820 };
821 if matches!(style.justify_content, JustifyContent::FlexStart) {
822 return;
823 }
824 if elem.children.is_empty() {
825 return;
826 }
827
828 let padding_top = style.padding.top + style.border_width.top;
829 let padding_bottom = style.padding.bottom + style.border_width.bottom;
830 let inner_h = elem.height - padding_top - padding_bottom;
831 let content_top = elem.y + padding_top;
832
833 let last_child = &elem.children[elem.children.len() - 1];
835 let children_bottom = last_child.y + last_child.height;
836 let children_span = children_bottom - content_top;
837 let slack = inner_h - children_span;
838 if slack < 0.001 {
839 return;
840 }
841
842 let n = elem.children.len();
843 let offsets: Vec<f64> = match style.justify_content {
844 JustifyContent::FlexEnd => vec![slack; n],
845 JustifyContent::Center => vec![slack / 2.0; n],
846 JustifyContent::SpaceBetween => {
847 if n <= 1 {
848 vec![0.0; n]
849 } else {
850 let per_gap = slack / (n - 1) as f64;
851 (0..n).map(|i| i as f64 * per_gap).collect()
852 }
853 }
854 JustifyContent::SpaceAround => {
855 let space = slack / n as f64;
856 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
857 }
858 JustifyContent::SpaceEvenly => {
859 let space = slack / (n + 1) as f64;
860 (0..n).map(|i| (i + 1) as f64 * space).collect()
861 }
862 JustifyContent::FlexStart => unreachable!(),
863 };
864
865 for (i, child) in elem.children.iter_mut().enumerate() {
866 let dy = offsets[i];
867 if dy.abs() > 0.001 {
868 offset_element_y(child, dy);
869 }
870 }
871}
872
873fn apply_text_transform(text: &str, transform: TextTransform) -> String {
875 match transform {
876 TextTransform::None => text.to_string(),
877 TextTransform::Uppercase => text.to_uppercase(),
878 TextTransform::Lowercase => text.to_lowercase(),
879 TextTransform::Capitalize => {
880 let mut result = String::with_capacity(text.len());
881 let mut prev_is_whitespace = true;
882 for ch in text.chars() {
883 if prev_is_whitespace && ch.is_alphabetic() {
884 for upper in ch.to_uppercase() {
885 result.push(upper);
886 }
887 } else {
888 result.push(ch);
889 }
890 prev_is_whitespace = ch.is_whitespace();
891 }
892 result
893 }
894 }
895}
896
897pub const PAGE_NUMBER_SENTINEL: char = '\x02';
901
902pub const TOTAL_PAGES_SENTINEL: char = '\x03';
904
905fn substitute_page_placeholders(text: &str) -> String {
910 if text.contains("{{pageNumber}}") || text.contains("{{totalPages}}") {
911 text.replace("{{pageNumber}}", &PAGE_NUMBER_SENTINEL.to_string())
912 .replace("{{totalPages}}", &TOTAL_PAGES_SENTINEL.to_string())
913 } else {
914 text.to_string()
915 }
916}
917
918fn apply_char_transform(ch: char, transform: TextTransform, is_word_start: bool) -> char {
921 match transform {
922 TextTransform::None => ch,
923 TextTransform::Uppercase => ch.to_uppercase().next().unwrap_or(ch),
924 TextTransform::Lowercase => ch.to_lowercase().next().unwrap_or(ch),
925 TextTransform::Capitalize => {
926 if is_word_start && ch.is_alphabetic() {
927 ch.to_uppercase().next().unwrap_or(ch)
928 } else {
929 ch
930 }
931 }
932 }
933}
934
935pub struct LayoutEngine {
937 text_layout: TextLayout,
938 image_dim_cache: RefCell<HashMap<String, (u32, u32)>>,
939}
940
941#[derive(Debug, Clone)]
943struct PageCursor {
944 config: PageConfig,
945 content_width: f64,
946 content_height: f64,
947 y: f64,
948 elements: Vec<LayoutElement>,
949 fixed_header: Vec<(Node, f64)>,
950 fixed_footer: Vec<(Node, f64)>,
951 watermarks: Vec<Node>,
953 content_x: f64,
954 content_y: f64,
955 continuation_top_offset: f64,
957}
958
959impl PageCursor {
960 fn new(config: &PageConfig) -> Self {
961 let (page_w, page_h) = config.size.dimensions();
962 let content_width = page_w - config.margin.horizontal();
963 let content_height = page_h - config.margin.vertical();
964
965 Self {
966 config: config.clone(),
967 content_width,
968 content_height,
969 y: 0.0,
970 elements: Vec::new(),
971 fixed_header: Vec::new(),
972 fixed_footer: Vec::new(),
973 watermarks: Vec::new(),
974 content_x: config.margin.left,
975 content_y: config.margin.top,
976 continuation_top_offset: 0.0,
977 }
978 }
979
980 fn remaining_height(&self) -> f64 {
981 let footer_height: f64 = self.fixed_footer.iter().map(|(_, h)| *h).sum();
982 (self.content_height - self.y - footer_height).max(0.0)
983 }
984
985 fn finalize(&self) -> LayoutPage {
986 let (page_w, page_h) = self.config.size.dimensions();
987 LayoutPage {
988 width: page_w,
989 height: page_h,
990 elements: self.elements.clone(),
991 fixed_header: self.fixed_header.clone(),
992 fixed_footer: self.fixed_footer.clone(),
993 watermarks: self.watermarks.clone(),
994 config: self.config.clone(),
995 }
996 }
997
998 fn new_page(&self) -> Self {
999 let mut cursor = PageCursor::new(&self.config);
1000 cursor.fixed_header = self.fixed_header.clone();
1001 cursor.fixed_footer = self.fixed_footer.clone();
1002 cursor.watermarks = self.watermarks.clone();
1003 cursor.continuation_top_offset = self.continuation_top_offset;
1004
1005 let header_height: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
1006 cursor.y = header_height + cursor.continuation_top_offset;
1007
1008 cursor
1009 }
1010}
1011
1012impl Default for LayoutEngine {
1013 fn default() -> Self {
1014 Self::new()
1015 }
1016}
1017
1018impl LayoutEngine {
1019 pub fn new() -> Self {
1020 Self {
1021 text_layout: TextLayout::new(),
1022 image_dim_cache: RefCell::new(HashMap::new()),
1023 }
1024 }
1025
1026 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
1028 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
1029 return Some(*dims);
1030 }
1031 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
1032 self.image_dim_cache
1033 .borrow_mut()
1034 .insert(src.to_string(), dims);
1035 Some(dims)
1036 } else {
1037 None
1038 }
1039 }
1040
1041 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
1043 let mut pages: Vec<LayoutPage> = Vec::new();
1044 let mut cursor = PageCursor::new(&document.default_page);
1045
1046 let base = document.default_style.clone().unwrap_or_default();
1048 let root_style = Style {
1049 lang: base.lang.clone().or(document.metadata.lang.clone()),
1050 ..base
1051 }
1052 .resolve(None, cursor.content_width);
1053
1054 for node in &document.children {
1055 match &node.kind {
1056 NodeKind::Page { config } => {
1057 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1058 pages.push(cursor.finalize());
1059 }
1060 cursor = PageCursor::new(config);
1061
1062 let mut page_root = node.style.resolve(Some(&root_style), cursor.content_width);
1069 page_root.height = SizeConstraint::Fixed(cursor.content_height);
1070
1071 let cx = cursor.content_x;
1072 let cw = cursor.content_width;
1073 self.layout_children(
1074 &node.children,
1075 &node.style,
1076 &mut cursor,
1077 &mut pages,
1078 cx,
1079 cw,
1080 Some(&page_root),
1081 font_context,
1082 );
1083 }
1084 NodeKind::PageBreak => {
1085 pages.push(cursor.finalize());
1086 cursor = cursor.new_page();
1087 }
1088 _ => {
1089 let cx = cursor.content_x;
1090 let cw = cursor.content_width;
1091 self.layout_node(
1092 node,
1093 &mut cursor,
1094 &mut pages,
1095 cx,
1096 cw,
1097 Some(&root_style),
1098 font_context,
1099 None,
1100 None,
1101 );
1102 }
1103 }
1104 }
1105
1106 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1107 pages.push(cursor.finalize());
1108 }
1109
1110 self.inject_fixed_elements(&mut pages, font_context);
1111
1112 pages
1113 }
1114
1115 #[allow(clippy::too_many_arguments)]
1116 fn layout_node(
1117 &self,
1118 node: &Node,
1119 cursor: &mut PageCursor,
1120 pages: &mut Vec<LayoutPage>,
1121 x: f64,
1122 available_width: f64,
1123 parent_style: Option<&ResolvedStyle>,
1124 font_context: &FontContext,
1125 cross_axis_height: Option<f64>,
1126 forced_outer_width: Option<f64>,
1127 ) {
1128 let mut style = node.style.resolve(parent_style, available_width);
1129
1130 if let Some(h) = cross_axis_height {
1133 if matches!(style.height, SizeConstraint::Auto) {
1134 style.height = SizeConstraint::Fixed(h);
1135 }
1136 }
1137
1138 if let Some(w) = forced_outer_width {
1143 style.width = SizeConstraint::Fixed(w);
1144 }
1145
1146 if style.break_before {
1147 pages.push(cursor.finalize());
1148 *cursor = cursor.new_page();
1149 }
1150
1151 match &node.kind {
1152 NodeKind::PageBreak => {
1153 pages.push(cursor.finalize());
1154 *cursor = cursor.new_page();
1155 }
1156
1157 NodeKind::Fixed { position } => {
1158 let height = self.measure_node_height(node, available_width, &style, font_context);
1159 match position {
1160 FixedPosition::Header => {
1161 cursor.fixed_header.push((node.clone(), height));
1162 cursor.y += height;
1163 }
1164 FixedPosition::Footer => {
1165 cursor.fixed_footer.push((node.clone(), height));
1166 }
1167 }
1168 }
1169
1170 NodeKind::Watermark { .. } => {
1171 cursor.watermarks.push(node.clone());
1173 }
1174
1175 NodeKind::TextField {
1176 name,
1177 value,
1178 placeholder,
1179 width: field_w,
1180 height: field_h,
1181 multiline,
1182 password,
1183 read_only,
1184 max_length,
1185 font_size,
1186 } => {
1187 self.layout_form_field(
1188 node,
1189 &style,
1190 cursor,
1191 pages,
1192 x,
1193 *field_w,
1194 *field_h,
1195 DrawCommand::FormField {
1196 field_type: FormFieldType::TextField {
1197 value: value.clone(),
1198 placeholder: placeholder.clone(),
1199 multiline: *multiline,
1200 password: *password,
1201 read_only: *read_only,
1202 max_length: *max_length,
1203 font_size: *font_size,
1204 },
1205 name: name.clone(),
1206 },
1207 "TextField",
1208 );
1209 }
1210
1211 NodeKind::Checkbox {
1212 name,
1213 checked,
1214 width: field_w,
1215 height: field_h,
1216 read_only,
1217 } => {
1218 self.layout_form_field(
1219 node,
1220 &style,
1221 cursor,
1222 pages,
1223 x,
1224 *field_w,
1225 *field_h,
1226 DrawCommand::FormField {
1227 field_type: FormFieldType::Checkbox {
1228 checked: *checked,
1229 read_only: *read_only,
1230 },
1231 name: name.clone(),
1232 },
1233 "Checkbox",
1234 );
1235 }
1236
1237 NodeKind::Dropdown {
1238 name,
1239 options,
1240 value,
1241 width: field_w,
1242 height: field_h,
1243 read_only,
1244 font_size,
1245 } => {
1246 self.layout_form_field(
1247 node,
1248 &style,
1249 cursor,
1250 pages,
1251 x,
1252 *field_w,
1253 *field_h,
1254 DrawCommand::FormField {
1255 field_type: FormFieldType::Dropdown {
1256 options: options.clone(),
1257 value: value.clone(),
1258 read_only: *read_only,
1259 font_size: *font_size,
1260 },
1261 name: name.clone(),
1262 },
1263 "Dropdown",
1264 );
1265 }
1266
1267 NodeKind::RadioButton {
1268 name,
1269 value,
1270 checked,
1271 width: field_w,
1272 height: field_h,
1273 read_only,
1274 } => {
1275 self.layout_form_field(
1276 node,
1277 &style,
1278 cursor,
1279 pages,
1280 x,
1281 *field_w,
1282 *field_h,
1283 DrawCommand::FormField {
1284 field_type: FormFieldType::RadioButton {
1285 value: value.clone(),
1286 checked: *checked,
1287 read_only: *read_only,
1288 },
1289 name: name.clone(),
1290 },
1291 "RadioButton",
1292 );
1293 }
1294
1295 NodeKind::Text {
1296 content,
1297 href,
1298 runs,
1299 } => {
1300 self.layout_text(
1301 content,
1302 href.as_deref(),
1303 runs,
1304 &style,
1305 cursor,
1306 pages,
1307 x,
1308 available_width,
1309 font_context,
1310 node.source_location.as_ref(),
1311 node.bookmark.as_deref(),
1312 None,
1313 );
1314 }
1315
1316 NodeKind::Heading {
1317 content,
1318 href,
1319 runs,
1320 ..
1321 } => {
1322 let heading_role = node_kind_name(&node.kind); self.layout_text(
1328 content,
1329 href.as_deref(),
1330 runs,
1331 &style,
1332 cursor,
1333 pages,
1334 x,
1335 available_width,
1336 font_context,
1337 node.source_location.as_ref(),
1338 node.bookmark.as_deref(),
1339 Some(heading_role),
1340 );
1341 }
1342
1343 NodeKind::List {
1344 ordered,
1345 marker_type,
1346 start,
1347 } => {
1348 self.layout_list(
1349 node,
1350 *ordered,
1351 *marker_type,
1352 *start,
1353 &style,
1354 cursor,
1355 pages,
1356 x,
1357 available_width,
1358 font_context,
1359 );
1360 }
1361
1362 NodeKind::ListItem => {
1363 self.layout_view(
1368 node,
1369 &style,
1370 cursor,
1371 pages,
1372 x,
1373 available_width,
1374 font_context,
1375 );
1376 }
1377
1378 NodeKind::Image { width, height, .. } => {
1379 self.layout_image(
1380 node,
1381 &style,
1382 cursor,
1383 pages,
1384 x,
1385 available_width,
1386 *width,
1387 *height,
1388 );
1389 }
1390
1391 NodeKind::Table { columns } => {
1392 self.layout_table(
1393 node,
1394 &style,
1395 columns,
1396 cursor,
1397 pages,
1398 x,
1399 available_width,
1400 font_context,
1401 );
1402 }
1403
1404 NodeKind::View | NodeKind::Page { .. } => {
1405 self.layout_view(
1406 node,
1407 &style,
1408 cursor,
1409 pages,
1410 x,
1411 available_width,
1412 font_context,
1413 );
1414 }
1415
1416 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
1417 self.layout_view(
1418 node,
1419 &style,
1420 cursor,
1421 pages,
1422 x,
1423 available_width,
1424 font_context,
1425 );
1426 }
1427
1428 NodeKind::Svg {
1429 width: svg_w,
1430 height: svg_h,
1431 view_box,
1432 content,
1433 } => {
1434 self.layout_svg(
1435 node,
1436 &style,
1437 cursor,
1438 pages,
1439 x,
1440 available_width,
1441 *svg_w,
1442 *svg_h,
1443 view_box.as_deref(),
1444 content,
1445 );
1446 }
1447
1448 NodeKind::Barcode {
1449 data,
1450 format,
1451 width: explicit_width,
1452 height: bar_height,
1453 } => {
1454 self.layout_barcode(
1455 node,
1456 &style,
1457 cursor,
1458 pages,
1459 x,
1460 available_width,
1461 data,
1462 *format,
1463 *explicit_width,
1464 *bar_height,
1465 );
1466 }
1467
1468 NodeKind::QrCode {
1469 data,
1470 size: explicit_size,
1471 } => {
1472 self.layout_qrcode(
1473 node,
1474 &style,
1475 cursor,
1476 pages,
1477 x,
1478 available_width,
1479 data,
1480 *explicit_size,
1481 );
1482 }
1483
1484 NodeKind::Canvas {
1485 width: canvas_w,
1486 height: canvas_h,
1487 operations,
1488 } => {
1489 self.layout_canvas(
1490 node,
1491 &style,
1492 cursor,
1493 pages,
1494 x,
1495 available_width,
1496 *canvas_w,
1497 *canvas_h,
1498 operations,
1499 );
1500 }
1501
1502 NodeKind::BarChart {
1503 data,
1504 width: chart_w,
1505 height: chart_h,
1506 color,
1507 show_labels,
1508 show_values,
1509 show_grid,
1510 title,
1511 } => {
1512 let config = crate::chart::bar::BarChartConfig {
1513 color: color.clone(),
1514 show_labels: *show_labels,
1515 show_values: *show_values,
1516 show_grid: *show_grid,
1517 title: title.clone(),
1518 };
1519 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
1520 self.layout_chart(
1521 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
1522 );
1523 }
1524
1525 NodeKind::LineChart {
1526 series,
1527 labels,
1528 width: chart_w,
1529 height: chart_h,
1530 show_points,
1531 show_grid,
1532 title,
1533 } => {
1534 let config = crate::chart::line::LineChartConfig {
1535 show_points: *show_points,
1536 show_grid: *show_grid,
1537 title: title.clone(),
1538 };
1539 let primitives =
1540 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
1541 self.layout_chart(
1542 node,
1543 &style,
1544 cursor,
1545 pages,
1546 x,
1547 *chart_w,
1548 *chart_h,
1549 primitives,
1550 "LineChart",
1551 );
1552 }
1553
1554 NodeKind::PieChart {
1555 data,
1556 width: chart_w,
1557 height: chart_h,
1558 donut,
1559 show_legend,
1560 title,
1561 } => {
1562 let config = crate::chart::pie::PieChartConfig {
1563 donut: *donut,
1564 show_legend: *show_legend,
1565 title: title.clone(),
1566 };
1567 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
1568 self.layout_chart(
1569 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
1570 );
1571 }
1572
1573 NodeKind::AreaChart {
1574 series,
1575 labels,
1576 width: chart_w,
1577 height: chart_h,
1578 show_grid,
1579 title,
1580 } => {
1581 let config = crate::chart::area::AreaChartConfig {
1582 show_grid: *show_grid,
1583 title: title.clone(),
1584 };
1585 let primitives =
1586 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
1587 self.layout_chart(
1588 node,
1589 &style,
1590 cursor,
1591 pages,
1592 x,
1593 *chart_w,
1594 *chart_h,
1595 primitives,
1596 "AreaChart",
1597 );
1598 }
1599
1600 NodeKind::DotPlot {
1601 groups,
1602 width: chart_w,
1603 height: chart_h,
1604 x_min,
1605 x_max,
1606 y_min,
1607 y_max,
1608 x_label,
1609 y_label,
1610 show_legend,
1611 dot_size,
1612 } => {
1613 let config = crate::chart::dot::DotPlotConfig {
1614 x_min: *x_min,
1615 x_max: *x_max,
1616 y_min: *y_min,
1617 y_max: *y_max,
1618 x_label: x_label.clone(),
1619 y_label: y_label.clone(),
1620 show_legend: *show_legend,
1621 dot_size: *dot_size,
1622 };
1623 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
1624 self.layout_chart(
1625 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
1626 );
1627 }
1628 }
1629 }
1630
1631 #[allow(clippy::too_many_arguments)]
1632 fn layout_view(
1633 &self,
1634 node: &Node,
1635 style: &ResolvedStyle,
1636 cursor: &mut PageCursor,
1637 pages: &mut Vec<LayoutPage>,
1638 x: f64,
1639 available_width: f64,
1640 font_context: &FontContext,
1641 ) {
1642 let padding = &style.padding;
1643 let margin = &style.margin.to_edges();
1644 let border = &style.border_width;
1645
1646 let outer_width = match style.width {
1647 SizeConstraint::Fixed(w) => w,
1648 SizeConstraint::Auto => available_width - margin.horizontal(),
1649 };
1650 let inner_width = outer_width - padding.horizontal() - border.horizontal();
1651
1652 let children_height =
1653 self.measure_children_height(&node.children, inner_width, style, font_context);
1654 let total_height = match style.height {
1655 SizeConstraint::Fixed(h) => h,
1656 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
1657 };
1658
1659 let node_x = x + margin.left;
1660
1661 let fits = total_height <= cursor.remaining_height() - margin.vertical();
1662
1663 if fits || !style.breakable {
1664 if !fits && !style.breakable {
1665 pages.push(cursor.finalize());
1666 *cursor = cursor.new_page();
1667 }
1668
1669 let rect_y = cursor.content_y + cursor.y + margin.top;
1671 let snapshot = cursor.elements.len();
1672
1673 let saved_y = cursor.y;
1674 cursor.y += margin.top + padding.top + border.top;
1675
1676 let children_x = node_x + padding.left + border.left;
1677 let is_grid =
1678 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
1679 if is_grid {
1680 self.layout_grid_children(
1681 &node.children,
1682 style,
1683 cursor,
1684 pages,
1685 children_x,
1686 inner_width,
1687 font_context,
1688 );
1689 } else {
1690 self.layout_children(
1691 &node.children,
1692 &node.style,
1693 cursor,
1694 pages,
1695 children_x,
1696 inner_width,
1697 Some(style),
1698 font_context,
1699 );
1700 }
1701
1702 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
1704
1705 let rect_element = LayoutElement {
1706 x: node_x,
1707 y: rect_y,
1708 width: outer_width,
1709 height: total_height,
1710 draw: DrawCommand::Rect {
1711 background: style.background_color,
1712 border_width: style.border_width,
1713 border_color: style.border_color,
1714 border_radius: style.border_radius,
1715 opacity: 1.0,
1716 box_shadow: style.box_shadow.map(Box::new),
1717 background_gradient: style.background.clone().map(Box::new),
1718 },
1719 children: child_elements,
1720 node_type: Some(node_kind_name(&node.kind).to_string()),
1721 resolved_style: Some(style.clone()),
1722 source_location: node.source_location.clone(),
1723 href: node.href.clone(),
1724 bookmark: node.bookmark.clone(),
1725 alt: None,
1726 is_header_row: false,
1727 overflow: style.overflow,
1728 opacity: style.opacity,
1729 };
1730 cursor.elements.push(rect_element);
1731
1732 cursor.y = saved_y + total_height + margin.vertical();
1733 } else {
1734 self.layout_breakable_view(
1735 node,
1736 style,
1737 cursor,
1738 pages,
1739 node_x,
1740 outer_width,
1741 inner_width,
1742 font_context,
1743 );
1744 }
1745 }
1746
1747 #[allow(clippy::too_many_arguments)]
1748 fn layout_breakable_view(
1749 &self,
1750 node: &Node,
1751 style: &ResolvedStyle,
1752 cursor: &mut PageCursor,
1753 pages: &mut Vec<LayoutPage>,
1754 node_x: f64,
1755 outer_width: f64,
1756 inner_width: f64,
1757 font_context: &FontContext,
1758 ) {
1759 let padding = &style.padding;
1760 let border = &style.border_width;
1761 let margin = &style.margin.to_edges();
1762
1763 let initial_page_count = pages.len();
1765 let snapshot = cursor.elements.len();
1766 let rect_start_y = cursor.content_y + cursor.y + margin.top;
1767
1768 cursor.y += margin.top + padding.top + border.top;
1769 let prev_continuation_offset = cursor.continuation_top_offset;
1770 cursor.continuation_top_offset = padding.top + border.top;
1771
1772 if node.bookmark.is_some() {
1774 cursor.elements.push(LayoutElement {
1775 x: node_x,
1776 y: cursor.content_y + cursor.y,
1777 width: 0.0,
1778 height: 0.0,
1779 draw: DrawCommand::None,
1780 children: vec![],
1781 node_type: None,
1782 resolved_style: None,
1783 source_location: None,
1784 href: None,
1785 bookmark: node.bookmark.clone(),
1786 alt: None,
1787 is_header_row: false,
1788 overflow: Overflow::default(),
1789 opacity: 1.0,
1790 });
1791 }
1792
1793 let children_x = node_x + padding.left + border.left;
1794 let is_grid =
1795 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
1796 if is_grid {
1797 self.layout_grid_children(
1798 &node.children,
1799 style,
1800 cursor,
1801 pages,
1802 children_x,
1803 inner_width,
1804 font_context,
1805 );
1806 } else {
1807 self.layout_children(
1808 &node.children,
1809 &node.style,
1810 cursor,
1811 pages,
1812 children_x,
1813 inner_width,
1814 Some(style),
1815 font_context,
1816 );
1817 }
1818
1819 cursor.continuation_top_offset = prev_continuation_offset;
1820
1821 let has_visual = style.background_color.is_some()
1823 || style.border_width.top > 0.0
1824 || style.border_width.right > 0.0
1825 || style.border_width.bottom > 0.0
1826 || style.border_width.left > 0.0;
1827 let needs_wrapper = has_visual || style.flex_grow > 0.0;
1829
1830 if !needs_wrapper {
1831 cursor.y += padding.bottom + border.bottom + margin.bottom;
1833 return;
1834 }
1835
1836 let draw_cmd = DrawCommand::Rect {
1837 background: style.background_color,
1838 border_width: style.border_width,
1839 border_color: style.border_color,
1840 border_radius: style.border_radius,
1841 opacity: 1.0,
1842 box_shadow: style.box_shadow.map(Box::new),
1843 background_gradient: style.background.clone().map(Box::new),
1844 };
1845
1846 if pages.len() == initial_page_count {
1847 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
1849 let rect_height =
1850 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
1851 cursor.elements.push(LayoutElement {
1852 x: node_x,
1853 y: rect_start_y,
1854 width: outer_width,
1855 height: rect_height,
1856 draw: draw_cmd,
1857 children: child_elements,
1858 node_type: Some(node_kind_name(&node.kind).to_string()),
1859 resolved_style: Some(style.clone()),
1860 source_location: node.source_location.clone(),
1861 href: node.href.clone(),
1862 bookmark: node.bookmark.clone(),
1863 alt: None,
1864 is_header_row: false,
1865 overflow: style.overflow,
1866 opacity: style.opacity,
1867 });
1868 } else {
1869 let page = &mut pages[initial_page_count];
1873 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
1874 let page_content_bottom =
1875 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
1876 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
1877 if !our_elements.is_empty() {
1878 let rect_height = page_content_bottom - rect_start_y;
1879 page.elements.push(LayoutElement {
1880 x: node_x,
1881 y: rect_start_y,
1882 width: outer_width,
1883 height: rect_height,
1884 draw: draw_cmd.clone(),
1885 children: our_elements,
1886 node_type: Some(node_kind_name(&node.kind).to_string()),
1887 resolved_style: Some(style.clone()),
1888 source_location: node.source_location.clone(),
1889 href: node.href.clone(),
1890 bookmark: node.bookmark.clone(),
1891 alt: None,
1892 is_header_row: false,
1893 overflow: Overflow::default(),
1894 opacity: 1.0,
1895 });
1896 }
1897
1898 for page in &mut pages[initial_page_count + 1..] {
1900 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
1901 let content_top = page.config.margin.top + header_h;
1902 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
1903 let content_bottom = page.config.margin.top
1904 + (page.height - page.config.margin.vertical())
1905 - footer_h;
1906 let all_elements: Vec<LayoutElement> = page.elements.drain(..).collect();
1907 if !all_elements.is_empty() {
1908 page.elements.push(LayoutElement {
1909 x: node_x,
1910 y: content_top,
1911 width: outer_width,
1912 height: content_bottom - content_top,
1913 draw: draw_cmd.clone(),
1914 children: all_elements,
1915 node_type: Some(node_kind_name(&node.kind).to_string()),
1916 resolved_style: Some(style.clone()),
1917 source_location: node.source_location.clone(),
1918 href: None,
1919 bookmark: None,
1920 alt: None,
1921 is_header_row: false,
1922 overflow: Overflow::default(),
1923 opacity: 1.0,
1924 });
1925 }
1926 }
1927
1928 let all_elements: Vec<LayoutElement> = cursor.elements.drain(..).collect();
1930 if !all_elements.is_empty() {
1931 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
1932 let content_top = cursor.content_y + header_h;
1933 let rect_height =
1934 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
1935 cursor.elements.push(LayoutElement {
1936 x: node_x,
1937 y: content_top,
1938 width: outer_width,
1939 height: rect_height,
1940 draw: draw_cmd,
1941 children: all_elements,
1942 node_type: Some(node_kind_name(&node.kind).to_string()),
1943 resolved_style: Some(style.clone()),
1944 source_location: node.source_location.clone(),
1945 href: None,
1946 bookmark: None,
1947 alt: None,
1948 is_header_row: false,
1949 overflow: Overflow::default(),
1950 opacity: 1.0,
1951 });
1952 }
1953 }
1954
1955 cursor.y += padding.bottom + border.bottom + margin.bottom;
1956 }
1957
1958 #[allow(clippy::too_many_arguments)]
1959 fn layout_children(
1960 &self,
1961 children: &[Node],
1962 _parent_raw_style: &Style,
1963 cursor: &mut PageCursor,
1964 pages: &mut Vec<LayoutPage>,
1965 content_x: f64,
1966 available_width: f64,
1967 parent_style: Option<&ResolvedStyle>,
1968 font_context: &FontContext,
1969 ) {
1970 let parent_box_y = cursor.content_y + cursor.y;
1972 let parent_box_x = content_x;
1973
1974 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
1976 .iter()
1977 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
1978
1979 let direction = parent_style
1980 .map(|s| s.flex_direction)
1981 .unwrap_or(FlexDirection::Column);
1982
1983 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
1984 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
1985
1986 match direction {
1988 FlexDirection::Column | FlexDirection::ColumnReverse => {
1989 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
1990 flow_children.into_iter().rev().collect()
1991 } else {
1992 flow_children
1993 };
1994
1995 let justify = parent_style
1996 .map(|s| s.justify_content)
1997 .unwrap_or(JustifyContent::FlexStart);
1998 let align = parent_style
1999 .map(|s| s.align_items)
2000 .unwrap_or(AlignItems::Stretch);
2001
2002 let start_y = cursor.y;
2003 let initial_pages = pages.len();
2004
2005 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
2007
2008 for (i, child) in items.iter().enumerate() {
2009 if i > 0 {
2010 cursor.y += row_gap;
2011 }
2012 let child_start = cursor.elements.len();
2013
2014 let child_margin = &child.style.resolve(parent_style, available_width).margin;
2017 let has_auto_h = child_margin.has_auto_horizontal();
2018
2019 let (child_x, layout_w) = if has_auto_h {
2026 let child_style = child.style.resolve(parent_style, available_width);
2027 let has_explicit_width =
2028 matches!(child_style.width, SizeConstraint::Fixed(_));
2029 let intrinsic = self
2030 .measure_intrinsic_width(child, &child_style, font_context)
2031 .min(available_width);
2032 let w = match child_style.width {
2033 SizeConstraint::Fixed(fw) => fw,
2034 SizeConstraint::Auto => intrinsic,
2035 };
2036 let lw = if has_explicit_width {
2037 available_width
2038 } else {
2039 w
2040 };
2041 let fixed_h = child_margin.horizontal();
2042 let slack = (available_width - w - fixed_h).max(0.0);
2043 let auto_left = child_margin.left.is_auto();
2044 let auto_right = child_margin.right.is_auto();
2045 let ml = match (auto_left, auto_right) {
2046 (true, true) => slack / 2.0,
2047 (true, false) => slack,
2048 (false, true) => 0.0,
2049 (false, false) => 0.0,
2050 };
2051 (content_x + child_margin.left.resolve() + ml, lw)
2052 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
2053 let child_style = child.style.resolve(parent_style, available_width);
2054 let has_explicit_width =
2055 matches!(child_style.width, SizeConstraint::Fixed(_));
2056 let intrinsic = self
2057 .measure_intrinsic_width(child, &child_style, font_context)
2058 .min(available_width);
2059 let w = match child_style.width {
2060 SizeConstraint::Fixed(fw) => fw,
2061 SizeConstraint::Auto => intrinsic,
2062 };
2063 let lw = if has_explicit_width {
2064 available_width
2065 } else {
2066 w
2067 };
2068 match align {
2069 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
2070 AlignItems::FlexEnd => (content_x + available_width - w, lw),
2071 _ => (content_x, available_width),
2072 }
2073 } else {
2074 (content_x, available_width)
2075 };
2076
2077 self.layout_node(
2078 child,
2079 cursor,
2080 pages,
2081 child_x,
2082 layout_w,
2083 parent_style,
2084 font_context,
2085 None,
2086 None,
2087 );
2088
2089 child_ranges.push((child_start, cursor.elements.len()));
2090 }
2091
2092 let container_inner_h: Option<f64> = parent_style
2095 .and_then(|ps| match ps.height {
2096 SizeConstraint::Fixed(h) => {
2097 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2098 }
2099 SizeConstraint::Auto => None,
2100 })
2101 .or_else(|| {
2102 if parent_style.is_none() {
2104 Some(cursor.content_height - start_y)
2105 } else {
2106 None
2107 }
2108 });
2109
2110 if let Some(inner_h) = container_inner_h {
2111 if pages.len() == initial_pages {
2112 let child_styles: Vec<ResolvedStyle> = items
2113 .iter()
2114 .map(|child| child.style.resolve(parent_style, available_width))
2115 .collect();
2116 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
2117 if total_grow > 0.0 {
2118 let children_total = cursor.y - start_y;
2119 let slack = (inner_h - children_total).max(0.0);
2120 if slack > 0.0 {
2121 let mut cumulative_shift = 0.0_f64;
2122 for (i, cs) in child_styles.iter().enumerate() {
2123 let (start, end) = child_ranges[i];
2124 if cumulative_shift > 0.001 {
2125 for j in start..end {
2126 offset_element_y(
2127 &mut cursor.elements[j],
2128 cumulative_shift,
2129 );
2130 }
2131 }
2132 if cs.flex_grow > 0.0 {
2133 let extra = slack * (cs.flex_grow / total_grow);
2134 if start < end {
2136 let elem = &mut cursor.elements[end - 1];
2137 elem.height += extra;
2138 reapply_justify_content(elem);
2139 }
2140 cumulative_shift += extra;
2141 }
2142 }
2143 cursor.y += cumulative_shift;
2144 }
2145 }
2146 }
2147 }
2148
2149 if let Some(inner_h) = container_inner_h {
2156 if pages.len() == initial_pages {
2157 let auto_styles: Vec<ResolvedStyle> = items
2158 .iter()
2159 .map(|child| child.style.resolve(parent_style, available_width))
2160 .collect();
2161 let total_autos: usize = auto_styles
2162 .iter()
2163 .map(|s| {
2164 s.margin.top.is_auto() as usize + s.margin.bottom.is_auto() as usize
2165 })
2166 .sum();
2167 if total_autos > 0 {
2168 let children_total = cursor.y - start_y;
2169 let total_slack = (inner_h - children_total).max(0.0);
2170 if total_slack > 0.0 {
2171 let per_auto = total_slack / total_autos as f64;
2172 let mut cumulative_shift = 0.0_f64;
2173 for (i, cs) in auto_styles.iter().enumerate() {
2174 let (start, end) = child_ranges[i];
2175 let mt_auto = cs.margin.top.is_auto();
2176 let mb_auto = cs.margin.bottom.is_auto();
2177 let this_child_shift =
2181 cumulative_shift + if mt_auto { per_auto } else { 0.0 };
2182 if this_child_shift > 0.001 {
2183 for j in start..end {
2184 offset_element_y(
2185 &mut cursor.elements[j],
2186 this_child_shift,
2187 );
2188 }
2189 }
2190 cumulative_shift =
2193 this_child_shift + if mb_auto { per_auto } else { 0.0 };
2194 }
2195 cursor.y += cumulative_shift;
2196 }
2197 }
2198 }
2199 }
2200
2201 let needs_justify =
2203 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
2204 if needs_justify {
2205 let justify_inner_h = container_inner_h.or_else(|| {
2207 parent_style.and_then(|ps| match ps.height {
2208 SizeConstraint::Fixed(h) => {
2209 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2210 }
2211 SizeConstraint::Auto => None,
2212 })
2213 });
2214 if let Some(inner_h) = justify_inner_h {
2215 let children_total = cursor.y - start_y;
2216 let slack = inner_h - children_total;
2217 if slack > 0.0 {
2218 let n = child_ranges.len();
2219 let offsets: Vec<f64> = match justify {
2220 JustifyContent::FlexEnd => vec![slack; n],
2221 JustifyContent::Center => vec![slack / 2.0; n],
2222 JustifyContent::SpaceBetween => {
2223 if n <= 1 {
2224 vec![0.0; n]
2225 } else {
2226 let per_gap = slack / (n - 1) as f64;
2227 (0..n).map(|i| i as f64 * per_gap).collect()
2228 }
2229 }
2230 JustifyContent::SpaceAround => {
2231 let space = slack / n as f64;
2232 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2233 }
2234 JustifyContent::SpaceEvenly => {
2235 let space = slack / (n + 1) as f64;
2236 (0..n).map(|i| (i + 1) as f64 * space).collect()
2237 }
2238 JustifyContent::FlexStart => vec![0.0; n],
2239 };
2240 for (i, &(start, end)) in child_ranges.iter().enumerate() {
2241 let dy = offsets[i];
2242 if dy.abs() > 0.001 {
2243 for j in start..end {
2244 offset_element_y(&mut cursor.elements[j], dy);
2245 }
2246 }
2247 }
2248 cursor.y += *offsets.last().unwrap_or(&0.0);
2249 }
2250 }
2251 }
2252 }
2253
2254 FlexDirection::Row | FlexDirection::RowReverse => {
2255 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
2256 self.layout_flex_row(
2257 &flow_owned,
2258 cursor,
2259 pages,
2260 content_x,
2261 available_width,
2262 parent_style,
2263 column_gap,
2264 row_gap,
2265 font_context,
2266 );
2267 }
2268 }
2269
2270 for abs_child in &abs_children {
2272 let abs_style = abs_child.style.resolve(parent_style, available_width);
2273
2274 let child_width = match abs_style.width {
2276 SizeConstraint::Fixed(w) => w,
2277 SizeConstraint::Auto => {
2278 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
2280 (available_width - l - r).max(0.0)
2281 } else {
2282 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
2283 }
2284 }
2285 };
2286
2287 let child_height = match abs_style.height {
2288 SizeConstraint::Fixed(h) => h,
2289 SizeConstraint::Auto => {
2290 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
2291 }
2292 };
2293
2294 let abs_x = if let Some(l) = abs_style.left {
2296 parent_box_x + l
2297 } else if let Some(r) = abs_style.right {
2298 parent_box_x + available_width - r - child_width
2299 } else {
2300 parent_box_x
2301 };
2302
2303 let parent_inner_height = parent_style
2305 .and_then(|ps| match ps.height {
2306 SizeConstraint::Fixed(h) => {
2307 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2308 }
2309 SizeConstraint::Auto => None,
2310 })
2311 .unwrap_or(cursor.content_y + cursor.y - parent_box_y);
2312
2313 let abs_y = if let Some(t) = abs_style.top {
2314 parent_box_y + t
2315 } else if let Some(b) = abs_style.bottom {
2316 parent_box_y + parent_inner_height - b - child_height
2317 } else {
2318 parent_box_y
2319 };
2320
2321 let mut abs_cursor = PageCursor::new(&cursor.config);
2323 abs_cursor.y = 0.0;
2324 abs_cursor.content_x = abs_x;
2325 abs_cursor.content_y = abs_y;
2326
2327 self.layout_node(
2328 abs_child,
2329 &mut abs_cursor,
2330 &mut Vec::new(),
2331 abs_x,
2332 child_width,
2333 parent_style,
2334 font_context,
2335 None,
2336 None,
2337 );
2338
2339 cursor.elements.extend(abs_cursor.elements);
2341 }
2342 }
2343
2344 #[allow(clippy::too_many_arguments)]
2345 fn layout_flex_row(
2346 &self,
2347 children: &[Node],
2348 cursor: &mut PageCursor,
2349 pages: &mut Vec<LayoutPage>,
2350 content_x: f64,
2351 available_width: f64,
2352 parent_style: Option<&ResolvedStyle>,
2353 column_gap: f64,
2354 row_gap: f64,
2355 font_context: &FontContext,
2356 ) {
2357 if children.is_empty() {
2358 return;
2359 }
2360
2361 let flex_wrap = parent_style
2362 .map(|s| s.flex_wrap)
2363 .unwrap_or(FlexWrap::NoWrap);
2364
2365 let items: Vec<FlexItem> = children
2368 .iter()
2369 .map(|child| {
2370 let style = child.style.resolve(parent_style, available_width);
2371 let base_width = match style.flex_basis {
2372 SizeConstraint::Fixed(w) => w,
2373 SizeConstraint::Auto => match style.width {
2374 SizeConstraint::Fixed(w) => w,
2375 SizeConstraint::Auto => {
2376 self.measure_intrinsic_width(child, &style, font_context)
2377 }
2378 },
2379 };
2380 let min_content_width = self.measure_min_content_width(child, &style, font_context);
2381 FlexItem {
2382 node: child,
2383 style,
2384 base_width,
2385 min_content_width,
2386 }
2387 })
2388 .collect();
2389
2390 let base_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2392 let lines = match flex_wrap {
2393 FlexWrap::NoWrap => {
2394 vec![flex::WrapLine {
2395 start: 0,
2396 end: items.len(),
2397 }]
2398 }
2399 FlexWrap::Wrap => flex::partition_into_lines(&base_widths, column_gap, available_width),
2400 FlexWrap::WrapReverse => {
2401 let mut l = flex::partition_into_lines(&base_widths, column_gap, available_width);
2402 l.reverse();
2403 l
2404 }
2405 };
2406
2407 if lines.is_empty() {
2408 return;
2409 }
2410
2411 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
2413
2414 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2416
2417 let initial_pages_count = pages.len();
2418 let flex_start_y = cursor.y;
2419 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
2420
2421 for (line_idx, line) in lines.iter().enumerate() {
2422 let line_items = &items[line.start..line.end];
2423 let line_count = line.end - line.start;
2424 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
2425 let distributable = available_width - line_gap;
2426
2427 let total_base: f64 = line_items.iter().map(|i| i.base_width).sum();
2429 let remaining = distributable - total_base;
2430
2431 if remaining > 0.0 {
2432 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
2433 if total_grow > 0.0 {
2434 for (j, item) in line_items.iter().enumerate() {
2435 final_widths[line.start + j] =
2436 item.base_width + remaining * (item.style.flex_grow / total_grow);
2437 }
2438 }
2439 } else if remaining < 0.0 {
2440 let total_shrink: f64 = line_items
2441 .iter()
2442 .map(|i| i.style.flex_shrink * i.base_width)
2443 .sum();
2444 if total_shrink > 0.0 {
2445 for (j, item) in line_items.iter().enumerate() {
2446 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
2447 let w = item.base_width + remaining * factor;
2448 let floor = item.style.min_width.max(item.min_content_width);
2449 final_widths[line.start + j] = w.max(floor);
2450 }
2451 }
2452 }
2453
2454 let line_height: f64 = line_items
2456 .iter()
2457 .enumerate()
2458 .map(|(j, item)| {
2459 let fw = final_widths[line.start + j];
2460 self.measure_node_height(item.node, fw, &item.style, font_context)
2461 + item.style.margin.vertical()
2462 })
2463 .fold(0.0f64, f64::max);
2464
2465 if line_height > cursor.remaining_height() {
2467 pages.push(cursor.finalize());
2468 *cursor = cursor.new_page();
2469 }
2470
2471 if line_idx > 0 {
2473 cursor.y += row_gap;
2474 }
2475
2476 let row_start_y = cursor.y;
2477
2478 let actual_total: f64 = (line.start..line.end).map(|i| final_widths[i]).sum();
2480 let slack = available_width - actual_total - line_gap;
2481
2482 let (start_offset, between_extra) = match justify {
2483 JustifyContent::FlexStart => (0.0, 0.0),
2484 JustifyContent::FlexEnd => (slack, 0.0),
2485 JustifyContent::Center => (slack / 2.0, 0.0),
2486 JustifyContent::SpaceBetween => {
2487 if line_count > 1 {
2488 (0.0, slack / (line_count as f64 - 1.0))
2489 } else {
2490 (0.0, 0.0)
2491 }
2492 }
2493 JustifyContent::SpaceAround => {
2494 let s = slack / line_count as f64;
2495 (s / 2.0, s)
2496 }
2497 JustifyContent::SpaceEvenly => {
2498 let s = slack / (line_count as f64 + 1.0);
2499 (s, s)
2500 }
2501 };
2502
2503 let line_elem_start = cursor.elements.len();
2504 let mut x = content_x + start_offset;
2505
2506 for (j, item) in line_items.iter().enumerate() {
2507 if j > 0 {
2508 x += column_gap + between_extra;
2509 }
2510
2511 let fw = final_widths[line.start + j];
2512
2513 let align = item
2514 .style
2515 .align_self
2516 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
2517
2518 let item_height =
2519 self.measure_node_height(item.node, fw, &item.style, font_context);
2520
2521 let has_auto_v = item.style.margin.has_auto_vertical();
2523 let y_offset = if has_auto_v {
2524 let fixed_v = item.style.margin.vertical();
2525 let slack = (line_height - item_height - fixed_v).max(0.0);
2526 let auto_top = item.style.margin.top.is_auto();
2527 let auto_bottom = item.style.margin.bottom.is_auto();
2528 match (auto_top, auto_bottom) {
2529 (true, true) => slack / 2.0,
2530 (true, false) => slack,
2531 (false, true) => 0.0,
2532 (false, false) => 0.0,
2533 }
2534 } else {
2535 match align {
2536 AlignItems::FlexStart => 0.0,
2537 AlignItems::FlexEnd => {
2538 line_height - item_height - item.style.margin.vertical()
2539 }
2540 AlignItems::Center => {
2541 (line_height - item_height - item.style.margin.vertical()) / 2.0
2542 }
2543 AlignItems::Stretch => 0.0,
2544 AlignItems::Baseline => 0.0,
2545 }
2546 };
2547
2548 let cross_h = if matches!(align, AlignItems::Stretch)
2552 && matches!(item.style.height, SizeConstraint::Auto)
2553 && !has_auto_v
2554 {
2555 let stretch_h = line_height - item.style.margin.vertical();
2556 if stretch_h > item_height {
2557 Some(stretch_h)
2558 } else {
2559 None
2560 }
2561 } else {
2562 None
2563 };
2564
2565 let saved_y = cursor.y;
2566 cursor.y = row_start_y + y_offset;
2567
2568 self.layout_node(
2569 item.node,
2570 cursor,
2571 pages,
2572 x,
2573 available_width,
2574 parent_style,
2575 font_context,
2576 cross_h,
2577 Some(fw),
2578 );
2579
2580 cursor.y = saved_y;
2581 x += fw;
2582 }
2583
2584 cursor.y = row_start_y + line_height;
2585 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
2586 }
2587
2588 if pages.len() == initial_pages_count && !line_infos.is_empty() {
2590 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
2591 if !matches!(align_content, AlignContent::FlexStart)
2592 && !matches!(flex_wrap, FlexWrap::NoWrap)
2593 {
2594 if let Some(parent) = parent_style {
2595 if let SizeConstraint::Fixed(container_h) = parent.height {
2596 let inner_h = container_h
2597 - parent.padding.vertical()
2598 - parent.border_width.vertical();
2599 let total_used = cursor.y - flex_start_y;
2600 let slack = inner_h - total_used;
2601 if slack > 0.0 {
2602 let n = line_infos.len();
2603 let offsets: Vec<f64> = match align_content {
2604 AlignContent::FlexEnd => vec![slack; n],
2605 AlignContent::Center => vec![slack / 2.0; n],
2606 AlignContent::SpaceBetween => {
2607 if n <= 1 {
2608 vec![0.0; n]
2609 } else {
2610 let per_gap = slack / (n - 1) as f64;
2611 (0..n).map(|i| i as f64 * per_gap).collect()
2612 }
2613 }
2614 AlignContent::SpaceAround => {
2615 let space = slack / n as f64;
2616 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2617 }
2618 AlignContent::SpaceEvenly => {
2619 let space = slack / (n + 1) as f64;
2620 (0..n).map(|i| (i + 1) as f64 * space).collect()
2621 }
2622 AlignContent::Stretch => {
2623 let extra = slack / n as f64;
2624 (0..n).map(|i| i as f64 * extra).collect()
2625 }
2626 AlignContent::FlexStart => vec![0.0; n],
2627 };
2628 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
2629 let dy = offsets[i];
2630 if dy.abs() > 0.001 {
2631 for j in start..end {
2632 offset_element_y(&mut cursor.elements[j], dy);
2633 }
2634 }
2635 }
2636 cursor.y += *offsets.last().unwrap_or(&0.0);
2637 }
2638 }
2639 }
2640 }
2641 }
2642 }
2643
2644 #[allow(clippy::too_many_arguments)]
2647 fn layout_list(
2648 &self,
2649 node: &Node,
2650 ordered: bool,
2651 marker_type: ListMarkerType,
2652 start: u32,
2653 style: &ResolvedStyle,
2654 cursor: &mut PageCursor,
2655 pages: &mut Vec<LayoutPage>,
2656 x: f64,
2657 available_width: f64,
2658 font_context: &FontContext,
2659 ) {
2660 let margin = &style.margin.to_edges();
2661 let padding = &style.padding;
2662
2663 cursor.y += margin.top;
2664
2665 let list_x = x + margin.left;
2666 let outer_width = available_width - margin.horizontal();
2667 let inner_width = outer_width - padding.horizontal();
2668
2669 let n_items = node
2672 .children
2673 .iter()
2674 .filter(|c| matches!(c.kind, NodeKind::ListItem))
2675 .count() as u32;
2676
2677 let marker_gutter =
2678 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
2679
2680 let list_inner_x = list_x + padding.left;
2681 let content_x = list_inner_x + marker_gutter;
2682 let content_width = (inner_width - marker_gutter).max(0.0);
2683
2684 let snapshot = cursor.elements.len();
2687 let list_start_y = cursor.content_y + cursor.y;
2688 cursor.y += padding.top;
2689
2690 let mut item_index: u32 = 0;
2691 for child in &node.children {
2692 if !matches!(child.kind, NodeKind::ListItem) {
2693 continue;
2694 }
2695 let marker_idx = start + item_index;
2696 self.layout_list_item(
2697 child,
2698 marker_idx,
2699 ordered,
2700 marker_type,
2701 marker_gutter,
2702 style,
2703 cursor,
2704 pages,
2705 list_inner_x,
2706 content_x,
2707 content_width,
2708 font_context,
2709 );
2710 item_index += 1;
2711 }
2712
2713 cursor.y += padding.bottom;
2714
2715 let item_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2717 let list_height = cursor.content_y + cursor.y - list_start_y;
2718 cursor.elements.push(LayoutElement {
2719 x: list_x,
2720 y: list_start_y,
2721 width: outer_width,
2722 height: list_height,
2723 draw: DrawCommand::None,
2724 children: item_elements,
2725 node_type: Some("List".to_string()),
2726 resolved_style: Some(style.clone()),
2727 source_location: node.source_location.clone(),
2728 href: None,
2729 bookmark: node.bookmark.clone(),
2730 alt: None,
2731 is_header_row: false,
2732 overflow: style.overflow,
2733 opacity: style.opacity,
2734 });
2735
2736 cursor.y += margin.bottom;
2737 }
2738
2739 #[allow(clippy::too_many_arguments)]
2740 fn layout_list_item(
2741 &self,
2742 item: &Node,
2743 marker_idx: u32,
2744 ordered: bool,
2745 marker_type: ListMarkerType,
2746 marker_gutter: f64,
2747 parent_style: &ResolvedStyle,
2748 cursor: &mut PageCursor,
2749 pages: &mut Vec<LayoutPage>,
2750 list_inner_x: f64,
2751 content_x: f64,
2752 content_width: f64,
2753 font_context: &FontContext,
2754 ) {
2755 let item_style = item.style.resolve(Some(parent_style), content_width);
2756 let item_margin = item_style.margin.to_edges();
2757
2758 cursor.y += item_margin.top;
2759 let item_start_y = cursor.content_y + cursor.y;
2760 let item_snapshot = cursor.elements.len();
2761
2762 let marker_str = format_marker(marker_idx, ordered, marker_type);
2766 if !marker_str.is_empty() {
2767 let saved_y = cursor.y;
2768 self.layout_text(
2769 &marker_str,
2770 None,
2771 &[],
2772 &item_style,
2773 cursor,
2774 pages,
2775 list_inner_x,
2776 marker_gutter,
2777 font_context,
2778 None,
2779 None,
2780 Some("Lbl"),
2781 );
2782 cursor.y = saved_y;
2783 }
2784
2785 self.layout_children(
2791 &item.children,
2792 &item.style,
2793 cursor,
2794 pages,
2795 content_x,
2796 content_width,
2797 Some(&item_style),
2798 font_context,
2799 );
2800
2801 let item_children: Vec<LayoutElement> = cursor.elements.drain(item_snapshot..).collect();
2804 let item_height = cursor.content_y + cursor.y - item_start_y;
2805 let item_width = content_x + content_width - list_inner_x;
2806 cursor.elements.push(LayoutElement {
2807 x: list_inner_x,
2808 y: item_start_y,
2809 width: item_width,
2810 height: item_height,
2811 draw: DrawCommand::None,
2812 children: item_children,
2813 node_type: Some("ListItem".to_string()),
2814 resolved_style: Some(item_style.clone()),
2815 source_location: item.source_location.clone(),
2816 href: None,
2817 bookmark: item.bookmark.clone(),
2818 alt: None,
2819 is_header_row: false,
2820 overflow: item_style.overflow,
2821 opacity: item_style.opacity,
2822 });
2823
2824 cursor.y += item_margin.bottom;
2825 }
2826
2827 #[allow(clippy::too_many_arguments)]
2828 fn layout_table(
2829 &self,
2830 node: &Node,
2831 style: &ResolvedStyle,
2832 column_defs: &[ColumnDef],
2833 cursor: &mut PageCursor,
2834 pages: &mut Vec<LayoutPage>,
2835 x: f64,
2836 available_width: f64,
2837 font_context: &FontContext,
2838 ) {
2839 let padding = &style.padding;
2840 let margin = &style.margin.to_edges();
2841 let border = &style.border_width;
2842
2843 let table_x = x + margin.left;
2844 let table_width = match style.width {
2845 SizeConstraint::Fixed(w) => w,
2846 SizeConstraint::Auto => available_width - margin.horizontal(),
2847 };
2848 let inner_width = table_width - padding.horizontal() - border.horizontal();
2849
2850 let col_widths = self.resolve_column_widths(column_defs, inner_width, &node.children);
2851
2852 let mut header_rows: Vec<&Node> = Vec::new();
2853 let mut body_rows: Vec<&Node> = Vec::new();
2854
2855 for child in &node.children {
2856 match &child.kind {
2857 NodeKind::TableRow { is_header: true } => header_rows.push(child),
2858 _ => body_rows.push(child),
2859 }
2860 }
2861
2862 cursor.y += margin.top + padding.top + border.top;
2863
2864 let cell_x_start = table_x + padding.left + border.left;
2865
2866 if !header_rows.is_empty() {
2891 let total_header_h: f64 = header_rows
2892 .iter()
2893 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
2894 .sum();
2895 let first_body_h = body_rows
2896 .first()
2897 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
2898 .unwrap_or(0.0);
2899
2900 let needed = total_header_h + first_body_h;
2901 let fresh_page_available = cursor.content_height
2902 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
2903 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
2904
2905 if needed > cursor.remaining_height() && needed <= fresh_page_available {
2906 pages.push(cursor.finalize());
2907 *cursor = cursor.new_page();
2908 cursor.y += padding.top + border.top;
2909 }
2910 }
2911
2912 for header_row in &header_rows {
2913 self.layout_table_row(
2914 header_row,
2915 &col_widths,
2916 style,
2917 cursor,
2918 cell_x_start,
2919 font_context,
2920 pages,
2921 );
2922 }
2923
2924 for body_row in &body_rows {
2925 let row_height =
2926 self.measure_table_row_height(body_row, &col_widths, style, font_context);
2927
2928 if row_height > cursor.remaining_height() {
2929 pages.push(cursor.finalize());
2930 *cursor = cursor.new_page();
2931
2932 cursor.y += padding.top + border.top;
2933 for header_row in &header_rows {
2934 self.layout_table_row(
2935 header_row,
2936 &col_widths,
2937 style,
2938 cursor,
2939 cell_x_start,
2940 font_context,
2941 pages,
2942 );
2943 }
2944 }
2945
2946 self.layout_table_row(
2947 body_row,
2948 &col_widths,
2949 style,
2950 cursor,
2951 cell_x_start,
2952 font_context,
2953 pages,
2954 );
2955 }
2956
2957 cursor.y += padding.bottom + border.bottom + margin.bottom;
2958 }
2959
2960 #[allow(clippy::too_many_arguments)]
2961 fn layout_table_row(
2962 &self,
2963 row: &Node,
2964 col_widths: &[f64],
2965 parent_style: &ResolvedStyle,
2966 cursor: &mut PageCursor,
2967 start_x: f64,
2968 font_context: &FontContext,
2969 pages: &mut Vec<LayoutPage>,
2970 ) {
2971 let row_style = row
2972 .style
2973 .resolve(Some(parent_style), col_widths.iter().sum());
2974
2975 let row_height = self.measure_table_row_height(row, col_widths, parent_style, font_context);
2976 let row_y = cursor.content_y + cursor.y;
2977 let total_width: f64 = col_widths.iter().sum();
2978
2979 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
2980
2981 let row_snapshot = cursor.elements.len();
2983
2984 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
2985 let mut cell_x = start_x;
2986 for (i, cell) in row.children.iter().enumerate() {
2987 let col_width = col_widths.get(i).copied().unwrap_or(0.0);
2988
2989 let cell_style = cell.style.resolve(Some(&row_style), col_width);
2990
2991 let cell_snapshot = cursor.elements.len();
2993
2994 let inner_width =
2995 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
2996
2997 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
2998 let saved_y = cursor.y;
2999 cursor.y += cell_style.padding.top + cell_style.border_width.top;
3000
3001 let cursor_before_cell = cursor.clone();
3003 let mut cell_pages: Vec<LayoutPage> = Vec::new();
3004 for child in &cell.children {
3005 self.layout_node(
3006 child,
3007 cursor,
3008 &mut cell_pages,
3009 content_x,
3010 inner_width,
3011 Some(&cell_style),
3012 font_context,
3013 None,
3014 None,
3015 );
3016 }
3017
3018 if !cell_pages.is_empty() {
3020 let post_break_elements = std::mem::take(&mut cursor.elements);
3021 if let Some(last_page) = cell_pages.last_mut() {
3022 last_page.elements.extend(post_break_elements);
3023 }
3024 if !is_header {
3030 all_overflow_pages.extend(cell_pages);
3031 }
3032 *cursor = cursor_before_cell;
3033 }
3034
3035 cursor.y = saved_y;
3036
3037 let cell_children: Vec<LayoutElement> =
3039 cursor.elements.drain(cell_snapshot..).collect();
3040
3041 cursor.elements.push(LayoutElement {
3043 x: cell_x,
3044 y: row_y,
3045 width: col_width,
3046 height: row_height,
3047 draw: if cell_style.background_color.is_some()
3048 || cell_style.border_width.horizontal() > 0.0
3049 || cell_style.border_width.vertical() > 0.0
3050 {
3051 DrawCommand::Rect {
3052 background: cell_style.background_color,
3053 border_width: cell_style.border_width,
3054 border_color: cell_style.border_color,
3055 border_radius: cell_style.border_radius,
3056 opacity: 1.0,
3057 box_shadow: cell_style.box_shadow.map(Box::new),
3058 background_gradient: cell_style.background.clone().map(Box::new),
3059 }
3060 } else {
3061 DrawCommand::None
3062 },
3063 children: cell_children,
3064 node_type: Some("TableCell".to_string()),
3065 resolved_style: Some(cell_style.clone()),
3066 source_location: cell.source_location.clone(),
3067 href: None,
3068 bookmark: cell.bookmark.clone(),
3069 alt: None,
3070 is_header_row: is_header,
3071 overflow: Overflow::default(),
3072 opacity: 1.0,
3073 });
3074
3075 cell_x += col_width;
3076 }
3077
3078 let row_children: Vec<LayoutElement> = cursor.elements.drain(row_snapshot..).collect();
3080 cursor.elements.push(LayoutElement {
3081 x: start_x,
3082 y: row_y,
3083 width: total_width,
3084 height: row_height,
3085 draw: if let Some(bg) = row_style.background_color {
3086 DrawCommand::Rect {
3087 background: Some(bg),
3088 border_width: Edges::default(),
3089 border_color: EdgeValues::uniform(Color::BLACK),
3090 border_radius: CornerValues::uniform(0.0),
3091 opacity: 1.0,
3092 box_shadow: row_style.box_shadow.map(Box::new),
3093 background_gradient: row_style.background.clone().map(Box::new),
3094 }
3095 } else {
3096 DrawCommand::None
3097 },
3098 children: row_children,
3099 node_type: Some("TableRow".to_string()),
3100 resolved_style: Some(row_style.clone()),
3101 source_location: row.source_location.clone(),
3102 href: None,
3103 bookmark: row.bookmark.clone(),
3104 alt: None,
3105 is_header_row: is_header,
3106 overflow: row_style.overflow,
3107 opacity: row_style.opacity,
3108 });
3109
3110 pages.extend(all_overflow_pages);
3112
3113 cursor.y += row_height;
3114 }
3115
3116 #[allow(clippy::too_many_arguments)]
3117 #[allow(clippy::too_many_arguments)]
3118 fn layout_text(
3119 &self,
3120 content: &str,
3121 href: Option<&str>,
3122 runs: &[TextRun],
3123 style: &ResolvedStyle,
3124 cursor: &mut PageCursor,
3125 pages: &mut Vec<LayoutPage>,
3126 x: f64,
3127 available_width: f64,
3128 font_context: &FontContext,
3129 source_location: Option<&SourceLocation>,
3130 bookmark: Option<&str>,
3131 node_type_override: Option<&str>,
3135 ) {
3136 let margin = &style.margin.to_edges();
3137 let text_x = x + margin.left;
3138 let text_width = match style.width {
3145 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
3146 SizeConstraint::Auto => available_width - margin.horizontal(),
3147 };
3148
3149 cursor.y += margin.top;
3150
3151 if !runs.is_empty() {
3153 self.layout_text_runs(
3154 runs,
3155 href,
3156 style,
3157 cursor,
3158 pages,
3159 text_x,
3160 text_width,
3161 font_context,
3162 source_location,
3163 bookmark,
3164 node_type_override,
3165 );
3166 cursor.y += margin.bottom;
3167 return;
3168 }
3169
3170 let content = substitute_page_placeholders(content);
3171 let transformed = apply_text_transform(&content, style.text_transform);
3172 let justify = matches!(style.text_align, TextAlign::Justify);
3173 let lines = match style.line_breaking {
3174 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
3175 font_context,
3176 &transformed,
3177 text_width,
3178 style.font_size,
3179 &style.font_family,
3180 style.font_weight,
3181 style.font_style,
3182 style.letter_spacing,
3183 style.hyphens,
3184 style.lang.as_deref(),
3185 justify,
3186 ),
3187 LineBreaking::Greedy => self.text_layout.break_into_lines(
3188 font_context,
3189 &transformed,
3190 text_width,
3191 style.font_size,
3192 &style.font_family,
3193 style.font_weight,
3194 style.font_style,
3195 style.letter_spacing,
3196 style.hyphens,
3197 style.lang.as_deref(),
3198 ),
3199 };
3200
3201 let lines = match style.text_overflow {
3203 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
3204 font_context,
3205 lines,
3206 text_width,
3207 style.font_size,
3208 &style.font_family,
3209 style.font_weight,
3210 style.font_style,
3211 style.letter_spacing,
3212 ),
3213 TextOverflow::Clip => self.text_layout.truncate_clip(
3214 font_context,
3215 lines,
3216 text_width,
3217 style.font_size,
3218 &style.font_family,
3219 style.font_weight,
3220 style.font_style,
3221 style.letter_spacing,
3222 ),
3223 TextOverflow::Wrap => lines,
3224 };
3225
3226 let line_height = style.font_size * style.line_height;
3227
3228 let line_heights: Vec<f64> = vec![line_height; lines.len()];
3230 let decision = page_break::decide_break(
3231 cursor.remaining_height(),
3232 &line_heights,
3233 true,
3234 style.min_orphan_lines as usize,
3235 style.min_widow_lines as usize,
3236 );
3237
3238 let mut snapshot = cursor.elements.len();
3240 let mut container_start_y = cursor.content_y + cursor.y;
3241 let mut is_first_element = true;
3242
3243 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
3245 pages.push(cursor.finalize());
3246 *cursor = cursor.new_page();
3247 snapshot = cursor.elements.len();
3248 container_start_y = cursor.content_y + cursor.y;
3249 }
3250
3251 let forced_break_at = match decision {
3253 page_break::BreakDecision::Split {
3254 items_on_current_page,
3255 } => Some(items_on_current_page),
3256 _ => None,
3257 };
3258 let mut first_break_done = false;
3259
3260 for (line_idx, line) in lines.iter().enumerate() {
3261 let needs_break = if let Some(break_at) = forced_break_at {
3263 if !first_break_done && line_idx == break_at {
3264 true
3265 } else {
3266 line_height > cursor.remaining_height()
3267 }
3268 } else {
3269 line_height > cursor.remaining_height()
3270 };
3271
3272 if needs_break {
3273 first_break_done = true;
3274 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3276 if !line_elements.is_empty() {
3277 let container_height = cursor.content_y + cursor.y - container_start_y;
3278 cursor.elements.push(LayoutElement {
3279 x: text_x,
3280 y: container_start_y,
3281 width: text_width,
3282 height: container_height,
3283 draw: DrawCommand::None,
3284 children: line_elements,
3285 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
3286 resolved_style: Some(style.clone()),
3287 source_location: source_location.cloned(),
3288 href: href.map(|s| s.to_string()),
3289 bookmark: if is_first_element {
3290 bookmark.map(|s| s.to_string())
3291 } else {
3292 None
3293 },
3294 alt: None,
3295 is_header_row: false,
3296 overflow: Overflow::default(),
3297 opacity: 1.0,
3298 });
3299 is_first_element = false;
3300 }
3301
3302 pages.push(cursor.finalize());
3303 *cursor = cursor.new_page();
3304
3305 snapshot = cursor.elements.len();
3307 container_start_y = cursor.content_y + cursor.y;
3308 }
3309
3310 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
3311
3312 let rendered_width = if glyphs.is_empty() {
3316 line.width
3317 } else {
3318 let last = &glyphs[glyphs.len() - 1];
3319 (last.x_offset + last.x_advance).max(line.width * 0.5)
3320 };
3321
3322 let line_x = match style.text_align {
3323 TextAlign::Left => text_x,
3324 TextAlign::Right => text_x + text_width - rendered_width,
3325 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
3326 TextAlign::Justify => text_x,
3327 };
3328
3329 let is_last_line = line_idx == lines.len() - 1;
3337 let user_ws = style.word_spacing;
3338 let (justified_width, word_spacing) =
3339 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
3340 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
3341 let (natural_width, space_count) = if let Some(idx) = last_non_space {
3342 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
3343 let s = glyphs[..=idx]
3344 .iter()
3345 .filter(|g| g.char_value == ' ')
3346 .count();
3347 (w, s)
3348 } else {
3349 (0.0, 0)
3350 };
3351 let slack = text_width - natural_width;
3352 let ws = if space_count > 0 && slack.abs() > 0.01 {
3353 slack / space_count as f64
3354 } else {
3355 0.0
3356 };
3357 (text_width, user_ws + ws)
3358 } else {
3359 (rendered_width, user_ws)
3360 };
3361
3362 let text_line = TextLine {
3363 x: line_x,
3364 y: cursor.content_y + cursor.y + style.font_size,
3365 glyphs,
3366 width: justified_width,
3367 height: line_height,
3368 word_spacing,
3369 };
3370
3371 cursor.elements.push(LayoutElement {
3372 x: line_x,
3373 y: cursor.content_y + cursor.y,
3374 width: justified_width,
3375 height: line_height,
3376 draw: DrawCommand::Text {
3377 lines: vec![text_line],
3378 color: style.color,
3379 text_decoration: style.text_decoration,
3380 opacity: 1.0,
3381 },
3382 children: vec![],
3383 node_type: Some("TextLine".to_string()),
3384 resolved_style: Some(style.clone()),
3385 source_location: None,
3386 href: href.map(|s| s.to_string()),
3387 bookmark: None,
3388 alt: None,
3389 is_header_row: false,
3390 overflow: Overflow::default(),
3391 opacity: 1.0,
3392 });
3393
3394 cursor.y += line_height;
3395 }
3396
3397 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3399 if !line_elements.is_empty() {
3400 let container_height = cursor.content_y + cursor.y - container_start_y;
3401 cursor.elements.push(LayoutElement {
3402 x: text_x,
3403 y: container_start_y,
3404 width: text_width,
3405 height: container_height,
3406 draw: DrawCommand::None,
3407 children: line_elements,
3408 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
3409 resolved_style: Some(style.clone()),
3410 source_location: source_location.cloned(),
3411 href: href.map(|s| s.to_string()),
3412 bookmark: if is_first_element {
3413 bookmark.map(|s| s.to_string())
3414 } else {
3415 None
3416 },
3417 alt: None,
3418 is_header_row: false,
3419 overflow: Overflow::default(),
3420 opacity: 1.0,
3421 });
3422 }
3423
3424 cursor.y += margin.bottom;
3425 }
3426
3427 #[allow(clippy::too_many_arguments)]
3429 #[allow(clippy::too_many_arguments)]
3430 fn layout_text_runs(
3431 &self,
3432 runs: &[TextRun],
3433 parent_href: Option<&str>,
3434 style: &ResolvedStyle,
3435 cursor: &mut PageCursor,
3436 pages: &mut Vec<LayoutPage>,
3437 text_x: f64,
3438 text_width: f64,
3439 font_context: &FontContext,
3440 source_location: Option<&SourceLocation>,
3441 bookmark: Option<&str>,
3442 node_type_override: Option<&str>,
3444 ) {
3445 let mut styled_chars: Vec<StyledChar> = Vec::new();
3447 for run in runs {
3448 let run_style = run.style.resolve(Some(style), text_width);
3449 let run_href = run.href.as_deref().or(parent_href);
3450 let transform = run_style.text_transform;
3451 let run_content = substitute_page_placeholders(&run.content);
3452 let mut prev_is_whitespace = true;
3453 for ch in run_content.chars() {
3454 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
3455 prev_is_whitespace = ch.is_whitespace();
3456 styled_chars.push(StyledChar {
3457 ch: transformed_ch,
3458 font_family: run_style.font_family.clone(),
3459 font_size: run_style.font_size,
3460 font_weight: run_style.font_weight,
3461 font_style: run_style.font_style,
3462 color: run_style.color,
3463 href: run_href.map(|s| s.to_string()),
3464 text_decoration: run_style.text_decoration,
3465 letter_spacing: run_style.letter_spacing,
3466 });
3467 }
3468 }
3469
3470 let justify = matches!(style.text_align, TextAlign::Justify);
3472 let broken_lines = match style.line_breaking {
3473 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
3474 font_context,
3475 &styled_chars,
3476 text_width,
3477 style.hyphens,
3478 style.lang.as_deref(),
3479 justify,
3480 ),
3481 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
3482 font_context,
3483 &styled_chars,
3484 text_width,
3485 style.hyphens,
3486 style.lang.as_deref(),
3487 ),
3488 };
3489
3490 let broken_lines = match style.text_overflow {
3492 TextOverflow::Ellipsis => {
3493 self.text_layout
3494 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
3495 }
3496 TextOverflow::Clip => {
3497 self.text_layout
3498 .truncate_runs_clip(font_context, broken_lines, text_width)
3499 }
3500 TextOverflow::Wrap => broken_lines,
3501 };
3502
3503 let line_height = style.font_size * style.line_height;
3504
3505 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
3507 let decision = page_break::decide_break(
3508 cursor.remaining_height(),
3509 &line_heights,
3510 true,
3511 style.min_orphan_lines as usize,
3512 style.min_widow_lines as usize,
3513 );
3514
3515 let mut snapshot = cursor.elements.len();
3516 let mut container_start_y = cursor.content_y + cursor.y;
3517 let mut is_first_element = true;
3518
3519 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
3520 pages.push(cursor.finalize());
3521 *cursor = cursor.new_page();
3522 snapshot = cursor.elements.len();
3523 container_start_y = cursor.content_y + cursor.y;
3524 }
3525
3526 let forced_break_at = match decision {
3527 page_break::BreakDecision::Split {
3528 items_on_current_page,
3529 } => Some(items_on_current_page),
3530 _ => None,
3531 };
3532 let mut first_break_done = false;
3533
3534 for (line_idx, run_line) in broken_lines.iter().enumerate() {
3535 let needs_break = if let Some(break_at) = forced_break_at {
3536 if !first_break_done && line_idx == break_at {
3537 true
3538 } else {
3539 line_height > cursor.remaining_height()
3540 }
3541 } else {
3542 line_height > cursor.remaining_height()
3543 };
3544
3545 if needs_break {
3546 first_break_done = true;
3547 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3548 if !line_elements.is_empty() {
3549 let container_height = cursor.content_y + cursor.y - container_start_y;
3550 cursor.elements.push(LayoutElement {
3551 x: text_x,
3552 y: container_start_y,
3553 width: text_width,
3554 height: container_height,
3555 draw: DrawCommand::None,
3556 children: line_elements,
3557 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
3558 resolved_style: Some(style.clone()),
3559 source_location: source_location.cloned(),
3560 href: parent_href.map(|s| s.to_string()),
3561 bookmark: if is_first_element {
3562 bookmark.map(|s| s.to_string())
3563 } else {
3564 None
3565 },
3566 alt: None,
3567 is_header_row: false,
3568 overflow: Overflow::default(),
3569 opacity: 1.0,
3570 });
3571 is_first_element = false;
3572 }
3573
3574 pages.push(cursor.finalize());
3575 *cursor = cursor.new_page();
3576
3577 snapshot = cursor.elements.len();
3578 container_start_y = cursor.content_y + cursor.y;
3579 }
3580
3581 let line_x = match style.text_align {
3582 TextAlign::Left => text_x,
3583 TextAlign::Right => text_x + text_width - run_line.width,
3584 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
3585 TextAlign::Justify => text_x,
3586 };
3587
3588 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
3589
3590 let is_last_line = line_idx == broken_lines.len() - 1;
3597 let user_ws = style.word_spacing;
3598 let (justified_width, word_spacing) =
3599 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
3600 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
3601 let (natural_width, space_count) = if let Some(idx) = last_non_space {
3602 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
3603 let s = glyphs[..=idx]
3604 .iter()
3605 .filter(|g| g.char_value == ' ')
3606 .count();
3607 (w, s)
3608 } else {
3609 (0.0, 0)
3610 };
3611 let slack = text_width - natural_width;
3612 let ws = if space_count > 0 && slack.abs() > 0.01 {
3613 slack / space_count as f64
3614 } else {
3615 0.0
3616 };
3617 (text_width, user_ws + ws)
3618 } else {
3619 (run_line.width, user_ws)
3620 };
3621
3622 let text_line = TextLine {
3623 x: line_x,
3624 y: cursor.content_y + cursor.y + style.font_size,
3625 glyphs,
3626 width: justified_width,
3627 height: line_height,
3628 word_spacing,
3629 };
3630
3631 let text_dec = run_line
3633 .chars
3634 .iter()
3635 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
3636 .map(|sc| sc.text_decoration)
3637 .unwrap_or(style.text_decoration);
3638
3639 cursor.elements.push(LayoutElement {
3640 x: line_x,
3641 y: cursor.content_y + cursor.y,
3642 width: justified_width,
3643 height: line_height,
3644 draw: DrawCommand::Text {
3645 lines: vec![text_line],
3646 color: style.color,
3647 text_decoration: text_dec,
3648 opacity: 1.0,
3649 },
3650 children: vec![],
3651 node_type: Some("TextLine".to_string()),
3652 resolved_style: Some(style.clone()),
3653 source_location: None,
3654 href: parent_href.map(|s| s.to_string()),
3655 bookmark: None,
3656 alt: None,
3657 is_header_row: false,
3658 overflow: Overflow::default(),
3659 opacity: 1.0,
3660 });
3661
3662 cursor.y += line_height;
3663 }
3664
3665 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3666 if !line_elements.is_empty() {
3667 let container_height = cursor.content_y + cursor.y - container_start_y;
3668 cursor.elements.push(LayoutElement {
3669 x: text_x,
3670 y: container_start_y,
3671 width: text_width,
3672 height: container_height,
3673 draw: DrawCommand::None,
3674 children: line_elements,
3675 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
3676 resolved_style: Some(style.clone()),
3677 source_location: source_location.cloned(),
3678 href: parent_href.map(|s| s.to_string()),
3679 bookmark: if is_first_element {
3680 bookmark.map(|s| s.to_string())
3681 } else {
3682 None
3683 },
3684 alt: None,
3685 is_header_row: false,
3686 overflow: Overflow::default(),
3687 opacity: 1.0,
3688 });
3689 }
3690 }
3691
3692 fn build_positioned_glyphs_single_style(
3696 &self,
3697 line: &BrokenLine,
3698 style: &ResolvedStyle,
3699 href: Option<&str>,
3700 font_context: &FontContext,
3701 ) -> Vec<PositionedGlyph> {
3702 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
3703 let line_text: String = line.chars.iter().collect();
3704 let direction = style.direction;
3705 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
3707
3708 let font_runs = crate::font::fallback::segment_by_font(
3711 &line.chars,
3712 &style.font_family,
3713 style.font_weight,
3714 italic,
3715 font_context.registry(),
3716 );
3717 let needs_per_char_fallback = font_runs.len() > 1
3718 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
3719
3720 if needs_per_char_fallback {
3722 let bidi_runs = if has_bidi {
3723 bidi::analyze_bidi(&line_text, direction)
3724 } else {
3725 vec![crate::text::bidi::BidiRun {
3726 char_start: 0,
3727 char_end: line.chars.len(),
3728 level: unicode_bidi::Level::ltr(),
3729 is_rtl: false,
3730 }]
3731 };
3732
3733 let mut all_glyphs = Vec::new();
3734 let mut bidi_levels = Vec::new();
3735 let mut x = 0.0_f64;
3736
3737 for bidi_run in &bidi_runs {
3739 for font_run in &font_runs {
3741 let start = font_run.start.max(bidi_run.char_start);
3743 let end = font_run.end.min(bidi_run.char_end);
3744 if start >= end {
3745 continue;
3746 }
3747
3748 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
3749 let sub_text: String = sub_chars.iter().collect();
3750 let resolved_family = &font_run.family;
3751
3752 if let Some(font_data) =
3753 font_context.font_data(resolved_family, style.font_weight, italic)
3754 {
3755 if let Some(shaped) = shaping::shape_text_with_direction(
3756 &sub_text,
3757 font_data,
3758 bidi_run.is_rtl,
3759 ) {
3760 let units_per_em = font_context.units_per_em(
3761 resolved_family,
3762 style.font_weight,
3763 italic,
3764 );
3765 let scale = style.font_size / units_per_em as f64;
3766
3767 for sg in &shaped {
3768 let cluster = sg.cluster as usize;
3769 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
3770
3771 let cluster_text = if shaped.len() < sub_chars.len() {
3772 let cluster_end =
3773 self.find_cluster_end(&shaped, sg, sub_chars.len());
3774 if cluster_end > cluster + 1 {
3775 Some(
3776 sub_chars[cluster..cluster_end]
3777 .iter()
3778 .collect::<String>(),
3779 )
3780 } else {
3781 None
3782 }
3783 } else {
3784 None
3785 };
3786
3787 let glyph_x = x + sg.x_offset as f64 * scale;
3788 let glyph_y = sg.y_offset as f64 * scale;
3789 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
3790
3791 all_glyphs.push(PositionedGlyph {
3792 glyph_id: sg.glyph_id,
3793 x_offset: glyph_x,
3794 y_offset: glyph_y,
3795 x_advance: advance,
3796 font_size: style.font_size,
3797 font_family: resolved_family.clone(),
3798 font_weight: style.font_weight,
3799 font_style: style.font_style,
3800 char_value,
3801 color: Some(style.color),
3802 href: href.map(|s| s.to_string()),
3803 text_decoration: style.text_decoration,
3804 letter_spacing: style.letter_spacing,
3805 cluster_text,
3806 });
3807 bidi_levels.push(bidi_run.level);
3808 x += advance;
3809 }
3810 continue;
3811 }
3812 }
3813
3814 for i in start..end {
3816 let ch = line.chars[i];
3817 let glyph_x = x;
3818 let char_width = font_context.char_width(
3819 ch,
3820 resolved_family,
3821 style.font_weight,
3822 italic,
3823 style.font_size,
3824 );
3825 let advance = char_width + style.letter_spacing;
3826 all_glyphs.push(PositionedGlyph {
3827 glyph_id: ch as u16,
3828 x_offset: glyph_x,
3829 y_offset: 0.0,
3830 x_advance: advance,
3831 font_size: style.font_size,
3832 font_family: resolved_family.clone(),
3833 font_weight: style.font_weight,
3834 font_style: style.font_style,
3835 char_value: ch,
3836 color: Some(style.color),
3837 href: href.map(|s| s.to_string()),
3838 text_decoration: style.text_decoration,
3839 letter_spacing: style.letter_spacing,
3840 cluster_text: None,
3841 });
3842 bidi_levels.push(bidi_run.level);
3843 x += advance;
3844 }
3845 }
3846 }
3847
3848 if has_bidi && !all_glyphs.is_empty() {
3850 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
3851 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
3852 }
3853 return all_glyphs;
3854 }
3855
3856 if let Some(font_data) =
3859 font_context.font_data(&style.font_family, style.font_weight, italic)
3860 {
3861 if has_bidi {
3862 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
3864 let units_per_em =
3865 font_context.units_per_em(&style.font_family, style.font_weight, italic);
3866 let scale = style.font_size / units_per_em as f64;
3867
3868 let mut all_glyphs = Vec::new();
3869 let mut bidi_levels = Vec::new();
3870 let mut x = 0.0_f64;
3871
3872 for run in &bidi_runs {
3873 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
3874 let run_text: String = run_chars.iter().collect();
3875
3876 if let Some(shaped) =
3877 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
3878 {
3879 for sg in &shaped {
3880 let cluster = sg.cluster as usize;
3881 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
3882
3883 let cluster_text = if shaped.len() < run_chars.len() {
3884 let cluster_end =
3885 self.find_cluster_end(&shaped, sg, run_chars.len());
3886 if cluster_end > cluster + 1 {
3887 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
3888 } else {
3889 None
3890 }
3891 } else {
3892 None
3893 };
3894
3895 let glyph_x = x + sg.x_offset as f64 * scale;
3896 let glyph_y = sg.y_offset as f64 * scale;
3897 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
3898
3899 all_glyphs.push(PositionedGlyph {
3900 glyph_id: sg.glyph_id,
3901 x_offset: glyph_x,
3902 y_offset: glyph_y,
3903 x_advance: advance,
3904 font_size: style.font_size,
3905 font_family: style.font_family.clone(),
3906 font_weight: style.font_weight,
3907 font_style: style.font_style,
3908 char_value,
3909 color: Some(style.color),
3910 href: href.map(|s| s.to_string()),
3911 text_decoration: style.text_decoration,
3912 letter_spacing: style.letter_spacing,
3913 cluster_text,
3914 });
3915 bidi_levels.push(run.level);
3916
3917 x += advance;
3918 }
3919 }
3920 }
3921
3922 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
3924 bidi::reposition_after_reorder(&mut glyphs, 0.0);
3925 return glyphs;
3926 }
3927
3928 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
3930 let units_per_em =
3931 font_context.units_per_em(&style.font_family, style.font_weight, italic);
3932 let scale = style.font_size / units_per_em as f64;
3933
3934 return self.shaped_glyphs_to_positioned(
3935 &shaped,
3936 &line.chars,
3937 &line.char_positions,
3938 scale,
3939 style.font_size,
3940 &style.font_family,
3941 style.font_weight,
3942 style.font_style,
3943 Some(style.color),
3944 href,
3945 style.text_decoration,
3946 style.letter_spacing,
3947 );
3948 }
3949 }
3950
3951 let mut glyphs: Vec<PositionedGlyph> = line
3953 .chars
3954 .iter()
3955 .enumerate()
3956 .map(|(j, ch)| {
3957 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
3958 let char_width = font_context.char_width(
3959 *ch,
3960 &style.font_family,
3961 style.font_weight,
3962 italic,
3963 style.font_size,
3964 );
3965 PositionedGlyph {
3966 glyph_id: *ch as u16,
3967 x_offset: glyph_x,
3968 y_offset: 0.0,
3969 x_advance: char_width,
3970 font_size: style.font_size,
3971 font_family: style.font_family.clone(),
3972 font_weight: style.font_weight,
3973 font_style: style.font_style,
3974 char_value: *ch,
3975 color: Some(style.color),
3976 href: href.map(|s| s.to_string()),
3977 text_decoration: style.text_decoration,
3978 letter_spacing: style.letter_spacing,
3979 cluster_text: None,
3980 }
3981 })
3982 .collect();
3983
3984 if has_bidi && !glyphs.is_empty() {
3986 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
3987 let mut levels = Vec::with_capacity(glyphs.len());
3988 let mut char_idx = 0;
3989 for run in &bidi_runs {
3990 for _ in run.char_start..run.char_end {
3991 if char_idx < glyphs.len() {
3992 levels.push(run.level);
3993 char_idx += 1;
3994 }
3995 }
3996 }
3997 while levels.len() < glyphs.len() {
3999 levels.push(unicode_bidi::Level::ltr());
4000 }
4001 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
4002 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4003 }
4004
4005 glyphs
4006 }
4007
4008 fn build_positioned_glyphs_runs(
4013 &self,
4014 run_line: &RunBrokenLine,
4015 font_context: &FontContext,
4016 direction: Direction,
4017 ) -> Vec<PositionedGlyph> {
4018 let chars = &run_line.chars;
4019 if chars.is_empty() {
4020 return vec![];
4021 }
4022
4023 let resolved_families: Vec<String> = chars
4026 .iter()
4027 .map(|sc| {
4028 if !sc.font_family.contains(',') {
4029 sc.font_family.clone()
4030 } else {
4031 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
4032 let (_, family) = font_context.registry().resolve_for_char(
4033 &sc.font_family,
4034 sc.ch,
4035 sc.font_weight,
4036 italic,
4037 );
4038 family
4039 }
4040 })
4041 .collect();
4042
4043 let line_text: String = chars.iter().map(|c| c.ch).collect();
4044 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
4045 let bidi_runs = if has_bidi {
4046 Some(bidi::analyze_bidi(&line_text, direction))
4047 } else {
4048 None
4049 };
4050
4051 let mut glyphs = Vec::new();
4052 let mut bidi_levels = Vec::new();
4053 let mut i = 0;
4054
4055 while i < chars.len() {
4056 let sc = &chars[i];
4057 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
4058 let resolved_family = &resolved_families[i];
4059
4060 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
4062 runs.iter()
4063 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
4064 });
4065
4066 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
4068 {
4069 let run_start = i;
4071 let mut run_end = i + 1;
4072 while run_end < chars.len() {
4073 let next = &chars[run_end];
4074 let next_italic =
4075 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
4076 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
4077 runs.iter()
4078 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
4079 });
4080 if resolved_families[run_end] == *resolved_family
4082 && next.font_weight == sc.font_weight
4083 && next_italic == italic
4084 && (next.font_size - sc.font_size).abs() < 0.001
4085 && next_is_rtl == is_rtl
4086 {
4087 run_end += 1;
4088 } else {
4089 break;
4090 }
4091 }
4092
4093 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
4094 if let Some(shaped) =
4095 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
4096 {
4097 let units_per_em =
4098 font_context.units_per_em(resolved_family, sc.font_weight, italic);
4099 let scale = sc.font_size / units_per_em as f64;
4100
4101 let run_chars: Vec<char> =
4103 chars[run_start..run_end].iter().map(|c| c.ch).collect();
4104 let run_positions: Vec<f64> = (run_start..run_end)
4105 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
4106 .collect();
4107
4108 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
4110 &shaped,
4111 &chars[run_start..run_end],
4112 &run_chars,
4113 &run_positions,
4114 scale,
4115 );
4116 for g in &mut run_glyphs {
4118 g.font_family = resolved_family.clone();
4119 }
4120 let run_level = if is_rtl {
4122 unicode_bidi::Level::rtl()
4123 } else {
4124 unicode_bidi::Level::ltr()
4125 };
4126 for _ in &run_glyphs {
4127 bidi_levels.push(run_level);
4128 }
4129 glyphs.extend(run_glyphs);
4130 i = run_end;
4131 continue;
4132 }
4133 }
4134
4135 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
4137 let char_width = font_context.char_width(
4138 sc.ch,
4139 resolved_family,
4140 sc.font_weight,
4141 italic,
4142 sc.font_size,
4143 );
4144 glyphs.push(PositionedGlyph {
4145 glyph_id: sc.ch as u16,
4146 x_offset: glyph_x,
4147 y_offset: 0.0,
4148 x_advance: char_width,
4149 font_size: sc.font_size,
4150 font_family: resolved_family.clone(),
4151 font_weight: sc.font_weight,
4152 font_style: sc.font_style,
4153 char_value: sc.ch,
4154 color: Some(sc.color),
4155 href: sc.href.clone(),
4156 text_decoration: sc.text_decoration,
4157 letter_spacing: sc.letter_spacing,
4158 cluster_text: None,
4159 });
4160 bidi_levels.push(if is_rtl {
4161 unicode_bidi::Level::rtl()
4162 } else {
4163 unicode_bidi::Level::ltr()
4164 });
4165 i += 1;
4166 }
4167
4168 if has_bidi && !glyphs.is_empty() {
4170 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
4171 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4172 }
4173
4174 glyphs
4175 }
4176
4177 #[allow(clippy::too_many_arguments)]
4179 fn shaped_glyphs_to_positioned(
4180 &self,
4181 shaped: &[shaping::ShapedGlyph],
4182 chars: &[char],
4183 _char_positions: &[f64],
4184 scale: f64,
4185 font_size: f64,
4186 font_family: &str,
4187 font_weight: u32,
4188 font_style: FontStyle,
4189 color: Option<Color>,
4190 href: Option<&str>,
4191 text_decoration: TextDecoration,
4192 letter_spacing: f64,
4193 ) -> Vec<PositionedGlyph> {
4194 let mut result = Vec::with_capacity(shaped.len());
4195 let mut x = 0.0_f64;
4196
4197 for sg in shaped {
4198 let cluster = sg.cluster as usize;
4199 let char_value = chars.get(cluster).copied().unwrap_or(' ');
4200
4201 let cluster_text = if shaped.len() < chars.len() {
4203 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
4206 if cluster_end > cluster + 1 {
4207 Some(chars[cluster..cluster_end].iter().collect::<String>())
4208 } else {
4209 None
4210 }
4211 } else {
4212 None
4213 };
4214
4215 let glyph_x = x + sg.x_offset as f64 * scale;
4217 let glyph_y = sg.y_offset as f64 * scale;
4218 let advance = sg.x_advance as f64 * scale + letter_spacing;
4219
4220 result.push(PositionedGlyph {
4221 glyph_id: sg.glyph_id,
4222 x_offset: glyph_x,
4223 y_offset: glyph_y,
4224 x_advance: advance,
4225 font_size,
4226 font_family: font_family.to_string(),
4227 font_weight,
4228 font_style,
4229 char_value,
4230 color,
4231 href: href.map(|s| s.to_string()),
4232 text_decoration,
4233 letter_spacing,
4234 cluster_text,
4235 });
4236
4237 x += advance;
4238 }
4239
4240 result
4241 }
4242
4243 fn shaped_glyphs_to_positioned_runs(
4245 &self,
4246 shaped: &[shaping::ShapedGlyph],
4247 styled_chars: &[StyledChar],
4248 chars: &[char],
4249 char_positions: &[f64],
4250 scale: f64,
4251 ) -> Vec<PositionedGlyph> {
4252 let mut result = Vec::with_capacity(shaped.len());
4253 let base_x = char_positions.first().copied().unwrap_or(0.0);
4255 let mut x = 0.0_f64;
4256
4257 for sg in shaped {
4258 let cluster = sg.cluster as usize;
4259 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
4260 let char_value = chars.get(cluster).copied().unwrap_or(' ');
4261
4262 let cluster_text = if shaped.len() < chars.len() {
4263 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
4264 if cluster_end > cluster + 1 {
4265 Some(chars[cluster..cluster_end].iter().collect::<String>())
4266 } else {
4267 None
4268 }
4269 } else {
4270 None
4271 };
4272
4273 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
4274 let glyph_y = sg.y_offset as f64 * scale;
4275 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
4276
4277 result.push(PositionedGlyph {
4278 glyph_id: sg.glyph_id,
4279 x_offset: glyph_x,
4280 y_offset: glyph_y,
4281 x_advance: advance,
4282 font_size: sc.font_size,
4283 font_family: sc.font_family.clone(),
4284 font_weight: sc.font_weight,
4285 font_style: sc.font_style,
4286 char_value,
4287 color: Some(sc.color),
4288 href: sc.href.clone(),
4289 text_decoration: sc.text_decoration,
4290 letter_spacing: sc.letter_spacing,
4291 cluster_text,
4292 });
4293
4294 x += advance;
4295 }
4296
4297 result
4298 }
4299
4300 fn find_cluster_end(
4302 &self,
4303 shaped: &[shaping::ShapedGlyph],
4304 current: &shaping::ShapedGlyph,
4305 num_chars: usize,
4306 ) -> usize {
4307 for sg in shaped {
4309 if sg.cluster > current.cluster {
4310 return sg.cluster as usize;
4311 }
4312 }
4313 num_chars
4315 }
4316
4317 #[allow(clippy::too_many_arguments)]
4318 fn layout_image(
4319 &self,
4320 node: &Node,
4321 style: &ResolvedStyle,
4322 cursor: &mut PageCursor,
4323 pages: &mut Vec<LayoutPage>,
4324 x: f64,
4325 available_width: f64,
4326 explicit_width: Option<f64>,
4327 explicit_height: Option<f64>,
4328 ) {
4329 let margin = &style.margin.to_edges();
4330
4331 let src = match &node.kind {
4333 NodeKind::Image { src, .. } => src.as_str(),
4334 _ => "",
4335 };
4336
4337 let loaded = if !src.is_empty() {
4338 crate::image_loader::load_image(src).ok()
4339 } else {
4340 None
4341 };
4342
4343 let (img_width, img_height) = if let Some(ref img) = loaded {
4345 let intrinsic_w = img.width_px as f64;
4346 let intrinsic_h = img.height_px as f64;
4347 let aspect = if intrinsic_w > 0.0 {
4348 intrinsic_h / intrinsic_w
4349 } else {
4350 0.75
4351 };
4352
4353 match (explicit_width, explicit_height) {
4354 (Some(w), Some(h)) => (w, h),
4355 (Some(w), None) => (w, w * aspect),
4356 (None, Some(h)) => (h / aspect, h),
4357 (None, None) => {
4358 let max_w = available_width - margin.horizontal();
4359 let w = intrinsic_w.min(max_w);
4360 (w, w * aspect)
4361 }
4362 }
4363 } else {
4364 let w = explicit_width.unwrap_or(available_width - margin.horizontal());
4366 let h = explicit_height.unwrap_or(w * 0.75);
4367 (w, h)
4368 };
4369
4370 let total_height = img_height + margin.vertical();
4371
4372 if total_height > cursor.remaining_height() {
4373 pages.push(cursor.finalize());
4374 *cursor = cursor.new_page();
4375 }
4376
4377 cursor.y += margin.top;
4378
4379 let draw = if let Some(image_data) = loaded {
4380 DrawCommand::Image { image_data }
4381 } else {
4382 DrawCommand::ImagePlaceholder
4383 };
4384
4385 cursor.elements.push(LayoutElement {
4386 x: x + margin.left,
4387 y: cursor.content_y + cursor.y,
4388 width: img_width,
4389 height: img_height,
4390 draw,
4391 children: vec![],
4392 node_type: Some(node_kind_name(&node.kind).to_string()),
4393 resolved_style: Some(style.clone()),
4394 source_location: node.source_location.clone(),
4395 href: node.href.clone(),
4396 bookmark: node.bookmark.clone(),
4397 alt: node.alt.clone(),
4398 is_header_row: false,
4399 overflow: style.overflow,
4400 opacity: style.opacity,
4401 });
4402
4403 cursor.y += img_height + margin.bottom;
4404 }
4405
4406 #[allow(clippy::too_many_arguments)]
4408 fn layout_svg(
4409 &self,
4410 node: &Node,
4411 style: &ResolvedStyle,
4412 cursor: &mut PageCursor,
4413 pages: &mut Vec<LayoutPage>,
4414 x: f64,
4415 _available_width: f64,
4416 svg_width: f64,
4417 svg_height: f64,
4418 view_box: Option<&str>,
4419 content: &str,
4420 ) {
4421 let margin = &style.margin.to_edges();
4422 let total_height = svg_height + margin.vertical();
4423
4424 if total_height > cursor.remaining_height() {
4425 pages.push(cursor.finalize());
4426 *cursor = cursor.new_page();
4427 }
4428
4429 cursor.y += margin.top;
4430
4431 let vb = view_box
4432 .and_then(crate::svg::parse_view_box)
4433 .unwrap_or(crate::svg::ViewBox {
4434 min_x: 0.0,
4435 min_y: 0.0,
4436 width: svg_width,
4437 height: svg_height,
4438 });
4439
4440 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
4441
4442 cursor.elements.push(LayoutElement {
4443 x: x + margin.left,
4444 y: cursor.content_y + cursor.y,
4445 width: svg_width,
4446 height: svg_height,
4447 draw: DrawCommand::Svg {
4448 commands,
4449 width: svg_width,
4450 height: svg_height,
4451 viewbox_min_x: vb.min_x,
4452 viewbox_min_y: vb.min_y,
4453 viewbox_width: vb.width,
4454 viewbox_height: vb.height,
4455 clip: false,
4456 },
4457 children: vec![],
4458 node_type: Some("Svg".to_string()),
4459 resolved_style: Some(style.clone()),
4460 source_location: node.source_location.clone(),
4461 href: node.href.clone(),
4462 bookmark: node.bookmark.clone(),
4463 alt: node.alt.clone(),
4464 is_header_row: false,
4465 overflow: style.overflow,
4466 opacity: style.opacity,
4467 });
4468
4469 cursor.y += svg_height + margin.bottom;
4470 }
4471
4472 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
4474 use crate::svg::SvgCommand;
4475
4476 let mut commands = Vec::new();
4477 let mut cur_x = 0.0_f64;
4478 let mut cur_y = 0.0_f64;
4479
4480 for op in operations {
4481 match op {
4482 CanvasOp::MoveTo { x, y } => {
4483 commands.push(SvgCommand::MoveTo(*x, *y));
4484 cur_x = *x;
4485 cur_y = *y;
4486 }
4487 CanvasOp::LineTo { x, y } => {
4488 commands.push(SvgCommand::LineTo(*x, *y));
4489 cur_x = *x;
4490 cur_y = *y;
4491 }
4492 CanvasOp::BezierCurveTo {
4493 cp1x,
4494 cp1y,
4495 cp2x,
4496 cp2y,
4497 x,
4498 y,
4499 } => {
4500 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
4501 cur_x = *x;
4502 cur_y = *y;
4503 }
4504 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
4505 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
4507 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
4508 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
4509 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
4510 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
4511 cur_x = *x;
4512 cur_y = *y;
4513 }
4514 CanvasOp::ClosePath => {
4515 commands.push(SvgCommand::ClosePath);
4516 }
4517 CanvasOp::Rect {
4518 x,
4519 y,
4520 width,
4521 height,
4522 } => {
4523 commands.push(SvgCommand::MoveTo(*x, *y));
4524 commands.push(SvgCommand::LineTo(*x + *width, *y));
4525 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
4526 commands.push(SvgCommand::LineTo(*x, *y + *height));
4527 commands.push(SvgCommand::ClosePath);
4528 cur_x = *x;
4529 cur_y = *y;
4530 }
4531 CanvasOp::Circle { cx, cy, r } => {
4532 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
4533 }
4534 CanvasOp::Ellipse { cx, cy, rx, ry } => {
4535 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
4536 }
4537 CanvasOp::Arc {
4538 cx,
4539 cy,
4540 r,
4541 start_angle,
4542 end_angle,
4543 counterclockwise,
4544 } => {
4545 let steps = 32;
4549 let mut sweep = end_angle - start_angle;
4550 if !counterclockwise && sweep < 0.0 {
4551 sweep += 2.0 * std::f64::consts::PI;
4552 }
4553 if *counterclockwise && sweep > 0.0 {
4554 sweep -= 2.0 * std::f64::consts::PI;
4555 }
4556 for i in 0..=steps {
4557 let t = *start_angle + sweep * (i as f64 / steps as f64);
4558 let px = cx + r * t.cos();
4559 let py = cy + r * t.sin();
4560 if i == 0 {
4561 commands.push(SvgCommand::MoveTo(px, py));
4562 } else {
4563 commands.push(SvgCommand::LineTo(px, py));
4564 }
4565 }
4566 }
4567 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
4568 CanvasOp::Fill => commands.push(SvgCommand::Fill),
4569 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
4570 CanvasOp::SetFillColor { r, g, b } => {
4571 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
4573 }
4574 CanvasOp::SetStrokeColor { r, g, b } => {
4575 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
4576 }
4577 CanvasOp::SetLineWidth { width } => {
4578 commands.push(SvgCommand::SetStrokeWidth(*width));
4579 }
4580 CanvasOp::SetLineCap { cap } => {
4581 commands.push(SvgCommand::SetLineCap(*cap));
4582 }
4583 CanvasOp::SetLineJoin { join } => {
4584 commands.push(SvgCommand::SetLineJoin(*join));
4585 }
4586 CanvasOp::Save => commands.push(SvgCommand::SaveState),
4587 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
4588 }
4589 }
4590
4591 commands
4592 }
4593
4594 #[allow(clippy::too_many_arguments)]
4596 fn layout_canvas(
4597 &self,
4598 node: &Node,
4599 style: &ResolvedStyle,
4600 cursor: &mut PageCursor,
4601 pages: &mut Vec<LayoutPage>,
4602 x: f64,
4603 _available_width: f64,
4604 canvas_width: f64,
4605 canvas_height: f64,
4606 operations: &[CanvasOp],
4607 ) {
4608 let margin = style.margin.to_edges();
4609 let total_height = canvas_height + margin.top + margin.bottom;
4610
4611 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
4613 pages.push(cursor.finalize());
4614 *cursor = cursor.new_page();
4615 }
4616
4617 cursor.y += margin.top;
4618
4619 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
4620
4621 cursor.elements.push(LayoutElement {
4622 x: x + margin.left,
4623 y: cursor.content_y + cursor.y,
4624 width: canvas_width,
4625 height: canvas_height,
4626 draw: DrawCommand::Svg {
4627 commands: svg_commands,
4628 width: canvas_width,
4629 height: canvas_height,
4630 viewbox_min_x: 0.0,
4633 viewbox_min_y: 0.0,
4634 viewbox_width: canvas_width,
4635 viewbox_height: canvas_height,
4636 clip: true,
4637 },
4638 children: vec![],
4639 node_type: Some("Canvas".to_string()),
4640 resolved_style: Some(style.clone()),
4641 source_location: node.source_location.clone(),
4642 href: node.href.clone(),
4643 bookmark: node.bookmark.clone(),
4644 alt: node.alt.clone(),
4645 is_header_row: false,
4646 overflow: style.overflow,
4647 opacity: style.opacity,
4648 });
4649
4650 cursor.y += canvas_height + margin.bottom;
4651 }
4652
4653 #[allow(clippy::too_many_arguments)]
4655 #[allow(clippy::too_many_arguments)]
4657 fn layout_chart(
4658 &self,
4659 node: &Node,
4660 style: &ResolvedStyle,
4661 cursor: &mut PageCursor,
4662 pages: &mut Vec<LayoutPage>,
4663 x: f64,
4664 chart_width: f64,
4665 chart_height: f64,
4666 primitives: Vec<crate::chart::ChartPrimitive>,
4667 node_type_name: &str,
4668 ) {
4669 let margin = &style.margin.to_edges();
4670 let total_height = chart_height + margin.vertical();
4671
4672 if total_height > cursor.remaining_height() {
4673 pages.push(cursor.finalize());
4674 *cursor = cursor.new_page();
4675 }
4676
4677 cursor.y += margin.top;
4678
4679 let draw = DrawCommand::Chart { primitives };
4680
4681 cursor.elements.push(LayoutElement {
4682 x: x + margin.left,
4683 y: cursor.content_y + cursor.y,
4684 width: chart_width,
4685 height: chart_height,
4686 draw,
4687 children: vec![],
4688 node_type: Some(node_type_name.to_string()),
4689 resolved_style: Some(style.clone()),
4690 source_location: node.source_location.clone(),
4691 href: node.href.clone(),
4692 bookmark: node.bookmark.clone(),
4693 alt: node.alt.clone(),
4694 is_header_row: false,
4695 overflow: style.overflow,
4696 opacity: style.opacity,
4697 });
4698
4699 cursor.y += chart_height + margin.bottom;
4700 }
4701
4702 #[allow(clippy::too_many_arguments)]
4704 fn layout_form_field(
4705 &self,
4706 node: &Node,
4707 style: &ResolvedStyle,
4708 cursor: &mut PageCursor,
4709 pages: &mut Vec<LayoutPage>,
4710 x: f64,
4711 field_width: f64,
4712 field_height: f64,
4713 draw: DrawCommand,
4714 node_type_name: &str,
4715 ) {
4716 let margin = &style.margin.to_edges();
4717 let total_height = field_height + margin.vertical();
4718
4719 if total_height > cursor.remaining_height() {
4720 pages.push(cursor.finalize());
4721 *cursor = cursor.new_page();
4722 }
4723
4724 cursor.y += margin.top;
4725
4726 cursor.elements.push(LayoutElement {
4727 x: x + margin.left,
4728 y: cursor.content_y + cursor.y,
4729 width: field_width,
4730 height: field_height,
4731 draw,
4732 children: vec![],
4733 node_type: Some(node_type_name.to_string()),
4734 resolved_style: Some(style.clone()),
4735 source_location: node.source_location.clone(),
4736 href: node.href.clone(),
4737 bookmark: node.bookmark.clone(),
4738 alt: node.alt.clone(),
4739 is_header_row: false,
4740 overflow: style.overflow,
4741 opacity: style.opacity,
4742 });
4743
4744 cursor.y += field_height + margin.bottom;
4745 }
4746
4747 #[allow(clippy::too_many_arguments)]
4748 fn layout_barcode(
4749 &self,
4750 node: &Node,
4751 style: &ResolvedStyle,
4752 cursor: &mut PageCursor,
4753 pages: &mut Vec<LayoutPage>,
4754 x: f64,
4755 available_width: f64,
4756 data: &str,
4757 format: crate::barcode::BarcodeFormat,
4758 explicit_width: Option<f64>,
4759 bar_height: f64,
4760 ) {
4761 let margin = &style.margin.to_edges();
4762 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
4763 let total_height = bar_height + margin.vertical();
4764
4765 if total_height > cursor.remaining_height() {
4766 pages.push(cursor.finalize());
4767 *cursor = cursor.new_page();
4768 }
4769
4770 cursor.y += margin.top;
4771
4772 let draw = match crate::barcode::generate_barcode(data, format) {
4773 Ok(barcode_data) => {
4774 let bar_width = if barcode_data.bars.is_empty() {
4775 0.0
4776 } else {
4777 display_width / barcode_data.bars.len() as f64
4778 };
4779 DrawCommand::Barcode {
4780 bars: barcode_data.bars,
4781 bar_width,
4782 height: bar_height,
4783 color: style.color,
4784 }
4785 }
4786 Err(_) => DrawCommand::None,
4787 };
4788
4789 cursor.elements.push(LayoutElement {
4790 x: x + margin.left,
4791 y: cursor.content_y + cursor.y,
4792 width: display_width,
4793 height: bar_height,
4794 draw,
4795 children: vec![],
4796 node_type: Some("Barcode".to_string()),
4797 resolved_style: Some(style.clone()),
4798 source_location: node.source_location.clone(),
4799 href: node.href.clone(),
4800 bookmark: node.bookmark.clone(),
4801 alt: node.alt.clone(),
4802 is_header_row: false,
4803 overflow: style.overflow,
4804 opacity: style.opacity,
4805 });
4806
4807 cursor.y += bar_height + margin.bottom;
4808 }
4809
4810 #[allow(clippy::too_many_arguments)]
4812 fn layout_qrcode(
4813 &self,
4814 node: &Node,
4815 style: &ResolvedStyle,
4816 cursor: &mut PageCursor,
4817 pages: &mut Vec<LayoutPage>,
4818 x: f64,
4819 available_width: f64,
4820 data: &str,
4821 explicit_size: Option<f64>,
4822 ) {
4823 let margin = &style.margin.to_edges();
4824 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
4825 let total_height = display_size + margin.vertical();
4826
4827 if total_height > cursor.remaining_height() {
4828 pages.push(cursor.finalize());
4829 *cursor = cursor.new_page();
4830 }
4831
4832 cursor.y += margin.top;
4833
4834 let draw = match crate::qrcode::generate_qr(data) {
4835 Ok(matrix) => {
4836 let module_size = display_size / matrix.size as f64;
4837 DrawCommand::QrCode {
4838 modules: matrix.modules,
4839 module_size,
4840 color: style.color,
4841 }
4842 }
4843 Err(_) => DrawCommand::None,
4844 };
4845
4846 cursor.elements.push(LayoutElement {
4847 x: x + margin.left,
4848 y: cursor.content_y + cursor.y,
4849 width: display_size,
4850 height: display_size,
4851 draw,
4852 children: vec![],
4853 node_type: Some("QrCode".to_string()),
4854 resolved_style: Some(style.clone()),
4855 source_location: node.source_location.clone(),
4856 href: node.href.clone(),
4857 bookmark: node.bookmark.clone(),
4858 alt: node.alt.clone(),
4859 is_header_row: false,
4860 overflow: style.overflow,
4861 opacity: style.opacity,
4862 });
4863
4864 cursor.y += display_size + margin.bottom;
4865 }
4866
4867 fn measure_node_height(
4870 &self,
4871 node: &Node,
4872 available_width: f64,
4873 style: &ResolvedStyle,
4874 font_context: &FontContext,
4875 ) -> f64 {
4876 match &node.kind {
4877 NodeKind::Text { content, runs, .. } => {
4878 let measure_width = match style.width {
4881 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
4882 SizeConstraint::Auto => available_width - style.margin.horizontal(),
4883 };
4884 if !runs.is_empty() {
4885 let mut styled_chars: Vec<StyledChar> = Vec::new();
4887 for run in runs {
4888 let run_style = run.style.resolve(Some(style), measure_width);
4889 let run_content = substitute_page_placeholders(&run.content);
4890 for ch in run_content.chars() {
4891 styled_chars.push(StyledChar {
4892 ch,
4893 font_family: run_style.font_family.clone(),
4894 font_size: run_style.font_size,
4895 font_weight: run_style.font_weight,
4896 font_style: run_style.font_style,
4897 color: run_style.color,
4898 href: None,
4899 text_decoration: run_style.text_decoration,
4900 letter_spacing: run_style.letter_spacing,
4901 });
4902 }
4903 }
4904 let broken_lines = self.text_layout.break_runs_into_lines(
4905 font_context,
4906 &styled_chars,
4907 measure_width,
4908 style.hyphens,
4909 style.lang.as_deref(),
4910 );
4911 let line_height = style.font_size * style.line_height;
4912 (broken_lines.len() as f64) * line_height + style.padding.vertical()
4913 } else {
4914 let content = substitute_page_placeholders(content);
4915 let lines = self.text_layout.break_into_lines(
4916 font_context,
4917 &content,
4918 measure_width,
4919 style.font_size,
4920 &style.font_family,
4921 style.font_weight,
4922 style.font_style,
4923 style.letter_spacing,
4924 style.hyphens,
4925 style.lang.as_deref(),
4926 );
4927 let line_height = style.font_size * style.line_height;
4928 (lines.len() as f64) * line_height + style.padding.vertical()
4929 }
4930 }
4931 NodeKind::Image {
4932 src,
4933 width: explicit_w,
4934 height: explicit_h,
4935 } => {
4936 if let SizeConstraint::Fixed(h) = style.height {
4938 return h + style.padding.vertical();
4939 }
4940 if let Some(h) = explicit_h {
4942 return *h + style.padding.vertical();
4943 }
4944 let aspect = self
4946 .get_image_dimensions(src)
4947 .map(|(w, h)| if w > 0 { h as f64 / w as f64 } else { 0.75 })
4948 .unwrap_or(0.75);
4949 let w = if let SizeConstraint::Fixed(w) = style.width {
4950 w
4951 } else {
4952 explicit_w.unwrap_or(available_width - style.margin.horizontal())
4953 };
4954 w * aspect + style.padding.vertical()
4955 }
4956 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
4957 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
4958 NodeKind::QrCode { size, .. } => {
4959 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
4960 display_size + style.margin.vertical()
4961 }
4962 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
4963 NodeKind::BarChart { height, .. }
4964 | NodeKind::LineChart { height, .. }
4965 | NodeKind::PieChart { height, .. }
4966 | NodeKind::AreaChart { height, .. }
4967 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
4968 NodeKind::TextField { height, .. }
4969 | NodeKind::Checkbox { height, .. }
4970 | NodeKind::Dropdown { height, .. }
4971 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
4972 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
4974 if let SizeConstraint::Fixed(h) = style.height {
4981 return h;
4982 }
4983 let outer_width = match style.width {
4984 SizeConstraint::Fixed(w) => w,
4985 SizeConstraint::Auto => available_width - style.margin.horizontal(),
4986 };
4987 let inner_width =
4988 outer_width - style.padding.horizontal() - style.border_width.horizontal();
4989 let col_widths = self.resolve_column_widths(columns, inner_width, &node.children);
4990 let row_gap = style.row_gap;
4991 let mut total = 0.0;
4992 for (i, row) in node.children.iter().enumerate() {
4993 if i > 0 {
4994 total += row_gap;
4995 }
4996 total += self.measure_table_row_height(row, &col_widths, style, font_context);
4997 }
4998 total + style.padding.vertical() + style.border_width.vertical()
4999 }
5000 NodeKind::TableRow { .. } => {
5001 let n = node.children.len().max(1);
5006 let usable = (available_width - style.margin.horizontal()).max(0.0);
5007 let col_w = usable / n as f64;
5008 let col_widths = vec![col_w; n];
5009 self.measure_table_row_height(node, &col_widths, style, font_context)
5010 }
5011 _ => {
5012 if let SizeConstraint::Fixed(h) = style.height {
5014 return h;
5015 }
5016 let outer_width = match style.width {
5018 SizeConstraint::Fixed(w) => w,
5019 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5020 };
5021 let inner_width =
5022 outer_width - style.padding.horizontal() - style.border_width.horizontal();
5023 let children_height =
5024 self.measure_children_height(&node.children, inner_width, style, font_context);
5025 children_height + style.padding.vertical() + style.border_width.vertical()
5026 }
5027 }
5028 }
5029
5030 fn measure_children_height(
5031 &self,
5032 children: &[Node],
5033 available_width: f64,
5034 parent_style: &ResolvedStyle,
5035 font_context: &FontContext,
5036 ) -> f64 {
5037 if matches!(parent_style.display, Display::Grid) {
5039 if let Some(template_cols) = &parent_style.grid_template_columns {
5040 let num_columns = template_cols.len();
5041 if num_columns > 0 && !children.is_empty() {
5042 let col_gap = parent_style.column_gap;
5043 let row_gap = parent_style.row_gap;
5044
5045 let content_sizes: Vec<f64> = template_cols
5046 .iter()
5047 .map(|track| {
5048 if matches!(track, GridTrackSize::Auto) {
5049 available_width / num_columns as f64
5050 } else {
5051 0.0
5052 }
5053 })
5054 .collect();
5055
5056 let col_widths = grid::resolve_tracks(
5057 template_cols,
5058 available_width,
5059 col_gap,
5060 &content_sizes,
5061 );
5062
5063 let placements: Vec<Option<&GridPlacement>> = children
5064 .iter()
5065 .map(|child| child.style.grid_placement.as_ref())
5066 .collect();
5067
5068 let item_placements = grid::place_items(&placements, num_columns);
5069 let num_rows = grid::compute_num_rows(&item_placements);
5070
5071 if num_rows == 0 {
5072 return 0.0;
5073 }
5074
5075 let mut row_heights = vec![0.0_f64; num_rows];
5076 for placement in &item_placements {
5077 let cell_width = grid::span_width(
5078 placement.col_start,
5079 placement.col_end,
5080 &col_widths,
5081 col_gap,
5082 );
5083 let child = &children[placement.child_index];
5084 let child_style = child.style.resolve(Some(parent_style), cell_width);
5085 let h =
5086 self.measure_node_height(child, cell_width, &child_style, font_context);
5087 let span = placement.row_end - placement.row_start;
5088 let per_row = h / span as f64;
5089 for rh in row_heights
5090 .iter_mut()
5091 .take(placement.row_end.min(num_rows))
5092 .skip(placement.row_start)
5093 {
5094 if per_row > *rh {
5095 *rh = per_row;
5096 }
5097 }
5098 }
5099
5100 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
5101 return row_heights.iter().sum::<f64>() + total_row_gap;
5102 }
5103 }
5104 }
5105
5106 let direction = parent_style.flex_direction;
5107 let row_gap = parent_style.row_gap;
5108 let column_gap = parent_style.column_gap;
5109
5110 match direction {
5111 FlexDirection::Row | FlexDirection::RowReverse => {
5112 let styles: Vec<ResolvedStyle> = children
5115 .iter()
5116 .map(|child| child.style.resolve(Some(parent_style), available_width))
5117 .collect();
5118
5119 let base_widths: Vec<f64> = children
5120 .iter()
5121 .zip(&styles)
5122 .map(|(child, style)| match style.flex_basis {
5123 SizeConstraint::Fixed(w) => w,
5124 SizeConstraint::Auto => match style.width {
5125 SizeConstraint::Fixed(w) => w,
5126 SizeConstraint::Auto => self
5127 .measure_intrinsic_width(child, style, font_context)
5128 .min(available_width),
5129 },
5130 })
5131 .collect();
5132
5133 let lines = match parent_style.flex_wrap {
5134 FlexWrap::NoWrap => {
5135 vec![flex::WrapLine {
5136 start: 0,
5137 end: children.len(),
5138 }]
5139 }
5140 FlexWrap::Wrap | FlexWrap::WrapReverse => {
5141 flex::partition_into_lines(&base_widths, column_gap, available_width)
5142 }
5143 };
5144
5145 let mut final_widths = base_widths.clone();
5147 for line in &lines {
5148 let line_count = line.end - line.start;
5149 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
5150 let distributable = available_width - line_gap;
5151 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
5152 let remaining = distributable - total_base;
5153
5154 if remaining > 0.0 {
5155 let total_grow: f64 = styles[line.start..line.end]
5156 .iter()
5157 .map(|s| s.flex_grow)
5158 .sum();
5159 if total_grow > 0.0 {
5160 for (j, s) in styles[line.start..line.end].iter().enumerate() {
5161 final_widths[line.start + j] = base_widths[line.start + j]
5162 + remaining * (s.flex_grow / total_grow);
5163 }
5164 }
5165 } else if remaining < 0.0 {
5166 let total_shrink: f64 = styles[line.start..line.end]
5167 .iter()
5168 .enumerate()
5169 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
5170 .sum();
5171 if total_shrink > 0.0 {
5172 for (j, s) in styles[line.start..line.end].iter().enumerate() {
5173 let factor =
5174 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
5175 let w = base_widths[line.start + j] + remaining * factor;
5176 final_widths[line.start + j] = w.max(s.min_width);
5177 }
5178 }
5179 }
5180 }
5181
5182 let mut total = 0.0;
5183 for (i, line) in lines.iter().enumerate() {
5184 let line_height: f64 = children[line.start..line.end]
5185 .iter()
5186 .enumerate()
5187 .map(|(j, child)| {
5188 let fw = final_widths[line.start + j];
5189 let child_style = child.style.resolve(Some(parent_style), fw);
5190 self.measure_node_height(child, fw, &child_style, font_context)
5191 + child_style.margin.vertical()
5192 })
5193 .fold(0.0f64, f64::max);
5194 total += line_height;
5195 if i > 0 {
5196 total += row_gap;
5197 }
5198 }
5199 total
5200 }
5201 FlexDirection::Column | FlexDirection::ColumnReverse => {
5202 let mut total = 0.0;
5203 for (i, child) in children.iter().enumerate() {
5204 let child_style = child.style.resolve(Some(parent_style), available_width);
5205 let child_height = self.measure_node_height(
5206 child,
5207 available_width,
5208 &child_style,
5209 font_context,
5210 );
5211 total += child_height + child_style.margin.vertical();
5212 if i > 0 {
5213 total += row_gap;
5214 }
5215 }
5216 total
5217 }
5218 }
5219 }
5220
5221 fn measure_intrinsic_width(
5223 &self,
5224 node: &Node,
5225 style: &ResolvedStyle,
5226 font_context: &FontContext,
5227 ) -> f64 {
5228 match &node.kind {
5229 NodeKind::Svg { width, .. } => {
5230 *width + style.padding.horizontal() + style.margin.horizontal()
5231 }
5232 NodeKind::Text { content, .. } => {
5233 let content = substitute_page_placeholders(content);
5234 let transformed = apply_text_transform(&content, style.text_transform);
5235 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
5236 let text_width = font_context.measure_string(
5237 &transformed,
5238 &style.font_family,
5239 style.font_weight,
5240 italic,
5241 style.font_size,
5242 style.letter_spacing,
5243 );
5244 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
5247 }
5248 NodeKind::Image {
5249 src, width, height, ..
5250 } => {
5251 let w = if let SizeConstraint::Fixed(w) = style.width {
5252 w
5253 } else if let Some(w) = width {
5254 *w
5255 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
5256 let pixel_w = iw as f64;
5257 let pixel_h = ih as f64;
5258 let aspect = if pixel_w > 0.0 {
5259 pixel_h / pixel_w
5260 } else {
5261 0.75
5262 };
5263 let constrained_h = match style.height {
5265 SizeConstraint::Fixed(h) => Some(h),
5266 SizeConstraint::Auto => *height,
5267 };
5268 if let Some(h) = constrained_h {
5269 h / aspect
5270 } else {
5271 pixel_w
5272 }
5273 } else {
5274 100.0
5275 };
5276 w + style.padding.horizontal() + style.margin.horizontal()
5277 }
5278 NodeKind::Barcode { width, .. } => {
5279 let w = width.unwrap_or(0.0);
5280 w + style.padding.horizontal() + style.margin.horizontal()
5281 }
5282 NodeKind::QrCode { size, .. } => {
5283 let display_size = size.unwrap_or(0.0);
5284 display_size + style.padding.horizontal() + style.margin.horizontal()
5285 }
5286 NodeKind::Canvas { width, .. } => {
5287 *width + style.padding.horizontal() + style.margin.horizontal()
5288 }
5289 NodeKind::BarChart { width, .. }
5290 | NodeKind::LineChart { width, .. }
5291 | NodeKind::PieChart { width, .. }
5292 | NodeKind::AreaChart { width, .. }
5293 | NodeKind::DotPlot { width, .. } => {
5294 *width + style.padding.horizontal() + style.margin.horizontal()
5295 }
5296 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
5297 *width + style.padding.horizontal() + style.margin.horizontal()
5298 }
5299 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
5300 *width + style.padding.horizontal() + style.margin.horizontal()
5301 }
5302 NodeKind::Watermark { .. } => 0.0, _ => {
5304 if node.children.is_empty() {
5306 style.padding.horizontal() + style.margin.horizontal()
5307 } else {
5308 let direction = style.flex_direction;
5309 let gap = style.gap;
5310 let mut total = 0.0f64;
5311 for (i, child) in node.children.iter().enumerate() {
5312 let child_style = child.style.resolve(Some(style), 0.0);
5313 let child_width =
5314 self.measure_intrinsic_width(child, &child_style, font_context);
5315 match direction {
5316 FlexDirection::Row | FlexDirection::RowReverse => {
5317 total += child_width;
5318 if i > 0 {
5319 total += gap;
5320 }
5321 }
5322 _ => {
5323 total = total.max(child_width);
5324 }
5325 }
5326 }
5327 total
5328 + style.padding.horizontal()
5329 + style.margin.horizontal()
5330 + style.border_width.horizontal()
5331 }
5332 }
5333 }
5334 }
5335
5336 pub fn measure_min_content_width(
5340 &self,
5341 node: &Node,
5342 style: &ResolvedStyle,
5343 font_context: &FontContext,
5344 ) -> f64 {
5345 match &node.kind {
5346 NodeKind::Text { content, runs, .. } => {
5347 let word_width = if !runs.is_empty() {
5348 runs.iter()
5350 .map(|run| {
5351 let run_style = run.style.resolve(Some(style), 0.0);
5352 let run_content = substitute_page_placeholders(&run.content);
5353 let transformed =
5354 apply_text_transform(&run_content, run_style.text_transform);
5355 self.text_layout.measure_widest_word(
5356 font_context,
5357 &transformed,
5358 run_style.font_size,
5359 &run_style.font_family,
5360 run_style.font_weight,
5361 run_style.font_style,
5362 run_style.letter_spacing,
5363 style.hyphens,
5364 style.lang.as_deref(),
5365 )
5366 })
5367 .fold(0.0f64, f64::max)
5368 } else {
5369 let content = substitute_page_placeholders(content);
5370 let transformed = apply_text_transform(&content, style.text_transform);
5371 self.text_layout.measure_widest_word(
5372 font_context,
5373 &transformed,
5374 style.font_size,
5375 &style.font_family,
5376 style.font_weight,
5377 style.font_style,
5378 style.letter_spacing,
5379 style.hyphens,
5380 style.lang.as_deref(),
5381 )
5382 };
5383 word_width + style.padding.horizontal() + style.margin.horizontal()
5384 }
5385 NodeKind::Image { width, .. } => {
5386 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
5387 }
5388 NodeKind::Svg { width, .. } => {
5389 *width + style.padding.horizontal() + style.margin.horizontal()
5390 }
5391 _ => {
5392 if node.children.is_empty() {
5393 style.padding.horizontal()
5394 + style.margin.horizontal()
5395 + style.border_width.horizontal()
5396 } else {
5397 let mut max_child_min = 0.0f64;
5398 for child in &node.children {
5399 let child_style = child.style.resolve(Some(style), 0.0);
5400 let child_min =
5401 self.measure_min_content_width(child, &child_style, font_context);
5402 max_child_min = max_child_min.max(child_min);
5403 }
5404 max_child_min
5405 + style.padding.horizontal()
5406 + style.margin.horizontal()
5407 + style.border_width.horizontal()
5408 }
5409 }
5410 }
5411 }
5412
5413 fn measure_table_row_height(
5414 &self,
5415 row: &Node,
5416 col_widths: &[f64],
5417 parent_style: &ResolvedStyle,
5418 font_context: &FontContext,
5419 ) -> f64 {
5420 let row_style = row
5421 .style
5422 .resolve(Some(parent_style), col_widths.iter().sum());
5423 let mut max_height: f64 = 0.0;
5424
5425 for (i, cell) in row.children.iter().enumerate() {
5426 let col_width = col_widths.get(i).copied().unwrap_or(0.0);
5427 let cell_style = cell.style.resolve(Some(&row_style), col_width);
5428 let inner_width =
5429 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
5430
5431 let mut cell_content_height = 0.0;
5432 for child in &cell.children {
5433 let child_style = child.style.resolve(Some(&cell_style), inner_width);
5434 cell_content_height +=
5435 self.measure_node_height(child, inner_width, &child_style, font_context);
5436 }
5437
5438 let total = cell_content_height
5439 + cell_style.padding.vertical()
5440 + cell_style.border_width.vertical();
5441 max_height = max_height.max(total);
5442 }
5443
5444 max_height.max(row_style.min_height)
5445 }
5446
5447 fn resolve_column_widths(
5448 &self,
5449 defs: &[ColumnDef],
5450 available_width: f64,
5451 children: &[Node],
5452 ) -> Vec<f64> {
5453 if defs.is_empty() {
5454 let num_cols = children.first().map(|row| row.children.len()).unwrap_or(1);
5455 return vec![available_width / num_cols as f64; num_cols];
5456 }
5457
5458 let mut widths = Vec::new();
5459 let mut remaining = available_width;
5460 let mut auto_count = 0;
5461
5462 for def in defs {
5463 match def.width {
5464 ColumnWidth::Fixed(w) => {
5465 widths.push(w);
5466 remaining -= w;
5467 }
5468 ColumnWidth::Fraction(f) => {
5469 let w = available_width * f;
5470 widths.push(w);
5471 remaining -= w;
5472 }
5473 ColumnWidth::Auto => {
5474 widths.push(0.0);
5475 auto_count += 1;
5476 }
5477 }
5478 }
5479
5480 if auto_count > 0 {
5481 let auto_width = remaining / auto_count as f64;
5482 for (i, def) in defs.iter().enumerate() {
5483 if matches!(def.width, ColumnWidth::Auto) {
5484 widths[i] = auto_width;
5485 }
5486 }
5487 }
5488
5489 widths
5490 }
5491
5492 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
5493 for page in pages.iter_mut() {
5494 if !page.watermarks.is_empty() {
5496 let (page_w, page_h) = page.config.size.dimensions();
5497 let cx = page_w / 2.0;
5498 let cy = page_h / 2.0;
5499
5500 let mut watermark_elements = Vec::new();
5501 for wm_node in &page.watermarks {
5502 if let NodeKind::Watermark {
5503 text,
5504 font_size,
5505 angle,
5506 } = &wm_node.kind
5507 {
5508 let style = wm_node.style.resolve(None, page_w);
5509 let color = style.color;
5510 let opacity = style.opacity;
5511 let angle_rad = angle.to_radians();
5512
5513 let italic =
5515 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
5516
5517 let shaped = self.text_layout.shape_text(
5519 font_context,
5520 text,
5521 &style.font_family,
5522 style.font_weight,
5523 style.font_style,
5524 );
5525
5526 let mut glyphs = Vec::new();
5527 let mut x_pos = 0.0;
5528 let text_chars: Vec<char> = text.chars().collect();
5529
5530 if let Some(shaped_glyphs) = shaped {
5531 let units_per_em = font_context.units_per_em(
5533 &style.font_family,
5534 style.font_weight,
5535 italic,
5536 ) as f64;
5537
5538 for sg in &shaped_glyphs {
5539 let advance = sg.x_advance as f64 / units_per_em * *font_size;
5540 let cluster_idx = sg.cluster as usize;
5541 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
5542 glyphs.push(PositionedGlyph {
5543 glyph_id: sg.glyph_id,
5544 char_value: ch,
5545 x_offset: x_pos,
5546 y_offset: 0.0,
5547 x_advance: advance,
5548 font_size: *font_size,
5549 font_family: style.font_family.clone(),
5550 font_weight: style.font_weight,
5551 font_style: style.font_style,
5552 color: Some(color),
5553 href: None,
5554 text_decoration: TextDecoration::None,
5555 letter_spacing: style.letter_spacing,
5556 cluster_text: None,
5557 });
5558 x_pos += advance + style.letter_spacing;
5559 }
5560 } else {
5561 for &ch in &text_chars {
5563 let w = font_context.char_width(
5564 ch,
5565 &style.font_family,
5566 style.font_weight,
5567 italic,
5568 *font_size,
5569 );
5570 glyphs.push(PositionedGlyph {
5571 glyph_id: ch as u16,
5572 char_value: ch,
5573 x_offset: x_pos,
5574 y_offset: 0.0,
5575 x_advance: w,
5576 font_size: *font_size,
5577 font_family: style.font_family.clone(),
5578 font_weight: style.font_weight,
5579 font_style: style.font_style,
5580 color: Some(color),
5581 href: None,
5582 text_decoration: TextDecoration::None,
5583 letter_spacing: style.letter_spacing,
5584 cluster_text: None,
5585 });
5586 x_pos += w + style.letter_spacing;
5587 }
5588 }
5589
5590 let text_width = x_pos;
5591
5592 let line = TextLine {
5593 x: 0.0,
5594 y: 0.0,
5595 glyphs,
5596 width: text_width,
5597 height: *font_size,
5598 word_spacing: 0.0,
5599 };
5600
5601 watermark_elements.push(LayoutElement {
5602 x: cx,
5603 y: cy,
5604 width: text_width,
5605 height: *font_size,
5606 draw: DrawCommand::Watermark {
5607 lines: vec![line],
5608 color,
5609 opacity,
5610 angle_rad,
5611 font_family: style.font_family.clone(),
5612 },
5613 children: vec![],
5614 node_type: Some("Watermark".to_string()),
5615 resolved_style: None,
5616 source_location: None,
5617 href: None,
5618 bookmark: None,
5619 alt: None,
5620 is_header_row: false,
5621 overflow: Overflow::default(),
5622 opacity: 1.0,
5623 });
5624 }
5625 }
5626
5627 watermark_elements.append(&mut page.elements);
5629 page.elements = watermark_elements;
5630 page.watermarks.clear();
5631 }
5632
5633 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
5634 continue;
5635 }
5636
5637 if !page.fixed_header.is_empty() {
5639 let mut hdr_cursor = PageCursor::new(&page.config);
5640 for (node, _h) in &page.fixed_header {
5641 let cw = hdr_cursor.content_width;
5642 let cx = hdr_cursor.content_x;
5643 let style = node.style.resolve(None, cw);
5644 self.layout_view(
5645 node,
5646 &style,
5647 &mut hdr_cursor,
5648 &mut Vec::new(),
5649 cx,
5650 cw,
5651 font_context,
5652 );
5653 }
5654 let mut combined = hdr_cursor.elements;
5656 combined.append(&mut page.elements);
5657 page.elements = combined;
5658 }
5659
5660 if !page.fixed_footer.is_empty() {
5665 let mut ftr_cursor = PageCursor::new(&page.config);
5666 let total_ftr: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
5667 let target_y = ftr_cursor.content_height - total_ftr;
5668 for (node, _h) in &page.fixed_footer {
5670 let cw = ftr_cursor.content_width;
5671 let cx = ftr_cursor.content_x;
5672 let style = node.style.resolve(None, cw);
5673 self.layout_view(
5674 node,
5675 &style,
5676 &mut ftr_cursor,
5677 &mut Vec::new(),
5678 cx,
5679 cw,
5680 font_context,
5681 );
5682 }
5683 for el in &mut ftr_cursor.elements {
5687 offset_element_y(el, target_y);
5688 }
5689 page.elements.extend(ftr_cursor.elements);
5690 }
5691
5692 page.fixed_header.clear();
5694 page.fixed_footer.clear();
5695 }
5696 }
5697
5698 #[allow(clippy::too_many_arguments)]
5703 fn layout_grid_children(
5704 &self,
5705 children: &[Node],
5706 parent_style: &ResolvedStyle,
5707 cursor: &mut PageCursor,
5708 pages: &mut Vec<LayoutPage>,
5709 x: f64,
5710 available_width: f64,
5711 font_context: &FontContext,
5712 ) {
5713 let template_cols = match &parent_style.grid_template_columns {
5714 Some(cols) => cols,
5715 None => return, };
5717
5718 let num_columns = template_cols.len();
5719 if num_columns == 0 || children.is_empty() {
5720 return;
5721 }
5722
5723 let col_gap = parent_style.column_gap;
5724 let row_gap = parent_style.row_gap;
5725
5726 let content_sizes: Vec<f64> = template_cols
5729 .iter()
5730 .map(|track| {
5731 if matches!(track, GridTrackSize::Auto) {
5732 available_width / num_columns as f64
5735 } else {
5736 0.0
5737 }
5738 })
5739 .collect();
5740
5741 let col_widths =
5742 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
5743
5744 let placements: Vec<Option<&GridPlacement>> = children
5746 .iter()
5747 .map(|child| child.style.grid_placement.as_ref())
5748 .collect();
5749
5750 let item_placements = grid::place_items(&placements, num_columns);
5752 let num_rows = grid::compute_num_rows(&item_placements);
5753
5754 if num_rows == 0 {
5755 return;
5756 }
5757
5758 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
5760 for placement in &item_placements {
5761 let cell_width =
5762 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
5763 let child = &children[placement.child_index];
5764 let child_style = child.style.resolve(Some(parent_style), cell_width);
5765 item_heights[placement.child_index] =
5766 self.measure_node_height(child, cell_width, &child_style, font_context);
5767 }
5768
5769 let template_rows = parent_style.grid_template_rows.as_deref();
5771 let mut row_heights = vec![0.0_f64; num_rows];
5772 for placement in &item_placements {
5773 let h = item_heights[placement.child_index];
5774 let span = placement.row_end - placement.row_start;
5775 let per_row = h / span as f64;
5776 for rh in row_heights
5777 .iter_mut()
5778 .take(placement.row_end.min(num_rows))
5779 .skip(placement.row_start)
5780 {
5781 if per_row > *rh {
5782 *rh = per_row;
5783 }
5784 }
5785 }
5786
5787 if let Some(template) = template_rows {
5789 let auto_row = parent_style.grid_auto_rows.as_ref();
5790 for (r, rh) in row_heights.iter_mut().enumerate() {
5791 let track = template.get(r).or(auto_row);
5792 if let Some(track) = track {
5793 match track {
5794 GridTrackSize::Pt(pts) => *rh = *pts,
5795 GridTrackSize::Auto => {} _ => {} }
5798 }
5799 }
5800 }
5801
5802 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
5804 if row_height > cursor.remaining_height() {
5809 pages.push(cursor.finalize());
5810 *cursor = cursor.new_page();
5811 }
5812
5813 let row_start_y = cursor.y;
5814
5815 for placement in &item_placements {
5817 if placement.row_start != row {
5818 continue; }
5820
5821 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
5822 let cell_width =
5823 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
5824
5825 let child = &children[placement.child_index];
5826
5827 self.layout_node(
5828 child,
5829 cursor,
5830 pages,
5831 cell_x,
5832 cell_width,
5833 Some(parent_style),
5834 font_context,
5835 None,
5836 None,
5837 );
5838 cursor.y = row_start_y;
5840 }
5841
5842 cursor.y = row_start_y + row_height + row_gap;
5843 }
5844
5845 if num_rows > 0 {
5847 cursor.y -= row_gap;
5848 }
5849 }
5850}
5851
5852struct FlexItem<'a> {
5853 node: &'a Node,
5854 style: ResolvedStyle,
5855 base_width: f64,
5856 min_content_width: f64,
5857}
5858
5859#[cfg(test)]
5860mod tests {
5861 use super::*;
5862 use crate::font::FontContext;
5863
5864 fn make_text(content: &str, font_size: f64) -> Node {
5865 Node {
5866 kind: NodeKind::Text {
5867 content: content.to_string(),
5868 href: None,
5869 runs: vec![],
5870 },
5871 style: Style {
5872 font_size: Some(font_size),
5873 ..Default::default()
5874 },
5875 children: vec![],
5876 id: None,
5877 source_location: None,
5878 bookmark: None,
5879 href: None,
5880 alt: None,
5881 }
5882 }
5883
5884 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
5885 Node {
5886 kind: NodeKind::View,
5887 style,
5888 children,
5889 id: None,
5890 source_location: None,
5891 bookmark: None,
5892 href: None,
5893 alt: None,
5894 }
5895 }
5896
5897 #[test]
5898 fn intrinsic_width_flex_row_sums_children() {
5899 let engine = LayoutEngine::new();
5900 let font_context = FontContext::new();
5901
5902 let child1 = make_text("Hello", 14.0);
5903 let child2 = make_text("World", 14.0);
5904
5905 let child1_style = child1.style.resolve(None, 0.0);
5906 let child2_style = child2.style.resolve(None, 0.0);
5907 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
5908 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
5909
5910 let row = make_styled_view(
5911 Style {
5912 flex_direction: Some(FlexDirection::Row),
5913 ..Default::default()
5914 },
5915 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
5916 );
5917 let row_style = row.style.resolve(None, 0.0);
5918 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
5919
5920 assert!(
5921 (row_w - (child1_w + child2_w)).abs() < 0.01,
5922 "Row intrinsic width ({}) should equal sum of children ({} + {})",
5923 row_w,
5924 child1_w,
5925 child2_w
5926 );
5927 }
5928
5929 #[test]
5930 fn intrinsic_width_flex_column_takes_max() {
5931 let engine = LayoutEngine::new();
5932 let font_context = FontContext::new();
5933
5934 let short = make_text("Hi", 14.0);
5935 let long = make_text("Hello World", 14.0);
5936
5937 let short_style = short.style.resolve(None, 0.0);
5938 let long_style = long.style.resolve(None, 0.0);
5939 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
5940 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
5941
5942 let col = make_styled_view(
5943 Style {
5944 flex_direction: Some(FlexDirection::Column),
5945 ..Default::default()
5946 },
5947 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
5948 );
5949 let col_style = col.style.resolve(None, 0.0);
5950 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
5951
5952 assert!(
5953 (col_w - long_w).abs() < 0.01,
5954 "Column intrinsic width ({}) should equal max child ({}, short was {})",
5955 col_w,
5956 long_w,
5957 short_w
5958 );
5959 }
5960
5961 #[test]
5962 fn intrinsic_width_nested_containers() {
5963 let engine = LayoutEngine::new();
5964 let font_context = FontContext::new();
5965
5966 let inner = make_styled_view(
5967 Style {
5968 flex_direction: Some(FlexDirection::Row),
5969 ..Default::default()
5970 },
5971 vec![make_text("A", 12.0), make_text("B", 12.0)],
5972 );
5973 let inner_style = inner.style.resolve(None, 0.0);
5974 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
5975
5976 let outer = make_styled_view(
5977 Style::default(),
5978 vec![make_styled_view(
5979 Style {
5980 flex_direction: Some(FlexDirection::Row),
5981 ..Default::default()
5982 },
5983 vec![make_text("A", 12.0), make_text("B", 12.0)],
5984 )],
5985 );
5986 let outer_style = outer.style.resolve(None, 0.0);
5987 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
5988
5989 assert!(
5990 (outer_w - inner_w).abs() < 0.01,
5991 "Nested container ({}) should match inner container ({})",
5992 outer_w,
5993 inner_w
5994 );
5995 }
5996
5997 #[test]
5998 fn intrinsic_width_row_with_gap() {
5999 let engine = LayoutEngine::new();
6000 let font_context = FontContext::new();
6001
6002 let no_gap = make_styled_view(
6003 Style {
6004 flex_direction: Some(FlexDirection::Row),
6005 ..Default::default()
6006 },
6007 vec![make_text("A", 12.0), make_text("B", 12.0)],
6008 );
6009 let with_gap = make_styled_view(
6010 Style {
6011 flex_direction: Some(FlexDirection::Row),
6012 gap: Some(10.0),
6013 ..Default::default()
6014 },
6015 vec![make_text("A", 12.0), make_text("B", 12.0)],
6016 );
6017
6018 let no_gap_style = no_gap.style.resolve(None, 0.0);
6019 let with_gap_style = with_gap.style.resolve(None, 0.0);
6020 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
6021 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
6022
6023 assert!(
6024 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
6025 "Gap should add 10pt: with_gap={}, no_gap={}",
6026 with_gap_w,
6027 no_gap_w
6028 );
6029 }
6030
6031 #[test]
6032 fn intrinsic_width_empty_container() {
6033 let engine = LayoutEngine::new();
6034 let font_context = FontContext::new();
6035
6036 let padding = 8.0;
6037 let empty = make_styled_view(
6038 Style {
6039 padding: Some(Edges::uniform(padding)),
6040 ..Default::default()
6041 },
6042 vec![],
6043 );
6044 let style = empty.style.resolve(None, 0.0);
6045 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
6046
6047 assert!(
6048 (w - padding * 2.0).abs() < 0.01,
6049 "Empty container width ({}) should equal horizontal padding ({})",
6050 w,
6051 padding * 2.0
6052 );
6053 }
6054
6055 #[test]
6058 fn flex_shrink_respects_min_content_width() {
6059 let engine = LayoutEngine::new();
6063 let font_context = FontContext::new();
6064
6065 let sale_text = make_text("SALE", 12.0);
6066 let sale_style = sale_text.style.resolve(None, 0.0);
6067 let sale_word_width =
6068 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
6069 assert!(
6070 sale_word_width > 0.0,
6071 "SALE should have non-zero min-content width"
6072 );
6073
6074 let container = make_styled_view(
6077 Style {
6078 flex_direction: Some(FlexDirection::Row),
6079 width: Some(Dimension::Pt(100.0)),
6080 ..Default::default()
6081 },
6082 vec![
6083 make_styled_view(
6084 Style {
6085 width: Some(Dimension::Pt(80.0)),
6086 flex_shrink: Some(1.0),
6087 ..Default::default()
6088 },
6089 vec![],
6090 ),
6091 make_styled_view(
6092 Style {
6093 width: Some(Dimension::Pt(60.0)),
6094 flex_shrink: Some(1.0),
6095 ..Default::default()
6096 },
6097 vec![make_text("SALE", 12.0)],
6098 ),
6099 ],
6100 );
6101
6102 let doc = Document {
6103 children: vec![Node::page(
6104 PageConfig::default(),
6105 Style::default(),
6106 vec![container],
6107 )],
6108 metadata: Default::default(),
6109 default_page: PageConfig::default(),
6110 fonts: vec![],
6111 tagged: false,
6112 pdfa: None,
6113 default_style: None,
6114 embedded_data: None,
6115 flatten_forms: false,
6116 pdf_ua: false,
6117 certification: None,
6118 };
6119
6120 let pages = engine.layout(&doc, &font_context);
6121 assert!(!pages.is_empty());
6122
6123 let page = &pages[0];
6126 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
6128 assert!(
6129 container_el.is_some(),
6130 "Should find container with 2 children"
6131 );
6132 let sale_child = &container_el.unwrap().children[1];
6133 assert!(
6134 sale_child.width >= sale_word_width - 0.01,
6135 "SALE child width ({}) should be >= min-content width ({})",
6136 sale_child.width,
6137 sale_word_width
6138 );
6139 }
6140
6141 #[test]
6144 fn column_justify_content_center() {
6145 let engine = LayoutEngine::new();
6148 let font_context = FontContext::new();
6149
6150 let container = make_styled_view(
6151 Style {
6152 flex_direction: Some(FlexDirection::Column),
6153 height: Some(Dimension::Pt(200.0)),
6154 justify_content: Some(JustifyContent::Center),
6155 ..Default::default()
6156 },
6157 vec![make_text("Centered", 12.0)],
6158 );
6159
6160 let doc = Document {
6161 children: vec![Node::page(
6162 PageConfig::default(),
6163 Style::default(),
6164 vec![container],
6165 )],
6166 metadata: Default::default(),
6167 default_page: PageConfig::default(),
6168 fonts: vec![],
6169 tagged: false,
6170 pdfa: None,
6171 default_style: None,
6172 embedded_data: None,
6173 flatten_forms: false,
6174 pdf_ua: false,
6175 certification: None,
6176 };
6177
6178 let pages = engine.layout(&doc, &font_context);
6179 let page = &pages[0];
6180
6181 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
6184 assert!(
6185 container_el.is_some(),
6186 "Should find container with children"
6187 );
6188 let container_el = container_el.unwrap();
6189 let child = &container_el.children[0];
6190
6191 let child_offset = child.y - container_el.y;
6193 let expected_offset = (200.0 - child.height) / 2.0;
6194 assert!(
6195 (child_offset - expected_offset).abs() < 2.0,
6196 "Child offset ({}) should be near center ({})",
6197 child_offset,
6198 expected_offset
6199 );
6200 }
6201
6202 #[test]
6203 fn column_align_items_center() {
6204 let engine = LayoutEngine::new();
6207 let font_context = FontContext::new();
6208
6209 let container = make_styled_view(
6210 Style {
6211 flex_direction: Some(FlexDirection::Column),
6212 width: Some(Dimension::Pt(300.0)),
6213 align_items: Some(AlignItems::Center),
6214 ..Default::default()
6215 },
6216 vec![make_text("Hi", 12.0)],
6217 );
6218
6219 let doc = Document {
6220 children: vec![Node::page(
6221 PageConfig::default(),
6222 Style::default(),
6223 vec![container],
6224 )],
6225 metadata: Default::default(),
6226 default_page: PageConfig::default(),
6227 fonts: vec![],
6228 tagged: false,
6229 pdfa: None,
6230 default_style: None,
6231 embedded_data: None,
6232 flatten_forms: false,
6233 pdf_ua: false,
6234 certification: None,
6235 };
6236
6237 let pages = engine.layout(&doc, &font_context);
6238 let page = &pages[0];
6239
6240 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
6241 assert!(container_el.is_some());
6242 let container_el = container_el.unwrap();
6243 let child = &container_el.children[0];
6244
6245 let child_center = child.x + child.width / 2.0;
6247 let container_center = container_el.x + container_el.width / 2.0;
6248 assert!(
6249 (child_center - container_center).abs() < 2.0,
6250 "Child center ({}) should be near container center ({})",
6251 child_center,
6252 container_center
6253 );
6254 }
6255
6256 #[test]
6259 fn absolute_child_positioned_relative_to_parent() {
6260 let engine = LayoutEngine::new();
6263 let font_context = FontContext::new();
6264
6265 let parent = make_styled_view(
6266 Style {
6267 margin: Some(MarginEdges::from_edges(Edges {
6268 top: 50.0,
6269 left: 50.0,
6270 ..Default::default()
6271 })),
6272 width: Some(Dimension::Pt(200.0)),
6273 height: Some(Dimension::Pt(200.0)),
6274 ..Default::default()
6275 },
6276 vec![make_styled_view(
6277 Style {
6278 position: Some(crate::model::Position::Absolute),
6279 top: Some(10.0),
6280 left: Some(10.0),
6281 width: Some(Dimension::Pt(50.0)),
6282 height: Some(Dimension::Pt(50.0)),
6283 ..Default::default()
6284 },
6285 vec![],
6286 )],
6287 );
6288
6289 let doc = Document {
6290 children: vec![Node::page(
6291 PageConfig::default(),
6292 Style::default(),
6293 vec![parent],
6294 )],
6295 metadata: Default::default(),
6296 default_page: PageConfig::default(),
6297 fonts: vec![],
6298 tagged: false,
6299 pdfa: None,
6300 default_style: None,
6301 embedded_data: None,
6302 flatten_forms: false,
6303 pdf_ua: false,
6304 certification: None,
6305 };
6306
6307 let pages = engine.layout(&doc, &font_context);
6308 let page = &pages[0];
6309
6310 let parent_el = page
6313 .elements
6314 .iter()
6315 .find(|e| e.width > 190.0 && e.width < 210.0);
6316 assert!(parent_el.is_some(), "Should find the 200x200 parent");
6317 let parent_el = parent_el.unwrap();
6318
6319 let abs_child = parent_el
6321 .children
6322 .iter()
6323 .find(|e| e.width > 45.0 && e.width < 55.0);
6324 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
6325 let abs_child = abs_child.unwrap();
6326
6327 let expected_x = parent_el.x + 10.0;
6328 let expected_y = parent_el.y + 10.0;
6329 assert!(
6330 (abs_child.x - expected_x).abs() < 1.0,
6331 "Absolute child x ({}) should be parent.x + 10 ({})",
6332 abs_child.x,
6333 expected_x
6334 );
6335 assert!(
6336 (abs_child.y - expected_y).abs() < 1.0,
6337 "Absolute child y ({}) should be parent.y + 10 ({})",
6338 abs_child.y,
6339 expected_y
6340 );
6341 }
6342
6343 #[test]
6344 fn text_transform_none_passthrough() {
6345 assert_eq!(
6346 apply_text_transform("Hello World", TextTransform::None),
6347 "Hello World"
6348 );
6349 }
6350
6351 #[test]
6352 fn text_transform_uppercase() {
6353 assert_eq!(
6354 apply_text_transform("hello world", TextTransform::Uppercase),
6355 "HELLO WORLD"
6356 );
6357 }
6358
6359 #[test]
6360 fn text_transform_lowercase() {
6361 assert_eq!(
6362 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
6363 "hello world"
6364 );
6365 }
6366
6367 #[test]
6368 fn text_transform_capitalize() {
6369 assert_eq!(
6370 apply_text_transform("hello world", TextTransform::Capitalize),
6371 "Hello World"
6372 );
6373 assert_eq!(
6374 apply_text_transform(" hello world ", TextTransform::Capitalize),
6375 " Hello World "
6376 );
6377 assert_eq!(
6378 apply_text_transform("already Capitalized", TextTransform::Capitalize),
6379 "Already Capitalized"
6380 );
6381 }
6382
6383 #[test]
6384 fn text_transform_capitalize_empty() {
6385 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
6386 }
6387
6388 #[test]
6389 fn apply_char_transform_uppercase() {
6390 assert_eq!(
6391 apply_char_transform('a', TextTransform::Uppercase, false),
6392 'A'
6393 );
6394 assert_eq!(
6395 apply_char_transform('A', TextTransform::Uppercase, false),
6396 'A'
6397 );
6398 }
6399
6400 #[test]
6401 fn apply_char_transform_capitalize_word_start() {
6402 assert_eq!(
6403 apply_char_transform('h', TextTransform::Capitalize, true),
6404 'H'
6405 );
6406 assert_eq!(
6407 apply_char_transform('h', TextTransform::Capitalize, false),
6408 'h'
6409 );
6410 }
6411
6412 #[test]
6415 fn column_flex_grow_single_child_fills_container() {
6416 let engine = LayoutEngine::new();
6419 let font_context = FontContext::new();
6420
6421 let child = make_styled_view(
6422 Style {
6423 flex_grow: Some(1.0),
6424 ..Default::default()
6425 },
6426 vec![make_text("Short", 12.0)],
6427 );
6428
6429 let container = make_styled_view(
6430 Style {
6431 flex_direction: Some(FlexDirection::Column),
6432 height: Some(Dimension::Pt(300.0)),
6433 ..Default::default()
6434 },
6435 vec![child],
6436 );
6437
6438 let doc = Document {
6439 children: vec![Node::page(
6440 PageConfig::default(),
6441 Style::default(),
6442 vec![container],
6443 )],
6444 metadata: Default::default(),
6445 default_page: PageConfig::default(),
6446 fonts: vec![],
6447 tagged: false,
6448 pdfa: None,
6449 default_style: None,
6450 embedded_data: None,
6451 flatten_forms: false,
6452 pdf_ua: false,
6453 certification: None,
6454 };
6455
6456 let pages = engine.layout(&doc, &font_context);
6457 let page = &pages[0];
6458
6459 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
6460 assert!(container_el.is_some());
6461 let container_el = container_el.unwrap();
6462 assert!(
6463 (container_el.height - 300.0).abs() < 1.0,
6464 "Container should be 300pt, got {}",
6465 container_el.height
6466 );
6467
6468 let child_el = &container_el.children[0];
6469 assert!(
6470 (child_el.height - 300.0).abs() < 1.0,
6471 "flex-grow child should expand to 300pt, got {}",
6472 child_el.height
6473 );
6474 }
6475
6476 #[test]
6477 fn column_flex_grow_two_children_proportional() {
6478 let engine = LayoutEngine::new();
6481 let font_context = FontContext::new();
6482
6483 let child1 = make_styled_view(
6484 Style {
6485 flex_grow: Some(1.0),
6486 ..Default::default()
6487 },
6488 vec![make_text("A", 12.0)],
6489 );
6490 let child2 = make_styled_view(
6491 Style {
6492 flex_grow: Some(2.0),
6493 ..Default::default()
6494 },
6495 vec![make_text("B", 12.0)],
6496 );
6497
6498 let container = make_styled_view(
6499 Style {
6500 flex_direction: Some(FlexDirection::Column),
6501 height: Some(Dimension::Pt(300.0)),
6502 ..Default::default()
6503 },
6504 vec![child1, child2],
6505 );
6506
6507 let doc = Document {
6508 children: vec![Node::page(
6509 PageConfig::default(),
6510 Style::default(),
6511 vec![container],
6512 )],
6513 metadata: Default::default(),
6514 default_page: PageConfig::default(),
6515 fonts: vec![],
6516 tagged: false,
6517 pdfa: None,
6518 default_style: None,
6519 embedded_data: None,
6520 flatten_forms: false,
6521 pdf_ua: false,
6522 certification: None,
6523 };
6524
6525 let pages = engine.layout(&doc, &font_context);
6526 let page = &pages[0];
6527
6528 let container_el = page
6529 .elements
6530 .iter()
6531 .find(|e| e.children.len() == 2)
6532 .expect("Should find container with two children");
6533
6534 let c1 = &container_el.children[0];
6535 let c2 = &container_el.children[1];
6536
6537 let total = c1.height + c2.height;
6541 assert!(
6542 (total - 300.0).abs() < 2.0,
6543 "Children should sum to ~300pt, got {}",
6544 total
6545 );
6546
6547 let ratio = c2.height / c1.height;
6550 assert!(
6551 ratio > 1.3 && ratio < 2.5,
6552 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
6553 ratio
6554 );
6555 }
6556
6557 #[test]
6558 fn column_flex_grow_mixed_grow_and_fixed() {
6559 let engine = LayoutEngine::new();
6562 let font_context = FontContext::new();
6563
6564 let fixed_child = make_styled_view(
6565 Style {
6566 height: Some(Dimension::Pt(50.0)),
6567 ..Default::default()
6568 },
6569 vec![make_text("Fixed", 12.0)],
6570 );
6571 let grow_child = make_styled_view(
6572 Style {
6573 flex_grow: Some(1.0),
6574 ..Default::default()
6575 },
6576 vec![make_text("Grow", 12.0)],
6577 );
6578
6579 let container = make_styled_view(
6580 Style {
6581 flex_direction: Some(FlexDirection::Column),
6582 height: Some(Dimension::Pt(300.0)),
6583 ..Default::default()
6584 },
6585 vec![fixed_child, grow_child],
6586 );
6587
6588 let doc = Document {
6589 children: vec![Node::page(
6590 PageConfig::default(),
6591 Style::default(),
6592 vec![container],
6593 )],
6594 metadata: Default::default(),
6595 default_page: PageConfig::default(),
6596 fonts: vec![],
6597 tagged: false,
6598 pdfa: None,
6599 default_style: None,
6600 embedded_data: None,
6601 flatten_forms: false,
6602 pdf_ua: false,
6603 certification: None,
6604 };
6605
6606 let pages = engine.layout(&doc, &font_context);
6607 let page = &pages[0];
6608
6609 let container_el = page
6610 .elements
6611 .iter()
6612 .find(|e| e.children.len() == 2)
6613 .expect("Should find container with two children");
6614
6615 let fixed_el = &container_el.children[0];
6616 let grow_el = &container_el.children[1];
6617
6618 assert!(
6620 (fixed_el.height - 50.0).abs() < 1.0,
6621 "Fixed child should stay at 50pt, got {}",
6622 fixed_el.height
6623 );
6624
6625 assert!(
6627 (grow_el.height - 250.0).abs() < 2.0,
6628 "Grow child should expand to ~250pt, got {}",
6629 grow_el.height
6630 );
6631 }
6632
6633 #[test]
6634 fn column_flex_grow_page_level() {
6635 let engine = LayoutEngine::new();
6637 let font_context = FontContext::new();
6638
6639 let grow_child = make_styled_view(
6640 Style {
6641 flex_grow: Some(1.0),
6642 ..Default::default()
6643 },
6644 vec![make_text("Fill page", 12.0)],
6645 );
6646
6647 let doc = Document {
6648 children: vec![Node::page(
6649 PageConfig::default(),
6650 Style::default(),
6651 vec![grow_child],
6652 )],
6653 metadata: Default::default(),
6654 default_page: PageConfig::default(),
6655 fonts: vec![],
6656 tagged: false,
6657 pdfa: None,
6658 default_style: None,
6659 embedded_data: None,
6660 flatten_forms: false,
6661 pdf_ua: false,
6662 certification: None,
6663 };
6664
6665 let pages = engine.layout(&doc, &font_context);
6666 let page = &pages[0];
6667
6668 assert!(
6670 !page.elements.is_empty(),
6671 "Page should have at least one element"
6672 );
6673
6674 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
6675 let el = &page.elements[0];
6676 assert!(
6677 (el.height - content_height).abs() < 2.0,
6678 "Page-level flex-grow child should fill content height ({}), got {}",
6679 content_height,
6680 el.height
6681 );
6682 }
6683
6684 #[test]
6685 fn column_flex_grow_with_justify_content() {
6686 let engine = LayoutEngine::new();
6690 let font_context = FontContext::new();
6691
6692 let fixed_child = make_styled_view(
6693 Style {
6694 height: Some(Dimension::Pt(50.0)),
6695 ..Default::default()
6696 },
6697 vec![make_text("Top", 12.0)],
6698 );
6699 let grow_child = make_styled_view(
6700 Style {
6701 flex_grow: Some(1.0),
6702 ..Default::default()
6703 },
6704 vec![make_text("Fill", 12.0)],
6705 );
6706
6707 let container = make_styled_view(
6708 Style {
6709 flex_direction: Some(FlexDirection::Column),
6710 height: Some(Dimension::Pt(300.0)),
6711 justify_content: Some(JustifyContent::Center),
6712 ..Default::default()
6713 },
6714 vec![fixed_child, grow_child],
6715 );
6716
6717 let doc = Document {
6718 children: vec![Node::page(
6719 PageConfig::default(),
6720 Style::default(),
6721 vec![container],
6722 )],
6723 metadata: Default::default(),
6724 default_page: PageConfig::default(),
6725 fonts: vec![],
6726 tagged: false,
6727 pdfa: None,
6728 default_style: None,
6729 embedded_data: None,
6730 flatten_forms: false,
6731 pdf_ua: false,
6732 certification: None,
6733 };
6734
6735 let pages = engine.layout(&doc, &font_context);
6736 let page = &pages[0];
6737
6738 let container_el = page
6739 .elements
6740 .iter()
6741 .find(|e| e.children.len() == 2)
6742 .expect("Should find container");
6743
6744 let first_child = &container_el.children[0];
6747 assert!(
6748 (first_child.y - container_el.y).abs() < 1.0,
6749 "First child should be at top of container"
6750 );
6751
6752 let total = container_el.children[0].height + container_el.children[1].height;
6754 assert!(
6755 (total - 300.0).abs() < 2.0,
6756 "Children should fill container, got {}",
6757 total
6758 );
6759 }
6760
6761 #[test]
6762 fn column_flex_grow_child_justify_content_center() {
6763 let engine = LayoutEngine::new();
6766 let font_context = FontContext::new();
6767
6768 let inner_box = make_styled_view(
6770 Style {
6771 height: Some(Dimension::Pt(40.0)),
6772 ..Default::default()
6773 },
6774 vec![make_text("Centered", 12.0)],
6775 );
6776
6777 let grow_child = make_styled_view(
6779 Style {
6780 flex_grow: Some(1.0),
6781 flex_direction: Some(FlexDirection::Column),
6782 justify_content: Some(JustifyContent::Center),
6783 ..Default::default()
6784 },
6785 vec![inner_box],
6786 );
6787
6788 let container = make_styled_view(
6790 Style {
6791 flex_direction: Some(FlexDirection::Column),
6792 height: Some(Dimension::Pt(400.0)),
6793 ..Default::default()
6794 },
6795 vec![grow_child],
6796 );
6797
6798 let doc = Document {
6799 children: vec![Node::page(
6800 PageConfig::default(),
6801 Style::default(),
6802 vec![container],
6803 )],
6804 metadata: Default::default(),
6805 default_page: PageConfig::default(),
6806 fonts: vec![],
6807 tagged: false,
6808 pdfa: None,
6809 default_style: None,
6810 embedded_data: None,
6811 flatten_forms: false,
6812 pdf_ua: false,
6813 certification: None,
6814 };
6815
6816 let pages = engine.layout(&doc, &font_context);
6817 let page = &pages[0];
6818
6819 let container_el = page
6821 .elements
6822 .iter()
6823 .find(|e| e.height > 350.0 && e.children.len() == 1)
6824 .expect("Should find outer container");
6825
6826 let grow_el = &container_el.children[0];
6827 assert!(
6828 (grow_el.height - 400.0).abs() < 2.0,
6829 "Grow child should expand to 400, got {}",
6830 grow_el.height
6831 );
6832
6833 let inner_el = &grow_el.children[0];
6835 let expected_center = grow_el.y + grow_el.height / 2.0;
6836 let actual_center = inner_el.y + inner_el.height / 2.0;
6837 assert!(
6838 (actual_center - expected_center).abs() < 2.0,
6839 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
6840 expected_center,
6841 actual_center
6842 );
6843 }
6844
6845 #[test]
6846 fn column_flex_grow_child_justify_content_flex_end() {
6847 let engine = LayoutEngine::new();
6849 let font_context = FontContext::new();
6850
6851 let inner_box = make_styled_view(
6852 Style {
6853 height: Some(Dimension::Pt(30.0)),
6854 ..Default::default()
6855 },
6856 vec![make_text("Bottom", 12.0)],
6857 );
6858
6859 let grow_child = make_styled_view(
6860 Style {
6861 flex_grow: Some(1.0),
6862 flex_direction: Some(FlexDirection::Column),
6863 justify_content: Some(JustifyContent::FlexEnd),
6864 ..Default::default()
6865 },
6866 vec![inner_box],
6867 );
6868
6869 let container = make_styled_view(
6870 Style {
6871 flex_direction: Some(FlexDirection::Column),
6872 height: Some(Dimension::Pt(300.0)),
6873 ..Default::default()
6874 },
6875 vec![grow_child],
6876 );
6877
6878 let doc = Document {
6879 children: vec![Node::page(
6880 PageConfig::default(),
6881 Style::default(),
6882 vec![container],
6883 )],
6884 metadata: Default::default(),
6885 default_page: PageConfig::default(),
6886 fonts: vec![],
6887 tagged: false,
6888 pdfa: None,
6889 default_style: None,
6890 embedded_data: None,
6891 flatten_forms: false,
6892 pdf_ua: false,
6893 certification: None,
6894 };
6895
6896 let pages = engine.layout(&doc, &font_context);
6897 let page = &pages[0];
6898
6899 let container_el = page
6900 .elements
6901 .iter()
6902 .find(|e| e.height > 250.0 && e.children.len() == 1)
6903 .expect("Should find outer container");
6904
6905 let grow_el = &container_el.children[0];
6906 let inner_el = &grow_el.children[0];
6907
6908 let inner_bottom = inner_el.y + inner_el.height;
6910 let grow_bottom = grow_el.y + grow_el.height;
6911 assert!(
6912 (inner_bottom - grow_bottom).abs() < 2.0,
6913 "Inner box bottom ({}) should align with grow child bottom ({})",
6914 inner_bottom,
6915 grow_bottom
6916 );
6917 }
6918
6919 #[test]
6920 fn column_flex_grow_child_no_justify_unchanged() {
6921 let engine = LayoutEngine::new();
6923 let font_context = FontContext::new();
6924
6925 let inner_box = make_styled_view(
6926 Style {
6927 height: Some(Dimension::Pt(50.0)),
6928 ..Default::default()
6929 },
6930 vec![make_text("Top", 12.0)],
6931 );
6932
6933 let grow_child = make_styled_view(
6934 Style {
6935 flex_grow: Some(1.0),
6936 flex_direction: Some(FlexDirection::Column),
6937 ..Default::default()
6939 },
6940 vec![inner_box],
6941 );
6942
6943 let container = make_styled_view(
6944 Style {
6945 flex_direction: Some(FlexDirection::Column),
6946 height: Some(Dimension::Pt(300.0)),
6947 ..Default::default()
6948 },
6949 vec![grow_child],
6950 );
6951
6952 let doc = Document {
6953 children: vec![Node::page(
6954 PageConfig::default(),
6955 Style::default(),
6956 vec![container],
6957 )],
6958 metadata: Default::default(),
6959 default_page: PageConfig::default(),
6960 fonts: vec![],
6961 tagged: false,
6962 pdfa: None,
6963 default_style: None,
6964 embedded_data: None,
6965 flatten_forms: false,
6966 pdf_ua: false,
6967 certification: None,
6968 };
6969
6970 let pages = engine.layout(&doc, &font_context);
6971 let page = &pages[0];
6972
6973 let container_el = page
6974 .elements
6975 .iter()
6976 .find(|e| e.height > 250.0 && e.children.len() == 1)
6977 .expect("Should find outer container");
6978
6979 let grow_el = &container_el.children[0];
6980 let inner_el = &grow_el.children[0];
6981
6982 assert!(
6984 (inner_el.y - grow_el.y).abs() < 2.0,
6985 "Inner box ({}) should be at top of grow child ({})",
6986 inner_el.y,
6987 grow_el.y
6988 );
6989 }
6990
6991 #[test]
6992 fn column_flex_grow_child_align_items_center() {
6993 let engine = LayoutEngine::new();
6995 let font_context = FontContext::new();
6996
6997 let text = make_text("Hello", 12.0);
6998
6999 let grow_child = make_styled_view(
7000 Style {
7001 flex_grow: Some(1.0),
7002 flex_direction: Some(FlexDirection::Column),
7003 align_items: Some(AlignItems::Center),
7004 ..Default::default()
7005 },
7006 vec![text],
7007 );
7008
7009 let container = make_styled_view(
7010 Style {
7011 flex_direction: Some(FlexDirection::Column),
7012 height: Some(Dimension::Pt(300.0)),
7013 ..Default::default()
7014 },
7015 vec![grow_child],
7016 );
7017
7018 let doc = Document {
7019 children: vec![Node::page(
7020 PageConfig::default(),
7021 Style::default(),
7022 vec![container],
7023 )],
7024 metadata: Default::default(),
7025 default_page: PageConfig::default(),
7026 fonts: vec![],
7027 tagged: false,
7028 pdfa: None,
7029 default_style: None,
7030 embedded_data: None,
7031 flatten_forms: false,
7032 pdf_ua: false,
7033 certification: None,
7034 };
7035
7036 let pages = engine.layout(&doc, &font_context);
7037 let page = &pages[0];
7038
7039 let container_el = page
7040 .elements
7041 .iter()
7042 .find(|e| e.height > 250.0 && e.children.len() == 1)
7043 .expect("Should find outer container");
7044
7045 let grow_el = &container_el.children[0];
7046 assert!(
7047 !grow_el.children.is_empty(),
7048 "Grow child should have text child"
7049 );
7050
7051 let text_el = &grow_el.children[0];
7052 let text_center = text_el.x + text_el.width / 2.0;
7053 let grow_center = grow_el.x + grow_el.width / 2.0;
7054 assert!(
7055 (text_center - grow_center).abs() < 2.0,
7056 "Text center ({}) should be near grow child center ({})",
7057 text_center,
7058 grow_center
7059 );
7060 }
7061
7062 #[test]
7063 fn image_intrinsic_width_respects_height_constraint() {
7064 let engine = LayoutEngine::new();
7068 let font_context = FontContext::new();
7069
7070 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
7072
7073 let image_node = Node {
7074 kind: NodeKind::Image {
7075 src: one_px_png.to_string(),
7076 width: None,
7077 height: Some(36.0),
7078 },
7079 style: Style::default(),
7080 children: vec![],
7081 id: None,
7082 source_location: None,
7083 bookmark: None,
7084 href: None,
7085 alt: None,
7086 };
7087
7088 let resolved = image_node.style.resolve(None, 0.0);
7089 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
7090
7091 assert!(
7093 (intrinsic - 36.0).abs() < 1.0,
7094 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
7095 intrinsic
7096 );
7097 }
7098}