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