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}
450
451fn node_kind_name(kind: &NodeKind) -> &'static str {
453 match kind {
454 NodeKind::View => "View",
455 NodeKind::Text { .. } => "Text",
456 NodeKind::Image { .. } => "Image",
457 NodeKind::Table { .. } => "Table",
458 NodeKind::TableRow { .. } => "TableRow",
459 NodeKind::TableCell { .. } => "TableCell",
460 NodeKind::Fixed {
461 position: FixedPosition::Header,
462 } => "FixedHeader",
463 NodeKind::Fixed {
464 position: FixedPosition::Footer,
465 } => "FixedFooter",
466 NodeKind::Page { .. } => "Page",
467 NodeKind::PageBreak => "PageBreak",
468 NodeKind::Svg { .. } => "Svg",
469 NodeKind::Canvas { .. } => "Canvas",
470 NodeKind::Barcode { .. } => "Barcode",
471 NodeKind::QrCode { .. } => "QrCode",
472 NodeKind::BarChart { .. } => "BarChart",
473 NodeKind::LineChart { .. } => "LineChart",
474 NodeKind::PieChart { .. } => "PieChart",
475 NodeKind::AreaChart { .. } => "AreaChart",
476 NodeKind::DotPlot { .. } => "DotPlot",
477 NodeKind::Watermark { .. } => "Watermark",
478 NodeKind::TextField { .. } => "TextField",
479 NodeKind::Checkbox { .. } => "Checkbox",
480 NodeKind::Dropdown { .. } => "Dropdown",
481 NodeKind::RadioButton { .. } => "RadioButton",
482 }
483}
484
485#[derive(Debug, Clone)]
487pub enum FormFieldType {
488 TextField {
489 value: Option<String>,
490 placeholder: Option<String>,
491 multiline: bool,
492 password: bool,
493 read_only: bool,
494 max_length: Option<u32>,
495 font_size: f64,
496 },
497 Checkbox {
498 checked: bool,
499 read_only: bool,
500 },
501 Dropdown {
502 options: Vec<String>,
503 value: Option<String>,
504 read_only: bool,
505 font_size: f64,
506 },
507 RadioButton {
508 value: String,
509 checked: bool,
510 read_only: bool,
511 },
512}
513
514#[derive(Debug, Clone)]
516pub enum DrawCommand {
517 None,
519 Rect {
521 background: Option<Color>,
522 border_width: Edges,
523 border_color: EdgeValues<Color>,
524 border_radius: CornerValues,
525 opacity: f64,
526 },
527 Text {
529 lines: Vec<TextLine>,
530 color: Color,
531 text_decoration: TextDecoration,
532 opacity: f64,
533 },
534 Image {
536 image_data: crate::image_loader::LoadedImage,
537 },
538 ImagePlaceholder,
540 Svg {
542 commands: Vec<crate::svg::SvgCommand>,
543 width: f64,
544 height: f64,
545 clip: bool,
547 },
548 Barcode {
550 bars: Vec<u8>,
551 bar_width: f64,
552 height: f64,
553 color: Color,
554 },
555 QrCode {
557 modules: Vec<Vec<bool>>,
558 module_size: f64,
559 color: Color,
560 },
561 Chart {
563 primitives: Vec<crate::chart::ChartPrimitive>,
564 },
565 Watermark {
567 lines: Vec<TextLine>,
568 color: Color,
569 opacity: f64,
570 angle_rad: f64,
571 font_family: String,
573 },
574 FormField {
576 field_type: FormFieldType,
577 name: String,
578 },
579}
580
581#[derive(Debug, Clone)]
582pub struct TextLine {
583 pub x: f64,
584 pub y: f64,
585 pub glyphs: Vec<PositionedGlyph>,
586 pub width: f64,
587 pub height: f64,
588 pub word_spacing: f64,
590}
591
592#[derive(Debug, Clone)]
593pub struct PositionedGlyph {
594 pub glyph_id: u16,
597 pub x_offset: f64,
599 pub y_offset: f64,
601 pub x_advance: f64,
603 pub font_size: f64,
604 pub font_family: String,
605 pub font_weight: u32,
606 pub font_style: FontStyle,
607 pub char_value: char,
609 pub color: Option<Color>,
611 pub href: Option<String>,
613 pub text_decoration: TextDecoration,
615 pub letter_spacing: f64,
617 pub cluster_text: Option<String>,
620}
621
622fn offset_element_y(el: &mut LayoutElement, dy: f64) {
625 el.y += dy;
626 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
627 for line in lines.iter_mut() {
628 line.y += dy;
629 }
630 }
631 for child in &mut el.children {
632 offset_element_y(child, dy);
633 }
634}
635
636#[allow(dead_code)]
638fn offset_element_x(el: &mut LayoutElement, dx: f64) {
639 el.x += dx;
640 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
641 for line in lines.iter_mut() {
642 line.x += dx;
643 }
644 }
645 for child in &mut el.children {
646 offset_element_x(child, dx);
647 }
648}
649
650fn reapply_justify_content(elem: &mut LayoutElement) {
654 let style = match elem.resolved_style {
655 Some(ref s) => s,
656 None => return,
657 };
658 if matches!(style.justify_content, JustifyContent::FlexStart) {
659 return;
660 }
661 if elem.children.is_empty() {
662 return;
663 }
664
665 let padding_top = style.padding.top + style.border_width.top;
666 let padding_bottom = style.padding.bottom + style.border_width.bottom;
667 let inner_h = elem.height - padding_top - padding_bottom;
668 let content_top = elem.y + padding_top;
669
670 let last_child = &elem.children[elem.children.len() - 1];
672 let children_bottom = last_child.y + last_child.height;
673 let children_span = children_bottom - content_top;
674 let slack = inner_h - children_span;
675 if slack < 0.001 {
676 return;
677 }
678
679 let n = elem.children.len();
680 let offsets: Vec<f64> = match style.justify_content {
681 JustifyContent::FlexEnd => vec![slack; n],
682 JustifyContent::Center => vec![slack / 2.0; n],
683 JustifyContent::SpaceBetween => {
684 if n <= 1 {
685 vec![0.0; n]
686 } else {
687 let per_gap = slack / (n - 1) as f64;
688 (0..n).map(|i| i as f64 * per_gap).collect()
689 }
690 }
691 JustifyContent::SpaceAround => {
692 let space = slack / n as f64;
693 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
694 }
695 JustifyContent::SpaceEvenly => {
696 let space = slack / (n + 1) as f64;
697 (0..n).map(|i| (i + 1) as f64 * space).collect()
698 }
699 JustifyContent::FlexStart => unreachable!(),
700 };
701
702 for (i, child) in elem.children.iter_mut().enumerate() {
703 let dy = offsets[i];
704 if dy.abs() > 0.001 {
705 offset_element_y(child, dy);
706 }
707 }
708}
709
710fn apply_text_transform(text: &str, transform: TextTransform) -> String {
712 match transform {
713 TextTransform::None => text.to_string(),
714 TextTransform::Uppercase => text.to_uppercase(),
715 TextTransform::Lowercase => text.to_lowercase(),
716 TextTransform::Capitalize => {
717 let mut result = String::with_capacity(text.len());
718 let mut prev_is_whitespace = true;
719 for ch in text.chars() {
720 if prev_is_whitespace && ch.is_alphabetic() {
721 for upper in ch.to_uppercase() {
722 result.push(upper);
723 }
724 } else {
725 result.push(ch);
726 }
727 prev_is_whitespace = ch.is_whitespace();
728 }
729 result
730 }
731 }
732}
733
734pub const PAGE_NUMBER_SENTINEL: char = '\x02';
738
739pub const TOTAL_PAGES_SENTINEL: char = '\x03';
741
742fn substitute_page_placeholders(text: &str) -> String {
747 if text.contains("{{pageNumber}}") || text.contains("{{totalPages}}") {
748 text.replace("{{pageNumber}}", &PAGE_NUMBER_SENTINEL.to_string())
749 .replace("{{totalPages}}", &TOTAL_PAGES_SENTINEL.to_string())
750 } else {
751 text.to_string()
752 }
753}
754
755fn apply_char_transform(ch: char, transform: TextTransform, is_word_start: bool) -> char {
758 match transform {
759 TextTransform::None => ch,
760 TextTransform::Uppercase => ch.to_uppercase().next().unwrap_or(ch),
761 TextTransform::Lowercase => ch.to_lowercase().next().unwrap_or(ch),
762 TextTransform::Capitalize => {
763 if is_word_start && ch.is_alphabetic() {
764 ch.to_uppercase().next().unwrap_or(ch)
765 } else {
766 ch
767 }
768 }
769 }
770}
771
772pub struct LayoutEngine {
774 text_layout: TextLayout,
775 image_dim_cache: RefCell<HashMap<String, (u32, u32)>>,
776}
777
778#[derive(Debug, Clone)]
780struct PageCursor {
781 config: PageConfig,
782 content_width: f64,
783 content_height: f64,
784 y: f64,
785 elements: Vec<LayoutElement>,
786 fixed_header: Vec<(Node, f64)>,
787 fixed_footer: Vec<(Node, f64)>,
788 watermarks: Vec<Node>,
790 content_x: f64,
791 content_y: f64,
792 continuation_top_offset: f64,
794}
795
796impl PageCursor {
797 fn new(config: &PageConfig) -> Self {
798 let (page_w, page_h) = config.size.dimensions();
799 let content_width = page_w - config.margin.horizontal();
800 let content_height = page_h - config.margin.vertical();
801
802 Self {
803 config: config.clone(),
804 content_width,
805 content_height,
806 y: 0.0,
807 elements: Vec::new(),
808 fixed_header: Vec::new(),
809 fixed_footer: Vec::new(),
810 watermarks: Vec::new(),
811 content_x: config.margin.left,
812 content_y: config.margin.top,
813 continuation_top_offset: 0.0,
814 }
815 }
816
817 fn remaining_height(&self) -> f64 {
818 let footer_height: f64 = self.fixed_footer.iter().map(|(_, h)| *h).sum();
819 (self.content_height - self.y - footer_height).max(0.0)
820 }
821
822 fn finalize(&self) -> LayoutPage {
823 let (page_w, page_h) = self.config.size.dimensions();
824 LayoutPage {
825 width: page_w,
826 height: page_h,
827 elements: self.elements.clone(),
828 fixed_header: self.fixed_header.clone(),
829 fixed_footer: self.fixed_footer.clone(),
830 watermarks: self.watermarks.clone(),
831 config: self.config.clone(),
832 }
833 }
834
835 fn new_page(&self) -> Self {
836 let mut cursor = PageCursor::new(&self.config);
837 cursor.fixed_header = self.fixed_header.clone();
838 cursor.fixed_footer = self.fixed_footer.clone();
839 cursor.watermarks = self.watermarks.clone();
840 cursor.continuation_top_offset = self.continuation_top_offset;
841
842 let header_height: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
843 cursor.y = header_height + cursor.continuation_top_offset;
844
845 cursor
846 }
847}
848
849impl Default for LayoutEngine {
850 fn default() -> Self {
851 Self::new()
852 }
853}
854
855impl LayoutEngine {
856 pub fn new() -> Self {
857 Self {
858 text_layout: TextLayout::new(),
859 image_dim_cache: RefCell::new(HashMap::new()),
860 }
861 }
862
863 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
865 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
866 return Some(*dims);
867 }
868 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
869 self.image_dim_cache
870 .borrow_mut()
871 .insert(src.to_string(), dims);
872 Some(dims)
873 } else {
874 None
875 }
876 }
877
878 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
880 let mut pages: Vec<LayoutPage> = Vec::new();
881 let mut cursor = PageCursor::new(&document.default_page);
882
883 let base = document.default_style.clone().unwrap_or_default();
885 let root_style = Style {
886 lang: base.lang.clone().or(document.metadata.lang.clone()),
887 ..base
888 }
889 .resolve(None, cursor.content_width);
890
891 for node in &document.children {
892 match &node.kind {
893 NodeKind::Page { config } => {
894 if !cursor.elements.is_empty() || cursor.y > 0.0 {
895 pages.push(cursor.finalize());
896 }
897 cursor = PageCursor::new(config);
898
899 let mut page_root = node.style.resolve(Some(&root_style), cursor.content_width);
906 page_root.height = SizeConstraint::Fixed(cursor.content_height);
907
908 let cx = cursor.content_x;
909 let cw = cursor.content_width;
910 self.layout_children(
911 &node.children,
912 &node.style,
913 &mut cursor,
914 &mut pages,
915 cx,
916 cw,
917 Some(&page_root),
918 font_context,
919 );
920 }
921 NodeKind::PageBreak => {
922 pages.push(cursor.finalize());
923 cursor = cursor.new_page();
924 }
925 _ => {
926 let cx = cursor.content_x;
927 let cw = cursor.content_width;
928 self.layout_node(
929 node,
930 &mut cursor,
931 &mut pages,
932 cx,
933 cw,
934 Some(&root_style),
935 font_context,
936 None,
937 );
938 }
939 }
940 }
941
942 if !cursor.elements.is_empty() || cursor.y > 0.0 {
943 pages.push(cursor.finalize());
944 }
945
946 self.inject_fixed_elements(&mut pages, font_context);
947
948 pages
949 }
950
951 #[allow(clippy::too_many_arguments)]
952 fn layout_node(
953 &self,
954 node: &Node,
955 cursor: &mut PageCursor,
956 pages: &mut Vec<LayoutPage>,
957 x: f64,
958 available_width: f64,
959 parent_style: Option<&ResolvedStyle>,
960 font_context: &FontContext,
961 cross_axis_height: Option<f64>,
962 ) {
963 let mut style = node.style.resolve(parent_style, available_width);
964
965 if let Some(h) = cross_axis_height {
968 if matches!(style.height, SizeConstraint::Auto) {
969 style.height = SizeConstraint::Fixed(h);
970 }
971 }
972
973 if style.break_before {
974 pages.push(cursor.finalize());
975 *cursor = cursor.new_page();
976 }
977
978 match &node.kind {
979 NodeKind::PageBreak => {
980 pages.push(cursor.finalize());
981 *cursor = cursor.new_page();
982 }
983
984 NodeKind::Fixed { position } => {
985 let height = self.measure_node_height(node, available_width, &style, font_context);
986 match position {
987 FixedPosition::Header => {
988 cursor.fixed_header.push((node.clone(), height));
989 cursor.y += height;
990 }
991 FixedPosition::Footer => {
992 cursor.fixed_footer.push((node.clone(), height));
993 }
994 }
995 }
996
997 NodeKind::Watermark { .. } => {
998 cursor.watermarks.push(node.clone());
1000 }
1001
1002 NodeKind::TextField {
1003 name,
1004 value,
1005 placeholder,
1006 width: field_w,
1007 height: field_h,
1008 multiline,
1009 password,
1010 read_only,
1011 max_length,
1012 font_size,
1013 } => {
1014 self.layout_form_field(
1015 node,
1016 &style,
1017 cursor,
1018 pages,
1019 x,
1020 *field_w,
1021 *field_h,
1022 DrawCommand::FormField {
1023 field_type: FormFieldType::TextField {
1024 value: value.clone(),
1025 placeholder: placeholder.clone(),
1026 multiline: *multiline,
1027 password: *password,
1028 read_only: *read_only,
1029 max_length: *max_length,
1030 font_size: *font_size,
1031 },
1032 name: name.clone(),
1033 },
1034 "TextField",
1035 );
1036 }
1037
1038 NodeKind::Checkbox {
1039 name,
1040 checked,
1041 width: field_w,
1042 height: field_h,
1043 read_only,
1044 } => {
1045 self.layout_form_field(
1046 node,
1047 &style,
1048 cursor,
1049 pages,
1050 x,
1051 *field_w,
1052 *field_h,
1053 DrawCommand::FormField {
1054 field_type: FormFieldType::Checkbox {
1055 checked: *checked,
1056 read_only: *read_only,
1057 },
1058 name: name.clone(),
1059 },
1060 "Checkbox",
1061 );
1062 }
1063
1064 NodeKind::Dropdown {
1065 name,
1066 options,
1067 value,
1068 width: field_w,
1069 height: field_h,
1070 read_only,
1071 font_size,
1072 } => {
1073 self.layout_form_field(
1074 node,
1075 &style,
1076 cursor,
1077 pages,
1078 x,
1079 *field_w,
1080 *field_h,
1081 DrawCommand::FormField {
1082 field_type: FormFieldType::Dropdown {
1083 options: options.clone(),
1084 value: value.clone(),
1085 read_only: *read_only,
1086 font_size: *font_size,
1087 },
1088 name: name.clone(),
1089 },
1090 "Dropdown",
1091 );
1092 }
1093
1094 NodeKind::RadioButton {
1095 name,
1096 value,
1097 checked,
1098 width: field_w,
1099 height: field_h,
1100 read_only,
1101 } => {
1102 self.layout_form_field(
1103 node,
1104 &style,
1105 cursor,
1106 pages,
1107 x,
1108 *field_w,
1109 *field_h,
1110 DrawCommand::FormField {
1111 field_type: FormFieldType::RadioButton {
1112 value: value.clone(),
1113 checked: *checked,
1114 read_only: *read_only,
1115 },
1116 name: name.clone(),
1117 },
1118 "RadioButton",
1119 );
1120 }
1121
1122 NodeKind::Text {
1123 content,
1124 href,
1125 runs,
1126 } => {
1127 self.layout_text(
1128 content,
1129 href.as_deref(),
1130 runs,
1131 &style,
1132 cursor,
1133 pages,
1134 x,
1135 available_width,
1136 font_context,
1137 node.source_location.as_ref(),
1138 node.bookmark.as_deref(),
1139 );
1140 }
1141
1142 NodeKind::Image { width, height, .. } => {
1143 self.layout_image(
1144 node,
1145 &style,
1146 cursor,
1147 pages,
1148 x,
1149 available_width,
1150 *width,
1151 *height,
1152 );
1153 }
1154
1155 NodeKind::Table { columns } => {
1156 self.layout_table(
1157 node,
1158 &style,
1159 columns,
1160 cursor,
1161 pages,
1162 x,
1163 available_width,
1164 font_context,
1165 );
1166 }
1167
1168 NodeKind::View | NodeKind::Page { .. } => {
1169 self.layout_view(
1170 node,
1171 &style,
1172 cursor,
1173 pages,
1174 x,
1175 available_width,
1176 font_context,
1177 );
1178 }
1179
1180 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
1181 self.layout_view(
1182 node,
1183 &style,
1184 cursor,
1185 pages,
1186 x,
1187 available_width,
1188 font_context,
1189 );
1190 }
1191
1192 NodeKind::Svg {
1193 width: svg_w,
1194 height: svg_h,
1195 view_box,
1196 content,
1197 } => {
1198 self.layout_svg(
1199 node,
1200 &style,
1201 cursor,
1202 pages,
1203 x,
1204 available_width,
1205 *svg_w,
1206 *svg_h,
1207 view_box.as_deref(),
1208 content,
1209 );
1210 }
1211
1212 NodeKind::Barcode {
1213 data,
1214 format,
1215 width: explicit_width,
1216 height: bar_height,
1217 } => {
1218 self.layout_barcode(
1219 node,
1220 &style,
1221 cursor,
1222 pages,
1223 x,
1224 available_width,
1225 data,
1226 *format,
1227 *explicit_width,
1228 *bar_height,
1229 );
1230 }
1231
1232 NodeKind::QrCode {
1233 data,
1234 size: explicit_size,
1235 } => {
1236 self.layout_qrcode(
1237 node,
1238 &style,
1239 cursor,
1240 pages,
1241 x,
1242 available_width,
1243 data,
1244 *explicit_size,
1245 );
1246 }
1247
1248 NodeKind::Canvas {
1249 width: canvas_w,
1250 height: canvas_h,
1251 operations,
1252 } => {
1253 self.layout_canvas(
1254 node,
1255 &style,
1256 cursor,
1257 pages,
1258 x,
1259 available_width,
1260 *canvas_w,
1261 *canvas_h,
1262 operations,
1263 );
1264 }
1265
1266 NodeKind::BarChart {
1267 data,
1268 width: chart_w,
1269 height: chart_h,
1270 color,
1271 show_labels,
1272 show_values,
1273 show_grid,
1274 title,
1275 } => {
1276 let config = crate::chart::bar::BarChartConfig {
1277 color: color.clone(),
1278 show_labels: *show_labels,
1279 show_values: *show_values,
1280 show_grid: *show_grid,
1281 title: title.clone(),
1282 };
1283 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
1284 self.layout_chart(
1285 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
1286 );
1287 }
1288
1289 NodeKind::LineChart {
1290 series,
1291 labels,
1292 width: chart_w,
1293 height: chart_h,
1294 show_points,
1295 show_grid,
1296 title,
1297 } => {
1298 let config = crate::chart::line::LineChartConfig {
1299 show_points: *show_points,
1300 show_grid: *show_grid,
1301 title: title.clone(),
1302 };
1303 let primitives =
1304 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
1305 self.layout_chart(
1306 node,
1307 &style,
1308 cursor,
1309 pages,
1310 x,
1311 *chart_w,
1312 *chart_h,
1313 primitives,
1314 "LineChart",
1315 );
1316 }
1317
1318 NodeKind::PieChart {
1319 data,
1320 width: chart_w,
1321 height: chart_h,
1322 donut,
1323 show_legend,
1324 title,
1325 } => {
1326 let config = crate::chart::pie::PieChartConfig {
1327 donut: *donut,
1328 show_legend: *show_legend,
1329 title: title.clone(),
1330 };
1331 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
1332 self.layout_chart(
1333 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
1334 );
1335 }
1336
1337 NodeKind::AreaChart {
1338 series,
1339 labels,
1340 width: chart_w,
1341 height: chart_h,
1342 show_grid,
1343 title,
1344 } => {
1345 let config = crate::chart::area::AreaChartConfig {
1346 show_grid: *show_grid,
1347 title: title.clone(),
1348 };
1349 let primitives =
1350 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
1351 self.layout_chart(
1352 node,
1353 &style,
1354 cursor,
1355 pages,
1356 x,
1357 *chart_w,
1358 *chart_h,
1359 primitives,
1360 "AreaChart",
1361 );
1362 }
1363
1364 NodeKind::DotPlot {
1365 groups,
1366 width: chart_w,
1367 height: chart_h,
1368 x_min,
1369 x_max,
1370 y_min,
1371 y_max,
1372 x_label,
1373 y_label,
1374 show_legend,
1375 dot_size,
1376 } => {
1377 let config = crate::chart::dot::DotPlotConfig {
1378 x_min: *x_min,
1379 x_max: *x_max,
1380 y_min: *y_min,
1381 y_max: *y_max,
1382 x_label: x_label.clone(),
1383 y_label: y_label.clone(),
1384 show_legend: *show_legend,
1385 dot_size: *dot_size,
1386 };
1387 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
1388 self.layout_chart(
1389 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
1390 );
1391 }
1392 }
1393 }
1394
1395 #[allow(clippy::too_many_arguments)]
1396 fn layout_view(
1397 &self,
1398 node: &Node,
1399 style: &ResolvedStyle,
1400 cursor: &mut PageCursor,
1401 pages: &mut Vec<LayoutPage>,
1402 x: f64,
1403 available_width: f64,
1404 font_context: &FontContext,
1405 ) {
1406 let padding = &style.padding;
1407 let margin = &style.margin.to_edges();
1408 let border = &style.border_width;
1409
1410 let outer_width = match style.width {
1411 SizeConstraint::Fixed(w) => w,
1412 SizeConstraint::Auto => available_width - margin.horizontal(),
1413 };
1414 let inner_width = outer_width - padding.horizontal() - border.horizontal();
1415
1416 let children_height =
1417 self.measure_children_height(&node.children, inner_width, style, font_context);
1418 let total_height = match style.height {
1419 SizeConstraint::Fixed(h) => h,
1420 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
1421 };
1422
1423 let node_x = x + margin.left;
1424
1425 let fits = total_height <= cursor.remaining_height() - margin.vertical();
1426
1427 if fits || !style.breakable {
1428 if !fits && !style.breakable {
1429 pages.push(cursor.finalize());
1430 *cursor = cursor.new_page();
1431 }
1432
1433 let rect_y = cursor.content_y + cursor.y + margin.top;
1435 let snapshot = cursor.elements.len();
1436
1437 let saved_y = cursor.y;
1438 cursor.y += margin.top + padding.top + border.top;
1439
1440 let children_x = node_x + padding.left + border.left;
1441 let is_grid =
1442 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
1443 if is_grid {
1444 self.layout_grid_children(
1445 &node.children,
1446 style,
1447 cursor,
1448 pages,
1449 children_x,
1450 inner_width,
1451 font_context,
1452 );
1453 } else {
1454 self.layout_children(
1455 &node.children,
1456 &node.style,
1457 cursor,
1458 pages,
1459 children_x,
1460 inner_width,
1461 Some(style),
1462 font_context,
1463 );
1464 }
1465
1466 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
1468
1469 let rect_element = LayoutElement {
1470 x: node_x,
1471 y: rect_y,
1472 width: outer_width,
1473 height: total_height,
1474 draw: DrawCommand::Rect {
1475 background: style.background_color,
1476 border_width: style.border_width,
1477 border_color: style.border_color,
1478 border_radius: style.border_radius,
1479 opacity: style.opacity,
1480 },
1481 children: child_elements,
1482 node_type: Some(node_kind_name(&node.kind).to_string()),
1483 resolved_style: Some(style.clone()),
1484 source_location: node.source_location.clone(),
1485 href: node.href.clone(),
1486 bookmark: node.bookmark.clone(),
1487 alt: None,
1488 is_header_row: false,
1489 overflow: style.overflow,
1490 };
1491 cursor.elements.push(rect_element);
1492
1493 cursor.y = saved_y + total_height + margin.vertical();
1494 } else {
1495 self.layout_breakable_view(
1496 node,
1497 style,
1498 cursor,
1499 pages,
1500 node_x,
1501 outer_width,
1502 inner_width,
1503 font_context,
1504 );
1505 }
1506 }
1507
1508 #[allow(clippy::too_many_arguments)]
1509 fn layout_breakable_view(
1510 &self,
1511 node: &Node,
1512 style: &ResolvedStyle,
1513 cursor: &mut PageCursor,
1514 pages: &mut Vec<LayoutPage>,
1515 node_x: f64,
1516 outer_width: f64,
1517 inner_width: f64,
1518 font_context: &FontContext,
1519 ) {
1520 let padding = &style.padding;
1521 let border = &style.border_width;
1522 let margin = &style.margin.to_edges();
1523
1524 let initial_page_count = pages.len();
1526 let snapshot = cursor.elements.len();
1527 let rect_start_y = cursor.content_y + cursor.y + margin.top;
1528
1529 cursor.y += margin.top + padding.top + border.top;
1530 let prev_continuation_offset = cursor.continuation_top_offset;
1531 cursor.continuation_top_offset = padding.top + border.top;
1532
1533 if node.bookmark.is_some() {
1535 cursor.elements.push(LayoutElement {
1536 x: node_x,
1537 y: cursor.content_y + cursor.y,
1538 width: 0.0,
1539 height: 0.0,
1540 draw: DrawCommand::None,
1541 children: vec![],
1542 node_type: None,
1543 resolved_style: None,
1544 source_location: None,
1545 href: None,
1546 bookmark: node.bookmark.clone(),
1547 alt: None,
1548 is_header_row: false,
1549 overflow: Overflow::default(),
1550 });
1551 }
1552
1553 let children_x = node_x + padding.left + border.left;
1554 let is_grid =
1555 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
1556 if is_grid {
1557 self.layout_grid_children(
1558 &node.children,
1559 style,
1560 cursor,
1561 pages,
1562 children_x,
1563 inner_width,
1564 font_context,
1565 );
1566 } else {
1567 self.layout_children(
1568 &node.children,
1569 &node.style,
1570 cursor,
1571 pages,
1572 children_x,
1573 inner_width,
1574 Some(style),
1575 font_context,
1576 );
1577 }
1578
1579 cursor.continuation_top_offset = prev_continuation_offset;
1580
1581 let has_visual = style.background_color.is_some()
1583 || style.border_width.top > 0.0
1584 || style.border_width.right > 0.0
1585 || style.border_width.bottom > 0.0
1586 || style.border_width.left > 0.0;
1587 let needs_wrapper = has_visual || style.flex_grow > 0.0;
1589
1590 if !needs_wrapper {
1591 cursor.y += padding.bottom + border.bottom + margin.bottom;
1593 return;
1594 }
1595
1596 let draw_cmd = DrawCommand::Rect {
1597 background: style.background_color,
1598 border_width: style.border_width,
1599 border_color: style.border_color,
1600 border_radius: style.border_radius,
1601 opacity: style.opacity,
1602 };
1603
1604 if pages.len() == initial_page_count {
1605 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
1607 let rect_height =
1608 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
1609 cursor.elements.push(LayoutElement {
1610 x: node_x,
1611 y: rect_start_y,
1612 width: outer_width,
1613 height: rect_height,
1614 draw: draw_cmd,
1615 children: child_elements,
1616 node_type: Some(node_kind_name(&node.kind).to_string()),
1617 resolved_style: Some(style.clone()),
1618 source_location: node.source_location.clone(),
1619 href: node.href.clone(),
1620 bookmark: node.bookmark.clone(),
1621 alt: None,
1622 is_header_row: false,
1623 overflow: style.overflow,
1624 });
1625 } else {
1626 let page = &mut pages[initial_page_count];
1630 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
1631 let page_content_bottom =
1632 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
1633 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
1634 if !our_elements.is_empty() {
1635 let rect_height = page_content_bottom - rect_start_y;
1636 page.elements.push(LayoutElement {
1637 x: node_x,
1638 y: rect_start_y,
1639 width: outer_width,
1640 height: rect_height,
1641 draw: draw_cmd.clone(),
1642 children: our_elements,
1643 node_type: Some(node_kind_name(&node.kind).to_string()),
1644 resolved_style: Some(style.clone()),
1645 source_location: node.source_location.clone(),
1646 href: node.href.clone(),
1647 bookmark: node.bookmark.clone(),
1648 alt: None,
1649 is_header_row: false,
1650 overflow: Overflow::default(),
1651 });
1652 }
1653
1654 for page in &mut pages[initial_page_count + 1..] {
1656 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
1657 let content_top = page.config.margin.top + header_h;
1658 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
1659 let content_bottom = page.config.margin.top
1660 + (page.height - page.config.margin.vertical())
1661 - footer_h;
1662 let all_elements: Vec<LayoutElement> = page.elements.drain(..).collect();
1663 if !all_elements.is_empty() {
1664 page.elements.push(LayoutElement {
1665 x: node_x,
1666 y: content_top,
1667 width: outer_width,
1668 height: content_bottom - content_top,
1669 draw: draw_cmd.clone(),
1670 children: all_elements,
1671 node_type: Some(node_kind_name(&node.kind).to_string()),
1672 resolved_style: Some(style.clone()),
1673 source_location: node.source_location.clone(),
1674 href: None,
1675 bookmark: None,
1676 alt: None,
1677 is_header_row: false,
1678 overflow: Overflow::default(),
1679 });
1680 }
1681 }
1682
1683 let all_elements: Vec<LayoutElement> = cursor.elements.drain(..).collect();
1685 if !all_elements.is_empty() {
1686 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
1687 let content_top = cursor.content_y + header_h;
1688 let rect_height =
1689 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
1690 cursor.elements.push(LayoutElement {
1691 x: node_x,
1692 y: content_top,
1693 width: outer_width,
1694 height: rect_height,
1695 draw: draw_cmd,
1696 children: all_elements,
1697 node_type: Some(node_kind_name(&node.kind).to_string()),
1698 resolved_style: Some(style.clone()),
1699 source_location: node.source_location.clone(),
1700 href: None,
1701 bookmark: None,
1702 alt: None,
1703 is_header_row: false,
1704 overflow: Overflow::default(),
1705 });
1706 }
1707 }
1708
1709 cursor.y += padding.bottom + border.bottom + margin.bottom;
1710 }
1711
1712 #[allow(clippy::too_many_arguments)]
1713 fn layout_children(
1714 &self,
1715 children: &[Node],
1716 _parent_raw_style: &Style,
1717 cursor: &mut PageCursor,
1718 pages: &mut Vec<LayoutPage>,
1719 content_x: f64,
1720 available_width: f64,
1721 parent_style: Option<&ResolvedStyle>,
1722 font_context: &FontContext,
1723 ) {
1724 let parent_box_y = cursor.content_y + cursor.y;
1726 let parent_box_x = content_x;
1727
1728 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
1730 .iter()
1731 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
1732
1733 let direction = parent_style
1734 .map(|s| s.flex_direction)
1735 .unwrap_or(FlexDirection::Column);
1736
1737 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
1738 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
1739
1740 match direction {
1742 FlexDirection::Column | FlexDirection::ColumnReverse => {
1743 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
1744 flow_children.into_iter().rev().collect()
1745 } else {
1746 flow_children
1747 };
1748
1749 let justify = parent_style
1750 .map(|s| s.justify_content)
1751 .unwrap_or(JustifyContent::FlexStart);
1752 let align = parent_style
1753 .map(|s| s.align_items)
1754 .unwrap_or(AlignItems::Stretch);
1755
1756 let start_y = cursor.y;
1757 let initial_pages = pages.len();
1758
1759 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
1761
1762 for (i, child) in items.iter().enumerate() {
1763 if i > 0 {
1764 cursor.y += row_gap;
1765 }
1766 let child_start = cursor.elements.len();
1767
1768 let child_margin = &child.style.resolve(parent_style, available_width).margin;
1771 let has_auto_h = child_margin.has_auto_horizontal();
1772
1773 let (child_x, layout_w) = if has_auto_h {
1780 let child_style = child.style.resolve(parent_style, available_width);
1781 let has_explicit_width =
1782 matches!(child_style.width, SizeConstraint::Fixed(_));
1783 let intrinsic = self
1784 .measure_intrinsic_width(child, &child_style, font_context)
1785 .min(available_width);
1786 let w = match child_style.width {
1787 SizeConstraint::Fixed(fw) => fw,
1788 SizeConstraint::Auto => intrinsic,
1789 };
1790 let lw = if has_explicit_width {
1791 available_width
1792 } else {
1793 w
1794 };
1795 let fixed_h = child_margin.horizontal();
1796 let slack = (available_width - w - fixed_h).max(0.0);
1797 let auto_left = child_margin.left.is_auto();
1798 let auto_right = child_margin.right.is_auto();
1799 let ml = match (auto_left, auto_right) {
1800 (true, true) => slack / 2.0,
1801 (true, false) => slack,
1802 (false, true) => 0.0,
1803 (false, false) => 0.0,
1804 };
1805 (content_x + child_margin.left.resolve() + ml, lw)
1806 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
1807 let child_style = child.style.resolve(parent_style, available_width);
1808 let has_explicit_width =
1809 matches!(child_style.width, SizeConstraint::Fixed(_));
1810 let intrinsic = self
1811 .measure_intrinsic_width(child, &child_style, font_context)
1812 .min(available_width);
1813 let w = match child_style.width {
1814 SizeConstraint::Fixed(fw) => fw,
1815 SizeConstraint::Auto => intrinsic,
1816 };
1817 let lw = if has_explicit_width {
1818 available_width
1819 } else {
1820 w
1821 };
1822 match align {
1823 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
1824 AlignItems::FlexEnd => (content_x + available_width - w, lw),
1825 _ => (content_x, available_width),
1826 }
1827 } else {
1828 (content_x, available_width)
1829 };
1830
1831 self.layout_node(
1832 child,
1833 cursor,
1834 pages,
1835 child_x,
1836 layout_w,
1837 parent_style,
1838 font_context,
1839 None,
1840 );
1841
1842 child_ranges.push((child_start, cursor.elements.len()));
1843 }
1844
1845 let container_inner_h: Option<f64> = parent_style
1848 .and_then(|ps| match ps.height {
1849 SizeConstraint::Fixed(h) => {
1850 Some(h - ps.padding.vertical() - ps.border_width.vertical())
1851 }
1852 SizeConstraint::Auto => None,
1853 })
1854 .or_else(|| {
1855 if parent_style.is_none() {
1857 Some(cursor.content_height - start_y)
1858 } else {
1859 None
1860 }
1861 });
1862
1863 if let Some(inner_h) = container_inner_h {
1864 if pages.len() == initial_pages {
1865 let child_styles: Vec<ResolvedStyle> = items
1866 .iter()
1867 .map(|child| child.style.resolve(parent_style, available_width))
1868 .collect();
1869 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
1870 if total_grow > 0.0 {
1871 let children_total = cursor.y - start_y;
1872 let slack = (inner_h - children_total).max(0.0);
1873 if slack > 0.0 {
1874 let mut cumulative_shift = 0.0_f64;
1875 for (i, cs) in child_styles.iter().enumerate() {
1876 let (start, end) = child_ranges[i];
1877 if cumulative_shift > 0.001 {
1878 for j in start..end {
1879 offset_element_y(
1880 &mut cursor.elements[j],
1881 cumulative_shift,
1882 );
1883 }
1884 }
1885 if cs.flex_grow > 0.0 {
1886 let extra = slack * (cs.flex_grow / total_grow);
1887 if start < end {
1889 let elem = &mut cursor.elements[end - 1];
1890 elem.height += extra;
1891 reapply_justify_content(elem);
1892 }
1893 cumulative_shift += extra;
1894 }
1895 }
1896 cursor.y += cumulative_shift;
1897 }
1898 }
1899 }
1900 }
1901
1902 let needs_justify =
1904 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
1905 if needs_justify {
1906 let justify_inner_h = container_inner_h.or_else(|| {
1908 parent_style.and_then(|ps| match ps.height {
1909 SizeConstraint::Fixed(h) => {
1910 Some(h - ps.padding.vertical() - ps.border_width.vertical())
1911 }
1912 SizeConstraint::Auto => None,
1913 })
1914 });
1915 if let Some(inner_h) = justify_inner_h {
1916 let children_total = cursor.y - start_y;
1917 let slack = inner_h - children_total;
1918 if slack > 0.0 {
1919 let n = child_ranges.len();
1920 let offsets: Vec<f64> = match justify {
1921 JustifyContent::FlexEnd => vec![slack; n],
1922 JustifyContent::Center => vec![slack / 2.0; n],
1923 JustifyContent::SpaceBetween => {
1924 if n <= 1 {
1925 vec![0.0; n]
1926 } else {
1927 let per_gap = slack / (n - 1) as f64;
1928 (0..n).map(|i| i as f64 * per_gap).collect()
1929 }
1930 }
1931 JustifyContent::SpaceAround => {
1932 let space = slack / n as f64;
1933 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
1934 }
1935 JustifyContent::SpaceEvenly => {
1936 let space = slack / (n + 1) as f64;
1937 (0..n).map(|i| (i + 1) as f64 * space).collect()
1938 }
1939 JustifyContent::FlexStart => vec![0.0; n],
1940 };
1941 for (i, &(start, end)) in child_ranges.iter().enumerate() {
1942 let dy = offsets[i];
1943 if dy.abs() > 0.001 {
1944 for j in start..end {
1945 offset_element_y(&mut cursor.elements[j], dy);
1946 }
1947 }
1948 }
1949 cursor.y += *offsets.last().unwrap_or(&0.0);
1950 }
1951 }
1952 }
1953 }
1954
1955 FlexDirection::Row | FlexDirection::RowReverse => {
1956 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
1957 self.layout_flex_row(
1958 &flow_owned,
1959 cursor,
1960 pages,
1961 content_x,
1962 available_width,
1963 parent_style,
1964 column_gap,
1965 row_gap,
1966 font_context,
1967 );
1968 }
1969 }
1970
1971 for abs_child in &abs_children {
1973 let abs_style = abs_child.style.resolve(parent_style, available_width);
1974
1975 let child_width = match abs_style.width {
1977 SizeConstraint::Fixed(w) => w,
1978 SizeConstraint::Auto => {
1979 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
1981 (available_width - l - r).max(0.0)
1982 } else {
1983 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
1984 }
1985 }
1986 };
1987
1988 let child_height = match abs_style.height {
1989 SizeConstraint::Fixed(h) => h,
1990 SizeConstraint::Auto => {
1991 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
1992 }
1993 };
1994
1995 let abs_x = if let Some(l) = abs_style.left {
1997 parent_box_x + l
1998 } else if let Some(r) = abs_style.right {
1999 parent_box_x + available_width - r - child_width
2000 } else {
2001 parent_box_x
2002 };
2003
2004 let parent_inner_height = parent_style
2006 .and_then(|ps| match ps.height {
2007 SizeConstraint::Fixed(h) => {
2008 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2009 }
2010 SizeConstraint::Auto => None,
2011 })
2012 .unwrap_or(cursor.content_y + cursor.y - parent_box_y);
2013
2014 let abs_y = if let Some(t) = abs_style.top {
2015 parent_box_y + t
2016 } else if let Some(b) = abs_style.bottom {
2017 parent_box_y + parent_inner_height - b - child_height
2018 } else {
2019 parent_box_y
2020 };
2021
2022 let mut abs_cursor = PageCursor::new(&cursor.config);
2024 abs_cursor.y = 0.0;
2025 abs_cursor.content_x = abs_x;
2026 abs_cursor.content_y = abs_y;
2027
2028 self.layout_node(
2029 abs_child,
2030 &mut abs_cursor,
2031 &mut Vec::new(),
2032 abs_x,
2033 child_width,
2034 parent_style,
2035 font_context,
2036 None,
2037 );
2038
2039 cursor.elements.extend(abs_cursor.elements);
2041 }
2042 }
2043
2044 #[allow(clippy::too_many_arguments)]
2045 fn layout_flex_row(
2046 &self,
2047 children: &[Node],
2048 cursor: &mut PageCursor,
2049 pages: &mut Vec<LayoutPage>,
2050 content_x: f64,
2051 available_width: f64,
2052 parent_style: Option<&ResolvedStyle>,
2053 column_gap: f64,
2054 row_gap: f64,
2055 font_context: &FontContext,
2056 ) {
2057 if children.is_empty() {
2058 return;
2059 }
2060
2061 let flex_wrap = parent_style
2062 .map(|s| s.flex_wrap)
2063 .unwrap_or(FlexWrap::NoWrap);
2064
2065 let items: Vec<FlexItem> = children
2068 .iter()
2069 .map(|child| {
2070 let style = child.style.resolve(parent_style, available_width);
2071 let base_width = match style.flex_basis {
2072 SizeConstraint::Fixed(w) => w,
2073 SizeConstraint::Auto => match style.width {
2074 SizeConstraint::Fixed(w) => w,
2075 SizeConstraint::Auto => {
2076 self.measure_intrinsic_width(child, &style, font_context)
2077 }
2078 },
2079 };
2080 let min_content_width = self.measure_min_content_width(child, &style, font_context);
2081 FlexItem {
2082 node: child,
2083 style,
2084 base_width,
2085 min_content_width,
2086 }
2087 })
2088 .collect();
2089
2090 let base_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2092 let lines = match flex_wrap {
2093 FlexWrap::NoWrap => {
2094 vec![flex::WrapLine {
2095 start: 0,
2096 end: items.len(),
2097 }]
2098 }
2099 FlexWrap::Wrap => flex::partition_into_lines(&base_widths, column_gap, available_width),
2100 FlexWrap::WrapReverse => {
2101 let mut l = flex::partition_into_lines(&base_widths, column_gap, available_width);
2102 l.reverse();
2103 l
2104 }
2105 };
2106
2107 if lines.is_empty() {
2108 return;
2109 }
2110
2111 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
2113
2114 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2116
2117 let initial_pages_count = pages.len();
2118 let flex_start_y = cursor.y;
2119 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
2120
2121 for (line_idx, line) in lines.iter().enumerate() {
2122 let line_items = &items[line.start..line.end];
2123 let line_count = line.end - line.start;
2124 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
2125 let distributable = available_width - line_gap;
2126
2127 let total_base: f64 = line_items.iter().map(|i| i.base_width).sum();
2129 let remaining = distributable - total_base;
2130
2131 if remaining > 0.0 {
2132 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
2133 if total_grow > 0.0 {
2134 for (j, item) in line_items.iter().enumerate() {
2135 final_widths[line.start + j] =
2136 item.base_width + remaining * (item.style.flex_grow / total_grow);
2137 }
2138 }
2139 } else if remaining < 0.0 {
2140 let total_shrink: f64 = line_items
2141 .iter()
2142 .map(|i| i.style.flex_shrink * i.base_width)
2143 .sum();
2144 if total_shrink > 0.0 {
2145 for (j, item) in line_items.iter().enumerate() {
2146 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
2147 let w = item.base_width + remaining * factor;
2148 let floor = item.style.min_width.max(item.min_content_width);
2149 final_widths[line.start + j] = w.max(floor);
2150 }
2151 }
2152 }
2153
2154 let line_height: f64 = line_items
2156 .iter()
2157 .enumerate()
2158 .map(|(j, item)| {
2159 let fw = final_widths[line.start + j];
2160 self.measure_node_height(item.node, fw, &item.style, font_context)
2161 + item.style.margin.vertical()
2162 })
2163 .fold(0.0f64, f64::max);
2164
2165 if line_height > cursor.remaining_height() {
2167 pages.push(cursor.finalize());
2168 *cursor = cursor.new_page();
2169 }
2170
2171 if line_idx > 0 {
2173 cursor.y += row_gap;
2174 }
2175
2176 let row_start_y = cursor.y;
2177
2178 let actual_total: f64 = (line.start..line.end).map(|i| final_widths[i]).sum();
2180 let slack = available_width - actual_total - line_gap;
2181
2182 let (start_offset, between_extra) = match justify {
2183 JustifyContent::FlexStart => (0.0, 0.0),
2184 JustifyContent::FlexEnd => (slack, 0.0),
2185 JustifyContent::Center => (slack / 2.0, 0.0),
2186 JustifyContent::SpaceBetween => {
2187 if line_count > 1 {
2188 (0.0, slack / (line_count as f64 - 1.0))
2189 } else {
2190 (0.0, 0.0)
2191 }
2192 }
2193 JustifyContent::SpaceAround => {
2194 let s = slack / line_count as f64;
2195 (s / 2.0, s)
2196 }
2197 JustifyContent::SpaceEvenly => {
2198 let s = slack / (line_count as f64 + 1.0);
2199 (s, s)
2200 }
2201 };
2202
2203 let line_elem_start = cursor.elements.len();
2204 let mut x = content_x + start_offset;
2205
2206 for (j, item) in line_items.iter().enumerate() {
2207 if j > 0 {
2208 x += column_gap + between_extra;
2209 }
2210
2211 let fw = final_widths[line.start + j];
2212
2213 let align = item
2214 .style
2215 .align_self
2216 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
2217
2218 let item_height =
2219 self.measure_node_height(item.node, fw, &item.style, font_context);
2220
2221 let has_auto_v = item.style.margin.has_auto_vertical();
2223 let y_offset = if has_auto_v {
2224 let fixed_v = item.style.margin.vertical();
2225 let slack = (line_height - item_height - fixed_v).max(0.0);
2226 let auto_top = item.style.margin.top.is_auto();
2227 let auto_bottom = item.style.margin.bottom.is_auto();
2228 match (auto_top, auto_bottom) {
2229 (true, true) => slack / 2.0,
2230 (true, false) => slack,
2231 (false, true) => 0.0,
2232 (false, false) => 0.0,
2233 }
2234 } else {
2235 match align {
2236 AlignItems::FlexStart => 0.0,
2237 AlignItems::FlexEnd => {
2238 line_height - item_height - item.style.margin.vertical()
2239 }
2240 AlignItems::Center => {
2241 (line_height - item_height - item.style.margin.vertical()) / 2.0
2242 }
2243 AlignItems::Stretch => 0.0,
2244 AlignItems::Baseline => 0.0,
2245 }
2246 };
2247
2248 let cross_h = if matches!(align, AlignItems::Stretch)
2252 && matches!(item.style.height, SizeConstraint::Auto)
2253 && !has_auto_v
2254 {
2255 let stretch_h = line_height - item.style.margin.vertical();
2256 if stretch_h > item_height {
2257 Some(stretch_h)
2258 } else {
2259 None
2260 }
2261 } else {
2262 None
2263 };
2264
2265 let saved_y = cursor.y;
2266 cursor.y = row_start_y + y_offset;
2267
2268 self.layout_node(
2269 item.node,
2270 cursor,
2271 pages,
2272 x,
2273 fw,
2274 parent_style,
2275 font_context,
2276 cross_h,
2277 );
2278
2279 cursor.y = saved_y;
2280 x += fw;
2281 }
2282
2283 cursor.y = row_start_y + line_height;
2284 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
2285 }
2286
2287 if pages.len() == initial_pages_count && !line_infos.is_empty() {
2289 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
2290 if !matches!(align_content, AlignContent::FlexStart)
2291 && !matches!(flex_wrap, FlexWrap::NoWrap)
2292 {
2293 if let Some(parent) = parent_style {
2294 if let SizeConstraint::Fixed(container_h) = parent.height {
2295 let inner_h = container_h
2296 - parent.padding.vertical()
2297 - parent.border_width.vertical();
2298 let total_used = cursor.y - flex_start_y;
2299 let slack = inner_h - total_used;
2300 if slack > 0.0 {
2301 let n = line_infos.len();
2302 let offsets: Vec<f64> = match align_content {
2303 AlignContent::FlexEnd => vec![slack; n],
2304 AlignContent::Center => vec![slack / 2.0; n],
2305 AlignContent::SpaceBetween => {
2306 if n <= 1 {
2307 vec![0.0; n]
2308 } else {
2309 let per_gap = slack / (n - 1) as f64;
2310 (0..n).map(|i| i as f64 * per_gap).collect()
2311 }
2312 }
2313 AlignContent::SpaceAround => {
2314 let space = slack / n as f64;
2315 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2316 }
2317 AlignContent::SpaceEvenly => {
2318 let space = slack / (n + 1) as f64;
2319 (0..n).map(|i| (i + 1) as f64 * space).collect()
2320 }
2321 AlignContent::Stretch => {
2322 let extra = slack / n as f64;
2323 (0..n).map(|i| i as f64 * extra).collect()
2324 }
2325 AlignContent::FlexStart => vec![0.0; n],
2326 };
2327 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
2328 let dy = offsets[i];
2329 if dy.abs() > 0.001 {
2330 for j in start..end {
2331 offset_element_y(&mut cursor.elements[j], dy);
2332 }
2333 }
2334 }
2335 cursor.y += *offsets.last().unwrap_or(&0.0);
2336 }
2337 }
2338 }
2339 }
2340 }
2341 }
2342
2343 #[allow(clippy::too_many_arguments)]
2344 fn layout_table(
2345 &self,
2346 node: &Node,
2347 style: &ResolvedStyle,
2348 column_defs: &[ColumnDef],
2349 cursor: &mut PageCursor,
2350 pages: &mut Vec<LayoutPage>,
2351 x: f64,
2352 available_width: f64,
2353 font_context: &FontContext,
2354 ) {
2355 let padding = &style.padding;
2356 let margin = &style.margin.to_edges();
2357 let border = &style.border_width;
2358
2359 let table_x = x + margin.left;
2360 let table_width = match style.width {
2361 SizeConstraint::Fixed(w) => w,
2362 SizeConstraint::Auto => available_width - margin.horizontal(),
2363 };
2364 let inner_width = table_width - padding.horizontal() - border.horizontal();
2365
2366 let col_widths = self.resolve_column_widths(column_defs, inner_width, &node.children);
2367
2368 let mut header_rows: Vec<&Node> = Vec::new();
2369 let mut body_rows: Vec<&Node> = Vec::new();
2370
2371 for child in &node.children {
2372 match &child.kind {
2373 NodeKind::TableRow { is_header: true } => header_rows.push(child),
2374 _ => body_rows.push(child),
2375 }
2376 }
2377
2378 cursor.y += margin.top + padding.top + border.top;
2379
2380 let cell_x_start = table_x + padding.left + border.left;
2381 for header_row in &header_rows {
2382 self.layout_table_row(
2383 header_row,
2384 &col_widths,
2385 style,
2386 cursor,
2387 cell_x_start,
2388 font_context,
2389 pages,
2390 );
2391 }
2392
2393 for body_row in &body_rows {
2394 let row_height =
2395 self.measure_table_row_height(body_row, &col_widths, style, font_context);
2396
2397 if row_height > cursor.remaining_height() {
2398 pages.push(cursor.finalize());
2399 *cursor = cursor.new_page();
2400
2401 cursor.y += padding.top + border.top;
2402 for header_row in &header_rows {
2403 self.layout_table_row(
2404 header_row,
2405 &col_widths,
2406 style,
2407 cursor,
2408 cell_x_start,
2409 font_context,
2410 pages,
2411 );
2412 }
2413 }
2414
2415 self.layout_table_row(
2416 body_row,
2417 &col_widths,
2418 style,
2419 cursor,
2420 cell_x_start,
2421 font_context,
2422 pages,
2423 );
2424 }
2425
2426 cursor.y += padding.bottom + border.bottom + margin.bottom;
2427 }
2428
2429 #[allow(clippy::too_many_arguments)]
2430 fn layout_table_row(
2431 &self,
2432 row: &Node,
2433 col_widths: &[f64],
2434 parent_style: &ResolvedStyle,
2435 cursor: &mut PageCursor,
2436 start_x: f64,
2437 font_context: &FontContext,
2438 pages: &mut Vec<LayoutPage>,
2439 ) {
2440 let row_style = row
2441 .style
2442 .resolve(Some(parent_style), col_widths.iter().sum());
2443
2444 let row_height = self.measure_table_row_height(row, col_widths, parent_style, font_context);
2445 let row_y = cursor.content_y + cursor.y;
2446 let total_width: f64 = col_widths.iter().sum();
2447
2448 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
2449
2450 let row_snapshot = cursor.elements.len();
2452
2453 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
2454 let mut cell_x = start_x;
2455 for (i, cell) in row.children.iter().enumerate() {
2456 let col_width = col_widths.get(i).copied().unwrap_or(0.0);
2457
2458 let cell_style = cell.style.resolve(Some(&row_style), col_width);
2459
2460 let cell_snapshot = cursor.elements.len();
2462
2463 let inner_width =
2464 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
2465
2466 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
2467 let saved_y = cursor.y;
2468 cursor.y += cell_style.padding.top + cell_style.border_width.top;
2469
2470 let cursor_before_cell = cursor.clone();
2472 let mut cell_pages: Vec<LayoutPage> = Vec::new();
2473 for child in &cell.children {
2474 self.layout_node(
2475 child,
2476 cursor,
2477 &mut cell_pages,
2478 content_x,
2479 inner_width,
2480 Some(&cell_style),
2481 font_context,
2482 None,
2483 );
2484 }
2485
2486 if !cell_pages.is_empty() {
2488 let post_break_elements = std::mem::take(&mut cursor.elements);
2489 if let Some(last_page) = cell_pages.last_mut() {
2490 last_page.elements.extend(post_break_elements);
2491 }
2492 all_overflow_pages.extend(cell_pages);
2493 *cursor = cursor_before_cell;
2494 }
2495
2496 cursor.y = saved_y;
2497
2498 let cell_children: Vec<LayoutElement> =
2500 cursor.elements.drain(cell_snapshot..).collect();
2501
2502 cursor.elements.push(LayoutElement {
2504 x: cell_x,
2505 y: row_y,
2506 width: col_width,
2507 height: row_height,
2508 draw: if cell_style.background_color.is_some()
2509 || cell_style.border_width.horizontal() > 0.0
2510 || cell_style.border_width.vertical() > 0.0
2511 {
2512 DrawCommand::Rect {
2513 background: cell_style.background_color,
2514 border_width: cell_style.border_width,
2515 border_color: cell_style.border_color,
2516 border_radius: cell_style.border_radius,
2517 opacity: cell_style.opacity,
2518 }
2519 } else {
2520 DrawCommand::None
2521 },
2522 children: cell_children,
2523 node_type: Some("TableCell".to_string()),
2524 resolved_style: Some(cell_style.clone()),
2525 source_location: cell.source_location.clone(),
2526 href: None,
2527 bookmark: cell.bookmark.clone(),
2528 alt: None,
2529 is_header_row: is_header,
2530 overflow: Overflow::default(),
2531 });
2532
2533 cell_x += col_width;
2534 }
2535
2536 let row_children: Vec<LayoutElement> = cursor.elements.drain(row_snapshot..).collect();
2538 cursor.elements.push(LayoutElement {
2539 x: start_x,
2540 y: row_y,
2541 width: total_width,
2542 height: row_height,
2543 draw: if let Some(bg) = row_style.background_color {
2544 DrawCommand::Rect {
2545 background: Some(bg),
2546 border_width: Edges::default(),
2547 border_color: EdgeValues::uniform(Color::BLACK),
2548 border_radius: CornerValues::uniform(0.0),
2549 opacity: row_style.opacity,
2550 }
2551 } else {
2552 DrawCommand::None
2553 },
2554 children: row_children,
2555 node_type: Some("TableRow".to_string()),
2556 resolved_style: Some(row_style.clone()),
2557 source_location: row.source_location.clone(),
2558 href: None,
2559 bookmark: row.bookmark.clone(),
2560 alt: None,
2561 is_header_row: is_header,
2562 overflow: row_style.overflow,
2563 });
2564
2565 pages.extend(all_overflow_pages);
2567
2568 cursor.y += row_height;
2569 }
2570
2571 #[allow(clippy::too_many_arguments)]
2572 fn layout_text(
2573 &self,
2574 content: &str,
2575 href: Option<&str>,
2576 runs: &[TextRun],
2577 style: &ResolvedStyle,
2578 cursor: &mut PageCursor,
2579 pages: &mut Vec<LayoutPage>,
2580 x: f64,
2581 available_width: f64,
2582 font_context: &FontContext,
2583 source_location: Option<&SourceLocation>,
2584 bookmark: Option<&str>,
2585 ) {
2586 let margin = &style.margin.to_edges();
2587 let text_x = x + margin.left;
2588 let text_width = available_width - margin.horizontal();
2589
2590 cursor.y += margin.top;
2591
2592 if !runs.is_empty() {
2594 self.layout_text_runs(
2595 runs,
2596 href,
2597 style,
2598 cursor,
2599 pages,
2600 text_x,
2601 text_width,
2602 font_context,
2603 source_location,
2604 bookmark,
2605 );
2606 cursor.y += margin.bottom;
2607 return;
2608 }
2609
2610 let content = substitute_page_placeholders(content);
2611 let transformed = apply_text_transform(&content, style.text_transform);
2612 let justify = matches!(style.text_align, TextAlign::Justify);
2613 let lines = match style.line_breaking {
2614 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
2615 font_context,
2616 &transformed,
2617 text_width,
2618 style.font_size,
2619 &style.font_family,
2620 style.font_weight,
2621 style.font_style,
2622 style.letter_spacing,
2623 style.hyphens,
2624 style.lang.as_deref(),
2625 justify,
2626 ),
2627 LineBreaking::Greedy => self.text_layout.break_into_lines(
2628 font_context,
2629 &transformed,
2630 text_width,
2631 style.font_size,
2632 &style.font_family,
2633 style.font_weight,
2634 style.font_style,
2635 style.letter_spacing,
2636 style.hyphens,
2637 style.lang.as_deref(),
2638 ),
2639 };
2640
2641 let lines = match style.text_overflow {
2643 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
2644 font_context,
2645 lines,
2646 text_width,
2647 style.font_size,
2648 &style.font_family,
2649 style.font_weight,
2650 style.font_style,
2651 style.letter_spacing,
2652 ),
2653 TextOverflow::Clip => self.text_layout.truncate_clip(
2654 font_context,
2655 lines,
2656 text_width,
2657 style.font_size,
2658 &style.font_family,
2659 style.font_weight,
2660 style.font_style,
2661 style.letter_spacing,
2662 ),
2663 TextOverflow::Wrap => lines,
2664 };
2665
2666 let line_height = style.font_size * style.line_height;
2667
2668 let line_heights: Vec<f64> = vec![line_height; lines.len()];
2670 let decision = page_break::decide_break(
2671 cursor.remaining_height(),
2672 &line_heights,
2673 true,
2674 style.min_orphan_lines as usize,
2675 style.min_widow_lines as usize,
2676 );
2677
2678 let mut snapshot = cursor.elements.len();
2680 let mut container_start_y = cursor.content_y + cursor.y;
2681 let mut is_first_element = true;
2682
2683 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
2685 pages.push(cursor.finalize());
2686 *cursor = cursor.new_page();
2687 snapshot = cursor.elements.len();
2688 container_start_y = cursor.content_y + cursor.y;
2689 }
2690
2691 let forced_break_at = match decision {
2693 page_break::BreakDecision::Split {
2694 items_on_current_page,
2695 } => Some(items_on_current_page),
2696 _ => None,
2697 };
2698 let mut first_break_done = false;
2699
2700 for (line_idx, line) in lines.iter().enumerate() {
2701 let needs_break = if let Some(break_at) = forced_break_at {
2703 if !first_break_done && line_idx == break_at {
2704 true
2705 } else {
2706 line_height > cursor.remaining_height()
2707 }
2708 } else {
2709 line_height > cursor.remaining_height()
2710 };
2711
2712 if needs_break {
2713 first_break_done = true;
2714 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2716 if !line_elements.is_empty() {
2717 let container_height = cursor.content_y + cursor.y - container_start_y;
2718 cursor.elements.push(LayoutElement {
2719 x: text_x,
2720 y: container_start_y,
2721 width: text_width,
2722 height: container_height,
2723 draw: DrawCommand::None,
2724 children: line_elements,
2725 node_type: Some("Text".to_string()),
2726 resolved_style: Some(style.clone()),
2727 source_location: source_location.cloned(),
2728 href: href.map(|s| s.to_string()),
2729 bookmark: if is_first_element {
2730 bookmark.map(|s| s.to_string())
2731 } else {
2732 None
2733 },
2734 alt: None,
2735 is_header_row: false,
2736 overflow: Overflow::default(),
2737 });
2738 is_first_element = false;
2739 }
2740
2741 pages.push(cursor.finalize());
2742 *cursor = cursor.new_page();
2743
2744 snapshot = cursor.elements.len();
2746 container_start_y = cursor.content_y + cursor.y;
2747 }
2748
2749 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
2750
2751 let rendered_width = if glyphs.is_empty() {
2755 line.width
2756 } else {
2757 let last = &glyphs[glyphs.len() - 1];
2758 (last.x_offset + last.x_advance).max(line.width * 0.5)
2759 };
2760
2761 let line_x = match style.text_align {
2762 TextAlign::Left => text_x,
2763 TextAlign::Right => text_x + text_width - rendered_width,
2764 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
2765 TextAlign::Justify => text_x,
2766 };
2767
2768 let is_last_line = line_idx == lines.len() - 1;
2773 let (justified_width, word_spacing) =
2774 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
2775 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
2776 let (natural_width, space_count) = if let Some(idx) = last_non_space {
2777 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
2778 let s = glyphs[..=idx]
2779 .iter()
2780 .filter(|g| g.char_value == ' ')
2781 .count();
2782 (w, s)
2783 } else {
2784 (0.0, 0)
2785 };
2786 let slack = text_width - natural_width;
2787 let ws = if space_count > 0 && slack.abs() > 0.01 {
2788 slack / space_count as f64
2789 } else {
2790 0.0
2791 };
2792 (text_width, ws)
2793 } else {
2794 (rendered_width, 0.0)
2795 };
2796
2797 let text_line = TextLine {
2798 x: line_x,
2799 y: cursor.content_y + cursor.y + style.font_size,
2800 glyphs,
2801 width: justified_width,
2802 height: line_height,
2803 word_spacing,
2804 };
2805
2806 cursor.elements.push(LayoutElement {
2807 x: line_x,
2808 y: cursor.content_y + cursor.y,
2809 width: justified_width,
2810 height: line_height,
2811 draw: DrawCommand::Text {
2812 lines: vec![text_line],
2813 color: style.color,
2814 text_decoration: style.text_decoration,
2815 opacity: style.opacity,
2816 },
2817 children: vec![],
2818 node_type: Some("TextLine".to_string()),
2819 resolved_style: Some(style.clone()),
2820 source_location: None,
2821 href: href.map(|s| s.to_string()),
2822 bookmark: None,
2823 alt: None,
2824 is_header_row: false,
2825 overflow: Overflow::default(),
2826 });
2827
2828 cursor.y += line_height;
2829 }
2830
2831 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2833 if !line_elements.is_empty() {
2834 let container_height = cursor.content_y + cursor.y - container_start_y;
2835 cursor.elements.push(LayoutElement {
2836 x: text_x,
2837 y: container_start_y,
2838 width: text_width,
2839 height: container_height,
2840 draw: DrawCommand::None,
2841 children: line_elements,
2842 node_type: Some("Text".to_string()),
2843 resolved_style: Some(style.clone()),
2844 source_location: source_location.cloned(),
2845 href: href.map(|s| s.to_string()),
2846 bookmark: if is_first_element {
2847 bookmark.map(|s| s.to_string())
2848 } else {
2849 None
2850 },
2851 alt: None,
2852 is_header_row: false,
2853 overflow: Overflow::default(),
2854 });
2855 }
2856
2857 cursor.y += margin.bottom;
2858 }
2859
2860 #[allow(clippy::too_many_arguments)]
2862 fn layout_text_runs(
2863 &self,
2864 runs: &[TextRun],
2865 parent_href: Option<&str>,
2866 style: &ResolvedStyle,
2867 cursor: &mut PageCursor,
2868 pages: &mut Vec<LayoutPage>,
2869 text_x: f64,
2870 text_width: f64,
2871 font_context: &FontContext,
2872 source_location: Option<&SourceLocation>,
2873 bookmark: Option<&str>,
2874 ) {
2875 let mut styled_chars: Vec<StyledChar> = Vec::new();
2877 for run in runs {
2878 let run_style = run.style.resolve(Some(style), text_width);
2879 let run_href = run.href.as_deref().or(parent_href);
2880 let transform = run_style.text_transform;
2881 let run_content = substitute_page_placeholders(&run.content);
2882 let mut prev_is_whitespace = true;
2883 for ch in run_content.chars() {
2884 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
2885 prev_is_whitespace = ch.is_whitespace();
2886 styled_chars.push(StyledChar {
2887 ch: transformed_ch,
2888 font_family: run_style.font_family.clone(),
2889 font_size: run_style.font_size,
2890 font_weight: run_style.font_weight,
2891 font_style: run_style.font_style,
2892 color: run_style.color,
2893 href: run_href.map(|s| s.to_string()),
2894 text_decoration: run_style.text_decoration,
2895 letter_spacing: run_style.letter_spacing,
2896 });
2897 }
2898 }
2899
2900 let justify = matches!(style.text_align, TextAlign::Justify);
2902 let broken_lines = match style.line_breaking {
2903 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
2904 font_context,
2905 &styled_chars,
2906 text_width,
2907 style.hyphens,
2908 style.lang.as_deref(),
2909 justify,
2910 ),
2911 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
2912 font_context,
2913 &styled_chars,
2914 text_width,
2915 style.hyphens,
2916 style.lang.as_deref(),
2917 ),
2918 };
2919
2920 let broken_lines = match style.text_overflow {
2922 TextOverflow::Ellipsis => {
2923 self.text_layout
2924 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
2925 }
2926 TextOverflow::Clip => {
2927 self.text_layout
2928 .truncate_runs_clip(font_context, broken_lines, text_width)
2929 }
2930 TextOverflow::Wrap => broken_lines,
2931 };
2932
2933 let line_height = style.font_size * style.line_height;
2934
2935 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
2937 let decision = page_break::decide_break(
2938 cursor.remaining_height(),
2939 &line_heights,
2940 true,
2941 style.min_orphan_lines as usize,
2942 style.min_widow_lines as usize,
2943 );
2944
2945 let mut snapshot = cursor.elements.len();
2946 let mut container_start_y = cursor.content_y + cursor.y;
2947 let mut is_first_element = true;
2948
2949 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
2950 pages.push(cursor.finalize());
2951 *cursor = cursor.new_page();
2952 snapshot = cursor.elements.len();
2953 container_start_y = cursor.content_y + cursor.y;
2954 }
2955
2956 let forced_break_at = match decision {
2957 page_break::BreakDecision::Split {
2958 items_on_current_page,
2959 } => Some(items_on_current_page),
2960 _ => None,
2961 };
2962 let mut first_break_done = false;
2963
2964 for (line_idx, run_line) in broken_lines.iter().enumerate() {
2965 let needs_break = if let Some(break_at) = forced_break_at {
2966 if !first_break_done && line_idx == break_at {
2967 true
2968 } else {
2969 line_height > cursor.remaining_height()
2970 }
2971 } else {
2972 line_height > cursor.remaining_height()
2973 };
2974
2975 if needs_break {
2976 first_break_done = true;
2977 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2978 if !line_elements.is_empty() {
2979 let container_height = cursor.content_y + cursor.y - container_start_y;
2980 cursor.elements.push(LayoutElement {
2981 x: text_x,
2982 y: container_start_y,
2983 width: text_width,
2984 height: container_height,
2985 draw: DrawCommand::None,
2986 children: line_elements,
2987 node_type: Some("Text".to_string()),
2988 resolved_style: Some(style.clone()),
2989 source_location: source_location.cloned(),
2990 href: parent_href.map(|s| s.to_string()),
2991 bookmark: if is_first_element {
2992 bookmark.map(|s| s.to_string())
2993 } else {
2994 None
2995 },
2996 alt: None,
2997 is_header_row: false,
2998 overflow: Overflow::default(),
2999 });
3000 is_first_element = false;
3001 }
3002
3003 pages.push(cursor.finalize());
3004 *cursor = cursor.new_page();
3005
3006 snapshot = cursor.elements.len();
3007 container_start_y = cursor.content_y + cursor.y;
3008 }
3009
3010 let line_x = match style.text_align {
3011 TextAlign::Left => text_x,
3012 TextAlign::Right => text_x + text_width - run_line.width,
3013 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
3014 TextAlign::Justify => text_x,
3015 };
3016
3017 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
3018
3019 let is_last_line = line_idx == broken_lines.len() - 1;
3023 let (justified_width, word_spacing) =
3024 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
3025 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
3026 let (natural_width, space_count) = if let Some(idx) = last_non_space {
3027 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
3028 let s = glyphs[..=idx]
3029 .iter()
3030 .filter(|g| g.char_value == ' ')
3031 .count();
3032 (w, s)
3033 } else {
3034 (0.0, 0)
3035 };
3036 let slack = text_width - natural_width;
3037 let ws = if space_count > 0 && slack.abs() > 0.01 {
3038 slack / space_count as f64
3039 } else {
3040 0.0
3041 };
3042 (text_width, ws)
3043 } else {
3044 (run_line.width, 0.0)
3045 };
3046
3047 let text_line = TextLine {
3048 x: line_x,
3049 y: cursor.content_y + cursor.y + style.font_size,
3050 glyphs,
3051 width: justified_width,
3052 height: line_height,
3053 word_spacing,
3054 };
3055
3056 let text_dec = run_line
3058 .chars
3059 .iter()
3060 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
3061 .map(|sc| sc.text_decoration)
3062 .unwrap_or(style.text_decoration);
3063
3064 cursor.elements.push(LayoutElement {
3065 x: line_x,
3066 y: cursor.content_y + cursor.y,
3067 width: justified_width,
3068 height: line_height,
3069 draw: DrawCommand::Text {
3070 lines: vec![text_line],
3071 color: style.color,
3072 text_decoration: text_dec,
3073 opacity: style.opacity,
3074 },
3075 children: vec![],
3076 node_type: Some("TextLine".to_string()),
3077 resolved_style: Some(style.clone()),
3078 source_location: None,
3079 href: parent_href.map(|s| s.to_string()),
3080 bookmark: None,
3081 alt: None,
3082 is_header_row: false,
3083 overflow: Overflow::default(),
3084 });
3085
3086 cursor.y += line_height;
3087 }
3088
3089 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3090 if !line_elements.is_empty() {
3091 let container_height = cursor.content_y + cursor.y - container_start_y;
3092 cursor.elements.push(LayoutElement {
3093 x: text_x,
3094 y: container_start_y,
3095 width: text_width,
3096 height: container_height,
3097 draw: DrawCommand::None,
3098 children: line_elements,
3099 node_type: Some("Text".to_string()),
3100 resolved_style: Some(style.clone()),
3101 source_location: source_location.cloned(),
3102 href: parent_href.map(|s| s.to_string()),
3103 bookmark: if is_first_element {
3104 bookmark.map(|s| s.to_string())
3105 } else {
3106 None
3107 },
3108 alt: None,
3109 is_header_row: false,
3110 overflow: Overflow::default(),
3111 });
3112 }
3113 }
3114
3115 fn build_positioned_glyphs_single_style(
3119 &self,
3120 line: &BrokenLine,
3121 style: &ResolvedStyle,
3122 href: Option<&str>,
3123 font_context: &FontContext,
3124 ) -> Vec<PositionedGlyph> {
3125 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
3126 let line_text: String = line.chars.iter().collect();
3127 let direction = style.direction;
3128 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
3130
3131 let font_runs = crate::font::fallback::segment_by_font(
3134 &line.chars,
3135 &style.font_family,
3136 style.font_weight,
3137 italic,
3138 font_context.registry(),
3139 );
3140 let needs_per_char_fallback = font_runs.len() > 1
3141 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
3142
3143 if needs_per_char_fallback {
3145 let bidi_runs = if has_bidi {
3146 bidi::analyze_bidi(&line_text, direction)
3147 } else {
3148 vec![crate::text::bidi::BidiRun {
3149 char_start: 0,
3150 char_end: line.chars.len(),
3151 level: unicode_bidi::Level::ltr(),
3152 is_rtl: false,
3153 }]
3154 };
3155
3156 let mut all_glyphs = Vec::new();
3157 let mut bidi_levels = Vec::new();
3158 let mut x = 0.0_f64;
3159
3160 for bidi_run in &bidi_runs {
3162 for font_run in &font_runs {
3164 let start = font_run.start.max(bidi_run.char_start);
3166 let end = font_run.end.min(bidi_run.char_end);
3167 if start >= end {
3168 continue;
3169 }
3170
3171 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
3172 let sub_text: String = sub_chars.iter().collect();
3173 let resolved_family = &font_run.family;
3174
3175 if let Some(font_data) =
3176 font_context.font_data(resolved_family, style.font_weight, italic)
3177 {
3178 if let Some(shaped) = shaping::shape_text_with_direction(
3179 &sub_text,
3180 font_data,
3181 bidi_run.is_rtl,
3182 ) {
3183 let units_per_em = font_context.units_per_em(
3184 resolved_family,
3185 style.font_weight,
3186 italic,
3187 );
3188 let scale = style.font_size / units_per_em as f64;
3189
3190 for sg in &shaped {
3191 let cluster = sg.cluster as usize;
3192 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
3193
3194 let cluster_text = if shaped.len() < sub_chars.len() {
3195 let cluster_end =
3196 self.find_cluster_end(&shaped, sg, sub_chars.len());
3197 if cluster_end > cluster + 1 {
3198 Some(
3199 sub_chars[cluster..cluster_end]
3200 .iter()
3201 .collect::<String>(),
3202 )
3203 } else {
3204 None
3205 }
3206 } else {
3207 None
3208 };
3209
3210 let glyph_x = x + sg.x_offset as f64 * scale;
3211 let glyph_y = sg.y_offset as f64 * scale;
3212 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
3213
3214 all_glyphs.push(PositionedGlyph {
3215 glyph_id: sg.glyph_id,
3216 x_offset: glyph_x,
3217 y_offset: glyph_y,
3218 x_advance: advance,
3219 font_size: style.font_size,
3220 font_family: resolved_family.clone(),
3221 font_weight: style.font_weight,
3222 font_style: style.font_style,
3223 char_value,
3224 color: Some(style.color),
3225 href: href.map(|s| s.to_string()),
3226 text_decoration: style.text_decoration,
3227 letter_spacing: style.letter_spacing,
3228 cluster_text,
3229 });
3230 bidi_levels.push(bidi_run.level);
3231 x += advance;
3232 }
3233 continue;
3234 }
3235 }
3236
3237 for i in start..end {
3239 let ch = line.chars[i];
3240 let glyph_x = x;
3241 let char_width = font_context.char_width(
3242 ch,
3243 resolved_family,
3244 style.font_weight,
3245 italic,
3246 style.font_size,
3247 );
3248 let advance = char_width + style.letter_spacing;
3249 all_glyphs.push(PositionedGlyph {
3250 glyph_id: ch as u16,
3251 x_offset: glyph_x,
3252 y_offset: 0.0,
3253 x_advance: advance,
3254 font_size: style.font_size,
3255 font_family: resolved_family.clone(),
3256 font_weight: style.font_weight,
3257 font_style: style.font_style,
3258 char_value: ch,
3259 color: Some(style.color),
3260 href: href.map(|s| s.to_string()),
3261 text_decoration: style.text_decoration,
3262 letter_spacing: style.letter_spacing,
3263 cluster_text: None,
3264 });
3265 bidi_levels.push(bidi_run.level);
3266 x += advance;
3267 }
3268 }
3269 }
3270
3271 if has_bidi && !all_glyphs.is_empty() {
3273 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
3274 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
3275 }
3276 return all_glyphs;
3277 }
3278
3279 if let Some(font_data) =
3282 font_context.font_data(&style.font_family, style.font_weight, italic)
3283 {
3284 if has_bidi {
3285 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
3287 let units_per_em =
3288 font_context.units_per_em(&style.font_family, style.font_weight, italic);
3289 let scale = style.font_size / units_per_em as f64;
3290
3291 let mut all_glyphs = Vec::new();
3292 let mut bidi_levels = Vec::new();
3293 let mut x = 0.0_f64;
3294
3295 for run in &bidi_runs {
3296 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
3297 let run_text: String = run_chars.iter().collect();
3298
3299 if let Some(shaped) =
3300 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
3301 {
3302 for sg in &shaped {
3303 let cluster = sg.cluster as usize;
3304 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
3305
3306 let cluster_text = if shaped.len() < run_chars.len() {
3307 let cluster_end =
3308 self.find_cluster_end(&shaped, sg, run_chars.len());
3309 if cluster_end > cluster + 1 {
3310 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
3311 } else {
3312 None
3313 }
3314 } else {
3315 None
3316 };
3317
3318 let glyph_x = x + sg.x_offset as f64 * scale;
3319 let glyph_y = sg.y_offset as f64 * scale;
3320 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
3321
3322 all_glyphs.push(PositionedGlyph {
3323 glyph_id: sg.glyph_id,
3324 x_offset: glyph_x,
3325 y_offset: glyph_y,
3326 x_advance: advance,
3327 font_size: style.font_size,
3328 font_family: style.font_family.clone(),
3329 font_weight: style.font_weight,
3330 font_style: style.font_style,
3331 char_value,
3332 color: Some(style.color),
3333 href: href.map(|s| s.to_string()),
3334 text_decoration: style.text_decoration,
3335 letter_spacing: style.letter_spacing,
3336 cluster_text,
3337 });
3338 bidi_levels.push(run.level);
3339
3340 x += advance;
3341 }
3342 }
3343 }
3344
3345 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
3347 bidi::reposition_after_reorder(&mut glyphs, 0.0);
3348 return glyphs;
3349 }
3350
3351 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
3353 let units_per_em =
3354 font_context.units_per_em(&style.font_family, style.font_weight, italic);
3355 let scale = style.font_size / units_per_em as f64;
3356
3357 return self.shaped_glyphs_to_positioned(
3358 &shaped,
3359 &line.chars,
3360 &line.char_positions,
3361 scale,
3362 style.font_size,
3363 &style.font_family,
3364 style.font_weight,
3365 style.font_style,
3366 Some(style.color),
3367 href,
3368 style.text_decoration,
3369 style.letter_spacing,
3370 );
3371 }
3372 }
3373
3374 let mut glyphs: Vec<PositionedGlyph> = line
3376 .chars
3377 .iter()
3378 .enumerate()
3379 .map(|(j, ch)| {
3380 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
3381 let char_width = font_context.char_width(
3382 *ch,
3383 &style.font_family,
3384 style.font_weight,
3385 italic,
3386 style.font_size,
3387 );
3388 PositionedGlyph {
3389 glyph_id: *ch as u16,
3390 x_offset: glyph_x,
3391 y_offset: 0.0,
3392 x_advance: char_width,
3393 font_size: style.font_size,
3394 font_family: style.font_family.clone(),
3395 font_weight: style.font_weight,
3396 font_style: style.font_style,
3397 char_value: *ch,
3398 color: Some(style.color),
3399 href: href.map(|s| s.to_string()),
3400 text_decoration: style.text_decoration,
3401 letter_spacing: style.letter_spacing,
3402 cluster_text: None,
3403 }
3404 })
3405 .collect();
3406
3407 if has_bidi && !glyphs.is_empty() {
3409 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
3410 let mut levels = Vec::with_capacity(glyphs.len());
3411 let mut char_idx = 0;
3412 for run in &bidi_runs {
3413 for _ in run.char_start..run.char_end {
3414 if char_idx < glyphs.len() {
3415 levels.push(run.level);
3416 char_idx += 1;
3417 }
3418 }
3419 }
3420 while levels.len() < glyphs.len() {
3422 levels.push(unicode_bidi::Level::ltr());
3423 }
3424 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
3425 bidi::reposition_after_reorder(&mut glyphs, 0.0);
3426 }
3427
3428 glyphs
3429 }
3430
3431 fn build_positioned_glyphs_runs(
3436 &self,
3437 run_line: &RunBrokenLine,
3438 font_context: &FontContext,
3439 direction: Direction,
3440 ) -> Vec<PositionedGlyph> {
3441 let chars = &run_line.chars;
3442 if chars.is_empty() {
3443 return vec![];
3444 }
3445
3446 let resolved_families: Vec<String> = chars
3449 .iter()
3450 .map(|sc| {
3451 if !sc.font_family.contains(',') {
3452 sc.font_family.clone()
3453 } else {
3454 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
3455 let (_, family) = font_context.registry().resolve_for_char(
3456 &sc.font_family,
3457 sc.ch,
3458 sc.font_weight,
3459 italic,
3460 );
3461 family
3462 }
3463 })
3464 .collect();
3465
3466 let line_text: String = chars.iter().map(|c| c.ch).collect();
3467 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
3468 let bidi_runs = if has_bidi {
3469 Some(bidi::analyze_bidi(&line_text, direction))
3470 } else {
3471 None
3472 };
3473
3474 let mut glyphs = Vec::new();
3475 let mut bidi_levels = Vec::new();
3476 let mut i = 0;
3477
3478 while i < chars.len() {
3479 let sc = &chars[i];
3480 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
3481 let resolved_family = &resolved_families[i];
3482
3483 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
3485 runs.iter()
3486 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
3487 });
3488
3489 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
3491 {
3492 let run_start = i;
3494 let mut run_end = i + 1;
3495 while run_end < chars.len() {
3496 let next = &chars[run_end];
3497 let next_italic =
3498 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
3499 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
3500 runs.iter()
3501 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
3502 });
3503 if resolved_families[run_end] == *resolved_family
3505 && next.font_weight == sc.font_weight
3506 && next_italic == italic
3507 && (next.font_size - sc.font_size).abs() < 0.001
3508 && next_is_rtl == is_rtl
3509 {
3510 run_end += 1;
3511 } else {
3512 break;
3513 }
3514 }
3515
3516 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
3517 if let Some(shaped) =
3518 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
3519 {
3520 let units_per_em =
3521 font_context.units_per_em(resolved_family, sc.font_weight, italic);
3522 let scale = sc.font_size / units_per_em as f64;
3523
3524 let run_chars: Vec<char> =
3526 chars[run_start..run_end].iter().map(|c| c.ch).collect();
3527 let run_positions: Vec<f64> = (run_start..run_end)
3528 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
3529 .collect();
3530
3531 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
3533 &shaped,
3534 &chars[run_start..run_end],
3535 &run_chars,
3536 &run_positions,
3537 scale,
3538 );
3539 for g in &mut run_glyphs {
3541 g.font_family = resolved_family.clone();
3542 }
3543 let run_level = if is_rtl {
3545 unicode_bidi::Level::rtl()
3546 } else {
3547 unicode_bidi::Level::ltr()
3548 };
3549 for _ in &run_glyphs {
3550 bidi_levels.push(run_level);
3551 }
3552 glyphs.extend(run_glyphs);
3553 i = run_end;
3554 continue;
3555 }
3556 }
3557
3558 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
3560 let char_width = font_context.char_width(
3561 sc.ch,
3562 resolved_family,
3563 sc.font_weight,
3564 italic,
3565 sc.font_size,
3566 );
3567 glyphs.push(PositionedGlyph {
3568 glyph_id: sc.ch as u16,
3569 x_offset: glyph_x,
3570 y_offset: 0.0,
3571 x_advance: char_width,
3572 font_size: sc.font_size,
3573 font_family: resolved_family.clone(),
3574 font_weight: sc.font_weight,
3575 font_style: sc.font_style,
3576 char_value: sc.ch,
3577 color: Some(sc.color),
3578 href: sc.href.clone(),
3579 text_decoration: sc.text_decoration,
3580 letter_spacing: sc.letter_spacing,
3581 cluster_text: None,
3582 });
3583 bidi_levels.push(if is_rtl {
3584 unicode_bidi::Level::rtl()
3585 } else {
3586 unicode_bidi::Level::ltr()
3587 });
3588 i += 1;
3589 }
3590
3591 if has_bidi && !glyphs.is_empty() {
3593 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
3594 bidi::reposition_after_reorder(&mut glyphs, 0.0);
3595 }
3596
3597 glyphs
3598 }
3599
3600 #[allow(clippy::too_many_arguments)]
3602 fn shaped_glyphs_to_positioned(
3603 &self,
3604 shaped: &[shaping::ShapedGlyph],
3605 chars: &[char],
3606 _char_positions: &[f64],
3607 scale: f64,
3608 font_size: f64,
3609 font_family: &str,
3610 font_weight: u32,
3611 font_style: FontStyle,
3612 color: Option<Color>,
3613 href: Option<&str>,
3614 text_decoration: TextDecoration,
3615 letter_spacing: f64,
3616 ) -> Vec<PositionedGlyph> {
3617 let mut result = Vec::with_capacity(shaped.len());
3618 let mut x = 0.0_f64;
3619
3620 for sg in shaped {
3621 let cluster = sg.cluster as usize;
3622 let char_value = chars.get(cluster).copied().unwrap_or(' ');
3623
3624 let cluster_text = if shaped.len() < chars.len() {
3626 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
3629 if cluster_end > cluster + 1 {
3630 Some(chars[cluster..cluster_end].iter().collect::<String>())
3631 } else {
3632 None
3633 }
3634 } else {
3635 None
3636 };
3637
3638 let glyph_x = x + sg.x_offset as f64 * scale;
3640 let glyph_y = sg.y_offset as f64 * scale;
3641 let advance = sg.x_advance as f64 * scale + letter_spacing;
3642
3643 result.push(PositionedGlyph {
3644 glyph_id: sg.glyph_id,
3645 x_offset: glyph_x,
3646 y_offset: glyph_y,
3647 x_advance: advance,
3648 font_size,
3649 font_family: font_family.to_string(),
3650 font_weight,
3651 font_style,
3652 char_value,
3653 color,
3654 href: href.map(|s| s.to_string()),
3655 text_decoration,
3656 letter_spacing,
3657 cluster_text,
3658 });
3659
3660 x += advance;
3661 }
3662
3663 result
3664 }
3665
3666 fn shaped_glyphs_to_positioned_runs(
3668 &self,
3669 shaped: &[shaping::ShapedGlyph],
3670 styled_chars: &[StyledChar],
3671 chars: &[char],
3672 char_positions: &[f64],
3673 scale: f64,
3674 ) -> Vec<PositionedGlyph> {
3675 let mut result = Vec::with_capacity(shaped.len());
3676 let base_x = char_positions.first().copied().unwrap_or(0.0);
3678 let mut x = 0.0_f64;
3679
3680 for sg in shaped {
3681 let cluster = sg.cluster as usize;
3682 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
3683 let char_value = chars.get(cluster).copied().unwrap_or(' ');
3684
3685 let cluster_text = if shaped.len() < chars.len() {
3686 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
3687 if cluster_end > cluster + 1 {
3688 Some(chars[cluster..cluster_end].iter().collect::<String>())
3689 } else {
3690 None
3691 }
3692 } else {
3693 None
3694 };
3695
3696 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
3697 let glyph_y = sg.y_offset as f64 * scale;
3698 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
3699
3700 result.push(PositionedGlyph {
3701 glyph_id: sg.glyph_id,
3702 x_offset: glyph_x,
3703 y_offset: glyph_y,
3704 x_advance: advance,
3705 font_size: sc.font_size,
3706 font_family: sc.font_family.clone(),
3707 font_weight: sc.font_weight,
3708 font_style: sc.font_style,
3709 char_value,
3710 color: Some(sc.color),
3711 href: sc.href.clone(),
3712 text_decoration: sc.text_decoration,
3713 letter_spacing: sc.letter_spacing,
3714 cluster_text,
3715 });
3716
3717 x += advance;
3718 }
3719
3720 result
3721 }
3722
3723 fn find_cluster_end(
3725 &self,
3726 shaped: &[shaping::ShapedGlyph],
3727 current: &shaping::ShapedGlyph,
3728 num_chars: usize,
3729 ) -> usize {
3730 for sg in shaped {
3732 if sg.cluster > current.cluster {
3733 return sg.cluster as usize;
3734 }
3735 }
3736 num_chars
3738 }
3739
3740 #[allow(clippy::too_many_arguments)]
3741 fn layout_image(
3742 &self,
3743 node: &Node,
3744 style: &ResolvedStyle,
3745 cursor: &mut PageCursor,
3746 pages: &mut Vec<LayoutPage>,
3747 x: f64,
3748 available_width: f64,
3749 explicit_width: Option<f64>,
3750 explicit_height: Option<f64>,
3751 ) {
3752 let margin = &style.margin.to_edges();
3753
3754 let src = match &node.kind {
3756 NodeKind::Image { src, .. } => src.as_str(),
3757 _ => "",
3758 };
3759
3760 let loaded = if !src.is_empty() {
3761 crate::image_loader::load_image(src).ok()
3762 } else {
3763 None
3764 };
3765
3766 let (img_width, img_height) = if let Some(ref img) = loaded {
3768 let intrinsic_w = img.width_px as f64;
3769 let intrinsic_h = img.height_px as f64;
3770 let aspect = if intrinsic_w > 0.0 {
3771 intrinsic_h / intrinsic_w
3772 } else {
3773 0.75
3774 };
3775
3776 match (explicit_width, explicit_height) {
3777 (Some(w), Some(h)) => (w, h),
3778 (Some(w), None) => (w, w * aspect),
3779 (None, Some(h)) => (h / aspect, h),
3780 (None, None) => {
3781 let max_w = available_width - margin.horizontal();
3782 let w = intrinsic_w.min(max_w);
3783 (w, w * aspect)
3784 }
3785 }
3786 } else {
3787 let w = explicit_width.unwrap_or(available_width - margin.horizontal());
3789 let h = explicit_height.unwrap_or(w * 0.75);
3790 (w, h)
3791 };
3792
3793 let total_height = img_height + margin.vertical();
3794
3795 if total_height > cursor.remaining_height() {
3796 pages.push(cursor.finalize());
3797 *cursor = cursor.new_page();
3798 }
3799
3800 cursor.y += margin.top;
3801
3802 let draw = if let Some(image_data) = loaded {
3803 DrawCommand::Image { image_data }
3804 } else {
3805 DrawCommand::ImagePlaceholder
3806 };
3807
3808 cursor.elements.push(LayoutElement {
3809 x: x + margin.left,
3810 y: cursor.content_y + cursor.y,
3811 width: img_width,
3812 height: img_height,
3813 draw,
3814 children: vec![],
3815 node_type: Some(node_kind_name(&node.kind).to_string()),
3816 resolved_style: Some(style.clone()),
3817 source_location: node.source_location.clone(),
3818 href: node.href.clone(),
3819 bookmark: node.bookmark.clone(),
3820 alt: node.alt.clone(),
3821 is_header_row: false,
3822 overflow: style.overflow,
3823 });
3824
3825 cursor.y += img_height + margin.bottom;
3826 }
3827
3828 #[allow(clippy::too_many_arguments)]
3830 fn layout_svg(
3831 &self,
3832 node: &Node,
3833 style: &ResolvedStyle,
3834 cursor: &mut PageCursor,
3835 pages: &mut Vec<LayoutPage>,
3836 x: f64,
3837 _available_width: f64,
3838 svg_width: f64,
3839 svg_height: f64,
3840 view_box: Option<&str>,
3841 content: &str,
3842 ) {
3843 let margin = &style.margin.to_edges();
3844 let total_height = svg_height + margin.vertical();
3845
3846 if total_height > cursor.remaining_height() {
3847 pages.push(cursor.finalize());
3848 *cursor = cursor.new_page();
3849 }
3850
3851 cursor.y += margin.top;
3852
3853 let vb = view_box
3854 .and_then(crate::svg::parse_view_box)
3855 .unwrap_or(crate::svg::ViewBox {
3856 min_x: 0.0,
3857 min_y: 0.0,
3858 width: svg_width,
3859 height: svg_height,
3860 });
3861
3862 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
3863
3864 cursor.elements.push(LayoutElement {
3865 x: x + margin.left,
3866 y: cursor.content_y + cursor.y,
3867 width: svg_width,
3868 height: svg_height,
3869 draw: DrawCommand::Svg {
3870 commands,
3871 width: svg_width,
3872 height: svg_height,
3873 clip: false,
3874 },
3875 children: vec![],
3876 node_type: Some("Svg".to_string()),
3877 resolved_style: Some(style.clone()),
3878 source_location: node.source_location.clone(),
3879 href: node.href.clone(),
3880 bookmark: node.bookmark.clone(),
3881 alt: node.alt.clone(),
3882 is_header_row: false,
3883 overflow: style.overflow,
3884 });
3885
3886 cursor.y += svg_height + margin.bottom;
3887 }
3888
3889 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
3891 use crate::svg::SvgCommand;
3892
3893 let mut commands = Vec::new();
3894 let mut cur_x = 0.0_f64;
3895 let mut cur_y = 0.0_f64;
3896
3897 for op in operations {
3898 match op {
3899 CanvasOp::MoveTo { x, y } => {
3900 commands.push(SvgCommand::MoveTo(*x, *y));
3901 cur_x = *x;
3902 cur_y = *y;
3903 }
3904 CanvasOp::LineTo { x, y } => {
3905 commands.push(SvgCommand::LineTo(*x, *y));
3906 cur_x = *x;
3907 cur_y = *y;
3908 }
3909 CanvasOp::BezierCurveTo {
3910 cp1x,
3911 cp1y,
3912 cp2x,
3913 cp2y,
3914 x,
3915 y,
3916 } => {
3917 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
3918 cur_x = *x;
3919 cur_y = *y;
3920 }
3921 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
3922 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
3924 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
3925 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
3926 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
3927 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
3928 cur_x = *x;
3929 cur_y = *y;
3930 }
3931 CanvasOp::ClosePath => {
3932 commands.push(SvgCommand::ClosePath);
3933 }
3934 CanvasOp::Rect {
3935 x,
3936 y,
3937 width,
3938 height,
3939 } => {
3940 commands.push(SvgCommand::MoveTo(*x, *y));
3941 commands.push(SvgCommand::LineTo(*x + *width, *y));
3942 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
3943 commands.push(SvgCommand::LineTo(*x, *y + *height));
3944 commands.push(SvgCommand::ClosePath);
3945 cur_x = *x;
3946 cur_y = *y;
3947 }
3948 CanvasOp::Circle { cx, cy, r } => {
3949 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
3950 }
3951 CanvasOp::Ellipse { cx, cy, rx, ry } => {
3952 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
3953 }
3954 CanvasOp::Arc {
3955 cx,
3956 cy,
3957 r,
3958 start_angle,
3959 end_angle,
3960 counterclockwise,
3961 } => {
3962 let steps = 32;
3966 let mut sweep = end_angle - start_angle;
3967 if !counterclockwise && sweep < 0.0 {
3968 sweep += 2.0 * std::f64::consts::PI;
3969 }
3970 if *counterclockwise && sweep > 0.0 {
3971 sweep -= 2.0 * std::f64::consts::PI;
3972 }
3973 for i in 0..=steps {
3974 let t = *start_angle + sweep * (i as f64 / steps as f64);
3975 let px = cx + r * t.cos();
3976 let py = cy + r * t.sin();
3977 if i == 0 {
3978 commands.push(SvgCommand::MoveTo(px, py));
3979 } else {
3980 commands.push(SvgCommand::LineTo(px, py));
3981 }
3982 }
3983 }
3984 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
3985 CanvasOp::Fill => commands.push(SvgCommand::Fill),
3986 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
3987 CanvasOp::SetFillColor { r, g, b } => {
3988 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
3990 }
3991 CanvasOp::SetStrokeColor { r, g, b } => {
3992 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
3993 }
3994 CanvasOp::SetLineWidth { width } => {
3995 commands.push(SvgCommand::SetStrokeWidth(*width));
3996 }
3997 CanvasOp::SetLineCap { cap } => {
3998 commands.push(SvgCommand::SetLineCap(*cap));
3999 }
4000 CanvasOp::SetLineJoin { join } => {
4001 commands.push(SvgCommand::SetLineJoin(*join));
4002 }
4003 CanvasOp::Save => commands.push(SvgCommand::SaveState),
4004 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
4005 }
4006 }
4007
4008 commands
4009 }
4010
4011 #[allow(clippy::too_many_arguments)]
4013 fn layout_canvas(
4014 &self,
4015 node: &Node,
4016 style: &ResolvedStyle,
4017 cursor: &mut PageCursor,
4018 pages: &mut Vec<LayoutPage>,
4019 x: f64,
4020 _available_width: f64,
4021 canvas_width: f64,
4022 canvas_height: f64,
4023 operations: &[CanvasOp],
4024 ) {
4025 let margin = style.margin.to_edges();
4026 let total_height = canvas_height + margin.top + margin.bottom;
4027
4028 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
4030 pages.push(cursor.finalize());
4031 *cursor = cursor.new_page();
4032 }
4033
4034 cursor.y += margin.top;
4035
4036 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
4037
4038 cursor.elements.push(LayoutElement {
4039 x: x + margin.left,
4040 y: cursor.content_y + cursor.y,
4041 width: canvas_width,
4042 height: canvas_height,
4043 draw: DrawCommand::Svg {
4044 commands: svg_commands,
4045 width: canvas_width,
4046 height: canvas_height,
4047 clip: true,
4048 },
4049 children: vec![],
4050 node_type: Some("Canvas".to_string()),
4051 resolved_style: Some(style.clone()),
4052 source_location: node.source_location.clone(),
4053 href: node.href.clone(),
4054 bookmark: node.bookmark.clone(),
4055 alt: node.alt.clone(),
4056 is_header_row: false,
4057 overflow: style.overflow,
4058 });
4059
4060 cursor.y += canvas_height + margin.bottom;
4061 }
4062
4063 #[allow(clippy::too_many_arguments)]
4065 #[allow(clippy::too_many_arguments)]
4067 fn layout_chart(
4068 &self,
4069 node: &Node,
4070 style: &ResolvedStyle,
4071 cursor: &mut PageCursor,
4072 pages: &mut Vec<LayoutPage>,
4073 x: f64,
4074 chart_width: f64,
4075 chart_height: f64,
4076 primitives: Vec<crate::chart::ChartPrimitive>,
4077 node_type_name: &str,
4078 ) {
4079 let margin = &style.margin.to_edges();
4080 let total_height = chart_height + margin.vertical();
4081
4082 if total_height > cursor.remaining_height() {
4083 pages.push(cursor.finalize());
4084 *cursor = cursor.new_page();
4085 }
4086
4087 cursor.y += margin.top;
4088
4089 let draw = DrawCommand::Chart { primitives };
4090
4091 cursor.elements.push(LayoutElement {
4092 x: x + margin.left,
4093 y: cursor.content_y + cursor.y,
4094 width: chart_width,
4095 height: chart_height,
4096 draw,
4097 children: vec![],
4098 node_type: Some(node_type_name.to_string()),
4099 resolved_style: Some(style.clone()),
4100 source_location: node.source_location.clone(),
4101 href: node.href.clone(),
4102 bookmark: node.bookmark.clone(),
4103 alt: node.alt.clone(),
4104 is_header_row: false,
4105 overflow: style.overflow,
4106 });
4107
4108 cursor.y += chart_height + margin.bottom;
4109 }
4110
4111 #[allow(clippy::too_many_arguments)]
4113 fn layout_form_field(
4114 &self,
4115 node: &Node,
4116 style: &ResolvedStyle,
4117 cursor: &mut PageCursor,
4118 pages: &mut Vec<LayoutPage>,
4119 x: f64,
4120 field_width: f64,
4121 field_height: f64,
4122 draw: DrawCommand,
4123 node_type_name: &str,
4124 ) {
4125 let margin = &style.margin.to_edges();
4126 let total_height = field_height + margin.vertical();
4127
4128 if total_height > cursor.remaining_height() {
4129 pages.push(cursor.finalize());
4130 *cursor = cursor.new_page();
4131 }
4132
4133 cursor.y += margin.top;
4134
4135 cursor.elements.push(LayoutElement {
4136 x: x + margin.left,
4137 y: cursor.content_y + cursor.y,
4138 width: field_width,
4139 height: field_height,
4140 draw,
4141 children: vec![],
4142 node_type: Some(node_type_name.to_string()),
4143 resolved_style: Some(style.clone()),
4144 source_location: node.source_location.clone(),
4145 href: node.href.clone(),
4146 bookmark: node.bookmark.clone(),
4147 alt: node.alt.clone(),
4148 is_header_row: false,
4149 overflow: style.overflow,
4150 });
4151
4152 cursor.y += field_height + margin.bottom;
4153 }
4154
4155 #[allow(clippy::too_many_arguments)]
4156 fn layout_barcode(
4157 &self,
4158 node: &Node,
4159 style: &ResolvedStyle,
4160 cursor: &mut PageCursor,
4161 pages: &mut Vec<LayoutPage>,
4162 x: f64,
4163 available_width: f64,
4164 data: &str,
4165 format: crate::barcode::BarcodeFormat,
4166 explicit_width: Option<f64>,
4167 bar_height: f64,
4168 ) {
4169 let margin = &style.margin.to_edges();
4170 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
4171 let total_height = bar_height + margin.vertical();
4172
4173 if total_height > cursor.remaining_height() {
4174 pages.push(cursor.finalize());
4175 *cursor = cursor.new_page();
4176 }
4177
4178 cursor.y += margin.top;
4179
4180 let draw = match crate::barcode::generate_barcode(data, format) {
4181 Ok(barcode_data) => {
4182 let bar_width = if barcode_data.bars.is_empty() {
4183 0.0
4184 } else {
4185 display_width / barcode_data.bars.len() as f64
4186 };
4187 DrawCommand::Barcode {
4188 bars: barcode_data.bars,
4189 bar_width,
4190 height: bar_height,
4191 color: style.color,
4192 }
4193 }
4194 Err(_) => DrawCommand::None,
4195 };
4196
4197 cursor.elements.push(LayoutElement {
4198 x: x + margin.left,
4199 y: cursor.content_y + cursor.y,
4200 width: display_width,
4201 height: bar_height,
4202 draw,
4203 children: vec![],
4204 node_type: Some("Barcode".to_string()),
4205 resolved_style: Some(style.clone()),
4206 source_location: node.source_location.clone(),
4207 href: node.href.clone(),
4208 bookmark: node.bookmark.clone(),
4209 alt: node.alt.clone(),
4210 is_header_row: false,
4211 overflow: style.overflow,
4212 });
4213
4214 cursor.y += bar_height + margin.bottom;
4215 }
4216
4217 #[allow(clippy::too_many_arguments)]
4219 fn layout_qrcode(
4220 &self,
4221 node: &Node,
4222 style: &ResolvedStyle,
4223 cursor: &mut PageCursor,
4224 pages: &mut Vec<LayoutPage>,
4225 x: f64,
4226 available_width: f64,
4227 data: &str,
4228 explicit_size: Option<f64>,
4229 ) {
4230 let margin = &style.margin.to_edges();
4231 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
4232 let total_height = display_size + margin.vertical();
4233
4234 if total_height > cursor.remaining_height() {
4235 pages.push(cursor.finalize());
4236 *cursor = cursor.new_page();
4237 }
4238
4239 cursor.y += margin.top;
4240
4241 let draw = match crate::qrcode::generate_qr(data) {
4242 Ok(matrix) => {
4243 let module_size = display_size / matrix.size as f64;
4244 DrawCommand::QrCode {
4245 modules: matrix.modules,
4246 module_size,
4247 color: style.color,
4248 }
4249 }
4250 Err(_) => DrawCommand::None,
4251 };
4252
4253 cursor.elements.push(LayoutElement {
4254 x: x + margin.left,
4255 y: cursor.content_y + cursor.y,
4256 width: display_size,
4257 height: display_size,
4258 draw,
4259 children: vec![],
4260 node_type: Some("QrCode".to_string()),
4261 resolved_style: Some(style.clone()),
4262 source_location: node.source_location.clone(),
4263 href: node.href.clone(),
4264 bookmark: node.bookmark.clone(),
4265 alt: node.alt.clone(),
4266 is_header_row: false,
4267 overflow: style.overflow,
4268 });
4269
4270 cursor.y += display_size + margin.bottom;
4271 }
4272
4273 fn measure_node_height(
4276 &self,
4277 node: &Node,
4278 available_width: f64,
4279 style: &ResolvedStyle,
4280 font_context: &FontContext,
4281 ) -> f64 {
4282 match &node.kind {
4283 NodeKind::Text { content, runs, .. } => {
4284 let measure_width = available_width - style.margin.horizontal();
4285 if !runs.is_empty() {
4286 let mut styled_chars: Vec<StyledChar> = Vec::new();
4288 for run in runs {
4289 let run_style = run.style.resolve(Some(style), measure_width);
4290 let run_content = substitute_page_placeholders(&run.content);
4291 for ch in run_content.chars() {
4292 styled_chars.push(StyledChar {
4293 ch,
4294 font_family: run_style.font_family.clone(),
4295 font_size: run_style.font_size,
4296 font_weight: run_style.font_weight,
4297 font_style: run_style.font_style,
4298 color: run_style.color,
4299 href: None,
4300 text_decoration: run_style.text_decoration,
4301 letter_spacing: run_style.letter_spacing,
4302 });
4303 }
4304 }
4305 let broken_lines = self.text_layout.break_runs_into_lines(
4306 font_context,
4307 &styled_chars,
4308 measure_width,
4309 style.hyphens,
4310 style.lang.as_deref(),
4311 );
4312 let line_height = style.font_size * style.line_height;
4313 (broken_lines.len() as f64) * line_height + style.padding.vertical()
4314 } else {
4315 let content = substitute_page_placeholders(content);
4316 let lines = self.text_layout.break_into_lines(
4317 font_context,
4318 &content,
4319 measure_width,
4320 style.font_size,
4321 &style.font_family,
4322 style.font_weight,
4323 style.font_style,
4324 style.letter_spacing,
4325 style.hyphens,
4326 style.lang.as_deref(),
4327 );
4328 let line_height = style.font_size * style.line_height;
4329 (lines.len() as f64) * line_height + style.padding.vertical()
4330 }
4331 }
4332 NodeKind::Image {
4333 src,
4334 width: explicit_w,
4335 height: explicit_h,
4336 } => {
4337 if let SizeConstraint::Fixed(h) = style.height {
4339 return h + style.padding.vertical();
4340 }
4341 if let Some(h) = explicit_h {
4343 return *h + style.padding.vertical();
4344 }
4345 let aspect = self
4347 .get_image_dimensions(src)
4348 .map(|(w, h)| if w > 0 { h as f64 / w as f64 } else { 0.75 })
4349 .unwrap_or(0.75);
4350 let w = if let SizeConstraint::Fixed(w) = style.width {
4351 w
4352 } else {
4353 explicit_w.unwrap_or(available_width - style.margin.horizontal())
4354 };
4355 w * aspect + style.padding.vertical()
4356 }
4357 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
4358 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
4359 NodeKind::QrCode { size, .. } => {
4360 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
4361 display_size + style.margin.vertical()
4362 }
4363 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
4364 NodeKind::BarChart { height, .. }
4365 | NodeKind::LineChart { height, .. }
4366 | NodeKind::PieChart { height, .. }
4367 | NodeKind::AreaChart { height, .. }
4368 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
4369 NodeKind::TextField { height, .. }
4370 | NodeKind::Checkbox { height, .. }
4371 | NodeKind::Dropdown { height, .. }
4372 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
4373 NodeKind::Watermark { .. } => 0.0, _ => {
4375 if let SizeConstraint::Fixed(h) = style.height {
4377 return h;
4378 }
4379 let outer_width = match style.width {
4381 SizeConstraint::Fixed(w) => w,
4382 SizeConstraint::Auto => available_width - style.margin.horizontal(),
4383 };
4384 let inner_width =
4385 outer_width - style.padding.horizontal() - style.border_width.horizontal();
4386 let children_height =
4387 self.measure_children_height(&node.children, inner_width, style, font_context);
4388 children_height + style.padding.vertical() + style.border_width.vertical()
4389 }
4390 }
4391 }
4392
4393 fn measure_children_height(
4394 &self,
4395 children: &[Node],
4396 available_width: f64,
4397 parent_style: &ResolvedStyle,
4398 font_context: &FontContext,
4399 ) -> f64 {
4400 if matches!(parent_style.display, Display::Grid) {
4402 if let Some(template_cols) = &parent_style.grid_template_columns {
4403 let num_columns = template_cols.len();
4404 if num_columns > 0 && !children.is_empty() {
4405 let col_gap = parent_style.column_gap;
4406 let row_gap = parent_style.row_gap;
4407
4408 let content_sizes: Vec<f64> = template_cols
4409 .iter()
4410 .map(|track| {
4411 if matches!(track, GridTrackSize::Auto) {
4412 available_width / num_columns as f64
4413 } else {
4414 0.0
4415 }
4416 })
4417 .collect();
4418
4419 let col_widths = grid::resolve_tracks(
4420 template_cols,
4421 available_width,
4422 col_gap,
4423 &content_sizes,
4424 );
4425
4426 let placements: Vec<Option<&GridPlacement>> = children
4427 .iter()
4428 .map(|child| child.style.grid_placement.as_ref())
4429 .collect();
4430
4431 let item_placements = grid::place_items(&placements, num_columns);
4432 let num_rows = grid::compute_num_rows(&item_placements);
4433
4434 if num_rows == 0 {
4435 return 0.0;
4436 }
4437
4438 let mut row_heights = vec![0.0_f64; num_rows];
4439 for placement in &item_placements {
4440 let cell_width = grid::span_width(
4441 placement.col_start,
4442 placement.col_end,
4443 &col_widths,
4444 col_gap,
4445 );
4446 let child = &children[placement.child_index];
4447 let child_style = child.style.resolve(Some(parent_style), cell_width);
4448 let h =
4449 self.measure_node_height(child, cell_width, &child_style, font_context);
4450 let span = placement.row_end - placement.row_start;
4451 let per_row = h / span as f64;
4452 for rh in row_heights
4453 .iter_mut()
4454 .take(placement.row_end.min(num_rows))
4455 .skip(placement.row_start)
4456 {
4457 if per_row > *rh {
4458 *rh = per_row;
4459 }
4460 }
4461 }
4462
4463 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
4464 return row_heights.iter().sum::<f64>() + total_row_gap;
4465 }
4466 }
4467 }
4468
4469 let direction = parent_style.flex_direction;
4470 let row_gap = parent_style.row_gap;
4471 let column_gap = parent_style.column_gap;
4472
4473 match direction {
4474 FlexDirection::Row | FlexDirection::RowReverse => {
4475 let styles: Vec<ResolvedStyle> = children
4478 .iter()
4479 .map(|child| child.style.resolve(Some(parent_style), available_width))
4480 .collect();
4481
4482 let base_widths: Vec<f64> = children
4483 .iter()
4484 .zip(&styles)
4485 .map(|(child, style)| match style.flex_basis {
4486 SizeConstraint::Fixed(w) => w,
4487 SizeConstraint::Auto => match style.width {
4488 SizeConstraint::Fixed(w) => w,
4489 SizeConstraint::Auto => self
4490 .measure_intrinsic_width(child, style, font_context)
4491 .min(available_width),
4492 },
4493 })
4494 .collect();
4495
4496 let lines = match parent_style.flex_wrap {
4497 FlexWrap::NoWrap => {
4498 vec![flex::WrapLine {
4499 start: 0,
4500 end: children.len(),
4501 }]
4502 }
4503 FlexWrap::Wrap | FlexWrap::WrapReverse => {
4504 flex::partition_into_lines(&base_widths, column_gap, available_width)
4505 }
4506 };
4507
4508 let mut final_widths = base_widths.clone();
4510 for line in &lines {
4511 let line_count = line.end - line.start;
4512 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
4513 let distributable = available_width - line_gap;
4514 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
4515 let remaining = distributable - total_base;
4516
4517 if remaining > 0.0 {
4518 let total_grow: f64 = styles[line.start..line.end]
4519 .iter()
4520 .map(|s| s.flex_grow)
4521 .sum();
4522 if total_grow > 0.0 {
4523 for (j, s) in styles[line.start..line.end].iter().enumerate() {
4524 final_widths[line.start + j] = base_widths[line.start + j]
4525 + remaining * (s.flex_grow / total_grow);
4526 }
4527 }
4528 } else if remaining < 0.0 {
4529 let total_shrink: f64 = styles[line.start..line.end]
4530 .iter()
4531 .enumerate()
4532 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
4533 .sum();
4534 if total_shrink > 0.0 {
4535 for (j, s) in styles[line.start..line.end].iter().enumerate() {
4536 let factor =
4537 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
4538 let w = base_widths[line.start + j] + remaining * factor;
4539 final_widths[line.start + j] = w.max(s.min_width);
4540 }
4541 }
4542 }
4543 }
4544
4545 let mut total = 0.0;
4546 for (i, line) in lines.iter().enumerate() {
4547 let line_height: f64 = children[line.start..line.end]
4548 .iter()
4549 .enumerate()
4550 .map(|(j, child)| {
4551 let fw = final_widths[line.start + j];
4552 let child_style = child.style.resolve(Some(parent_style), fw);
4553 self.measure_node_height(child, fw, &child_style, font_context)
4554 + child_style.margin.vertical()
4555 })
4556 .fold(0.0f64, f64::max);
4557 total += line_height;
4558 if i > 0 {
4559 total += row_gap;
4560 }
4561 }
4562 total
4563 }
4564 FlexDirection::Column | FlexDirection::ColumnReverse => {
4565 let mut total = 0.0;
4566 for (i, child) in children.iter().enumerate() {
4567 let child_style = child.style.resolve(Some(parent_style), available_width);
4568 let child_height = self.measure_node_height(
4569 child,
4570 available_width,
4571 &child_style,
4572 font_context,
4573 );
4574 total += child_height + child_style.margin.vertical();
4575 if i > 0 {
4576 total += row_gap;
4577 }
4578 }
4579 total
4580 }
4581 }
4582 }
4583
4584 fn measure_intrinsic_width(
4586 &self,
4587 node: &Node,
4588 style: &ResolvedStyle,
4589 font_context: &FontContext,
4590 ) -> f64 {
4591 match &node.kind {
4592 NodeKind::Svg { width, .. } => {
4593 *width + style.padding.horizontal() + style.margin.horizontal()
4594 }
4595 NodeKind::Text { content, .. } => {
4596 let content = substitute_page_placeholders(content);
4597 let transformed = apply_text_transform(&content, style.text_transform);
4598 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
4599 let text_width = font_context.measure_string(
4600 &transformed,
4601 &style.font_family,
4602 style.font_weight,
4603 italic,
4604 style.font_size,
4605 style.letter_spacing,
4606 );
4607 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
4610 }
4611 NodeKind::Image {
4612 src, width, height, ..
4613 } => {
4614 let w = if let SizeConstraint::Fixed(w) = style.width {
4615 w
4616 } else if let Some(w) = width {
4617 *w
4618 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
4619 let pixel_w = iw as f64;
4620 let pixel_h = ih as f64;
4621 let aspect = if pixel_w > 0.0 {
4622 pixel_h / pixel_w
4623 } else {
4624 0.75
4625 };
4626 let constrained_h = match style.height {
4628 SizeConstraint::Fixed(h) => Some(h),
4629 SizeConstraint::Auto => *height,
4630 };
4631 if let Some(h) = constrained_h {
4632 h / aspect
4633 } else {
4634 pixel_w
4635 }
4636 } else {
4637 100.0
4638 };
4639 w + style.padding.horizontal() + style.margin.horizontal()
4640 }
4641 NodeKind::Barcode { width, .. } => {
4642 let w = width.unwrap_or(0.0);
4643 w + style.padding.horizontal() + style.margin.horizontal()
4644 }
4645 NodeKind::QrCode { size, .. } => {
4646 let display_size = size.unwrap_or(0.0);
4647 display_size + style.padding.horizontal() + style.margin.horizontal()
4648 }
4649 NodeKind::Canvas { width, .. } => {
4650 *width + style.padding.horizontal() + style.margin.horizontal()
4651 }
4652 NodeKind::BarChart { width, .. }
4653 | NodeKind::LineChart { width, .. }
4654 | NodeKind::PieChart { width, .. }
4655 | NodeKind::AreaChart { width, .. }
4656 | NodeKind::DotPlot { width, .. } => {
4657 *width + style.padding.horizontal() + style.margin.horizontal()
4658 }
4659 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
4660 *width + style.padding.horizontal() + style.margin.horizontal()
4661 }
4662 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
4663 *width + style.padding.horizontal() + style.margin.horizontal()
4664 }
4665 NodeKind::Watermark { .. } => 0.0, _ => {
4667 if node.children.is_empty() {
4669 style.padding.horizontal() + style.margin.horizontal()
4670 } else {
4671 let direction = style.flex_direction;
4672 let gap = style.gap;
4673 let mut total = 0.0f64;
4674 for (i, child) in node.children.iter().enumerate() {
4675 let child_style = child.style.resolve(Some(style), 0.0);
4676 let child_width =
4677 self.measure_intrinsic_width(child, &child_style, font_context);
4678 match direction {
4679 FlexDirection::Row | FlexDirection::RowReverse => {
4680 total += child_width;
4681 if i > 0 {
4682 total += gap;
4683 }
4684 }
4685 _ => {
4686 total = total.max(child_width);
4687 }
4688 }
4689 }
4690 total
4691 + style.padding.horizontal()
4692 + style.margin.horizontal()
4693 + style.border_width.horizontal()
4694 }
4695 }
4696 }
4697 }
4698
4699 pub fn measure_min_content_width(
4703 &self,
4704 node: &Node,
4705 style: &ResolvedStyle,
4706 font_context: &FontContext,
4707 ) -> f64 {
4708 match &node.kind {
4709 NodeKind::Text { content, runs, .. } => {
4710 let word_width = if !runs.is_empty() {
4711 runs.iter()
4713 .map(|run| {
4714 let run_style = run.style.resolve(Some(style), 0.0);
4715 let run_content = substitute_page_placeholders(&run.content);
4716 let transformed =
4717 apply_text_transform(&run_content, run_style.text_transform);
4718 self.text_layout.measure_widest_word(
4719 font_context,
4720 &transformed,
4721 run_style.font_size,
4722 &run_style.font_family,
4723 run_style.font_weight,
4724 run_style.font_style,
4725 run_style.letter_spacing,
4726 style.hyphens,
4727 style.lang.as_deref(),
4728 )
4729 })
4730 .fold(0.0f64, f64::max)
4731 } else {
4732 let content = substitute_page_placeholders(content);
4733 let transformed = apply_text_transform(&content, style.text_transform);
4734 self.text_layout.measure_widest_word(
4735 font_context,
4736 &transformed,
4737 style.font_size,
4738 &style.font_family,
4739 style.font_weight,
4740 style.font_style,
4741 style.letter_spacing,
4742 style.hyphens,
4743 style.lang.as_deref(),
4744 )
4745 };
4746 word_width + style.padding.horizontal() + style.margin.horizontal()
4747 }
4748 NodeKind::Image { width, .. } => {
4749 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
4750 }
4751 NodeKind::Svg { width, .. } => {
4752 *width + style.padding.horizontal() + style.margin.horizontal()
4753 }
4754 _ => {
4755 if node.children.is_empty() {
4756 style.padding.horizontal()
4757 + style.margin.horizontal()
4758 + style.border_width.horizontal()
4759 } else {
4760 let mut max_child_min = 0.0f64;
4761 for child in &node.children {
4762 let child_style = child.style.resolve(Some(style), 0.0);
4763 let child_min =
4764 self.measure_min_content_width(child, &child_style, font_context);
4765 max_child_min = max_child_min.max(child_min);
4766 }
4767 max_child_min
4768 + style.padding.horizontal()
4769 + style.margin.horizontal()
4770 + style.border_width.horizontal()
4771 }
4772 }
4773 }
4774 }
4775
4776 fn measure_table_row_height(
4777 &self,
4778 row: &Node,
4779 col_widths: &[f64],
4780 parent_style: &ResolvedStyle,
4781 font_context: &FontContext,
4782 ) -> f64 {
4783 let row_style = row
4784 .style
4785 .resolve(Some(parent_style), col_widths.iter().sum());
4786 let mut max_height: f64 = 0.0;
4787
4788 for (i, cell) in row.children.iter().enumerate() {
4789 let col_width = col_widths.get(i).copied().unwrap_or(0.0);
4790 let cell_style = cell.style.resolve(Some(&row_style), col_width);
4791 let inner_width =
4792 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
4793
4794 let mut cell_content_height = 0.0;
4795 for child in &cell.children {
4796 let child_style = child.style.resolve(Some(&cell_style), inner_width);
4797 cell_content_height +=
4798 self.measure_node_height(child, inner_width, &child_style, font_context);
4799 }
4800
4801 let total = cell_content_height
4802 + cell_style.padding.vertical()
4803 + cell_style.border_width.vertical();
4804 max_height = max_height.max(total);
4805 }
4806
4807 max_height.max(row_style.min_height)
4808 }
4809
4810 fn resolve_column_widths(
4811 &self,
4812 defs: &[ColumnDef],
4813 available_width: f64,
4814 children: &[Node],
4815 ) -> Vec<f64> {
4816 if defs.is_empty() {
4817 let num_cols = children.first().map(|row| row.children.len()).unwrap_or(1);
4818 return vec![available_width / num_cols as f64; num_cols];
4819 }
4820
4821 let mut widths = Vec::new();
4822 let mut remaining = available_width;
4823 let mut auto_count = 0;
4824
4825 for def in defs {
4826 match def.width {
4827 ColumnWidth::Fixed(w) => {
4828 widths.push(w);
4829 remaining -= w;
4830 }
4831 ColumnWidth::Fraction(f) => {
4832 let w = available_width * f;
4833 widths.push(w);
4834 remaining -= w;
4835 }
4836 ColumnWidth::Auto => {
4837 widths.push(0.0);
4838 auto_count += 1;
4839 }
4840 }
4841 }
4842
4843 if auto_count > 0 {
4844 let auto_width = remaining / auto_count as f64;
4845 for (i, def) in defs.iter().enumerate() {
4846 if matches!(def.width, ColumnWidth::Auto) {
4847 widths[i] = auto_width;
4848 }
4849 }
4850 }
4851
4852 widths
4853 }
4854
4855 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
4856 for page in pages.iter_mut() {
4857 if !page.watermarks.is_empty() {
4859 let (page_w, page_h) = page.config.size.dimensions();
4860 let cx = page_w / 2.0;
4861 let cy = page_h / 2.0;
4862
4863 let mut watermark_elements = Vec::new();
4864 for wm_node in &page.watermarks {
4865 if let NodeKind::Watermark {
4866 text,
4867 font_size,
4868 angle,
4869 } = &wm_node.kind
4870 {
4871 let style = wm_node.style.resolve(None, page_w);
4872 let color = style.color;
4873 let opacity = style.opacity;
4874 let angle_rad = angle.to_radians();
4875
4876 let italic =
4878 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
4879
4880 let shaped = self.text_layout.shape_text(
4882 font_context,
4883 text,
4884 &style.font_family,
4885 style.font_weight,
4886 style.font_style,
4887 );
4888
4889 let mut glyphs = Vec::new();
4890 let mut x_pos = 0.0;
4891 let text_chars: Vec<char> = text.chars().collect();
4892
4893 if let Some(shaped_glyphs) = shaped {
4894 let units_per_em = font_context.units_per_em(
4896 &style.font_family,
4897 style.font_weight,
4898 italic,
4899 ) as f64;
4900
4901 for sg in &shaped_glyphs {
4902 let advance = sg.x_advance as f64 / units_per_em * *font_size;
4903 let cluster_idx = sg.cluster as usize;
4904 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
4905 glyphs.push(PositionedGlyph {
4906 glyph_id: sg.glyph_id,
4907 char_value: ch,
4908 x_offset: x_pos,
4909 y_offset: 0.0,
4910 x_advance: advance,
4911 font_size: *font_size,
4912 font_family: style.font_family.clone(),
4913 font_weight: style.font_weight,
4914 font_style: style.font_style,
4915 color: Some(color),
4916 href: None,
4917 text_decoration: TextDecoration::None,
4918 letter_spacing: style.letter_spacing,
4919 cluster_text: None,
4920 });
4921 x_pos += advance + style.letter_spacing;
4922 }
4923 } else {
4924 for &ch in &text_chars {
4926 let w = font_context.char_width(
4927 ch,
4928 &style.font_family,
4929 style.font_weight,
4930 italic,
4931 *font_size,
4932 );
4933 glyphs.push(PositionedGlyph {
4934 glyph_id: ch as u16,
4935 char_value: ch,
4936 x_offset: x_pos,
4937 y_offset: 0.0,
4938 x_advance: w,
4939 font_size: *font_size,
4940 font_family: style.font_family.clone(),
4941 font_weight: style.font_weight,
4942 font_style: style.font_style,
4943 color: Some(color),
4944 href: None,
4945 text_decoration: TextDecoration::None,
4946 letter_spacing: style.letter_spacing,
4947 cluster_text: None,
4948 });
4949 x_pos += w + style.letter_spacing;
4950 }
4951 }
4952
4953 let text_width = x_pos;
4954
4955 let line = TextLine {
4956 x: 0.0,
4957 y: 0.0,
4958 glyphs,
4959 width: text_width,
4960 height: *font_size,
4961 word_spacing: 0.0,
4962 };
4963
4964 watermark_elements.push(LayoutElement {
4965 x: cx,
4966 y: cy,
4967 width: text_width,
4968 height: *font_size,
4969 draw: DrawCommand::Watermark {
4970 lines: vec![line],
4971 color,
4972 opacity,
4973 angle_rad,
4974 font_family: style.font_family.clone(),
4975 },
4976 children: vec![],
4977 node_type: Some("Watermark".to_string()),
4978 resolved_style: None,
4979 source_location: None,
4980 href: None,
4981 bookmark: None,
4982 alt: None,
4983 is_header_row: false,
4984 overflow: Overflow::default(),
4985 });
4986 }
4987 }
4988
4989 watermark_elements.append(&mut page.elements);
4991 page.elements = watermark_elements;
4992 page.watermarks.clear();
4993 }
4994
4995 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
4996 continue;
4997 }
4998
4999 if !page.fixed_header.is_empty() {
5001 let mut hdr_cursor = PageCursor::new(&page.config);
5002 for (node, _h) in &page.fixed_header {
5003 let cw = hdr_cursor.content_width;
5004 let cx = hdr_cursor.content_x;
5005 let style = node.style.resolve(None, cw);
5006 self.layout_view(
5007 node,
5008 &style,
5009 &mut hdr_cursor,
5010 &mut Vec::new(),
5011 cx,
5012 cw,
5013 font_context,
5014 );
5015 }
5016 let mut combined = hdr_cursor.elements;
5018 combined.append(&mut page.elements);
5019 page.elements = combined;
5020 }
5021
5022 if !page.fixed_footer.is_empty() {
5027 let mut ftr_cursor = PageCursor::new(&page.config);
5028 let total_ftr: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
5029 let target_y = ftr_cursor.content_height - total_ftr;
5030 for (node, _h) in &page.fixed_footer {
5032 let cw = ftr_cursor.content_width;
5033 let cx = ftr_cursor.content_x;
5034 let style = node.style.resolve(None, cw);
5035 self.layout_view(
5036 node,
5037 &style,
5038 &mut ftr_cursor,
5039 &mut Vec::new(),
5040 cx,
5041 cw,
5042 font_context,
5043 );
5044 }
5045 for el in &mut ftr_cursor.elements {
5049 offset_element_y(el, target_y);
5050 }
5051 page.elements.extend(ftr_cursor.elements);
5052 }
5053
5054 page.fixed_header.clear();
5056 page.fixed_footer.clear();
5057 }
5058 }
5059
5060 #[allow(clippy::too_many_arguments)]
5065 fn layout_grid_children(
5066 &self,
5067 children: &[Node],
5068 parent_style: &ResolvedStyle,
5069 cursor: &mut PageCursor,
5070 pages: &mut Vec<LayoutPage>,
5071 x: f64,
5072 available_width: f64,
5073 font_context: &FontContext,
5074 ) {
5075 let template_cols = match &parent_style.grid_template_columns {
5076 Some(cols) => cols,
5077 None => return, };
5079
5080 let num_columns = template_cols.len();
5081 if num_columns == 0 || children.is_empty() {
5082 return;
5083 }
5084
5085 let col_gap = parent_style.column_gap;
5086 let row_gap = parent_style.row_gap;
5087
5088 let content_sizes: Vec<f64> = template_cols
5091 .iter()
5092 .map(|track| {
5093 if matches!(track, GridTrackSize::Auto) {
5094 available_width / num_columns as f64
5097 } else {
5098 0.0
5099 }
5100 })
5101 .collect();
5102
5103 let col_widths =
5104 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
5105
5106 let placements: Vec<Option<&GridPlacement>> = children
5108 .iter()
5109 .map(|child| child.style.grid_placement.as_ref())
5110 .collect();
5111
5112 let item_placements = grid::place_items(&placements, num_columns);
5114 let num_rows = grid::compute_num_rows(&item_placements);
5115
5116 if num_rows == 0 {
5117 return;
5118 }
5119
5120 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
5122 for placement in &item_placements {
5123 let cell_width =
5124 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
5125 let child = &children[placement.child_index];
5126 let child_style = child.style.resolve(Some(parent_style), cell_width);
5127 item_heights[placement.child_index] =
5128 self.measure_node_height(child, cell_width, &child_style, font_context);
5129 }
5130
5131 let template_rows = parent_style.grid_template_rows.as_deref();
5133 let mut row_heights = vec![0.0_f64; num_rows];
5134 for placement in &item_placements {
5135 let h = item_heights[placement.child_index];
5136 let span = placement.row_end - placement.row_start;
5137 let per_row = h / span as f64;
5138 for rh in row_heights
5139 .iter_mut()
5140 .take(placement.row_end.min(num_rows))
5141 .skip(placement.row_start)
5142 {
5143 if per_row > *rh {
5144 *rh = per_row;
5145 }
5146 }
5147 }
5148
5149 if let Some(template) = template_rows {
5151 let auto_row = parent_style.grid_auto_rows.as_ref();
5152 for (r, rh) in row_heights.iter_mut().enumerate() {
5153 let track = template.get(r).or(auto_row);
5154 if let Some(track) = track {
5155 match track {
5156 GridTrackSize::Pt(pts) => *rh = *pts,
5157 GridTrackSize::Auto => {} _ => {} }
5160 }
5161 }
5162 }
5163
5164 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
5166 if row_height > cursor.remaining_height() && row > 0 {
5168 pages.push(cursor.finalize());
5169 *cursor = cursor.new_page();
5170 }
5171
5172 for placement in &item_placements {
5174 if placement.row_start != row {
5175 continue; }
5177
5178 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
5179 let cell_width =
5180 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
5181
5182 let child = &children[placement.child_index];
5183
5184 let saved_y = cursor.y;
5186 self.layout_node(
5187 child,
5188 cursor,
5189 pages,
5190 cell_x,
5191 cell_width,
5192 Some(parent_style),
5193 font_context,
5194 None,
5195 );
5196 cursor.y = saved_y;
5198 }
5199
5200 cursor.y += row_height + row_gap;
5201 }
5202
5203 if num_rows > 0 {
5205 cursor.y -= row_gap;
5206 }
5207 }
5208}
5209
5210struct FlexItem<'a> {
5211 node: &'a Node,
5212 style: ResolvedStyle,
5213 base_width: f64,
5214 min_content_width: f64,
5215}
5216
5217#[cfg(test)]
5218mod tests {
5219 use super::*;
5220 use crate::font::FontContext;
5221
5222 fn make_text(content: &str, font_size: f64) -> Node {
5223 Node {
5224 kind: NodeKind::Text {
5225 content: content.to_string(),
5226 href: None,
5227 runs: vec![],
5228 },
5229 style: Style {
5230 font_size: Some(font_size),
5231 ..Default::default()
5232 },
5233 children: vec![],
5234 id: None,
5235 source_location: None,
5236 bookmark: None,
5237 href: None,
5238 alt: None,
5239 }
5240 }
5241
5242 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
5243 Node {
5244 kind: NodeKind::View,
5245 style,
5246 children,
5247 id: None,
5248 source_location: None,
5249 bookmark: None,
5250 href: None,
5251 alt: None,
5252 }
5253 }
5254
5255 #[test]
5256 fn intrinsic_width_flex_row_sums_children() {
5257 let engine = LayoutEngine::new();
5258 let font_context = FontContext::new();
5259
5260 let child1 = make_text("Hello", 14.0);
5261 let child2 = make_text("World", 14.0);
5262
5263 let child1_style = child1.style.resolve(None, 0.0);
5264 let child2_style = child2.style.resolve(None, 0.0);
5265 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
5266 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
5267
5268 let row = make_styled_view(
5269 Style {
5270 flex_direction: Some(FlexDirection::Row),
5271 ..Default::default()
5272 },
5273 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
5274 );
5275 let row_style = row.style.resolve(None, 0.0);
5276 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
5277
5278 assert!(
5279 (row_w - (child1_w + child2_w)).abs() < 0.01,
5280 "Row intrinsic width ({}) should equal sum of children ({} + {})",
5281 row_w,
5282 child1_w,
5283 child2_w
5284 );
5285 }
5286
5287 #[test]
5288 fn intrinsic_width_flex_column_takes_max() {
5289 let engine = LayoutEngine::new();
5290 let font_context = FontContext::new();
5291
5292 let short = make_text("Hi", 14.0);
5293 let long = make_text("Hello World", 14.0);
5294
5295 let short_style = short.style.resolve(None, 0.0);
5296 let long_style = long.style.resolve(None, 0.0);
5297 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
5298 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
5299
5300 let col = make_styled_view(
5301 Style {
5302 flex_direction: Some(FlexDirection::Column),
5303 ..Default::default()
5304 },
5305 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
5306 );
5307 let col_style = col.style.resolve(None, 0.0);
5308 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
5309
5310 assert!(
5311 (col_w - long_w).abs() < 0.01,
5312 "Column intrinsic width ({}) should equal max child ({}, short was {})",
5313 col_w,
5314 long_w,
5315 short_w
5316 );
5317 }
5318
5319 #[test]
5320 fn intrinsic_width_nested_containers() {
5321 let engine = LayoutEngine::new();
5322 let font_context = FontContext::new();
5323
5324 let inner = make_styled_view(
5325 Style {
5326 flex_direction: Some(FlexDirection::Row),
5327 ..Default::default()
5328 },
5329 vec![make_text("A", 12.0), make_text("B", 12.0)],
5330 );
5331 let inner_style = inner.style.resolve(None, 0.0);
5332 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
5333
5334 let outer = make_styled_view(
5335 Style::default(),
5336 vec![make_styled_view(
5337 Style {
5338 flex_direction: Some(FlexDirection::Row),
5339 ..Default::default()
5340 },
5341 vec![make_text("A", 12.0), make_text("B", 12.0)],
5342 )],
5343 );
5344 let outer_style = outer.style.resolve(None, 0.0);
5345 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
5346
5347 assert!(
5348 (outer_w - inner_w).abs() < 0.01,
5349 "Nested container ({}) should match inner container ({})",
5350 outer_w,
5351 inner_w
5352 );
5353 }
5354
5355 #[test]
5356 fn intrinsic_width_row_with_gap() {
5357 let engine = LayoutEngine::new();
5358 let font_context = FontContext::new();
5359
5360 let no_gap = make_styled_view(
5361 Style {
5362 flex_direction: Some(FlexDirection::Row),
5363 ..Default::default()
5364 },
5365 vec![make_text("A", 12.0), make_text("B", 12.0)],
5366 );
5367 let with_gap = make_styled_view(
5368 Style {
5369 flex_direction: Some(FlexDirection::Row),
5370 gap: Some(10.0),
5371 ..Default::default()
5372 },
5373 vec![make_text("A", 12.0), make_text("B", 12.0)],
5374 );
5375
5376 let no_gap_style = no_gap.style.resolve(None, 0.0);
5377 let with_gap_style = with_gap.style.resolve(None, 0.0);
5378 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
5379 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
5380
5381 assert!(
5382 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
5383 "Gap should add 10pt: with_gap={}, no_gap={}",
5384 with_gap_w,
5385 no_gap_w
5386 );
5387 }
5388
5389 #[test]
5390 fn intrinsic_width_empty_container() {
5391 let engine = LayoutEngine::new();
5392 let font_context = FontContext::new();
5393
5394 let padding = 8.0;
5395 let empty = make_styled_view(
5396 Style {
5397 padding: Some(Edges::uniform(padding)),
5398 ..Default::default()
5399 },
5400 vec![],
5401 );
5402 let style = empty.style.resolve(None, 0.0);
5403 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
5404
5405 assert!(
5406 (w - padding * 2.0).abs() < 0.01,
5407 "Empty container width ({}) should equal horizontal padding ({})",
5408 w,
5409 padding * 2.0
5410 );
5411 }
5412
5413 #[test]
5416 fn flex_shrink_respects_min_content_width() {
5417 let engine = LayoutEngine::new();
5421 let font_context = FontContext::new();
5422
5423 let sale_text = make_text("SALE", 12.0);
5424 let sale_style = sale_text.style.resolve(None, 0.0);
5425 let sale_word_width =
5426 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
5427 assert!(
5428 sale_word_width > 0.0,
5429 "SALE should have non-zero min-content width"
5430 );
5431
5432 let container = make_styled_view(
5435 Style {
5436 flex_direction: Some(FlexDirection::Row),
5437 width: Some(Dimension::Pt(100.0)),
5438 ..Default::default()
5439 },
5440 vec![
5441 make_styled_view(
5442 Style {
5443 width: Some(Dimension::Pt(80.0)),
5444 flex_shrink: Some(1.0),
5445 ..Default::default()
5446 },
5447 vec![],
5448 ),
5449 make_styled_view(
5450 Style {
5451 width: Some(Dimension::Pt(60.0)),
5452 flex_shrink: Some(1.0),
5453 ..Default::default()
5454 },
5455 vec![make_text("SALE", 12.0)],
5456 ),
5457 ],
5458 );
5459
5460 let doc = Document {
5461 children: vec![Node::page(
5462 PageConfig::default(),
5463 Style::default(),
5464 vec![container],
5465 )],
5466 metadata: Default::default(),
5467 default_page: PageConfig::default(),
5468 fonts: vec![],
5469 tagged: false,
5470 pdfa: None,
5471 default_style: None,
5472 embedded_data: None,
5473 flatten_forms: false,
5474 pdf_ua: false,
5475 signature: None,
5476 };
5477
5478 let pages = engine.layout(&doc, &font_context);
5479 assert!(!pages.is_empty());
5480
5481 let page = &pages[0];
5484 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
5486 assert!(
5487 container_el.is_some(),
5488 "Should find container with 2 children"
5489 );
5490 let sale_child = &container_el.unwrap().children[1];
5491 assert!(
5492 sale_child.width >= sale_word_width - 0.01,
5493 "SALE child width ({}) should be >= min-content width ({})",
5494 sale_child.width,
5495 sale_word_width
5496 );
5497 }
5498
5499 #[test]
5502 fn column_justify_content_center() {
5503 let engine = LayoutEngine::new();
5506 let font_context = FontContext::new();
5507
5508 let container = make_styled_view(
5509 Style {
5510 flex_direction: Some(FlexDirection::Column),
5511 height: Some(Dimension::Pt(200.0)),
5512 justify_content: Some(JustifyContent::Center),
5513 ..Default::default()
5514 },
5515 vec![make_text("Centered", 12.0)],
5516 );
5517
5518 let doc = Document {
5519 children: vec![Node::page(
5520 PageConfig::default(),
5521 Style::default(),
5522 vec![container],
5523 )],
5524 metadata: Default::default(),
5525 default_page: PageConfig::default(),
5526 fonts: vec![],
5527 tagged: false,
5528 pdfa: None,
5529 default_style: None,
5530 embedded_data: None,
5531 flatten_forms: false,
5532 pdf_ua: false,
5533 signature: None,
5534 };
5535
5536 let pages = engine.layout(&doc, &font_context);
5537 let page = &pages[0];
5538
5539 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
5542 assert!(
5543 container_el.is_some(),
5544 "Should find container with children"
5545 );
5546 let container_el = container_el.unwrap();
5547 let child = &container_el.children[0];
5548
5549 let child_offset = child.y - container_el.y;
5551 let expected_offset = (200.0 - child.height) / 2.0;
5552 assert!(
5553 (child_offset - expected_offset).abs() < 2.0,
5554 "Child offset ({}) should be near center ({})",
5555 child_offset,
5556 expected_offset
5557 );
5558 }
5559
5560 #[test]
5561 fn column_align_items_center() {
5562 let engine = LayoutEngine::new();
5565 let font_context = FontContext::new();
5566
5567 let container = make_styled_view(
5568 Style {
5569 flex_direction: Some(FlexDirection::Column),
5570 width: Some(Dimension::Pt(300.0)),
5571 align_items: Some(AlignItems::Center),
5572 ..Default::default()
5573 },
5574 vec![make_text("Hi", 12.0)],
5575 );
5576
5577 let doc = Document {
5578 children: vec![Node::page(
5579 PageConfig::default(),
5580 Style::default(),
5581 vec![container],
5582 )],
5583 metadata: Default::default(),
5584 default_page: PageConfig::default(),
5585 fonts: vec![],
5586 tagged: false,
5587 pdfa: None,
5588 default_style: None,
5589 embedded_data: None,
5590 flatten_forms: false,
5591 pdf_ua: false,
5592 signature: None,
5593 };
5594
5595 let pages = engine.layout(&doc, &font_context);
5596 let page = &pages[0];
5597
5598 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
5599 assert!(container_el.is_some());
5600 let container_el = container_el.unwrap();
5601 let child = &container_el.children[0];
5602
5603 let child_center = child.x + child.width / 2.0;
5605 let container_center = container_el.x + container_el.width / 2.0;
5606 assert!(
5607 (child_center - container_center).abs() < 2.0,
5608 "Child center ({}) should be near container center ({})",
5609 child_center,
5610 container_center
5611 );
5612 }
5613
5614 #[test]
5617 fn absolute_child_positioned_relative_to_parent() {
5618 let engine = LayoutEngine::new();
5621 let font_context = FontContext::new();
5622
5623 let parent = make_styled_view(
5624 Style {
5625 margin: Some(MarginEdges::from_edges(Edges {
5626 top: 50.0,
5627 left: 50.0,
5628 ..Default::default()
5629 })),
5630 width: Some(Dimension::Pt(200.0)),
5631 height: Some(Dimension::Pt(200.0)),
5632 ..Default::default()
5633 },
5634 vec![make_styled_view(
5635 Style {
5636 position: Some(crate::model::Position::Absolute),
5637 top: Some(10.0),
5638 left: Some(10.0),
5639 width: Some(Dimension::Pt(50.0)),
5640 height: Some(Dimension::Pt(50.0)),
5641 ..Default::default()
5642 },
5643 vec![],
5644 )],
5645 );
5646
5647 let doc = Document {
5648 children: vec![Node::page(
5649 PageConfig::default(),
5650 Style::default(),
5651 vec![parent],
5652 )],
5653 metadata: Default::default(),
5654 default_page: PageConfig::default(),
5655 fonts: vec![],
5656 tagged: false,
5657 pdfa: None,
5658 default_style: None,
5659 embedded_data: None,
5660 flatten_forms: false,
5661 pdf_ua: false,
5662 signature: None,
5663 };
5664
5665 let pages = engine.layout(&doc, &font_context);
5666 let page = &pages[0];
5667
5668 let parent_el = page
5671 .elements
5672 .iter()
5673 .find(|e| e.width > 190.0 && e.width < 210.0);
5674 assert!(parent_el.is_some(), "Should find the 200x200 parent");
5675 let parent_el = parent_el.unwrap();
5676
5677 let abs_child = parent_el
5679 .children
5680 .iter()
5681 .find(|e| e.width > 45.0 && e.width < 55.0);
5682 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
5683 let abs_child = abs_child.unwrap();
5684
5685 let expected_x = parent_el.x + 10.0;
5686 let expected_y = parent_el.y + 10.0;
5687 assert!(
5688 (abs_child.x - expected_x).abs() < 1.0,
5689 "Absolute child x ({}) should be parent.x + 10 ({})",
5690 abs_child.x,
5691 expected_x
5692 );
5693 assert!(
5694 (abs_child.y - expected_y).abs() < 1.0,
5695 "Absolute child y ({}) should be parent.y + 10 ({})",
5696 abs_child.y,
5697 expected_y
5698 );
5699 }
5700
5701 #[test]
5702 fn text_transform_none_passthrough() {
5703 assert_eq!(
5704 apply_text_transform("Hello World", TextTransform::None),
5705 "Hello World"
5706 );
5707 }
5708
5709 #[test]
5710 fn text_transform_uppercase() {
5711 assert_eq!(
5712 apply_text_transform("hello world", TextTransform::Uppercase),
5713 "HELLO WORLD"
5714 );
5715 }
5716
5717 #[test]
5718 fn text_transform_lowercase() {
5719 assert_eq!(
5720 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
5721 "hello world"
5722 );
5723 }
5724
5725 #[test]
5726 fn text_transform_capitalize() {
5727 assert_eq!(
5728 apply_text_transform("hello world", TextTransform::Capitalize),
5729 "Hello World"
5730 );
5731 assert_eq!(
5732 apply_text_transform(" hello world ", TextTransform::Capitalize),
5733 " Hello World "
5734 );
5735 assert_eq!(
5736 apply_text_transform("already Capitalized", TextTransform::Capitalize),
5737 "Already Capitalized"
5738 );
5739 }
5740
5741 #[test]
5742 fn text_transform_capitalize_empty() {
5743 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
5744 }
5745
5746 #[test]
5747 fn apply_char_transform_uppercase() {
5748 assert_eq!(
5749 apply_char_transform('a', TextTransform::Uppercase, false),
5750 'A'
5751 );
5752 assert_eq!(
5753 apply_char_transform('A', TextTransform::Uppercase, false),
5754 'A'
5755 );
5756 }
5757
5758 #[test]
5759 fn apply_char_transform_capitalize_word_start() {
5760 assert_eq!(
5761 apply_char_transform('h', TextTransform::Capitalize, true),
5762 'H'
5763 );
5764 assert_eq!(
5765 apply_char_transform('h', TextTransform::Capitalize, false),
5766 'h'
5767 );
5768 }
5769
5770 #[test]
5773 fn column_flex_grow_single_child_fills_container() {
5774 let engine = LayoutEngine::new();
5777 let font_context = FontContext::new();
5778
5779 let child = make_styled_view(
5780 Style {
5781 flex_grow: Some(1.0),
5782 ..Default::default()
5783 },
5784 vec![make_text("Short", 12.0)],
5785 );
5786
5787 let container = make_styled_view(
5788 Style {
5789 flex_direction: Some(FlexDirection::Column),
5790 height: Some(Dimension::Pt(300.0)),
5791 ..Default::default()
5792 },
5793 vec![child],
5794 );
5795
5796 let doc = Document {
5797 children: vec![Node::page(
5798 PageConfig::default(),
5799 Style::default(),
5800 vec![container],
5801 )],
5802 metadata: Default::default(),
5803 default_page: PageConfig::default(),
5804 fonts: vec![],
5805 tagged: false,
5806 pdfa: None,
5807 default_style: None,
5808 embedded_data: None,
5809 flatten_forms: false,
5810 pdf_ua: false,
5811 signature: None,
5812 };
5813
5814 let pages = engine.layout(&doc, &font_context);
5815 let page = &pages[0];
5816
5817 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
5818 assert!(container_el.is_some());
5819 let container_el = container_el.unwrap();
5820 assert!(
5821 (container_el.height - 300.0).abs() < 1.0,
5822 "Container should be 300pt, got {}",
5823 container_el.height
5824 );
5825
5826 let child_el = &container_el.children[0];
5827 assert!(
5828 (child_el.height - 300.0).abs() < 1.0,
5829 "flex-grow child should expand to 300pt, got {}",
5830 child_el.height
5831 );
5832 }
5833
5834 #[test]
5835 fn column_flex_grow_two_children_proportional() {
5836 let engine = LayoutEngine::new();
5839 let font_context = FontContext::new();
5840
5841 let child1 = make_styled_view(
5842 Style {
5843 flex_grow: Some(1.0),
5844 ..Default::default()
5845 },
5846 vec![make_text("A", 12.0)],
5847 );
5848 let child2 = make_styled_view(
5849 Style {
5850 flex_grow: Some(2.0),
5851 ..Default::default()
5852 },
5853 vec![make_text("B", 12.0)],
5854 );
5855
5856 let container = make_styled_view(
5857 Style {
5858 flex_direction: Some(FlexDirection::Column),
5859 height: Some(Dimension::Pt(300.0)),
5860 ..Default::default()
5861 },
5862 vec![child1, child2],
5863 );
5864
5865 let doc = Document {
5866 children: vec![Node::page(
5867 PageConfig::default(),
5868 Style::default(),
5869 vec![container],
5870 )],
5871 metadata: Default::default(),
5872 default_page: PageConfig::default(),
5873 fonts: vec![],
5874 tagged: false,
5875 pdfa: None,
5876 default_style: None,
5877 embedded_data: None,
5878 flatten_forms: false,
5879 pdf_ua: false,
5880 signature: None,
5881 };
5882
5883 let pages = engine.layout(&doc, &font_context);
5884 let page = &pages[0];
5885
5886 let container_el = page
5887 .elements
5888 .iter()
5889 .find(|e| e.children.len() == 2)
5890 .expect("Should find container with two children");
5891
5892 let c1 = &container_el.children[0];
5893 let c2 = &container_el.children[1];
5894
5895 let total = c1.height + c2.height;
5899 assert!(
5900 (total - 300.0).abs() < 2.0,
5901 "Children should sum to ~300pt, got {}",
5902 total
5903 );
5904
5905 let ratio = c2.height / c1.height;
5908 assert!(
5909 ratio > 1.3 && ratio < 2.5,
5910 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
5911 ratio
5912 );
5913 }
5914
5915 #[test]
5916 fn column_flex_grow_mixed_grow_and_fixed() {
5917 let engine = LayoutEngine::new();
5920 let font_context = FontContext::new();
5921
5922 let fixed_child = make_styled_view(
5923 Style {
5924 height: Some(Dimension::Pt(50.0)),
5925 ..Default::default()
5926 },
5927 vec![make_text("Fixed", 12.0)],
5928 );
5929 let grow_child = make_styled_view(
5930 Style {
5931 flex_grow: Some(1.0),
5932 ..Default::default()
5933 },
5934 vec![make_text("Grow", 12.0)],
5935 );
5936
5937 let container = make_styled_view(
5938 Style {
5939 flex_direction: Some(FlexDirection::Column),
5940 height: Some(Dimension::Pt(300.0)),
5941 ..Default::default()
5942 },
5943 vec![fixed_child, grow_child],
5944 );
5945
5946 let doc = Document {
5947 children: vec![Node::page(
5948 PageConfig::default(),
5949 Style::default(),
5950 vec![container],
5951 )],
5952 metadata: Default::default(),
5953 default_page: PageConfig::default(),
5954 fonts: vec![],
5955 tagged: false,
5956 pdfa: None,
5957 default_style: None,
5958 embedded_data: None,
5959 flatten_forms: false,
5960 pdf_ua: false,
5961 signature: None,
5962 };
5963
5964 let pages = engine.layout(&doc, &font_context);
5965 let page = &pages[0];
5966
5967 let container_el = page
5968 .elements
5969 .iter()
5970 .find(|e| e.children.len() == 2)
5971 .expect("Should find container with two children");
5972
5973 let fixed_el = &container_el.children[0];
5974 let grow_el = &container_el.children[1];
5975
5976 assert!(
5978 (fixed_el.height - 50.0).abs() < 1.0,
5979 "Fixed child should stay at 50pt, got {}",
5980 fixed_el.height
5981 );
5982
5983 assert!(
5985 (grow_el.height - 250.0).abs() < 2.0,
5986 "Grow child should expand to ~250pt, got {}",
5987 grow_el.height
5988 );
5989 }
5990
5991 #[test]
5992 fn column_flex_grow_page_level() {
5993 let engine = LayoutEngine::new();
5995 let font_context = FontContext::new();
5996
5997 let grow_child = make_styled_view(
5998 Style {
5999 flex_grow: Some(1.0),
6000 ..Default::default()
6001 },
6002 vec![make_text("Fill page", 12.0)],
6003 );
6004
6005 let doc = Document {
6006 children: vec![Node::page(
6007 PageConfig::default(),
6008 Style::default(),
6009 vec![grow_child],
6010 )],
6011 metadata: Default::default(),
6012 default_page: PageConfig::default(),
6013 fonts: vec![],
6014 tagged: false,
6015 pdfa: None,
6016 default_style: None,
6017 embedded_data: None,
6018 flatten_forms: false,
6019 pdf_ua: false,
6020 signature: None,
6021 };
6022
6023 let pages = engine.layout(&doc, &font_context);
6024 let page = &pages[0];
6025
6026 assert!(
6028 !page.elements.is_empty(),
6029 "Page should have at least one element"
6030 );
6031
6032 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
6033 let el = &page.elements[0];
6034 assert!(
6035 (el.height - content_height).abs() < 2.0,
6036 "Page-level flex-grow child should fill content height ({}), got {}",
6037 content_height,
6038 el.height
6039 );
6040 }
6041
6042 #[test]
6043 fn column_flex_grow_with_justify_content() {
6044 let engine = LayoutEngine::new();
6048 let font_context = FontContext::new();
6049
6050 let fixed_child = make_styled_view(
6051 Style {
6052 height: Some(Dimension::Pt(50.0)),
6053 ..Default::default()
6054 },
6055 vec![make_text("Top", 12.0)],
6056 );
6057 let grow_child = make_styled_view(
6058 Style {
6059 flex_grow: Some(1.0),
6060 ..Default::default()
6061 },
6062 vec![make_text("Fill", 12.0)],
6063 );
6064
6065 let container = make_styled_view(
6066 Style {
6067 flex_direction: Some(FlexDirection::Column),
6068 height: Some(Dimension::Pt(300.0)),
6069 justify_content: Some(JustifyContent::Center),
6070 ..Default::default()
6071 },
6072 vec![fixed_child, grow_child],
6073 );
6074
6075 let doc = Document {
6076 children: vec![Node::page(
6077 PageConfig::default(),
6078 Style::default(),
6079 vec![container],
6080 )],
6081 metadata: Default::default(),
6082 default_page: PageConfig::default(),
6083 fonts: vec![],
6084 tagged: false,
6085 pdfa: None,
6086 default_style: None,
6087 embedded_data: None,
6088 flatten_forms: false,
6089 pdf_ua: false,
6090 signature: None,
6091 };
6092
6093 let pages = engine.layout(&doc, &font_context);
6094 let page = &pages[0];
6095
6096 let container_el = page
6097 .elements
6098 .iter()
6099 .find(|e| e.children.len() == 2)
6100 .expect("Should find container");
6101
6102 let first_child = &container_el.children[0];
6105 assert!(
6106 (first_child.y - container_el.y).abs() < 1.0,
6107 "First child should be at top of container"
6108 );
6109
6110 let total = container_el.children[0].height + container_el.children[1].height;
6112 assert!(
6113 (total - 300.0).abs() < 2.0,
6114 "Children should fill container, got {}",
6115 total
6116 );
6117 }
6118
6119 #[test]
6120 fn column_flex_grow_child_justify_content_center() {
6121 let engine = LayoutEngine::new();
6124 let font_context = FontContext::new();
6125
6126 let inner_box = make_styled_view(
6128 Style {
6129 height: Some(Dimension::Pt(40.0)),
6130 ..Default::default()
6131 },
6132 vec![make_text("Centered", 12.0)],
6133 );
6134
6135 let grow_child = make_styled_view(
6137 Style {
6138 flex_grow: Some(1.0),
6139 flex_direction: Some(FlexDirection::Column),
6140 justify_content: Some(JustifyContent::Center),
6141 ..Default::default()
6142 },
6143 vec![inner_box],
6144 );
6145
6146 let container = make_styled_view(
6148 Style {
6149 flex_direction: Some(FlexDirection::Column),
6150 height: Some(Dimension::Pt(400.0)),
6151 ..Default::default()
6152 },
6153 vec![grow_child],
6154 );
6155
6156 let doc = Document {
6157 children: vec![Node::page(
6158 PageConfig::default(),
6159 Style::default(),
6160 vec![container],
6161 )],
6162 metadata: Default::default(),
6163 default_page: PageConfig::default(),
6164 fonts: vec![],
6165 tagged: false,
6166 pdfa: None,
6167 default_style: None,
6168 embedded_data: None,
6169 flatten_forms: false,
6170 pdf_ua: false,
6171 signature: None,
6172 };
6173
6174 let pages = engine.layout(&doc, &font_context);
6175 let page = &pages[0];
6176
6177 let container_el = page
6179 .elements
6180 .iter()
6181 .find(|e| e.height > 350.0 && e.children.len() == 1)
6182 .expect("Should find outer container");
6183
6184 let grow_el = &container_el.children[0];
6185 assert!(
6186 (grow_el.height - 400.0).abs() < 2.0,
6187 "Grow child should expand to 400, got {}",
6188 grow_el.height
6189 );
6190
6191 let inner_el = &grow_el.children[0];
6193 let expected_center = grow_el.y + grow_el.height / 2.0;
6194 let actual_center = inner_el.y + inner_el.height / 2.0;
6195 assert!(
6196 (actual_center - expected_center).abs() < 2.0,
6197 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
6198 expected_center,
6199 actual_center
6200 );
6201 }
6202
6203 #[test]
6204 fn column_flex_grow_child_justify_content_flex_end() {
6205 let engine = LayoutEngine::new();
6207 let font_context = FontContext::new();
6208
6209 let inner_box = make_styled_view(
6210 Style {
6211 height: Some(Dimension::Pt(30.0)),
6212 ..Default::default()
6213 },
6214 vec![make_text("Bottom", 12.0)],
6215 );
6216
6217 let grow_child = make_styled_view(
6218 Style {
6219 flex_grow: Some(1.0),
6220 flex_direction: Some(FlexDirection::Column),
6221 justify_content: Some(JustifyContent::FlexEnd),
6222 ..Default::default()
6223 },
6224 vec![inner_box],
6225 );
6226
6227 let container = make_styled_view(
6228 Style {
6229 flex_direction: Some(FlexDirection::Column),
6230 height: Some(Dimension::Pt(300.0)),
6231 ..Default::default()
6232 },
6233 vec![grow_child],
6234 );
6235
6236 let doc = Document {
6237 children: vec![Node::page(
6238 PageConfig::default(),
6239 Style::default(),
6240 vec![container],
6241 )],
6242 metadata: Default::default(),
6243 default_page: PageConfig::default(),
6244 fonts: vec![],
6245 tagged: false,
6246 pdfa: None,
6247 default_style: None,
6248 embedded_data: None,
6249 flatten_forms: false,
6250 pdf_ua: false,
6251 signature: None,
6252 };
6253
6254 let pages = engine.layout(&doc, &font_context);
6255 let page = &pages[0];
6256
6257 let container_el = page
6258 .elements
6259 .iter()
6260 .find(|e| e.height > 250.0 && e.children.len() == 1)
6261 .expect("Should find outer container");
6262
6263 let grow_el = &container_el.children[0];
6264 let inner_el = &grow_el.children[0];
6265
6266 let inner_bottom = inner_el.y + inner_el.height;
6268 let grow_bottom = grow_el.y + grow_el.height;
6269 assert!(
6270 (inner_bottom - grow_bottom).abs() < 2.0,
6271 "Inner box bottom ({}) should align with grow child bottom ({})",
6272 inner_bottom,
6273 grow_bottom
6274 );
6275 }
6276
6277 #[test]
6278 fn column_flex_grow_child_no_justify_unchanged() {
6279 let engine = LayoutEngine::new();
6281 let font_context = FontContext::new();
6282
6283 let inner_box = make_styled_view(
6284 Style {
6285 height: Some(Dimension::Pt(50.0)),
6286 ..Default::default()
6287 },
6288 vec![make_text("Top", 12.0)],
6289 );
6290
6291 let grow_child = make_styled_view(
6292 Style {
6293 flex_grow: Some(1.0),
6294 flex_direction: Some(FlexDirection::Column),
6295 ..Default::default()
6297 },
6298 vec![inner_box],
6299 );
6300
6301 let container = make_styled_view(
6302 Style {
6303 flex_direction: Some(FlexDirection::Column),
6304 height: Some(Dimension::Pt(300.0)),
6305 ..Default::default()
6306 },
6307 vec![grow_child],
6308 );
6309
6310 let doc = Document {
6311 children: vec![Node::page(
6312 PageConfig::default(),
6313 Style::default(),
6314 vec![container],
6315 )],
6316 metadata: Default::default(),
6317 default_page: PageConfig::default(),
6318 fonts: vec![],
6319 tagged: false,
6320 pdfa: None,
6321 default_style: None,
6322 embedded_data: None,
6323 flatten_forms: false,
6324 pdf_ua: false,
6325 signature: None,
6326 };
6327
6328 let pages = engine.layout(&doc, &font_context);
6329 let page = &pages[0];
6330
6331 let container_el = page
6332 .elements
6333 .iter()
6334 .find(|e| e.height > 250.0 && e.children.len() == 1)
6335 .expect("Should find outer container");
6336
6337 let grow_el = &container_el.children[0];
6338 let inner_el = &grow_el.children[0];
6339
6340 assert!(
6342 (inner_el.y - grow_el.y).abs() < 2.0,
6343 "Inner box ({}) should be at top of grow child ({})",
6344 inner_el.y,
6345 grow_el.y
6346 );
6347 }
6348
6349 #[test]
6350 fn column_flex_grow_child_align_items_center() {
6351 let engine = LayoutEngine::new();
6353 let font_context = FontContext::new();
6354
6355 let text = make_text("Hello", 12.0);
6356
6357 let grow_child = make_styled_view(
6358 Style {
6359 flex_grow: Some(1.0),
6360 flex_direction: Some(FlexDirection::Column),
6361 align_items: Some(AlignItems::Center),
6362 ..Default::default()
6363 },
6364 vec![text],
6365 );
6366
6367 let container = make_styled_view(
6368 Style {
6369 flex_direction: Some(FlexDirection::Column),
6370 height: Some(Dimension::Pt(300.0)),
6371 ..Default::default()
6372 },
6373 vec![grow_child],
6374 );
6375
6376 let doc = Document {
6377 children: vec![Node::page(
6378 PageConfig::default(),
6379 Style::default(),
6380 vec![container],
6381 )],
6382 metadata: Default::default(),
6383 default_page: PageConfig::default(),
6384 fonts: vec![],
6385 tagged: false,
6386 pdfa: None,
6387 default_style: None,
6388 embedded_data: None,
6389 flatten_forms: false,
6390 pdf_ua: false,
6391 signature: None,
6392 };
6393
6394 let pages = engine.layout(&doc, &font_context);
6395 let page = &pages[0];
6396
6397 let container_el = page
6398 .elements
6399 .iter()
6400 .find(|e| e.height > 250.0 && e.children.len() == 1)
6401 .expect("Should find outer container");
6402
6403 let grow_el = &container_el.children[0];
6404 assert!(
6405 !grow_el.children.is_empty(),
6406 "Grow child should have text child"
6407 );
6408
6409 let text_el = &grow_el.children[0];
6410 let text_center = text_el.x + text_el.width / 2.0;
6411 let grow_center = grow_el.x + grow_el.width / 2.0;
6412 assert!(
6413 (text_center - grow_center).abs() < 2.0,
6414 "Text center ({}) should be near grow child center ({})",
6415 text_center,
6416 grow_center
6417 );
6418 }
6419
6420 #[test]
6421 fn image_intrinsic_width_respects_height_constraint() {
6422 let engine = LayoutEngine::new();
6426 let font_context = FontContext::new();
6427
6428 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
6430
6431 let image_node = Node {
6432 kind: NodeKind::Image {
6433 src: one_px_png.to_string(),
6434 width: None,
6435 height: Some(36.0),
6436 },
6437 style: Style::default(),
6438 children: vec![],
6439 id: None,
6440 source_location: None,
6441 bookmark: None,
6442 href: None,
6443 alt: None,
6444 };
6445
6446 let resolved = image_node.style.resolve(None, 0.0);
6447 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
6448
6449 assert!(
6451 (intrinsic - 36.0).abs() < 1.0,
6452 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
6453 intrinsic
6454 );
6455 }
6456}