1use std::{
28 collections::{BTreeMap, HashMap},
29 sync::Arc,
30};
31
32use azul_core::{
33 dom::{FormattingContext, NodeId, NodeType},
34 geom::{LogicalPosition, LogicalRect, LogicalSize},
35 resources::RendererResources,
36 styled_dom::{StyledDom, StyledNodeState},
37};
38use azul_css::{
39 css::CssPropertyValue,
40 props::{
41 basic::{
42 font::{StyleFontStyle, StyleFontWeight},
43 pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
44 ColorU, PhysicalSize, PropertyContext, ResolutionContext, SizeMetric,
45 },
46 layout::{
47 ColumnCount, LayoutBorderSpacing, LayoutClear, LayoutDisplay, LayoutFloat,
48 LayoutHeight, LayoutJustifyContent, LayoutOverflow, LayoutPosition, LayoutTableLayout,
49 LayoutTextJustify, LayoutWidth, LayoutWritingMode, ShapeInside, ShapeOutside,
50 StyleBorderCollapse, StyleCaptionSide,
51 },
52 property::CssProperty,
53 style::{
54 BorderStyle, StyleDirection, StyleHyphens, StyleListStylePosition, StyleListStyleType,
55 StyleTextAlign, StyleTextCombineUpright, StyleVerticalAlign, StyleVisibility,
56 StyleWhiteSpace,
57 },
58 },
59};
60use rust_fontconfig::FcWeight;
61use taffy::{AvailableSpace, LayoutInput, Line, Size as TaffySize};
62
63#[cfg(feature = "text_layout")]
64use crate::text3;
65use crate::{
66 debug_ifc_layout, debug_info, debug_log, debug_table_layout, debug_warning,
67 font_traits::{
68 ContentIndex, FontLoaderTrait, ImageSource, InlineContent, InlineImage, InlineShape,
69 LayoutFragment, ObjectFit, ParsedFontTrait, SegmentAlignment, ShapeBoundary,
70 ShapeDefinition, ShapedItem, Size, StyleProperties, StyledRun, TextLayoutCache,
71 UnifiedConstraints,
72 },
73 solver3::{
74 geometry::{BoxProps, EdgeSizes, IntrinsicSizes},
75 getters::{
76 get_css_height, get_css_width, get_display_property, get_element_font_size, get_float,
77 get_list_style_position, get_list_style_type, get_overflow_x, get_overflow_y,
78 get_parent_font_size, get_root_font_size, get_style_properties, get_writing_mode,
79 MultiValue,
80 },
81 layout_tree::{
82 AnonymousBoxType, CachedInlineLayout, LayoutNode, LayoutTree, PseudoElement,
83 },
84 positioning::get_position_type,
85 scrollbar::ScrollbarRequirements,
86 sizing::extract_text_from_node,
87 taffy_bridge, LayoutContext, LayoutDebugMessage, LayoutError, Result,
88 },
89 text3::cache::{AvailableSpace as Text3AvailableSpace, TextAlign as Text3TextAlign},
90};
91
92pub const SCROLLBAR_WIDTH_PX: f32 = 16.0;
95
96#[derive(Debug, Clone)]
100pub(crate) struct BfcLayoutResult {
101 pub output: LayoutOutput,
103 pub escaped_top_margin: Option<f32>,
106 pub escaped_bottom_margin: Option<f32>,
109}
110
111impl BfcLayoutResult {
112 pub fn from_output(output: LayoutOutput) -> Self {
113 Self {
114 output,
115 escaped_top_margin: None,
116 escaped_bottom_margin: None,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum OverflowBehavior {
124 Visible,
125 Hidden,
126 Clip,
127 Scroll,
128 Auto,
129}
130
131impl OverflowBehavior {
132 pub fn is_clipped(&self) -> bool {
133 matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
134 }
135
136 pub fn is_scroll(&self) -> bool {
137 matches!(self, Self::Scroll | Self::Auto)
138 }
139}
140
141#[derive(Debug)]
143pub struct LayoutConstraints<'a> {
144 pub available_size: LogicalSize,
146 pub writing_mode: LayoutWritingMode,
148 pub bfc_state: Option<&'a mut BfcState>,
151 pub text_align: TextAlign,
153 pub containing_block_size: LogicalSize,
156 pub available_width_type: Text3AvailableSpace,
167}
168
169#[derive(Debug, Clone)]
172pub struct BfcState {
173 pub pen: LogicalPosition,
175 pub floats: FloatingContext,
177 pub margins: MarginCollapseContext,
179}
180
181impl BfcState {
182 pub fn new() -> Self {
183 Self {
184 pen: LogicalPosition::zero(),
185 floats: FloatingContext::default(),
186 margins: MarginCollapseContext::default(),
187 }
188 }
189}
190
191#[derive(Debug, Default, Clone)]
193pub struct MarginCollapseContext {
194 pub last_in_flow_margin_bottom: f32,
197}
198
199#[derive(Debug, Default, Clone)]
201pub struct LayoutOutput {
202 pub positions: BTreeMap<usize, LogicalPosition>,
204 pub overflow_size: LogicalSize,
206 pub baseline: Option<f32>,
208}
209
210#[derive(Debug, Clone, Copy, Default)]
212pub enum TextAlign {
213 #[default]
214 Start,
215 End,
216 Center,
217 Justify,
218}
219
220#[derive(Debug, Clone, Copy)]
222struct FloatBox {
223 kind: LayoutFloat,
225 rect: LogicalRect,
227 margin: EdgeSizes,
229}
230
231#[derive(Debug, Default, Clone)]
233pub struct FloatingContext {
234 pub floats: Vec<FloatBox>,
236}
237
238impl FloatingContext {
239 pub fn add_float(&mut self, kind: LayoutFloat, rect: LogicalRect, margin: EdgeSizes) {
241 self.floats.push(FloatBox { kind, rect, margin });
242 }
243
244 pub fn available_line_box_space(
249 &self,
250 main_start: f32,
251 main_end: f32,
252 bfc_cross_size: f32,
253 wm: LayoutWritingMode,
254 ) -> (f32, f32) {
255 let mut available_cross_start = 0.0_f32;
256 let mut available_cross_end = bfc_cross_size;
257
258 for float in &self.floats {
259 let float_main_start = float.rect.origin.main(wm);
261 let float_main_end = float_main_start + float.rect.size.main(wm);
262
263 if main_end > float_main_start && main_start < float_main_end {
265 let float_cross_start = float.rect.origin.cross(wm);
267 let float_cross_end = float_cross_start + float.rect.size.cross(wm);
268
269 if float.kind == LayoutFloat::Left {
270 available_cross_start = available_cross_start.max(float_cross_end);
272 } else {
273 available_cross_end = available_cross_end.min(float_cross_start);
275 }
276 }
277 }
278 (available_cross_start, available_cross_end)
279 }
280
281 pub fn clearance_offset(
283 &self,
284 clear: LayoutClear,
285 current_main_offset: f32,
286 wm: LayoutWritingMode,
287 ) -> f32 {
288 let mut max_end_offset = 0.0_f32;
289
290 let check_left = clear == LayoutClear::Left || clear == LayoutClear::Both;
291 let check_right = clear == LayoutClear::Right || clear == LayoutClear::Both;
292
293 for float in &self.floats {
294 let should_clear_this_float = (check_left && float.kind == LayoutFloat::Left)
295 || (check_right && float.kind == LayoutFloat::Right);
296
297 if should_clear_this_float {
298 let float_margin_box_end = float.rect.origin.main(wm)
301 + float.rect.size.main(wm)
302 + float.margin.main_end(wm);
303 max_end_offset = max_end_offset.max(float_margin_box_end);
304 }
305 }
306
307 if max_end_offset > current_main_offset {
308 max_end_offset
309 } else {
310 current_main_offset
311 }
312 }
313}
314
315struct BfcLayoutState {
317 pen: LogicalPosition,
319 floats: FloatingContext,
320 margins: MarginCollapseContext,
321 writing_mode: LayoutWritingMode,
323}
324
325#[derive(Debug, Default)]
327pub struct LayoutResult {
328 pub positions: Vec<(usize, LogicalPosition)>,
329 pub overflow_size: Option<LogicalSize>,
330 pub baseline_offset: f32,
331}
332
333pub fn layout_formatting_context<T: ParsedFontTrait>(
345 ctx: &mut LayoutContext<'_, T>,
346 tree: &mut LayoutTree,
347 text_cache: &mut crate::font_traits::TextLayoutCache,
348 node_index: usize,
349 constraints: &LayoutConstraints,
350 float_cache: &mut std::collections::BTreeMap<usize, FloatingContext>,
351) -> Result<BfcLayoutResult> {
352 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
353
354 debug_info!(
355 ctx,
356 "[layout_formatting_context] node_index={}, fc={:?}, available_size={:?}",
357 node_index,
358 node.formatting_context,
359 constraints.available_size
360 );
361
362 match node.formatting_context {
363 FormattingContext::Block { .. } => {
364 layout_bfc(ctx, tree, text_cache, node_index, constraints, float_cache)
365 }
366 FormattingContext::Inline => layout_ifc(ctx, text_cache, tree, node_index, constraints)
367 .map(BfcLayoutResult::from_output),
368 FormattingContext::Table => layout_table_fc(ctx, tree, text_cache, node_index, constraints)
369 .map(BfcLayoutResult::from_output),
370 FormattingContext::Flex | FormattingContext::Grid => {
371 layout_flex_grid(ctx, tree, text_cache, node_index, constraints)
372 }
373 _ => {
374 let mut temp_float_cache = std::collections::BTreeMap::new();
376 layout_bfc(
377 ctx,
378 tree,
379 text_cache,
380 node_index,
381 constraints,
382 &mut temp_float_cache,
383 )
384 }
385 }
386}
387
388fn layout_flex_grid<T: ParsedFontTrait>(
403 ctx: &mut LayoutContext<'_, T>,
404 tree: &mut LayoutTree,
405 text_cache: &mut crate::font_traits::TextLayoutCache,
406 node_index: usize,
407 constraints: &LayoutConstraints,
408) -> Result<BfcLayoutResult> {
409 let available_space = TaffySize {
410 width: AvailableSpace::Definite(constraints.available_size.width),
411 height: AvailableSpace::Definite(constraints.available_size.height),
412 };
413
414 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
415
416 let (explicit_width, has_explicit_width) =
420 resolve_explicit_dimension_width(ctx, node, constraints);
421 let (explicit_height, has_explicit_height) =
422 resolve_explicit_dimension_height(ctx, node, constraints);
423
424 let width_adjustment = node.box_props.border.left
429 + node.box_props.border.right
430 + node.box_props.padding.left
431 + node.box_props.padding.right;
432 let height_adjustment = node.box_props.border.top
433 + node.box_props.border.bottom
434 + node.box_props.padding.top
435 + node.box_props.padding.bottom;
436
437 let adjusted_width = explicit_width.map(|w| w + width_adjustment);
439 let adjusted_height = explicit_height.map(|h| h + height_adjustment);
440
441 let sizing_mode = if has_explicit_width || has_explicit_height {
444 taffy::SizingMode::InherentSize
445 } else {
446 taffy::SizingMode::ContentSize
447 };
448
449 let known_dimensions = TaffySize {
450 width: adjusted_width,
451 height: adjusted_height,
452 };
453
454 let taffy_inputs = LayoutInput {
455 known_dimensions,
456 parent_size: translate_taffy_size(constraints.containing_block_size),
457 available_space,
458 run_mode: taffy::RunMode::PerformLayout,
459 sizing_mode,
460 axis: taffy::RequestedAxis::Both,
461 vertical_margins_are_collapsible: Line::FALSE,
463 };
464
465 debug_info!(
466 ctx,
467 "CALLING LAYOUT_TAFFY FOR FLEX/GRID FC node_index={:?}",
468 node_index
469 );
470
471 let taffy_output =
472 taffy_bridge::layout_taffy_subtree(ctx, tree, text_cache, node_index, taffy_inputs);
473
474 let mut output = LayoutOutput::default();
476 output.overflow_size = translate_taffy_size_back(taffy_output.content_size);
479
480 let children: Vec<usize> = tree.get(node_index).unwrap().children.clone();
481 for &child_idx in &children {
482 if let Some(child_node) = tree.get(child_idx) {
483 if let Some(pos) = child_node.relative_position {
484 output.positions.insert(child_idx, pos);
485 }
486 }
487 }
488
489 Ok(BfcLayoutResult::from_output(output))
490}
491
492fn resolve_explicit_dimension_width<T: ParsedFontTrait>(
494 ctx: &LayoutContext<'_, T>,
495 node: &LayoutNode,
496 constraints: &LayoutConstraints,
497) -> (Option<f32>, bool) {
498 node.dom_node_id
499 .map(|id| {
500 let width = get_css_width(
501 ctx.styled_dom,
502 id,
503 &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
504 );
505 match width.unwrap_or_default() {
506 LayoutWidth::Auto => (None, false),
507 LayoutWidth::Px(px) => {
508 let pixels = resolve_size_metric(
509 px.metric,
510 px.number.get(),
511 constraints.available_size.width,
512 );
513 (Some(pixels), true)
514 }
515 LayoutWidth::MinContent | LayoutWidth::MaxContent => (None, false),
516 }
517 })
518 .unwrap_or((None, false))
519}
520
521fn resolve_explicit_dimension_height<T: ParsedFontTrait>(
523 ctx: &LayoutContext<'_, T>,
524 node: &LayoutNode,
525 constraints: &LayoutConstraints,
526) -> (Option<f32>, bool) {
527 node.dom_node_id
528 .map(|id| {
529 let height = get_css_height(
530 ctx.styled_dom,
531 id,
532 &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
533 );
534 match height.unwrap_or_default() {
535 LayoutHeight::Auto => (None, false),
536 LayoutHeight::Px(px) => {
537 let pixels = resolve_size_metric(
538 px.metric,
539 px.number.get(),
540 constraints.available_size.height,
541 );
542 (Some(pixels), true)
543 }
544 LayoutHeight::MinContent | LayoutHeight::MaxContent => (None, false),
545 }
546 })
547 .unwrap_or((None, false))
548}
549
550fn position_float(
553 float_ctx: &FloatingContext,
554 float_type: LayoutFloat,
555 size: LogicalSize,
556 margin: &EdgeSizes,
557 current_main_offset: f32,
558 bfc_cross_size: f32,
559 wm: LayoutWritingMode,
560) -> LogicalRect {
561 let mut main_start = current_main_offset;
563
564 let total_main = size.main(wm) + margin.main_start(wm) + margin.main_end(wm);
566 let total_cross = size.cross(wm) + margin.cross_start(wm) + margin.cross_end(wm);
567
568 let cross_start = loop {
570 let (avail_start, avail_end) = float_ctx.available_line_box_space(
571 main_start,
572 main_start + total_main,
573 bfc_cross_size,
574 wm,
575 );
576
577 let available_width = avail_end - avail_start;
578
579 if available_width >= total_cross {
580 if float_type == LayoutFloat::Left {
582 break avail_start + margin.cross_start(wm);
584 } else {
585 break avail_end - total_cross + margin.cross_start(wm);
587 }
588 }
589
590 let next_main = float_ctx
592 .floats
593 .iter()
594 .filter(|f| {
595 let f_main_start = f.rect.origin.main(wm);
596 let f_main_end = f_main_start + f.rect.size.main(wm);
597 f_main_end > main_start && f_main_start < main_start + total_main
598 })
599 .map(|f| f.rect.origin.main(wm) + f.rect.size.main(wm))
600 .max_by(|a, b| a.partial_cmp(b).unwrap());
601
602 if let Some(next) = next_main {
603 main_start = next;
604 } else {
605 if float_type == LayoutFloat::Left {
607 break avail_start + margin.cross_start(wm);
608 } else {
609 break avail_end - total_cross + margin.cross_start(wm);
610 }
611 }
612 };
613
614 LogicalRect {
615 origin: LogicalPosition::from_main_cross(
616 main_start + margin.main_start(wm),
617 cross_start,
618 wm,
619 ),
620 size,
621 }
622}
623
624fn layout_bfc<T: ParsedFontTrait>(
671 ctx: &mut LayoutContext<'_, T>,
672 tree: &mut LayoutTree,
673 text_cache: &mut crate::font_traits::TextLayoutCache,
674 node_index: usize,
675 constraints: &LayoutConstraints,
676 float_cache: &mut std::collections::BTreeMap<usize, FloatingContext>,
677) -> Result<BfcLayoutResult> {
678 let node = tree
679 .get(node_index)
680 .ok_or(LayoutError::InvalidTree)?
681 .clone();
682 let writing_mode = constraints.writing_mode;
683 let mut output = LayoutOutput::default();
684
685 debug_info!(
686 ctx,
687 "\n[layout_bfc] ENTERED for node_index={}, children.len()={}, incoming_bfc_state={}",
688 node_index,
689 node.children.len(),
690 constraints.bfc_state.is_some()
691 );
692
693 let mut float_context = FloatingContext::default();
698
699 let mut children_containing_block_size = if let Some(used_size) = node.used_size {
707 node.box_props.inner_size(used_size, writing_mode)
709 } else {
710 constraints.available_size
713 };
714
715 let scrollbar_reservation = node
719 .dom_node_id
720 .map(|dom_id| {
721 let styled_node_state = ctx
722 .styled_dom
723 .styled_nodes
724 .as_container()
725 .get(dom_id)
726 .map(|s| s.styled_node_state.clone())
727 .unwrap_or_default();
728 let overflow_y =
729 crate::solver3::getters::get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state);
730 use azul_css::props::layout::LayoutOverflow;
731 match overflow_y.unwrap_or_default() {
732 LayoutOverflow::Scroll | LayoutOverflow::Auto => SCROLLBAR_WIDTH_PX,
733 _ => 0.0,
734 }
735 })
736 .unwrap_or(0.0);
737
738 if scrollbar_reservation > 0.0 {
739 children_containing_block_size.width =
740 (children_containing_block_size.width - scrollbar_reservation).max(0.0);
741 }
742
743 for &child_index in &node.children {
745 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
746 let child_dom_id = child_node.dom_node_id;
747
748 let position_type = get_position_type(ctx.styled_dom, child_dom_id);
750 if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
751 continue;
752 }
753
754 let mut temp_positions = BTreeMap::new();
756 crate::solver3::cache::calculate_layout_for_subtree(
757 ctx,
758 tree,
759 text_cache,
760 child_index,
761 LogicalPosition::zero(),
762 children_containing_block_size, &mut temp_positions,
764 &mut bool::default(),
765 float_cache,
766 )?;
767 }
768
769 let mut main_pen = 0.0f32;
772 let mut max_cross_size = 0.0f32;
773
774 let mut total_escaped_top_margin = 0.0f32;
778 let mut total_sibling_margins = 0.0f32;
780
781 let mut last_margin_bottom = 0.0f32;
783 let mut is_first_child = true;
784 let mut first_child_index: Option<usize> = None;
785 let mut last_child_index: Option<usize> = None;
786
787 let parent_margin_top = node.box_props.margin.main_start(writing_mode);
789 let parent_margin_bottom = node.box_props.margin.main_end(writing_mode);
790
791 let parent_has_top_blocker = has_margin_collapse_blocker(&node.box_props, writing_mode, true);
793 let parent_has_bottom_blocker =
794 has_margin_collapse_blocker(&node.box_props, writing_mode, false);
795
796 let mut accumulated_top_margin = 0.0f32;
798 let mut top_margin_resolved = false;
799 let mut top_margin_escaped = false;
801
802 let mut has_content = false;
804
805 for &child_index in &node.children {
806 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
807 let child_dom_id = child_node.dom_node_id;
808
809 let position_type = get_position_type(ctx.styled_dom, child_dom_id);
810 if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
811 continue;
812 }
813
814 let is_float = if let Some(node_id) = child_dom_id {
816 let float_type = get_float_property(ctx.styled_dom, Some(node_id));
817
818 if float_type != LayoutFloat::None {
819 let float_size = child_node.used_size.unwrap_or_default();
820 let float_margin = &child_node.box_props.margin;
821
822 let float_y = main_pen + last_margin_bottom;
826
827 debug_info!(
828 ctx,
829 "[layout_bfc] Positioning float: index={}, type={:?}, size={:?}, at Y={} \
830 (main_pen={} + last_margin={})",
831 child_index,
832 float_type,
833 float_size,
834 float_y,
835 main_pen,
836 last_margin_bottom
837 );
838
839 let float_rect = position_float(
841 &float_context,
842 float_type,
843 float_size,
844 float_margin,
845 float_y,
847 constraints.available_size.cross(writing_mode),
848 writing_mode,
849 );
850
851 debug_info!(ctx, "[layout_bfc] Float positioned at: {:?}", float_rect);
852
853 float_context.add_float(float_type, float_rect, *float_margin);
855
856 output.positions.insert(child_index, float_rect.origin);
858
859 debug_info!(
860 ctx,
861 "[layout_bfc] *** FLOAT POSITIONED: child={}, main_pen={} (unchanged - floats \
862 don't advance pen)",
863 child_index,
864 main_pen
865 );
866
867 continue;
870 }
871 false
872 } else {
873 false
874 };
875
876 if is_float {
878 continue;
879 }
880
881 if first_child_index.is_none() {
885 first_child_index = Some(child_index);
886 }
887 last_child_index = Some(child_index);
888
889 let child_size = child_node.used_size.unwrap_or_default();
890 let child_margin = &child_node.box_props.margin;
891
892 debug_info!(
893 ctx,
894 "[layout_bfc] Child {} margin from box_props: top={}, right={}, bottom={}, left={}",
895 child_index,
896 child_margin.top,
897 child_margin.right,
898 child_margin.bottom,
899 child_margin.left
900 );
901
902 let child_margin_top = child_margin.main_start(writing_mode);
915 let child_margin_bottom = child_margin.main_end(writing_mode);
916
917 debug_info!(
918 ctx,
919 "[layout_bfc] Child {} final margins: margin_top={}, margin_bottom={}",
920 child_index,
921 child_margin_top,
922 child_margin_bottom
923 );
924
925 let child_has_top_blocker =
927 has_margin_collapse_blocker(&child_node.box_props, writing_mode, true);
928 let child_has_bottom_blocker =
929 has_margin_collapse_blocker(&child_node.box_props, writing_mode, false);
930
931 let child_clear = if let Some(node_id) = child_dom_id {
935 get_clear_property(ctx.styled_dom, Some(node_id))
936 } else {
937 LayoutClear::None
938 };
939 debug_info!(
940 ctx,
941 "[layout_bfc] Child {} clear property: {:?}",
942 child_index,
943 child_clear
944 );
945
946 let is_empty = is_empty_block(child_node);
948
949 if is_empty
953 && !child_has_top_blocker
954 && !child_has_bottom_blocker
955 && child_clear == LayoutClear::None
956 {
957 let self_collapsed = collapse_margins(child_margin_top, child_margin_bottom);
959
960 if is_first_child {
962 is_first_child = false;
963 if !parent_has_top_blocker {
965 accumulated_top_margin = collapse_margins(parent_margin_top, self_collapsed);
966 } else {
967 if accumulated_top_margin == 0.0 {
969 accumulated_top_margin = parent_margin_top;
970 }
971 main_pen += accumulated_top_margin + self_collapsed;
972 top_margin_resolved = true;
973 accumulated_top_margin = 0.0;
974 }
975 last_margin_bottom = self_collapsed;
976 } else {
977 last_margin_bottom = collapse_margins(last_margin_bottom, self_collapsed);
979 }
980
981 continue;
983 }
984
985 let clearance_applied = if child_clear != LayoutClear::None {
990 let cleared_offset =
991 float_context.clearance_offset(child_clear, main_pen, writing_mode);
992 debug_info!(
993 ctx,
994 "[layout_bfc] Child {} clearance check: cleared_offset={}, main_pen={}",
995 child_index,
996 cleared_offset,
997 main_pen
998 );
999 if cleared_offset > main_pen {
1000 debug_info!(
1001 ctx,
1002 "[layout_bfc] Applying clearance: child={}, clear={:?}, old_pen={}, new_pen={}",
1003 child_index,
1004 child_clear,
1005 main_pen,
1006 cleared_offset
1007 );
1008 main_pen = cleared_offset;
1009 true } else {
1011 false
1012 }
1013 } else {
1014 false
1015 };
1016
1017 if is_first_child {
1024 is_first_child = false;
1025
1026 if clearance_applied {
1028 main_pen += child_margin_top;
1034 debug_info!(
1035 ctx,
1036 "[layout_bfc] First child {} with CLEARANCE: no collapse, child_margin={}, \
1037 main_pen={}",
1038 child_index,
1039 child_margin_top,
1040 main_pen
1041 );
1042 } else if !parent_has_top_blocker {
1043 accumulated_top_margin = collapse_margins(parent_margin_top, child_margin_top);
1085 top_margin_resolved = true;
1086 top_margin_escaped = true;
1087
1088 total_escaped_top_margin = accumulated_top_margin;
1092
1093 debug_info!(
1095 ctx,
1096 "[layout_bfc] First child {} margin ESCAPES: parent_margin={}, \
1097 child_margin={}, collapsed={}, total_escaped={}",
1098 child_index,
1099 parent_margin_top,
1100 child_margin_top,
1101 accumulated_top_margin,
1102 total_escaped_top_margin
1103 );
1104 } else {
1105 main_pen += child_margin_top;
1156 debug_info!(
1157 ctx,
1158 "[layout_bfc] First child {} BLOCKED: parent_has_blocker={}, advanced by \
1159 child_margin={}, main_pen={}",
1160 child_index,
1161 parent_has_top_blocker,
1162 child_margin_top,
1163 main_pen
1164 );
1165 }
1166 } else {
1167 if !top_margin_resolved {
1173 main_pen += accumulated_top_margin;
1174 top_margin_resolved = true;
1175 debug_info!(
1176 ctx,
1177 "[layout_bfc] RESOLVED top margin for node {} at sibling {}: accumulated={}, \
1178 main_pen={}",
1179 node_index,
1180 child_index,
1181 accumulated_top_margin,
1182 main_pen
1183 );
1184 }
1185
1186 if clearance_applied {
1187 main_pen += child_margin_top;
1189 debug_info!(
1190 ctx,
1191 "[layout_bfc] Child {} with CLEARANCE: no collapse with sibling, \
1192 child_margin_top={}, main_pen={}",
1193 child_index,
1194 child_margin_top,
1195 main_pen
1196 );
1197 } else {
1198 let collapsed = collapse_margins(last_margin_bottom, child_margin_top);
1232 main_pen += collapsed;
1233 total_sibling_margins += collapsed;
1234 debug_info!(
1235 ctx,
1236 "[layout_bfc] Sibling collapse for child {}: last_margin_bottom={}, \
1237 child_margin_top={}, collapsed={}, main_pen={}, total_sibling_margins={}",
1238 child_index,
1239 last_margin_bottom,
1240 child_margin_top,
1241 collapsed,
1242 main_pen,
1243 total_sibling_margins
1244 );
1245 }
1246 }
1247
1248 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1262 let establishes_bfc = establishes_new_bfc(ctx, child_node);
1263
1264 let (cross_start, cross_end, available_cross) = if establishes_bfc {
1266 let (start, end) = float_context.available_line_box_space(
1268 main_pen,
1269 main_pen + child_size.main(writing_mode),
1270 constraints.available_size.cross(writing_mode),
1271 writing_mode,
1272 );
1273 let available = end - start;
1274
1275 debug_info!(
1276 ctx,
1277 "[layout_bfc] Child {} establishes BFC: shrinking to avoid floats, \
1278 cross_range={}..{}, available_cross={}",
1279 child_index,
1280 start,
1281 end,
1282 available
1283 );
1284
1285 (start, end, available)
1286 } else {
1287 let start = 0.0;
1290 let end = constraints.available_size.cross(writing_mode);
1291 let available = end - start;
1292
1293 debug_info!(
1294 ctx,
1295 "[layout_bfc] Child {} is normal flow: overlapping floats at full width, \
1296 available_cross={}",
1297 child_index,
1298 available
1299 );
1300
1301 (start, end, available)
1302 };
1303
1304 let (child_margin_cloned, is_inline_fc) = {
1306 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1307 (
1308 child_node.box_props.margin.clone(),
1309 child_node.formatting_context == FormattingContext::Inline,
1310 )
1311 };
1312 let child_margin = &child_margin_cloned;
1313
1314 let (child_cross_pos, mut child_main_pos) = if establishes_bfc {
1318 (
1320 cross_start + child_margin.cross_start(writing_mode),
1321 main_pen,
1322 )
1323 } else {
1324 (child_margin.cross_start(writing_mode), main_pen)
1326 };
1327
1328 let child_escaped_margin = child_node.escaped_top_margin.unwrap_or(0.0);
1335 let is_first_in_flow_child = Some(child_index) == first_child_index;
1336
1337 if child_escaped_margin > 0.0 && is_first_in_flow_child {
1338 child_main_pos += child_escaped_margin;
1339 total_escaped_top_margin += child_escaped_margin;
1340 debug_info!(
1341 ctx,
1342 "[layout_bfc] FIRST child {} has escaped_top_margin={}, adjusting position from \
1343 {} to {}, total_escaped={}",
1344 child_index,
1345 child_escaped_margin,
1346 main_pen,
1347 child_main_pos,
1348 total_escaped_top_margin
1349 );
1350 } else if child_escaped_margin > 0.0 {
1351 debug_info!(
1352 ctx,
1353 "[layout_bfc] NON-FIRST child {} has escaped_top_margin={} but NOT adjusting \
1354 position (sibling margin collapse handles this)",
1355 child_index,
1356 child_escaped_margin
1357 );
1358 }
1359
1360 let final_pos =
1361 LogicalPosition::from_main_cross(child_main_pos, child_cross_pos, writing_mode);
1362
1363 debug_info!(
1364 ctx,
1365 "[layout_bfc] *** NORMAL FLOW BLOCK POSITIONED: child={}, final_pos={:?}, \
1366 main_pen={}, establishes_bfc={}",
1367 child_index,
1368 final_pos,
1369 main_pen,
1370 establishes_bfc
1371 );
1372
1373 if is_inline_fc && !establishes_bfc {
1376 let floats_for_ifc = float_cache.get(&node_index).unwrap_or(&float_context);
1379
1380 debug_info!(
1381 ctx,
1382 "[layout_bfc] Re-layouting IFC child {} (normal flow) with parent's float context \
1383 at Y={}, child_cross_pos={}",
1384 child_index,
1385 main_pen,
1386 child_cross_pos
1387 );
1388 debug_info!(
1389 ctx,
1390 "[layout_bfc] Using {} floats (from cache: {})",
1391 floats_for_ifc.floats.len(),
1392 float_cache.contains_key(&node_index)
1393 );
1394
1395 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1399 let padding_border_cross = child_node.box_props.padding.cross_start(writing_mode)
1400 + child_node.box_props.border.cross_start(writing_mode);
1401 let padding_border_main = child_node.box_props.padding.main_start(writing_mode)
1402 + child_node.box_props.border.main_start(writing_mode);
1403
1404 let content_box_cross = child_cross_pos + padding_border_cross;
1406 let content_box_main = main_pen + padding_border_main;
1407
1408 debug_info!(
1409 ctx,
1410 "[layout_bfc] Border-box at ({}, {}), Content-box at ({}, {}), \
1411 padding+border=({}, {})",
1412 child_cross_pos,
1413 main_pen,
1414 content_box_cross,
1415 content_box_main,
1416 padding_border_cross,
1417 padding_border_main
1418 );
1419
1420 let mut ifc_floats = FloatingContext::default();
1421 for float_box in &floats_for_ifc.floats {
1422 let float_rel_to_ifc = LogicalRect {
1424 origin: LogicalPosition {
1425 x: float_box.rect.origin.x - content_box_cross,
1426 y: float_box.rect.origin.y - content_box_main,
1427 },
1428 size: float_box.rect.size,
1429 };
1430
1431 debug_info!(
1432 ctx,
1433 "[layout_bfc] Float {:?}: BFC coords = {:?}, IFC-content-relative = {:?}",
1434 float_box.kind,
1435 float_box.rect,
1436 float_rel_to_ifc
1437 );
1438
1439 ifc_floats.add_float(float_box.kind, float_rel_to_ifc, float_box.margin);
1440 }
1441
1442 let mut bfc_state = BfcState {
1444 pen: LogicalPosition::zero(), floats: ifc_floats.clone(),
1446 margins: MarginCollapseContext::default(),
1447 };
1448
1449 debug_info!(
1450 ctx,
1451 "[layout_bfc] Created IFC-relative FloatingContext with {} floats",
1452 ifc_floats.floats.len()
1453 );
1454
1455 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1457 let child_dom_id = child_node.dom_node_id;
1458
1459 let display = get_display_property(ctx.styled_dom, child_dom_id).unwrap_or_default();
1463 let child_content_size = if display == LayoutDisplay::Inline {
1464 LogicalSize::new(
1466 children_containing_block_size.width,
1467 children_containing_block_size.height,
1468 )
1469 } else {
1470 child_node.box_props.inner_size(child_size, writing_mode)
1472 };
1473
1474 debug_info!(
1475 ctx,
1476 "[layout_bfc] IFC child size: border-box={:?}, content-box={:?}",
1477 child_size,
1478 child_content_size
1479 );
1480
1481 let ifc_constraints = LayoutConstraints {
1484 available_size: child_content_size,
1485 bfc_state: Some(&mut bfc_state),
1486 writing_mode,
1487 text_align: constraints.text_align,
1488 containing_block_size: constraints.containing_block_size,
1489 available_width_type: Text3AvailableSpace::Definite(child_content_size.width),
1490 };
1491
1492 let ifc_result = layout_formatting_context(
1495 ctx,
1496 tree,
1497 text_cache,
1498 child_index,
1499 &ifc_constraints,
1500 float_cache,
1501 )?;
1502
1503 debug_info!(
1507 ctx,
1508 "[layout_bfc] IFC child {} re-layouted with float context (text will wrap, box \
1509 stays full width)",
1510 child_index
1511 );
1512
1513 for (idx, pos) in ifc_result.output.positions {
1515 output.positions.insert(idx, pos);
1516 }
1517 }
1518
1519 output.positions.insert(child_index, final_pos);
1520
1521 if is_first_in_flow_child && child_escaped_margin > 0.0 {
1526 main_pen += child_size.main(writing_mode) + child_escaped_margin;
1527 debug_info!(
1528 ctx,
1529 "[layout_bfc] Advanced main_pen by child_size={} + escaped={} = {} total",
1530 child_size.main(writing_mode),
1531 child_escaped_margin,
1532 main_pen
1533 );
1534 } else {
1535 main_pen += child_size.main(writing_mode);
1536 }
1537 has_content = true;
1538
1539 if clearance_applied {
1544 last_margin_bottom = 0.0;
1546 } else {
1547 last_margin_bottom = child_margin_bottom;
1548 }
1549
1550 debug_info!(
1551 ctx,
1552 "[layout_bfc] Child {} positioned at final_pos={:?}, size={:?}, advanced main_pen to \
1553 {}, last_margin_bottom={}, clearance_applied={}",
1554 child_index,
1555 final_pos,
1556 child_size,
1557 main_pen,
1558 last_margin_bottom,
1559 clearance_applied
1560 );
1561
1562 let child_cross_extent =
1564 child_cross_pos + child_size.cross(writing_mode) + child_margin.cross_end(writing_mode);
1565 max_cross_size = max_cross_size.max(child_cross_extent);
1566 }
1567
1568 debug_info!(
1571 ctx,
1572 "[layout_bfc] Storing {} floats in cache for node {}",
1573 float_context.floats.len(),
1574 node_index
1575 );
1576 float_cache.insert(node_index, float_context.clone());
1577
1578 let mut escaped_top_margin = None;
1580 let mut escaped_bottom_margin = None;
1581
1582 if top_margin_escaped {
1584 escaped_top_margin = Some(accumulated_top_margin);
1586 debug_info!(
1587 ctx,
1588 "[layout_bfc] Returning escaped top margin: accumulated={}, node={}",
1589 accumulated_top_margin,
1590 node_index
1591 );
1592 } else if !top_margin_resolved && accumulated_top_margin > 0.0 {
1593 escaped_top_margin = Some(accumulated_top_margin);
1595 debug_info!(
1596 ctx,
1597 "[layout_bfc] Escaping top margin (no content): accumulated={}, node={}",
1598 accumulated_top_margin,
1599 node_index
1600 );
1601 } else if !top_margin_resolved {
1602 escaped_top_margin = Some(accumulated_top_margin);
1604 debug_info!(
1605 ctx,
1606 "[layout_bfc] Escaping top margin (zero, no content): accumulated={}, node={}",
1607 accumulated_top_margin,
1608 node_index
1609 );
1610 } else {
1611 debug_info!(
1612 ctx,
1613 "[layout_bfc] NOT escaping top margin: top_margin_resolved={}, escaped={}, \
1614 accumulated={}, node={}",
1615 top_margin_resolved,
1616 top_margin_escaped,
1617 accumulated_top_margin,
1618 node_index
1619 );
1620 }
1621
1622 if let Some(last_idx) = last_child_index {
1624 let last_child = tree.get(last_idx).ok_or(LayoutError::InvalidTree)?;
1625 let last_has_bottom_blocker =
1626 has_margin_collapse_blocker(&last_child.box_props, writing_mode, false);
1627
1628 debug_info!(
1629 ctx,
1630 "[layout_bfc] Bottom margin for node {}: parent_has_bottom_blocker={}, \
1631 last_has_bottom_blocker={}, last_margin_bottom={}, main_pen_before={}",
1632 node_index,
1633 parent_has_bottom_blocker,
1634 last_has_bottom_blocker,
1635 last_margin_bottom,
1636 main_pen
1637 );
1638
1639 if !parent_has_bottom_blocker && !last_has_bottom_blocker && has_content {
1640 let collapsed_bottom = collapse_margins(parent_margin_bottom, last_margin_bottom);
1642 escaped_bottom_margin = Some(collapsed_bottom);
1643 debug_info!(
1644 ctx,
1645 "[layout_bfc] Bottom margin ESCAPED for node {}: collapsed={}",
1646 node_index,
1647 collapsed_bottom
1648 );
1649 } else {
1651 main_pen += last_margin_bottom;
1653 debug_info!(
1657 ctx,
1658 "[layout_bfc] Bottom margin BLOCKED for node {}: added last_margin_bottom={}, \
1659 main_pen_after={}",
1660 node_index,
1661 last_margin_bottom,
1662 main_pen
1663 );
1664 }
1665 } else {
1666 if !top_margin_resolved {
1668 main_pen += parent_margin_top;
1669 }
1670 main_pen += parent_margin_bottom;
1671 }
1672
1673 let is_root_node = node.parent.is_none();
1676 if is_root_node {
1677 if let Some(top) = escaped_top_margin {
1678 for (_, pos) in output.positions.iter_mut() {
1680 let current_main = pos.main(writing_mode);
1681 *pos = LogicalPosition::from_main_cross(
1682 current_main + top,
1683 pos.cross(writing_mode),
1684 writing_mode,
1685 );
1686 }
1687 main_pen += top;
1688 }
1689 if let Some(bottom) = escaped_bottom_margin {
1690 main_pen += bottom;
1691 }
1692 escaped_top_margin = None;
1694 escaped_bottom_margin = None;
1695 }
1696
1697 let content_box_height = main_pen - total_escaped_top_margin;
1764 output.overflow_size =
1765 LogicalSize::from_main_cross(content_box_height, max_cross_size, writing_mode);
1766
1767 debug_info!(
1768 ctx,
1769 "[layout_bfc] FINAL for node {}: main_pen={}, total_escaped_top={}, \
1770 total_sibling_margins={}, content_box_height={}",
1771 node_index,
1772 main_pen,
1773 total_escaped_top_margin,
1774 total_sibling_margins,
1775 content_box_height
1776 );
1777
1778 output.baseline = None;
1780
1781 if let Some(node_mut) = tree.get_mut(node_index) {
1783 node_mut.escaped_top_margin = escaped_top_margin;
1784 node_mut.escaped_bottom_margin = escaped_bottom_margin;
1785 }
1786
1787 if let Some(node_mut) = tree.get_mut(node_index) {
1788 node_mut.baseline = output.baseline;
1789 }
1790
1791 Ok(BfcLayoutResult {
1792 output,
1793 escaped_top_margin,
1794 escaped_bottom_margin,
1795 })
1796}
1797
1798fn layout_ifc<T: ParsedFontTrait>(
1826 ctx: &mut LayoutContext<'_, T>,
1827 text_cache: &mut crate::font_traits::TextLayoutCache,
1828 tree: &mut LayoutTree,
1829 node_index: usize,
1830 constraints: &LayoutConstraints,
1831) -> Result<LayoutOutput> {
1832 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1833 let float_count = constraints
1834 .bfc_state
1835 .as_ref()
1836 .map(|s| s.floats.floats.len())
1837 .unwrap_or(0);
1838 debug_info!(
1839 ctx,
1840 "[layout_ifc] ENTRY: node_index={}, has_bfc_state={}, float_count={}",
1841 node_index,
1842 constraints.bfc_state.is_some(),
1843 float_count
1844 );
1845 debug_ifc_layout!(ctx, "CALLED for node_index={}", node_index);
1846
1847 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1850 let ifc_root_dom_id = match node.dom_node_id {
1851 Some(id) => id,
1852 None => {
1853 let parent_dom_id = node
1855 .parent
1856 .and_then(|p| tree.get(p))
1857 .and_then(|n| n.dom_node_id);
1858
1859 if let Some(id) = parent_dom_id {
1860 id
1861 } else {
1862 node.children
1864 .iter()
1865 .filter_map(|&child_idx| tree.get(child_idx))
1866 .filter_map(|n| n.dom_node_id)
1867 .next()
1868 .ok_or(LayoutError::InvalidTree)?
1869 }
1870 }
1871 };
1872
1873 debug_ifc_layout!(ctx, "ifc_root_dom_id={:?}", ifc_root_dom_id);
1874
1875 let (inline_content, child_map) =
1877 collect_and_measure_inline_content(ctx, text_cache, tree, node_index, constraints)?;
1878
1879 debug_info!(
1880 ctx,
1881 "[layout_ifc] Collected {} inline content items for node {}",
1882 inline_content.len(),
1883 node_index
1884 );
1885 for (i, item) in inline_content.iter().enumerate() {
1886 match item {
1887 InlineContent::Text(run) => debug_info!(ctx, " [{}] Text: '{}'", i, run.text),
1888 InlineContent::Marker {
1889 run,
1890 position_outside,
1891 } => debug_info!(
1892 ctx,
1893 " [{}] Marker: '{}' (outside={})",
1894 i,
1895 run.text,
1896 position_outside
1897 ),
1898 InlineContent::Shape(_) => debug_info!(ctx, " [{}] Shape", i),
1899 InlineContent::Image(_) => debug_info!(ctx, " [{}] Image", i),
1900 _ => debug_info!(ctx, " [{}] Other", i),
1901 }
1902 }
1903
1904 debug_ifc_layout!(
1905 ctx,
1906 "Collected {} inline content items",
1907 inline_content.len()
1908 );
1909
1910 if inline_content.is_empty() {
1911 debug_warning!(ctx, "inline_content is empty, returning default output!");
1912 return Ok(LayoutOutput::default());
1913 }
1914
1915 let text3_constraints =
1917 translate_to_text3_constraints(ctx, constraints, ctx.styled_dom, ifc_root_dom_id);
1918
1919 let cached_constraints = text3_constraints.clone();
1921
1922 debug_info!(
1923 ctx,
1924 "[layout_ifc] CALLING text_cache.layout_flow for node {} with {} exclusions",
1925 node_index,
1926 text3_constraints.shape_exclusions.len()
1927 );
1928
1929 let fragments = vec![LayoutFragment {
1930 id: "main".to_string(),
1931 constraints: text3_constraints,
1932 }];
1933
1934 let loaded_fonts = ctx.font_manager.get_loaded_fonts();
1937 let text_layout_result = match text_cache.layout_flow(
1938 &inline_content,
1939 &[],
1940 &fragments,
1941 &ctx.font_manager.font_chain_cache,
1942 &ctx.font_manager.fc_cache,
1943 &loaded_fonts,
1944 ctx.debug_messages,
1945 ) {
1946 Ok(result) => result,
1947 Err(e) => {
1948 debug_warning!(ctx, "Text layout failed: {:?}", e);
1951 debug_warning!(
1952 ctx,
1953 "Continuing with zero-sized layout for node {}",
1954 node_index
1955 );
1956
1957 let mut output = LayoutOutput::default();
1958 output.overflow_size = LogicalSize::new(0.0, 0.0);
1959 return Ok(output);
1960 }
1961 };
1962
1963 let mut output = LayoutOutput::default();
1965 let node = tree.get_mut(node_index).ok_or(LayoutError::InvalidTree)?;
1966
1967 debug_ifc_layout!(
1968 ctx,
1969 "text_layout_result has {} fragment_layouts",
1970 text_layout_result.fragment_layouts.len()
1971 );
1972
1973 if let Some(main_frag) = text_layout_result.fragment_layouts.get("main") {
1974 let frag_bounds = main_frag.bounds();
1975 debug_ifc_layout!(
1976 ctx,
1977 "Found 'main' fragment with {} items, bounds={}x{}",
1978 main_frag.items.len(),
1979 frag_bounds.width,
1980 frag_bounds.height
1981 );
1982 debug_ifc_layout!(ctx, "Storing inline_layout_result on node {}", node_index);
1983
1984 let has_floats = constraints
1995 .bfc_state
1996 .as_ref()
1997 .map(|s| !s.floats.floats.is_empty())
1998 .unwrap_or(false);
1999 let current_width_type = constraints.available_width_type;
2000
2001 let should_store = match &node.inline_layout_result {
2002 None => {
2003 debug_info!(
2005 ctx,
2006 "[layout_ifc] Storing NEW inline_layout_result for node {} (width_type={:?}, \
2007 has_floats={})",
2008 node_index,
2009 current_width_type,
2010 has_floats
2011 );
2012 true
2013 }
2014 Some(cached) => {
2015 if cached.should_replace_with(current_width_type, has_floats) {
2017 debug_info!(
2018 ctx,
2019 "[layout_ifc] REPLACING inline_layout_result for node {} (old: \
2020 width={:?}, floats={}) with (new: width={:?}, floats={})",
2021 node_index,
2022 cached.available_width,
2023 cached.has_floats,
2024 current_width_type,
2025 has_floats
2026 );
2027 true
2028 } else {
2029 debug_info!(
2030 ctx,
2031 "[layout_ifc] KEEPING cached inline_layout_result for node {} (cached: \
2032 width={:?}, floats={}, new: width={:?}, floats={})",
2033 node_index,
2034 cached.available_width,
2035 cached.has_floats,
2036 current_width_type,
2037 has_floats
2038 );
2039 false
2040 }
2041 }
2042 };
2043
2044 if should_store {
2045 node.inline_layout_result = Some(CachedInlineLayout::new_with_constraints(
2046 main_frag.clone(),
2047 current_width_type,
2048 has_floats,
2049 cached_constraints,
2050 ));
2051 }
2052
2053 output.overflow_size = LogicalSize::new(frag_bounds.width, frag_bounds.height);
2055 output.baseline = main_frag.last_baseline();
2056 node.baseline = output.baseline;
2057
2058 for positioned_item in &main_frag.items {
2060 if let ShapedItem::Object { source, content, .. } = &positioned_item.item {
2061 if let Some(&child_node_index) = child_map.get(source) {
2062 let new_relative_pos = LogicalPosition {
2063 x: positioned_item.position.x,
2064 y: positioned_item.position.y,
2065 };
2066 output.positions.insert(child_node_index, new_relative_pos);
2067 }
2068 }
2069 }
2070 }
2071
2072 Ok(output)
2073}
2074
2075fn translate_taffy_size(size: LogicalSize) -> TaffySize<Option<f32>> {
2076 TaffySize {
2077 width: Some(size.width),
2078 height: Some(size.height),
2079 }
2080}
2081
2082pub(crate) fn convert_font_style(style: StyleFontStyle) -> crate::font_traits::FontStyle {
2084 match style {
2085 StyleFontStyle::Normal => crate::font_traits::FontStyle::Normal,
2086 StyleFontStyle::Italic => crate::font_traits::FontStyle::Italic,
2087 StyleFontStyle::Oblique => crate::font_traits::FontStyle::Oblique,
2088 }
2089}
2090
2091pub(crate) fn convert_font_weight(weight: StyleFontWeight) -> FcWeight {
2093 match weight {
2094 StyleFontWeight::W100 => FcWeight::Thin,
2095 StyleFontWeight::W200 => FcWeight::ExtraLight,
2096 StyleFontWeight::W300 | StyleFontWeight::Lighter => FcWeight::Light,
2097 StyleFontWeight::Normal => FcWeight::Normal,
2098 StyleFontWeight::W500 => FcWeight::Medium,
2099 StyleFontWeight::W600 => FcWeight::SemiBold,
2100 StyleFontWeight::Bold => FcWeight::Bold,
2101 StyleFontWeight::W800 => FcWeight::ExtraBold,
2102 StyleFontWeight::W900 | StyleFontWeight::Bolder => FcWeight::Black,
2103 }
2104}
2105
2106#[inline]
2108fn resolve_size_metric(metric: SizeMetric, value: f32, containing_block_size: f32) -> f32 {
2109 match metric {
2110 SizeMetric::Px => value,
2111 SizeMetric::Pt => value * PT_TO_PX,
2112 SizeMetric::Percent => value / 100.0 * containing_block_size,
2113 SizeMetric::Em | SizeMetric::Rem => value * DEFAULT_FONT_SIZE,
2114 _ => value, }
2116}
2117
2118pub fn translate_taffy_size_back(size: TaffySize<f32>) -> LogicalSize {
2119 LogicalSize {
2120 width: size.width,
2121 height: size.height,
2122 }
2123}
2124
2125pub fn translate_taffy_point_back(point: taffy::Point<f32>) -> LogicalPosition {
2126 LogicalPosition {
2127 x: point.x,
2128 y: point.y,
2129 }
2130}
2131
2132fn establishes_new_bfc<T: ParsedFontTrait>(ctx: &LayoutContext<'_, T>, node: &LayoutNode) -> bool {
2147 let Some(dom_id) = node.dom_node_id else {
2148 return false;
2149 };
2150
2151 let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2152
2153 let float_val = get_float(ctx.styled_dom, dom_id, node_state);
2155 if matches!(
2156 float_val,
2157 MultiValue::Exact(LayoutFloat::Left | LayoutFloat::Right)
2158 ) {
2159 return true;
2160 }
2161
2162 let position = crate::solver3::positioning::get_position_type(ctx.styled_dom, Some(dom_id));
2164 if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
2165 return true;
2166 }
2167
2168 let display = get_display_property(ctx.styled_dom, Some(dom_id));
2170 if matches!(
2171 display,
2172 MultiValue::Exact(
2173 LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption
2174 )
2175 ) {
2176 return true;
2177 }
2178
2179 if matches!(display, MultiValue::Exact(LayoutDisplay::FlowRoot)) {
2181 return true;
2182 }
2183
2184 let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, node_state);
2187 let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, node_state);
2188
2189 let creates_bfc_via_overflow = |ov: &MultiValue<LayoutOverflow>| {
2190 matches!(
2191 ov,
2192 &MultiValue::Exact(
2193 LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto
2194 )
2195 )
2196 };
2197
2198 if creates_bfc_via_overflow(&overflow_x) || creates_bfc_via_overflow(&overflow_y) {
2199 return true;
2200 }
2201
2202 if matches!(
2204 node.formatting_context,
2205 FormattingContext::Table | FormattingContext::Flex | FormattingContext::Grid
2206 ) {
2207 return true;
2208 }
2209
2210 false
2212}
2213
2214fn translate_to_text3_constraints<'a, T: ParsedFontTrait>(
2216 ctx: &mut LayoutContext<'_, T>,
2217 constraints: &'a LayoutConstraints<'a>,
2218 styled_dom: &StyledDom,
2219 dom_id: NodeId,
2220) -> UnifiedConstraints {
2221 let mut shape_exclusions = if let Some(ref bfc_state) = constraints.bfc_state {
2223 debug_info!(
2224 ctx,
2225 "[translate_to_text3] dom_id={:?}, converting {} floats to exclusions",
2226 dom_id,
2227 bfc_state.floats.floats.len()
2228 );
2229 bfc_state
2230 .floats
2231 .floats
2232 .iter()
2233 .enumerate()
2234 .map(|(i, float_box)| {
2235 let rect = crate::text3::cache::Rect {
2236 x: float_box.rect.origin.x,
2237 y: float_box.rect.origin.y,
2238 width: float_box.rect.size.width,
2239 height: float_box.rect.size.height,
2240 };
2241 debug_info!(
2242 ctx,
2243 "[translate_to_text3] Exclusion #{}: {:?} at ({}, {}) size {}x{}",
2244 i,
2245 float_box.kind,
2246 rect.x,
2247 rect.y,
2248 rect.width,
2249 rect.height
2250 );
2251 ShapeBoundary::Rectangle(rect)
2252 })
2253 .collect()
2254 } else {
2255 debug_info!(
2256 ctx,
2257 "[translate_to_text3] dom_id={:?}, NO bfc_state - no float exclusions",
2258 dom_id
2259 );
2260 Vec::new()
2261 };
2262
2263 debug_info!(
2264 ctx,
2265 "[translate_to_text3] dom_id={:?}, available_size={}x{}, shape_exclusions.len()={}",
2266 dom_id,
2267 constraints.available_size.width,
2268 constraints.available_size.height,
2269 shape_exclusions.len()
2270 );
2271
2272 let id = dom_id;
2274 let node_data = &styled_dom.node_data.as_container()[id];
2275 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2276
2277 let ref_box_height = if constraints.available_size.height.is_finite() {
2282 constraints.available_size.height
2283 } else {
2284 styled_dom
2288 .css_property_cache
2289 .ptr
2290 .get_height(node_data, &id, node_state)
2291 .and_then(|v| v.get_property())
2292 .and_then(|h| match h {
2293 LayoutHeight::Px(v) => {
2294 match v.metric {
2297 SizeMetric::Px => Some(v.number.get()),
2298 SizeMetric::Pt => Some(v.number.get() * PT_TO_PX),
2299 SizeMetric::In => Some(v.number.get() * 96.0),
2300 SizeMetric::Cm => Some(v.number.get() * 96.0 / 2.54),
2301 SizeMetric::Mm => Some(v.number.get() * 96.0 / 25.4),
2302 _ => None, }
2304 }
2305 _ => None,
2306 })
2307 .unwrap_or(constraints.available_size.width) };
2309
2310 let reference_box = crate::text3::cache::Rect {
2311 x: 0.0,
2312 y: 0.0,
2313 width: constraints.available_size.width,
2314 height: ref_box_height,
2315 };
2316
2317 debug_info!(ctx, "Checking shape-inside for node {:?}", id);
2319 debug_info!(
2320 ctx,
2321 "Reference box: {:?} (available_size height was: {})",
2322 reference_box,
2323 constraints.available_size.height
2324 );
2325
2326 let shape_boundaries = styled_dom
2327 .css_property_cache
2328 .ptr
2329 .get_shape_inside(node_data, &id, node_state)
2330 .and_then(|v| {
2331 debug_info!(ctx, "Got shape-inside value: {:?}", v);
2332 v.get_property()
2333 })
2334 .and_then(|shape_inside| {
2335 debug_info!(ctx, "shape-inside property: {:?}", shape_inside);
2336 if let ShapeInside::Shape(css_shape) = shape_inside {
2337 debug_info!(
2338 ctx,
2339 "Converting CSS shape to ShapeBoundary: {:?}",
2340 css_shape
2341 );
2342 let boundary =
2343 ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
2344 debug_info!(ctx, "Created ShapeBoundary: {:?}", boundary);
2345 Some(vec![boundary])
2346 } else {
2347 debug_info!(ctx, "shape-inside is None");
2348 None
2349 }
2350 })
2351 .unwrap_or_default();
2352
2353 debug_info!(
2354 ctx,
2355 "Final shape_boundaries count: {}",
2356 shape_boundaries.len()
2357 );
2358
2359 debug_info!(ctx, "Checking shape-outside for node {:?}", id);
2361 if let Some(shape_outside_value) = styled_dom
2362 .css_property_cache
2363 .ptr
2364 .get_shape_outside(node_data, &id, node_state)
2365 {
2366 debug_info!(ctx, "Got shape-outside value: {:?}", shape_outside_value);
2367 if let Some(shape_outside) = shape_outside_value.get_property() {
2368 debug_info!(ctx, "shape-outside property: {:?}", shape_outside);
2369 if let ShapeOutside::Shape(css_shape) = shape_outside {
2370 debug_info!(
2371 ctx,
2372 "Converting CSS shape-outside to ShapeBoundary: {:?}",
2373 css_shape
2374 );
2375 let boundary =
2376 ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
2377 debug_info!(ctx, "Created ShapeBoundary (exclusion): {:?}", boundary);
2378 shape_exclusions.push(boundary);
2379 }
2380 }
2381 } else {
2382 debug_info!(ctx, "No shape-outside value found");
2383 }
2384
2385 let writing_mode = styled_dom
2388 .css_property_cache
2389 .ptr
2390 .get_writing_mode(node_data, &id, node_state)
2391 .and_then(|s| s.get_property().copied())
2392 .unwrap_or_default();
2393
2394 let text_align = styled_dom
2395 .css_property_cache
2396 .ptr
2397 .get_text_align(node_data, &id, node_state)
2398 .and_then(|s| s.get_property().copied())
2399 .unwrap_or_default();
2400
2401 let text_justify = styled_dom
2402 .css_property_cache
2403 .ptr
2404 .get_text_justify(node_data, &id, node_state)
2405 .and_then(|s| s.get_property().copied())
2406 .unwrap_or_default();
2407
2408 let font_size = get_element_font_size(styled_dom, id, node_state);
2411
2412 let line_height_value = styled_dom
2413 .css_property_cache
2414 .ptr
2415 .get_line_height(node_data, &id, node_state)
2416 .and_then(|s| s.get_property().cloned())
2417 .unwrap_or_default();
2418
2419 let hyphenation = styled_dom
2420 .css_property_cache
2421 .ptr
2422 .get_hyphens(node_data, &id, node_state)
2423 .and_then(|s| s.get_property().copied())
2424 .unwrap_or_default();
2425
2426 let overflow_behaviour = styled_dom
2427 .css_property_cache
2428 .ptr
2429 .get_overflow_x(node_data, &id, node_state)
2430 .and_then(|s| s.get_property().copied())
2431 .unwrap_or_default();
2432
2433 let vertical_align = styled_dom
2435 .css_property_cache
2436 .ptr
2437 .get_vertical_align(node_data, &id, node_state)
2438 .and_then(|s| s.get_property().copied())
2439 .unwrap_or_default();
2440
2441 let vertical_align = match vertical_align {
2442 StyleVerticalAlign::Baseline => text3::cache::VerticalAlign::Baseline,
2443 StyleVerticalAlign::Top => text3::cache::VerticalAlign::Top,
2444 StyleVerticalAlign::Middle => text3::cache::VerticalAlign::Middle,
2445 StyleVerticalAlign::Bottom => text3::cache::VerticalAlign::Bottom,
2446 StyleVerticalAlign::Sub => text3::cache::VerticalAlign::Sub,
2447 StyleVerticalAlign::Superscript => text3::cache::VerticalAlign::Super,
2448 StyleVerticalAlign::TextTop => text3::cache::VerticalAlign::TextTop,
2449 StyleVerticalAlign::TextBottom => text3::cache::VerticalAlign::TextBottom,
2450 };
2451 let text_orientation = text3::cache::TextOrientation::default();
2452
2453 let direction = styled_dom
2455 .css_property_cache
2456 .ptr
2457 .get_direction(node_data, &id, node_state)
2458 .and_then(|s| s.get_property().copied())
2459 .map(|d| match d {
2460 StyleDirection::Ltr => text3::cache::BidiDirection::Ltr,
2461 StyleDirection::Rtl => text3::cache::BidiDirection::Rtl,
2462 });
2463
2464 debug_info!(
2465 ctx,
2466 "dom_id={:?}, available_size={}x{}, setting available_width={}",
2467 dom_id,
2468 constraints.available_size.width,
2469 constraints.available_size.height,
2470 constraints.available_size.width
2471 );
2472
2473 let text_indent = styled_dom
2475 .css_property_cache
2476 .ptr
2477 .get_text_indent(node_data, &id, node_state)
2478 .and_then(|s| s.get_property())
2479 .map(|ti| {
2480 let context = ResolutionContext {
2481 element_font_size: get_element_font_size(styled_dom, id, node_state),
2482 parent_font_size: get_parent_font_size(styled_dom, id, node_state),
2483 root_font_size: get_root_font_size(styled_dom, node_state),
2484 containing_block_size: PhysicalSize::new(constraints.available_size.width, 0.0),
2485 element_size: None,
2486 viewport_size: PhysicalSize::new(0.0, 0.0),
2487 };
2488 ti.inner
2489 .resolve_with_context(&context, PropertyContext::Other)
2490 })
2491 .unwrap_or(0.0);
2492
2493 let columns = styled_dom
2495 .css_property_cache
2496 .ptr
2497 .get_column_count(node_data, &id, node_state)
2498 .and_then(|s| s.get_property())
2499 .map(|cc| match cc {
2500 ColumnCount::Integer(n) => *n,
2501 ColumnCount::Auto => 1,
2502 })
2503 .unwrap_or(1);
2504
2505 let column_gap = styled_dom
2507 .css_property_cache
2508 .ptr
2509 .get_column_gap(node_data, &id, node_state)
2510 .and_then(|s| s.get_property())
2511 .map(|cg| {
2512 let context = ResolutionContext {
2513 element_font_size: get_element_font_size(styled_dom, id, node_state),
2514 parent_font_size: get_parent_font_size(styled_dom, id, node_state),
2515 root_font_size: get_root_font_size(styled_dom, node_state),
2516 containing_block_size: PhysicalSize::new(0.0, 0.0),
2517 element_size: None,
2518 viewport_size: PhysicalSize::new(0.0, 0.0),
2519 };
2520 cg.inner
2521 .resolve_with_context(&context, PropertyContext::Other)
2522 })
2523 .unwrap_or_else(|| {
2524 get_element_font_size(styled_dom, id, node_state)
2526 });
2527
2528 let text_wrap = styled_dom
2530 .css_property_cache
2531 .ptr
2532 .get_white_space(node_data, &id, node_state)
2533 .and_then(|s| s.get_property())
2534 .map(|ws| match ws {
2535 StyleWhiteSpace::Normal => text3::cache::TextWrap::Wrap,
2536 StyleWhiteSpace::Nowrap => text3::cache::TextWrap::NoWrap,
2537 StyleWhiteSpace::Pre => text3::cache::TextWrap::NoWrap,
2538 })
2539 .unwrap_or(text3::cache::TextWrap::Wrap);
2540
2541 let initial_letter = styled_dom
2543 .css_property_cache
2544 .ptr
2545 .get_initial_letter(node_data, &id, node_state)
2546 .and_then(|s| s.get_property())
2547 .map(|il| {
2548 use std::num::NonZeroUsize;
2549 let sink = match il.sink {
2550 azul_css::corety::OptionU32::Some(s) => s,
2551 azul_css::corety::OptionU32::None => il.size,
2552 };
2553 text3::cache::InitialLetter {
2554 size: il.size as f32,
2555 sink,
2556 count: NonZeroUsize::new(1).unwrap(),
2557 }
2558 });
2559
2560 let line_clamp = styled_dom
2562 .css_property_cache
2563 .ptr
2564 .get_line_clamp(node_data, &id, node_state)
2565 .and_then(|s| s.get_property())
2566 .and_then(|lc| std::num::NonZeroUsize::new(lc.max_lines));
2567
2568 let hanging_punctuation = styled_dom
2570 .css_property_cache
2571 .ptr
2572 .get_hanging_punctuation(node_data, &id, node_state)
2573 .and_then(|s| s.get_property())
2574 .map(|hp| hp.enabled)
2575 .unwrap_or(false);
2576
2577 let text_combine_upright = styled_dom
2579 .css_property_cache
2580 .ptr
2581 .get_text_combine_upright(node_data, &id, node_state)
2582 .and_then(|s| s.get_property())
2583 .map(|tcu| match tcu {
2584 StyleTextCombineUpright::None => text3::cache::TextCombineUpright::None,
2585 StyleTextCombineUpright::All => text3::cache::TextCombineUpright::All,
2586 StyleTextCombineUpright::Digits(n) => text3::cache::TextCombineUpright::Digits(*n),
2587 });
2588
2589 let exclusion_margin = styled_dom
2591 .css_property_cache
2592 .ptr
2593 .get_exclusion_margin(node_data, &id, node_state)
2594 .and_then(|s| s.get_property())
2595 .map(|em| em.inner.get() as f32)
2596 .unwrap_or(0.0);
2597
2598 let hyphenation_language = styled_dom
2600 .css_property_cache
2601 .ptr
2602 .get_hyphenation_language(node_data, &id, node_state)
2603 .and_then(|s| s.get_property())
2604 .and_then(|hl| {
2605 #[cfg(feature = "text_layout_hyphenation")]
2606 {
2607 use hyphenation::{Language, Load};
2608 match hl.inner.as_str() {
2610 "en-US" | "en" => Some(Language::EnglishUS),
2611 "de-DE" | "de" => Some(Language::German1996),
2612 "fr-FR" | "fr" => Some(Language::French),
2613 "es-ES" | "es" => Some(Language::Spanish),
2614 "it-IT" | "it" => Some(Language::Italian),
2615 "pt-PT" | "pt" => Some(Language::Portuguese),
2616 "nl-NL" | "nl" => Some(Language::Dutch),
2617 "pl-PL" | "pl" => Some(Language::Polish),
2618 "ru-RU" | "ru" => Some(Language::Russian),
2619 "zh-CN" | "zh" => Some(Language::Chinese),
2620 _ => None, }
2622 }
2623 #[cfg(not(feature = "text_layout_hyphenation"))]
2624 {
2625 None::<crate::text3::script::Language>
2626 }
2627 });
2628
2629 UnifiedConstraints {
2630 exclusion_margin,
2631 hyphenation_language,
2632 text_indent,
2633 initial_letter,
2634 line_clamp,
2635 columns,
2636 column_gap,
2637 hanging_punctuation,
2638 text_wrap,
2639 text_combine_upright,
2640 segment_alignment: SegmentAlignment::Total,
2641 overflow: match overflow_behaviour {
2642 LayoutOverflow::Visible => text3::cache::OverflowBehavior::Visible,
2643 LayoutOverflow::Hidden | LayoutOverflow::Clip => text3::cache::OverflowBehavior::Hidden,
2644 LayoutOverflow::Scroll => text3::cache::OverflowBehavior::Scroll,
2645 LayoutOverflow::Auto => text3::cache::OverflowBehavior::Auto,
2646 },
2647 available_width: text3::cache::AvailableSpace::from_f32(constraints.available_size.width),
2648 available_height: match overflow_behaviour {
2651 LayoutOverflow::Scroll | LayoutOverflow::Auto => None,
2652 _ => Some(constraints.available_size.height),
2653 },
2654 shape_boundaries, shape_exclusions, writing_mode: Some(match writing_mode {
2657 LayoutWritingMode::HorizontalTb => text3::cache::WritingMode::HorizontalTb,
2658 LayoutWritingMode::VerticalRl => text3::cache::WritingMode::VerticalRl,
2659 LayoutWritingMode::VerticalLr => text3::cache::WritingMode::VerticalLr,
2660 }),
2661 direction, hyphenation: match hyphenation {
2663 StyleHyphens::None => false,
2664 StyleHyphens::Auto => true,
2665 },
2666 text_orientation,
2667 text_align: match text_align {
2668 StyleTextAlign::Start => text3::cache::TextAlign::Start,
2669 StyleTextAlign::End => text3::cache::TextAlign::End,
2670 StyleTextAlign::Left => text3::cache::TextAlign::Left,
2671 StyleTextAlign::Right => text3::cache::TextAlign::Right,
2672 StyleTextAlign::Center => text3::cache::TextAlign::Center,
2673 StyleTextAlign::Justify => text3::cache::TextAlign::Justify,
2674 },
2675 text_justify: match text_justify {
2676 LayoutTextJustify::None => text3::cache::JustifyContent::None,
2677 LayoutTextJustify::Auto => text3::cache::JustifyContent::None,
2678 LayoutTextJustify::InterWord => text3::cache::JustifyContent::InterWord,
2679 LayoutTextJustify::InterCharacter => text3::cache::JustifyContent::InterCharacter,
2680 LayoutTextJustify::Distribute => text3::cache::JustifyContent::Distribute,
2681 },
2682 line_height: line_height_value.inner.normalized() * font_size, vertical_align, }
2685}
2686
2687#[derive(Debug, Clone)]
2692pub struct TableColumnInfo {
2693 pub min_width: f32,
2695 pub max_width: f32,
2697 pub computed_width: Option<f32>,
2699}
2700
2701#[derive(Debug, Clone)]
2703pub struct TableCellInfo {
2704 pub node_index: usize,
2706 pub column: usize,
2708 pub colspan: usize,
2710 pub row: usize,
2712 pub rowspan: usize,
2714}
2715
2716#[derive(Debug)]
2718struct TableLayoutContext {
2719 columns: Vec<TableColumnInfo>,
2721 cells: Vec<TableCellInfo>,
2723 num_rows: usize,
2725 use_fixed_layout: bool,
2727 row_heights: Vec<f32>,
2729 border_collapse: StyleBorderCollapse,
2731 border_spacing: LayoutBorderSpacing,
2733 caption_index: Option<usize>,
2735 collapsed_rows: std::collections::HashSet<usize>,
2738 collapsed_columns: std::collections::HashSet<usize>,
2741}
2742
2743impl TableLayoutContext {
2744 fn new() -> Self {
2745 Self {
2746 columns: Vec::new(),
2747 cells: Vec::new(),
2748 num_rows: 0,
2749 use_fixed_layout: false,
2750 row_heights: Vec::new(),
2751 border_collapse: StyleBorderCollapse::Separate,
2752 border_spacing: LayoutBorderSpacing::default(),
2753 caption_index: None,
2754 collapsed_rows: std::collections::HashSet::new(),
2755 collapsed_columns: std::collections::HashSet::new(),
2756 }
2757 }
2758}
2759
2760#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2762pub enum BorderSource {
2763 Table = 0,
2764 ColumnGroup = 1,
2765 Column = 2,
2766 RowGroup = 3,
2767 Row = 4,
2768 Cell = 5,
2769}
2770
2771#[derive(Debug, Clone)]
2773pub struct BorderInfo {
2774 pub width: f32,
2775 pub style: BorderStyle,
2776 pub color: ColorU,
2777 pub source: BorderSource,
2778}
2779
2780impl BorderInfo {
2781 pub fn new(width: f32, style: BorderStyle, color: ColorU, source: BorderSource) -> Self {
2782 Self {
2783 width,
2784 style,
2785 color,
2786 source,
2787 }
2788 }
2789
2790 pub fn style_priority(style: &BorderStyle) -> u8 {
2793 match style {
2794 BorderStyle::Hidden => 255, BorderStyle::None => 0, BorderStyle::Double => 8,
2797 BorderStyle::Solid => 7,
2798 BorderStyle::Dashed => 6,
2799 BorderStyle::Dotted => 5,
2800 BorderStyle::Ridge => 4,
2801 BorderStyle::Outset => 3,
2802 BorderStyle::Groove => 2,
2803 BorderStyle::Inset => 1,
2804 }
2805 }
2806
2807 pub fn resolve_conflict(a: &BorderInfo, b: &BorderInfo) -> Option<BorderInfo> {
2810 if a.style == BorderStyle::Hidden || b.style == BorderStyle::Hidden {
2812 return None;
2813 }
2814
2815 let a_is_none = a.style == BorderStyle::None;
2817 let b_is_none = b.style == BorderStyle::None;
2818
2819 if a_is_none && b_is_none {
2820 return None;
2821 }
2822 if a_is_none {
2823 return Some(b.clone());
2824 }
2825 if b_is_none {
2826 return Some(a.clone());
2827 }
2828
2829 if a.width > b.width {
2831 return Some(a.clone());
2832 }
2833 if b.width > a.width {
2834 return Some(b.clone());
2835 }
2836
2837 let a_priority = Self::style_priority(&a.style);
2839 let b_priority = Self::style_priority(&b.style);
2840
2841 if a_priority > b_priority {
2842 return Some(a.clone());
2843 }
2844 if b_priority > a_priority {
2845 return Some(b.clone());
2846 }
2847
2848 if a.source > b.source {
2851 return Some(a.clone());
2852 }
2853 if b.source > a.source {
2854 return Some(b.clone());
2855 }
2856
2857 Some(a.clone())
2859 }
2860}
2861
2862fn get_border_info<T: ParsedFontTrait>(
2864 ctx: &LayoutContext<'_, T>,
2865 node: &LayoutNode,
2866 source: BorderSource,
2867) -> (BorderInfo, BorderInfo, BorderInfo, BorderInfo) {
2868 use azul_css::props::{
2869 basic::{
2870 pixel::{PhysicalSize, PropertyContext, ResolutionContext},
2871 ColorU,
2872 },
2873 style::BorderStyle,
2874 };
2875 use get_element_font_size;
2876 use get_parent_font_size;
2877 use get_root_font_size;
2878
2879 let default_border = BorderInfo::new(
2880 0.0,
2881 BorderStyle::None,
2882 ColorU {
2883 r: 0,
2884 g: 0,
2885 b: 0,
2886 a: 0,
2887 },
2888 source,
2889 );
2890
2891 let Some(dom_id) = node.dom_node_id else {
2892 return (
2893 default_border.clone(),
2894 default_border.clone(),
2895 default_border.clone(),
2896 default_border.clone(),
2897 );
2898 };
2899
2900 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
2901 let node_state = StyledNodeState::default();
2902 let cache = &ctx.styled_dom.css_property_cache.ptr;
2903
2904 let element_font_size = get_element_font_size(ctx.styled_dom, dom_id, &node_state);
2906 let parent_font_size = get_parent_font_size(ctx.styled_dom, dom_id, &node_state);
2907 let root_font_size = get_root_font_size(ctx.styled_dom, &node_state);
2908
2909 let resolution_context = ResolutionContext {
2910 element_font_size,
2911 parent_font_size,
2912 root_font_size,
2913 containing_block_size: PhysicalSize::new(0.0, 0.0),
2915 element_size: None,
2917 viewport_size: PhysicalSize::new(0.0, 0.0),
2918 };
2919
2920 let top = cache
2922 .get_border_top_style(node_data, &dom_id, &node_state)
2923 .and_then(|s| s.get_property())
2924 .map(|style_val| {
2925 let width = cache
2926 .get_border_top_width(node_data, &dom_id, &node_state)
2927 .and_then(|w| w.get_property())
2928 .map(|w| {
2929 w.inner
2930 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
2931 })
2932 .unwrap_or(0.0);
2933 let color = cache
2934 .get_border_top_color(node_data, &dom_id, &node_state)
2935 .and_then(|c| c.get_property())
2936 .map(|c| c.inner)
2937 .unwrap_or(ColorU {
2938 r: 0,
2939 g: 0,
2940 b: 0,
2941 a: 255,
2942 });
2943 BorderInfo::new(width, style_val.inner, color, source)
2944 })
2945 .unwrap_or_else(|| default_border.clone());
2946
2947 let right = cache
2949 .get_border_right_style(node_data, &dom_id, &node_state)
2950 .and_then(|s| s.get_property())
2951 .map(|style_val| {
2952 let width = cache
2953 .get_border_right_width(node_data, &dom_id, &node_state)
2954 .and_then(|w| w.get_property())
2955 .map(|w| {
2956 w.inner
2957 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
2958 })
2959 .unwrap_or(0.0);
2960 let color = cache
2961 .get_border_right_color(node_data, &dom_id, &node_state)
2962 .and_then(|c| c.get_property())
2963 .map(|c| c.inner)
2964 .unwrap_or(ColorU {
2965 r: 0,
2966 g: 0,
2967 b: 0,
2968 a: 255,
2969 });
2970 BorderInfo::new(width, style_val.inner, color, source)
2971 })
2972 .unwrap_or_else(|| default_border.clone());
2973
2974 let bottom = cache
2976 .get_border_bottom_style(node_data, &dom_id, &node_state)
2977 .and_then(|s| s.get_property())
2978 .map(|style_val| {
2979 let width = cache
2980 .get_border_bottom_width(node_data, &dom_id, &node_state)
2981 .and_then(|w| w.get_property())
2982 .map(|w| {
2983 w.inner
2984 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
2985 })
2986 .unwrap_or(0.0);
2987 let color = cache
2988 .get_border_bottom_color(node_data, &dom_id, &node_state)
2989 .and_then(|c| c.get_property())
2990 .map(|c| c.inner)
2991 .unwrap_or(ColorU {
2992 r: 0,
2993 g: 0,
2994 b: 0,
2995 a: 255,
2996 });
2997 BorderInfo::new(width, style_val.inner, color, source)
2998 })
2999 .unwrap_or_else(|| default_border.clone());
3000
3001 let left = cache
3003 .get_border_left_style(node_data, &dom_id, &node_state)
3004 .and_then(|s| s.get_property())
3005 .map(|style_val| {
3006 let width = cache
3007 .get_border_left_width(node_data, &dom_id, &node_state)
3008 .and_then(|w| w.get_property())
3009 .map(|w| {
3010 w.inner
3011 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
3012 })
3013 .unwrap_or(0.0);
3014 let color = cache
3015 .get_border_left_color(node_data, &dom_id, &node_state)
3016 .and_then(|c| c.get_property())
3017 .map(|c| c.inner)
3018 .unwrap_or(ColorU {
3019 r: 0,
3020 g: 0,
3021 b: 0,
3022 a: 255,
3023 });
3024 BorderInfo::new(width, style_val.inner, color, source)
3025 })
3026 .unwrap_or_else(|| default_border.clone());
3027
3028 (top, right, bottom, left)
3029}
3030
3031fn get_table_layout_property<T: ParsedFontTrait>(
3033 ctx: &LayoutContext<'_, T>,
3034 node: &LayoutNode,
3035) -> LayoutTableLayout {
3036 let Some(dom_id) = node.dom_node_id else {
3037 return LayoutTableLayout::Auto;
3038 };
3039
3040 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3041 let node_state = StyledNodeState::default();
3042
3043 ctx.styled_dom
3044 .css_property_cache
3045 .ptr
3046 .get_table_layout(node_data, &dom_id, &node_state)
3047 .and_then(|prop| prop.get_property().copied())
3048 .unwrap_or(LayoutTableLayout::Auto)
3049}
3050
3051fn get_border_collapse_property<T: ParsedFontTrait>(
3053 ctx: &LayoutContext<'_, T>,
3054 node: &LayoutNode,
3055) -> StyleBorderCollapse {
3056 let Some(dom_id) = node.dom_node_id else {
3057 return StyleBorderCollapse::Separate;
3058 };
3059
3060 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3061 let node_state = StyledNodeState::default();
3062
3063 ctx.styled_dom
3064 .css_property_cache
3065 .ptr
3066 .get_border_collapse(node_data, &dom_id, &node_state)
3067 .and_then(|prop| prop.get_property().copied())
3068 .unwrap_or(StyleBorderCollapse::Separate)
3069}
3070
3071fn get_border_spacing_property<T: ParsedFontTrait>(
3073 ctx: &LayoutContext<'_, T>,
3074 node: &LayoutNode,
3075) -> LayoutBorderSpacing {
3076 if let Some(dom_id) = node.dom_node_id {
3077 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3078 let node_state = StyledNodeState::default();
3079
3080 if let Some(prop) = ctx.styled_dom.css_property_cache.ptr.get_border_spacing(
3081 node_data,
3082 &dom_id,
3083 &node_state,
3084 ) {
3085 if let Some(value) = prop.get_property() {
3086 return *value;
3087 }
3088 }
3089 }
3090
3091 LayoutBorderSpacing::default() }
3093
3094fn get_caption_side_property<T: ParsedFontTrait>(
3103 ctx: &LayoutContext<'_, T>,
3104 node: &LayoutNode,
3105) -> StyleCaptionSide {
3106 if let Some(dom_id) = node.dom_node_id {
3107 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3108 let node_state = StyledNodeState::default();
3109
3110 if let Some(prop) =
3111 ctx.styled_dom
3112 .css_property_cache
3113 .ptr
3114 .get_caption_side(node_data, &dom_id, &node_state)
3115 {
3116 if let Some(value) = prop.get_property() {
3117 return *value;
3118 }
3119 }
3120 }
3121
3122 StyleCaptionSide::Top }
3124
3125fn is_visibility_collapsed<T: ParsedFontTrait>(
3136 ctx: &LayoutContext<'_, T>,
3137 node: &LayoutNode,
3138) -> bool {
3139 if let Some(dom_id) = node.dom_node_id {
3140 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3141 let node_state = StyledNodeState::default();
3142
3143 if let Some(prop) =
3144 ctx.styled_dom
3145 .css_property_cache
3146 .ptr
3147 .get_visibility(node_data, &dom_id, &node_state)
3148 {
3149 if let Some(value) = prop.get_property() {
3150 return matches!(value, StyleVisibility::Collapse);
3151 }
3152 }
3153 }
3154
3155 false
3156}
3157
3158fn is_cell_empty(tree: &LayoutTree, cell_index: usize) -> bool {
3177 let cell_node = match tree.get(cell_index) {
3178 Some(node) => node,
3179 None => return true, };
3181
3182 if cell_node.children.is_empty() {
3184 return true;
3185 }
3186
3187 if let Some(ref cached_layout) = cell_node.inline_layout_result {
3189 return cached_layout.layout.items.is_empty();
3193 }
3194
3195 false
3203}
3204
3205pub fn layout_table_fc<T: ParsedFontTrait>(
3207 ctx: &mut LayoutContext<'_, T>,
3208 tree: &mut LayoutTree,
3209 text_cache: &mut crate::font_traits::TextLayoutCache,
3210 node_index: usize,
3211 constraints: &LayoutConstraints,
3212) -> Result<LayoutOutput> {
3213 debug_log!(ctx, "Laying out table");
3214
3215 debug_table_layout!(
3216 ctx,
3217 "node_index={}, available_size={:?}, writing_mode={:?}",
3218 node_index,
3219 constraints.available_size,
3220 constraints.writing_mode
3221 );
3222
3223 let table_node = tree
3233 .get(node_index)
3234 .ok_or(LayoutError::InvalidTree)?
3235 .clone();
3236
3237 let table_border_box_width = if let Some(dom_id) = table_node.dom_node_id {
3240 let intrinsic = table_node.intrinsic_sizes.clone().unwrap_or_default();
3242 let containing_block_size = LogicalSize {
3243 width: constraints.available_size.width,
3244 height: constraints.available_size.height,
3245 };
3246
3247 let table_size = crate::solver3::sizing::calculate_used_size_for_node(
3248 ctx.styled_dom,
3249 Some(dom_id),
3250 containing_block_size,
3251 intrinsic,
3252 &table_node.box_props,
3253 )?;
3254
3255 table_size.width
3256 } else {
3257 constraints.available_size.width
3258 };
3259
3260 let table_content_box_width = {
3262 let padding_width = table_node.box_props.padding.left + table_node.box_props.padding.right;
3263 let border_width = table_node.box_props.border.left + table_node.box_props.border.right;
3264 (table_border_box_width - padding_width - border_width).max(0.0)
3265 };
3266
3267 debug_table_layout!(ctx, "Table Layout Debug");
3268 debug_table_layout!(ctx, "Node index: {}", node_index);
3269 debug_table_layout!(
3270 ctx,
3271 "Available size from parent: {:.2} x {:.2}",
3272 constraints.available_size.width,
3273 constraints.available_size.height
3274 );
3275 debug_table_layout!(ctx, "Table border-box width: {:.2}", table_border_box_width);
3276 debug_table_layout!(
3277 ctx,
3278 "Table content-box width: {:.2}",
3279 table_content_box_width
3280 );
3281 debug_table_layout!(
3282 ctx,
3283 "Table padding: L={:.2} R={:.2}",
3284 table_node.box_props.padding.left,
3285 table_node.box_props.padding.right
3286 );
3287 debug_table_layout!(
3288 ctx,
3289 "Table border: L={:.2} R={:.2}",
3290 table_node.box_props.border.left,
3291 table_node.box_props.border.right
3292 );
3293 debug_table_layout!(ctx, "=");
3294
3295 let mut table_ctx = analyze_table_structure(tree, node_index, ctx)?;
3297
3298 let table_layout = get_table_layout_property(ctx, &table_node);
3300 table_ctx.use_fixed_layout = matches!(table_layout, LayoutTableLayout::Fixed);
3301
3302 table_ctx.border_collapse = get_border_collapse_property(ctx, &table_node);
3304 table_ctx.border_spacing = get_border_spacing_property(ctx, &table_node);
3305
3306 debug_log!(
3307 ctx,
3308 "Table layout: {:?}, border-collapse: {:?}, border-spacing: {:?}",
3309 table_layout,
3310 table_ctx.border_collapse,
3311 table_ctx.border_spacing
3312 );
3313
3314 if table_ctx.use_fixed_layout {
3316 debug_table_layout!(
3318 ctx,
3319 "FIXED layout: table_content_box_width={:.2}",
3320 table_content_box_width
3321 );
3322 calculate_column_widths_fixed(ctx, &mut table_ctx, table_content_box_width);
3323 } else {
3324 calculate_column_widths_auto_with_width(
3326 &mut table_ctx,
3327 tree,
3328 text_cache,
3329 ctx,
3330 constraints,
3331 table_content_box_width,
3332 )?;
3333 }
3334
3335 debug_table_layout!(ctx, "After column width calculation:");
3336 debug_table_layout!(ctx, " Number of columns: {}", table_ctx.columns.len());
3337 for (i, col) in table_ctx.columns.iter().enumerate() {
3338 debug_table_layout!(
3339 ctx,
3340 " Column {}: width={:.2}",
3341 i,
3342 col.computed_width.unwrap_or(0.0)
3343 );
3344 }
3345 let total_col_width: f32 = table_ctx
3346 .columns
3347 .iter()
3348 .filter_map(|c| c.computed_width)
3349 .sum();
3350 debug_table_layout!(ctx, " Total column width: {:.2}", total_col_width);
3351
3352 calculate_row_heights(&mut table_ctx, tree, text_cache, ctx, constraints)?;
3354
3355 let mut cell_positions =
3357 position_table_cells(&mut table_ctx, tree, ctx, node_index, constraints)?;
3358
3359 let mut table_width: f32 = table_ctx
3361 .columns
3362 .iter()
3363 .filter_map(|col| col.computed_width)
3364 .sum();
3365 let mut table_height: f32 = table_ctx.row_heights.iter().sum();
3366
3367 debug_table_layout!(
3368 ctx,
3369 "After calculate_row_heights: table_height={:.2}, row_heights={:?}",
3370 table_height,
3371 table_ctx.row_heights
3372 );
3373
3374 if table_ctx.border_collapse == StyleBorderCollapse::Separate {
3376 use get_element_font_size;
3377 use get_parent_font_size;
3378 use get_root_font_size;
3379 use PhysicalSize;
3380 use PropertyContext;
3381 use ResolutionContext;
3382
3383 let styled_dom = ctx.styled_dom;
3384 let table_id = tree.nodes[node_index].dom_node_id.unwrap();
3385 let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
3386
3387 let spacing_context = ResolutionContext {
3388 element_font_size: get_element_font_size(styled_dom, table_id, table_state),
3389 parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
3390 root_font_size: get_root_font_size(styled_dom, table_state),
3391 containing_block_size: PhysicalSize::new(0.0, 0.0),
3392 element_size: None,
3393 viewport_size: PhysicalSize::new(0.0, 0.0),
3395 };
3396
3397 let h_spacing = table_ctx
3398 .border_spacing
3399 .horizontal
3400 .resolve_with_context(&spacing_context, PropertyContext::Other);
3401 let v_spacing = table_ctx
3402 .border_spacing
3403 .vertical
3404 .resolve_with_context(&spacing_context, PropertyContext::Other);
3405
3406 let num_cols = table_ctx.columns.len();
3408 if num_cols > 0 {
3409 table_width += h_spacing * (num_cols + 1) as f32;
3410 }
3411
3412 if table_ctx.num_rows > 0 {
3414 table_height += v_spacing * (table_ctx.num_rows + 1) as f32;
3415 }
3416 }
3417
3418 let caption_side = get_caption_side_property(ctx, &table_node);
3423 let mut caption_height = 0.0;
3424 let mut table_y_offset = 0.0;
3425
3426 if let Some(caption_idx) = table_ctx.caption_index {
3427 debug_log!(
3428 ctx,
3429 "Laying out caption with caption-side: {:?}",
3430 caption_side
3431 );
3432
3433 let caption_constraints = LayoutConstraints {
3435 available_size: LogicalSize {
3436 width: table_width,
3437 height: constraints.available_size.height,
3438 },
3439 writing_mode: constraints.writing_mode,
3440 bfc_state: None, text_align: constraints.text_align,
3442 containing_block_size: constraints.containing_block_size,
3443 available_width_type: Text3AvailableSpace::Definite(table_width),
3444 };
3445
3446 let mut empty_float_cache = std::collections::BTreeMap::new();
3448 let caption_result = layout_formatting_context(
3449 ctx,
3450 tree,
3451 text_cache,
3452 caption_idx,
3453 &caption_constraints,
3454 &mut empty_float_cache,
3455 )?;
3456 caption_height = caption_result.output.overflow_size.height;
3457
3458 let caption_position = match caption_side {
3460 StyleCaptionSide::Top => {
3461 table_y_offset = caption_height;
3463 LogicalPosition { x: 0.0, y: 0.0 }
3464 }
3465 StyleCaptionSide::Bottom => {
3466 LogicalPosition {
3468 x: 0.0,
3469 y: table_height,
3470 }
3471 }
3472 };
3473
3474 cell_positions.insert(caption_idx, caption_position);
3476
3477 debug_log!(
3478 ctx,
3479 "Caption positioned at x={:.2}, y={:.2}, height={:.2}",
3480 caption_position.x,
3481 caption_position.y,
3482 caption_height
3483 );
3484 }
3485
3486 if table_y_offset > 0.0 {
3488 debug_log!(
3489 ctx,
3490 "Adjusting table cells by y offset: {:.2}",
3491 table_y_offset
3492 );
3493
3494 for cell_info in &table_ctx.cells {
3496 if let Some(pos) = cell_positions.get_mut(&cell_info.node_index) {
3497 pos.y += table_y_offset;
3498 }
3499 }
3500 }
3501
3502 let total_height = table_height + caption_height;
3504
3505 debug_table_layout!(ctx, "Final table dimensions:");
3506 debug_table_layout!(ctx, " Content width (columns): {:.2}", table_width);
3507 debug_table_layout!(ctx, " Content height (rows): {:.2}", table_height);
3508 debug_table_layout!(ctx, " Caption height: {:.2}", caption_height);
3509 debug_table_layout!(ctx, " Total height: {:.2}", total_height);
3510 debug_table_layout!(ctx, "End Table Debug");
3511
3512 let output = LayoutOutput {
3514 overflow_size: LogicalSize {
3515 width: table_width,
3516 height: total_height,
3517 },
3518 positions: cell_positions,
3520 baseline: None,
3522 };
3523
3524 Ok(output)
3525}
3526
3527fn analyze_table_structure<T: ParsedFontTrait>(
3529 tree: &LayoutTree,
3530 table_index: usize,
3531 ctx: &mut LayoutContext<'_, T>,
3532) -> Result<TableLayoutContext> {
3533 let mut table_ctx = TableLayoutContext::new();
3534
3535 let table_node = tree.get(table_index).ok_or(LayoutError::InvalidTree)?;
3536
3537 for &child_idx in &table_node.children {
3540 if let Some(child) = tree.get(child_idx) {
3541 if matches!(child.formatting_context, FormattingContext::TableCaption) {
3543 debug_log!(ctx, "Found table caption at index {}", child_idx);
3544 table_ctx.caption_index = Some(child_idx);
3545 continue;
3546 }
3547
3548 if matches!(
3550 child.formatting_context,
3551 FormattingContext::TableColumnGroup
3552 ) {
3553 analyze_table_colgroup(tree, child_idx, &mut table_ctx, ctx)?;
3554 continue;
3555 }
3556
3557 match child.formatting_context {
3559 FormattingContext::TableRow => {
3560 analyze_table_row(tree, child_idx, &mut table_ctx, ctx)?;
3561 }
3562 FormattingContext::TableRowGroup => {
3563 for &row_idx in &child.children {
3565 if let Some(row) = tree.get(row_idx) {
3566 if matches!(row.formatting_context, FormattingContext::TableRow) {
3567 analyze_table_row(tree, row_idx, &mut table_ctx, ctx)?;
3568 }
3569 }
3570 }
3571 }
3572 _ => {}
3573 }
3574 }
3575 }
3576
3577 debug_log!(
3578 ctx,
3579 "Table structure: {} rows, {} columns, {} cells{}",
3580 table_ctx.num_rows,
3581 table_ctx.columns.len(),
3582 table_ctx.cells.len(),
3583 if table_ctx.caption_index.is_some() {
3584 ", has caption"
3585 } else {
3586 ""
3587 }
3588 );
3589
3590 Ok(table_ctx)
3591}
3592
3593fn analyze_table_colgroup<T: ParsedFontTrait>(
3598 tree: &LayoutTree,
3599 colgroup_index: usize,
3600 table_ctx: &mut TableLayoutContext,
3601 ctx: &mut LayoutContext<'_, T>,
3602) -> Result<()> {
3603 let colgroup_node = tree.get(colgroup_index).ok_or(LayoutError::InvalidTree)?;
3604
3605 if is_visibility_collapsed(ctx, colgroup_node) {
3607 debug_log!(
3610 ctx,
3611 "Column group at index {} has visibility:collapse",
3612 colgroup_index
3613 );
3614 }
3615
3616 for &col_idx in &colgroup_node.children {
3618 if let Some(col_node) = tree.get(col_idx) {
3619 if is_visibility_collapsed(ctx, col_node) {
3623 debug_log!(ctx, "Column at index {} has visibility:collapse", col_idx);
3626 }
3627 }
3628 }
3629
3630 Ok(())
3631}
3632
3633fn analyze_table_row<T: ParsedFontTrait>(
3635 tree: &LayoutTree,
3636 row_index: usize,
3637 table_ctx: &mut TableLayoutContext,
3638 ctx: &mut LayoutContext<'_, T>,
3639) -> Result<()> {
3640 let row_node = tree.get(row_index).ok_or(LayoutError::InvalidTree)?;
3641 let row_num = table_ctx.num_rows;
3642 table_ctx.num_rows += 1;
3643
3644 if is_visibility_collapsed(ctx, row_node) {
3646 debug_log!(ctx, "Row {} has visibility:collapse", row_num);
3647 table_ctx.collapsed_rows.insert(row_num);
3648 }
3649
3650 let mut col_index = 0;
3651
3652 for &cell_idx in &row_node.children {
3653 if let Some(cell) = tree.get(cell_idx) {
3654 if matches!(cell.formatting_context, FormattingContext::TableCell) {
3655 let colspan = 1; let rowspan = 1; let cell_info = TableCellInfo {
3660 node_index: cell_idx,
3661 column: col_index,
3662 colspan,
3663 row: row_num,
3664 rowspan,
3665 };
3666
3667 table_ctx.cells.push(cell_info);
3668
3669 let max_col = col_index + colspan;
3671 while table_ctx.columns.len() < max_col {
3672 table_ctx.columns.push(TableColumnInfo {
3673 min_width: 0.0,
3674 max_width: 0.0,
3675 computed_width: None,
3676 });
3677 }
3678
3679 col_index += colspan;
3680 }
3681 }
3682 }
3683
3684 Ok(())
3685}
3686
3687fn calculate_column_widths_fixed<T: ParsedFontTrait>(
3695 ctx: &mut LayoutContext<'_, T>,
3696 table_ctx: &mut TableLayoutContext,
3697 available_width: f32,
3698) {
3699 debug_table_layout!(
3700 ctx,
3701 "calculate_column_widths_fixed: num_cols={}, available_width={:.2}",
3702 table_ctx.columns.len(),
3703 available_width
3704 );
3705
3706 let num_cols = table_ctx.columns.len();
3709 if num_cols == 0 {
3710 return;
3711 }
3712
3713 let num_visible_cols = num_cols - table_ctx.collapsed_columns.len();
3715 if num_visible_cols == 0 {
3716 for col in &mut table_ctx.columns {
3718 col.computed_width = Some(0.0);
3719 }
3720 return;
3721 }
3722
3723 let col_width = available_width / num_visible_cols as f32;
3725 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
3726 if table_ctx.collapsed_columns.contains(&col_idx) {
3727 col.computed_width = Some(0.0);
3728 } else {
3729 col.computed_width = Some(col_width);
3730 }
3731 }
3732}
3733
3734fn measure_cell_min_content_width<T: ParsedFontTrait>(
3736 ctx: &mut LayoutContext<'_, T>,
3737 tree: &mut LayoutTree,
3738 text_cache: &mut crate::font_traits::TextLayoutCache,
3739 cell_index: usize,
3740 constraints: &LayoutConstraints,
3741) -> Result<f32> {
3742 use crate::text3::cache::AvailableSpace;
3748 let min_constraints = LayoutConstraints {
3749 available_size: LogicalSize {
3750 width: AvailableSpace::MinContent.to_f32_for_layout(),
3751 height: f32::INFINITY,
3752 },
3753 writing_mode: constraints.writing_mode,
3754 bfc_state: None, text_align: constraints.text_align,
3756 containing_block_size: constraints.containing_block_size,
3757 available_width_type: Text3AvailableSpace::MinContent,
3760 };
3761
3762 let mut temp_positions = BTreeMap::new();
3763 let mut temp_scrollbar_reflow = false;
3764 let mut temp_float_cache = std::collections::BTreeMap::new();
3765
3766 crate::solver3::cache::calculate_layout_for_subtree(
3767 ctx,
3768 tree,
3769 text_cache,
3770 cell_index,
3771 LogicalPosition::zero(),
3772 min_constraints.available_size,
3773 &mut temp_positions,
3774 &mut temp_scrollbar_reflow,
3775 &mut temp_float_cache,
3776 )?;
3777
3778 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
3779 let size = cell_node.used_size.unwrap_or_default();
3780
3781 let padding = &cell_node.box_props.padding;
3783 let border = &cell_node.box_props.border;
3784 let writing_mode = constraints.writing_mode;
3785
3786 let min_width = size.width
3787 + padding.cross_start(writing_mode)
3788 + padding.cross_end(writing_mode)
3789 + border.cross_start(writing_mode)
3790 + border.cross_end(writing_mode);
3791
3792 Ok(min_width)
3793}
3794
3795fn measure_cell_max_content_width<T: ParsedFontTrait>(
3797 ctx: &mut LayoutContext<'_, T>,
3798 tree: &mut LayoutTree,
3799 text_cache: &mut crate::font_traits::TextLayoutCache,
3800 cell_index: usize,
3801 constraints: &LayoutConstraints,
3802) -> Result<f32> {
3803 use crate::text3::cache::AvailableSpace;
3809 let max_constraints = LayoutConstraints {
3810 available_size: LogicalSize {
3811 width: AvailableSpace::MaxContent.to_f32_for_layout(),
3812 height: f32::INFINITY,
3813 },
3814 writing_mode: constraints.writing_mode,
3815 bfc_state: None, text_align: constraints.text_align,
3817 containing_block_size: constraints.containing_block_size,
3818 available_width_type: Text3AvailableSpace::MaxContent,
3821 };
3822
3823 let mut temp_positions = BTreeMap::new();
3824 let mut temp_scrollbar_reflow = false;
3825 let mut temp_float_cache = std::collections::BTreeMap::new();
3826
3827 crate::solver3::cache::calculate_layout_for_subtree(
3828 ctx,
3829 tree,
3830 text_cache,
3831 cell_index,
3832 LogicalPosition::zero(),
3833 max_constraints.available_size,
3834 &mut temp_positions,
3835 &mut temp_scrollbar_reflow,
3836 &mut temp_float_cache,
3837 )?;
3838
3839 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
3840 let size = cell_node.used_size.unwrap_or_default();
3841
3842 let padding = &cell_node.box_props.padding;
3844 let border = &cell_node.box_props.border;
3845 let writing_mode = constraints.writing_mode;
3846
3847 let max_width = size.width
3848 + padding.cross_start(writing_mode)
3849 + padding.cross_end(writing_mode)
3850 + border.cross_start(writing_mode)
3851 + border.cross_end(writing_mode);
3852
3853 Ok(max_width)
3854}
3855
3856fn calculate_column_widths_auto<T: ParsedFontTrait>(
3858 table_ctx: &mut TableLayoutContext,
3859 tree: &mut LayoutTree,
3860 text_cache: &mut crate::font_traits::TextLayoutCache,
3861 ctx: &mut LayoutContext<'_, T>,
3862 constraints: &LayoutConstraints,
3863) -> Result<()> {
3864 calculate_column_widths_auto_with_width(
3865 table_ctx,
3866 tree,
3867 text_cache,
3868 ctx,
3869 constraints,
3870 constraints.available_size.width,
3871 )
3872}
3873
3874fn calculate_column_widths_auto_with_width<T: ParsedFontTrait>(
3876 table_ctx: &mut TableLayoutContext,
3877 tree: &mut LayoutTree,
3878 text_cache: &mut crate::font_traits::TextLayoutCache,
3879 ctx: &mut LayoutContext<'_, T>,
3880 constraints: &LayoutConstraints,
3881 table_width: f32,
3882) -> Result<()> {
3883 let num_cols = table_ctx.columns.len();
3885 if num_cols == 0 {
3886 return Ok(());
3887 }
3888
3889 for cell_info in &table_ctx.cells {
3892 if table_ctx.collapsed_columns.contains(&cell_info.column) {
3894 continue;
3895 }
3896
3897 let mut spans_collapsed = false;
3899 for col_offset in 0..cell_info.colspan {
3900 if table_ctx
3901 .collapsed_columns
3902 .contains(&(cell_info.column + col_offset))
3903 {
3904 spans_collapsed = true;
3905 break;
3906 }
3907 }
3908 if spans_collapsed {
3909 continue;
3910 }
3911
3912 let min_width = measure_cell_min_content_width(
3913 ctx,
3914 tree,
3915 text_cache,
3916 cell_info.node_index,
3917 constraints,
3918 )?;
3919
3920 let max_width = measure_cell_max_content_width(
3921 ctx,
3922 tree,
3923 text_cache,
3924 cell_info.node_index,
3925 constraints,
3926 )?;
3927
3928 if cell_info.colspan == 1 {
3930 let col = &mut table_ctx.columns[cell_info.column];
3931 col.min_width = col.min_width.max(min_width);
3932 col.max_width = col.max_width.max(max_width);
3933 } else {
3934 distribute_cell_width_across_columns(
3937 &mut table_ctx.columns,
3938 cell_info.column,
3939 cell_info.colspan,
3940 min_width,
3941 max_width,
3942 &table_ctx.collapsed_columns,
3943 );
3944 }
3945 }
3946
3947 let total_min_width: f32 = table_ctx
3950 .columns
3951 .iter()
3952 .enumerate()
3953 .filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
3954 .map(|(_, c)| c.min_width)
3955 .sum();
3956 let total_max_width: f32 = table_ctx
3957 .columns
3958 .iter()
3959 .enumerate()
3960 .filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
3961 .map(|(_, c)| c.max_width)
3962 .sum();
3963 let available_width = table_width; debug_table_layout!(
3966 ctx,
3967 "calculate_column_widths_auto: min={:.2}, max={:.2}, table_width={:.2}",
3968 total_min_width,
3969 total_max_width,
3970 table_width
3971 );
3972
3973 if !total_max_width.is_finite() || !available_width.is_finite() {
3975 let num_non_collapsed = table_ctx.columns.len() - table_ctx.collapsed_columns.len();
3977 let width_per_column = if num_non_collapsed > 0 {
3978 available_width / num_non_collapsed as f32
3979 } else {
3980 0.0
3981 };
3982
3983 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
3984 if table_ctx.collapsed_columns.contains(&col_idx) {
3985 col.computed_width = Some(0.0);
3986 } else {
3987 col.computed_width = Some(col.min_width.max(width_per_column));
3989 }
3990 }
3991 } else if available_width >= total_max_width {
3992 let excess_width = available_width - total_max_width;
3997
3998 let column_info: Vec<(usize, f32, bool)> = table_ctx
4000 .columns
4001 .iter()
4002 .enumerate()
4003 .map(|(idx, c)| (idx, c.max_width, table_ctx.collapsed_columns.contains(&idx)))
4004 .collect();
4005
4006 let total_weight: f32 = column_info.iter()
4008 .filter(|(_, _, is_collapsed)| !is_collapsed)
4009 .map(|(_, max_w, _)| max_w.max(1.0)) .sum();
4011
4012 let num_non_collapsed = column_info
4013 .iter()
4014 .filter(|(_, _, is_collapsed)| !is_collapsed)
4015 .count();
4016
4017 for (col_idx, max_width, is_collapsed) in column_info {
4019 let col = &mut table_ctx.columns[col_idx];
4020 if is_collapsed {
4021 col.computed_width = Some(0.0);
4022 } else {
4023 let weight_factor = if total_weight > 0.0 {
4025 max_width.max(1.0) / total_weight
4026 } else {
4027 1.0 / num_non_collapsed.max(1) as f32
4029 };
4030
4031 let final_width = max_width + (excess_width * weight_factor);
4032 col.computed_width = Some(final_width);
4033 }
4034 }
4035 } else if available_width >= total_min_width {
4036 let scale = if total_max_width > total_min_width {
4039 (available_width - total_min_width) / (total_max_width - total_min_width)
4040 } else {
4041 0.0 };
4043 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
4044 if table_ctx.collapsed_columns.contains(&col_idx) {
4045 col.computed_width = Some(0.0);
4046 } else {
4047 let interpolated = col.min_width + (col.max_width - col.min_width) * scale;
4048 col.computed_width = Some(interpolated);
4049 }
4050 }
4051 } else {
4052 let scale = available_width / total_min_width;
4054 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
4055 if table_ctx.collapsed_columns.contains(&col_idx) {
4056 col.computed_width = Some(0.0);
4057 } else {
4058 col.computed_width = Some(col.min_width * scale);
4059 }
4060 }
4061 }
4062
4063 Ok(())
4064}
4065
4066fn distribute_cell_width_across_columns(
4068 columns: &mut [TableColumnInfo],
4069 start_col: usize,
4070 colspan: usize,
4071 cell_min_width: f32,
4072 cell_max_width: f32,
4073 collapsed_columns: &std::collections::HashSet<usize>,
4074) {
4075 let end_col = start_col + colspan;
4076 if end_col > columns.len() {
4077 return;
4078 }
4079
4080 let current_min_total: f32 = columns[start_col..end_col]
4082 .iter()
4083 .enumerate()
4084 .filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
4085 .map(|(_, c)| c.min_width)
4086 .sum();
4087 let current_max_total: f32 = columns[start_col..end_col]
4088 .iter()
4089 .enumerate()
4090 .filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
4091 .map(|(_, c)| c.max_width)
4092 .sum();
4093
4094 let num_visible_cols = (start_col..end_col)
4096 .filter(|idx| !collapsed_columns.contains(idx))
4097 .count();
4098
4099 if num_visible_cols == 0 {
4100 return; }
4102
4103 if cell_min_width > current_min_total {
4105 let extra_min = cell_min_width - current_min_total;
4106 let per_col = extra_min / num_visible_cols as f32;
4107 for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
4108 if !collapsed_columns.contains(&(start_col + idx)) {
4109 col.min_width += per_col;
4110 }
4111 }
4112 }
4113
4114 if cell_max_width > current_max_total {
4115 let extra_max = cell_max_width - current_max_total;
4116 let per_col = extra_max / num_visible_cols as f32;
4117 for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
4118 if !collapsed_columns.contains(&(start_col + idx)) {
4119 col.max_width += per_col;
4120 }
4121 }
4122 }
4123}
4124
4125fn layout_cell_for_height<T: ParsedFontTrait>(
4127 ctx: &mut LayoutContext<'_, T>,
4128 tree: &mut LayoutTree,
4129 text_cache: &mut crate::font_traits::TextLayoutCache,
4130 cell_index: usize,
4131 cell_width: f32,
4132 constraints: &LayoutConstraints,
4133) -> Result<f32> {
4134 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4135 let cell_dom_id = cell_node.dom_node_id.ok_or(LayoutError::InvalidTree)?;
4136
4137 let has_text_children = cell_dom_id
4141 .az_children(&ctx.styled_dom.node_hierarchy.as_container())
4142 .any(|child_id| {
4143 let node_data = &ctx.styled_dom.node_data.as_container()[child_id];
4144 matches!(node_data.get_node_type(), NodeType::Text(_))
4145 });
4146
4147 debug_table_layout!(
4148 ctx,
4149 "layout_cell_for_height: cell_index={}, has_text_children={}",
4150 cell_index,
4151 has_text_children
4152 );
4153
4154 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4156 let padding = &cell_node.box_props.padding;
4157 let border = &cell_node.box_props.border;
4158 let writing_mode = constraints.writing_mode;
4159
4160 let content_width = cell_width
4163 - padding.cross_start(writing_mode)
4164 - padding.cross_end(writing_mode)
4165 - border.cross_start(writing_mode)
4166 - border.cross_end(writing_mode);
4167
4168 debug_table_layout!(
4169 ctx,
4170 "Cell width: border_box={:.2}, content_box={:.2}",
4171 cell_width,
4172 content_width
4173 );
4174
4175 let content_height = if has_text_children {
4176 debug_table_layout!(ctx, "Using IFC to measure text content");
4178
4179 let cell_constraints = LayoutConstraints {
4180 available_size: LogicalSize {
4181 width: content_width, height: f32::INFINITY,
4183 },
4184 writing_mode: constraints.writing_mode,
4185 bfc_state: None,
4186 text_align: constraints.text_align,
4187 containing_block_size: constraints.containing_block_size,
4188 available_width_type: Text3AvailableSpace::Definite(content_width),
4191 };
4192
4193 let output = layout_ifc(ctx, text_cache, tree, cell_index, &cell_constraints)?;
4194
4195 debug_table_layout!(
4196 ctx,
4197 "IFC returned height={:.2}",
4198 output.overflow_size.height
4199 );
4200
4201 output.overflow_size.height
4202 } else {
4203 debug_table_layout!(ctx, "Using regular layout for block children");
4205
4206 let cell_constraints = LayoutConstraints {
4207 available_size: LogicalSize {
4208 width: content_width, height: f32::INFINITY,
4210 },
4211 writing_mode: constraints.writing_mode,
4212 bfc_state: None,
4213 text_align: constraints.text_align,
4214 containing_block_size: constraints.containing_block_size,
4215 available_width_type: Text3AvailableSpace::Definite(content_width),
4217 };
4218
4219 let mut temp_positions = BTreeMap::new();
4220 let mut temp_scrollbar_reflow = false;
4221 let mut temp_float_cache = std::collections::BTreeMap::new();
4222
4223 crate::solver3::cache::calculate_layout_for_subtree(
4224 ctx,
4225 tree,
4226 text_cache,
4227 cell_index,
4228 LogicalPosition::zero(),
4229 cell_constraints.available_size,
4230 &mut temp_positions,
4231 &mut temp_scrollbar_reflow,
4232 &mut temp_float_cache,
4233 )?;
4234
4235 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4236 cell_node.used_size.unwrap_or_default().height
4237 };
4238
4239 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4241 let padding = &cell_node.box_props.padding;
4242 let border = &cell_node.box_props.border;
4243 let writing_mode = constraints.writing_mode;
4244
4245 let total_height = content_height
4246 + padding.main_start(writing_mode)
4247 + padding.main_end(writing_mode)
4248 + border.main_start(writing_mode)
4249 + border.main_end(writing_mode);
4250
4251 debug_table_layout!(
4252 ctx,
4253 "Cell total height: cell_index={}, content={:.2}, padding/border={:.2}, total={:.2}",
4254 cell_index,
4255 content_height,
4256 padding.main_start(writing_mode)
4257 + padding.main_end(writing_mode)
4258 + border.main_start(writing_mode)
4259 + border.main_end(writing_mode),
4260 total_height
4261 );
4262
4263 Ok(total_height)
4264}
4265
4266fn calculate_row_heights<T: ParsedFontTrait>(
4268 table_ctx: &mut TableLayoutContext,
4269 tree: &mut LayoutTree,
4270 text_cache: &mut crate::font_traits::TextLayoutCache,
4271 ctx: &mut LayoutContext<'_, T>,
4272 constraints: &LayoutConstraints,
4273) -> Result<()> {
4274 debug_table_layout!(
4275 ctx,
4276 "calculate_row_heights: num_rows={}, available_size={:?}",
4277 table_ctx.num_rows,
4278 constraints.available_size
4279 );
4280
4281 table_ctx.row_heights = vec![0.0; table_ctx.num_rows];
4283
4284 for &row_idx in &table_ctx.collapsed_rows {
4286 if row_idx < table_ctx.row_heights.len() {
4287 table_ctx.row_heights[row_idx] = 0.0;
4288 }
4289 }
4290
4291 for cell_info in &table_ctx.cells {
4293 if table_ctx.collapsed_rows.contains(&cell_info.row) {
4295 continue;
4296 }
4297
4298 let mut cell_width = 0.0;
4300 for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
4301 if let Some(col) = table_ctx.columns.get(col_idx) {
4302 if let Some(width) = col.computed_width {
4303 cell_width += width;
4304 }
4305 }
4306 }
4307
4308 debug_table_layout!(
4309 ctx,
4310 "Cell layout: node_index={}, row={}, col={}, width={:.2}",
4311 cell_info.node_index,
4312 cell_info.row,
4313 cell_info.column,
4314 cell_width
4315 );
4316
4317 let cell_height = layout_cell_for_height(
4319 ctx,
4320 tree,
4321 text_cache,
4322 cell_info.node_index,
4323 cell_width,
4324 constraints,
4325 )?;
4326
4327 debug_table_layout!(
4328 ctx,
4329 "Cell height calculated: node_index={}, height={:.2}",
4330 cell_info.node_index,
4331 cell_height
4332 );
4333
4334 if cell_info.rowspan == 1 {
4336 let current_height = table_ctx.row_heights[cell_info.row];
4337 table_ctx.row_heights[cell_info.row] = current_height.max(cell_height);
4338 }
4339 }
4340
4341 for cell_info in &table_ctx.cells {
4343 if table_ctx.collapsed_rows.contains(&cell_info.row) {
4345 continue;
4346 }
4347
4348 if cell_info.rowspan > 1 {
4349 let mut cell_width = 0.0;
4351 for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
4352 if let Some(col) = table_ctx.columns.get(col_idx) {
4353 if let Some(width) = col.computed_width {
4354 cell_width += width;
4355 }
4356 }
4357 }
4358
4359 let cell_height = layout_cell_for_height(
4361 ctx,
4362 tree,
4363 text_cache,
4364 cell_info.node_index,
4365 cell_width,
4366 constraints,
4367 )?;
4368
4369 let end_row = cell_info.row + cell_info.rowspan;
4371 let current_total: f32 = table_ctx.row_heights[cell_info.row..end_row]
4372 .iter()
4373 .enumerate()
4374 .filter(|(idx, _)| !table_ctx.collapsed_rows.contains(&(cell_info.row + idx)))
4375 .map(|(_, height)| height)
4376 .sum();
4377
4378 if cell_height > current_total {
4381 let extra_height = cell_height - current_total;
4382
4383 let non_collapsed_rows = (cell_info.row..end_row)
4385 .filter(|row_idx| !table_ctx.collapsed_rows.contains(row_idx))
4386 .count();
4387
4388 if non_collapsed_rows > 0 {
4389 let per_row = extra_height / non_collapsed_rows as f32;
4390
4391 for row_idx in cell_info.row..end_row {
4392 if !table_ctx.collapsed_rows.contains(&row_idx) {
4393 table_ctx.row_heights[row_idx] += per_row;
4394 }
4395 }
4396 }
4397 }
4398 }
4399 }
4400
4401 for &row_idx in &table_ctx.collapsed_rows {
4403 if row_idx < table_ctx.row_heights.len() {
4404 table_ctx.row_heights[row_idx] = 0.0;
4405 }
4406 }
4407
4408 Ok(())
4409}
4410
4411fn position_table_cells<T: ParsedFontTrait>(
4413 table_ctx: &mut TableLayoutContext,
4414 tree: &mut LayoutTree,
4415 ctx: &mut LayoutContext<'_, T>,
4416 table_index: usize,
4417 constraints: &LayoutConstraints,
4418) -> Result<BTreeMap<usize, LogicalPosition>> {
4419 debug_log!(ctx, "Positioning table cells in grid");
4420
4421 let mut positions = BTreeMap::new();
4422
4423 let (h_spacing, v_spacing) = if table_ctx.border_collapse == StyleBorderCollapse::Separate {
4425 let styled_dom = ctx.styled_dom;
4426 let table_id = tree.nodes[table_index].dom_node_id.unwrap();
4427 let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
4428
4429 let spacing_context = ResolutionContext {
4430 element_font_size: get_element_font_size(styled_dom, table_id, table_state),
4431 parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
4432 root_font_size: get_root_font_size(styled_dom, table_state),
4433 containing_block_size: PhysicalSize::new(0.0, 0.0),
4434 element_size: None,
4435 viewport_size: PhysicalSize::new(0.0, 0.0), };
4437
4438 let h = table_ctx
4439 .border_spacing
4440 .horizontal
4441 .resolve_with_context(&spacing_context, PropertyContext::Other);
4442
4443 let v = table_ctx
4444 .border_spacing
4445 .vertical
4446 .resolve_with_context(&spacing_context, PropertyContext::Other);
4447
4448 (h, v)
4449 } else {
4450 (0.0, 0.0)
4451 };
4452
4453 debug_log!(
4454 ctx,
4455 "Border spacing: h={:.2}, v={:.2}",
4456 h_spacing,
4457 v_spacing
4458 );
4459
4460 let mut col_positions = vec![0.0; table_ctx.columns.len()];
4462 let mut x_offset = h_spacing; for (i, col) in table_ctx.columns.iter().enumerate() {
4464 col_positions[i] = x_offset;
4465 if let Some(width) = col.computed_width {
4466 x_offset += width + h_spacing; }
4468 }
4469
4470 let mut row_positions = vec![0.0; table_ctx.num_rows];
4472 let mut y_offset = v_spacing; for (i, &height) in table_ctx.row_heights.iter().enumerate() {
4474 row_positions[i] = y_offset;
4475 y_offset += height + v_spacing; }
4477
4478 for cell_info in &table_ctx.cells {
4480 let cell_node = tree
4481 .get_mut(cell_info.node_index)
4482 .ok_or(LayoutError::InvalidTree)?;
4483
4484 let x = col_positions.get(cell_info.column).copied().unwrap_or(0.0);
4486 let y = row_positions.get(cell_info.row).copied().unwrap_or(0.0);
4487
4488 let mut width = 0.0;
4490 debug_info!(
4491 ctx,
4492 "[position_table_cells] Cell {}: calculating width from cols {}..{}",
4493 cell_info.node_index,
4494 cell_info.column,
4495 cell_info.column + cell_info.colspan
4496 );
4497 for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
4498 if let Some(col) = table_ctx.columns.get(col_idx) {
4499 debug_info!(
4500 ctx,
4501 "[position_table_cells] Col {}: computed_width={:?}",
4502 col_idx,
4503 col.computed_width
4504 );
4505 if let Some(col_width) = col.computed_width {
4506 width += col_width;
4507 if col_idx < cell_info.column + cell_info.colspan - 1 {
4509 width += h_spacing;
4510 }
4511 } else {
4512 debug_info!(
4513 ctx,
4514 "[position_table_cells] WARN: Col {} has NO computed_width!",
4515 col_idx
4516 );
4517 }
4518 } else {
4519 debug_info!(
4520 ctx,
4521 "[position_table_cells] WARN: Col {} not found in table_ctx.columns!",
4522 col_idx
4523 );
4524 }
4525 }
4526
4527 let mut height = 0.0;
4528 let end_row = cell_info.row + cell_info.rowspan;
4529 for row_idx in cell_info.row..end_row {
4530 if let Some(&row_height) = table_ctx.row_heights.get(row_idx) {
4531 height += row_height;
4532 if row_idx < end_row - 1 {
4534 height += v_spacing;
4535 }
4536 }
4537 }
4538
4539 let writing_mode = constraints.writing_mode;
4541 debug_info!(
4544 ctx,
4545 "[position_table_cells] Cell {}: BEFORE from_main_cross: width={}, height={}, \
4546 writing_mode={:?}",
4547 cell_info.node_index,
4548 width,
4549 height,
4550 writing_mode
4551 );
4552
4553 cell_node.used_size = Some(LogicalSize::from_main_cross(height, width, writing_mode));
4554
4555 debug_info!(
4556 ctx,
4557 "[position_table_cells] Cell {}: AFTER from_main_cross: used_size={:?}",
4558 cell_info.node_index,
4559 cell_node.used_size
4560 );
4561
4562 debug_info!(
4563 ctx,
4564 "[position_table_cells] Cell {}: setting used_size to {}x{} (row_heights={:?})",
4565 cell_info.node_index,
4566 width,
4567 height,
4568 table_ctx.row_heights
4569 );
4570
4571 if let Some(ref cached_layout) = cell_node.inline_layout_result {
4573 let inline_result = &cached_layout.layout;
4574 use StyleVerticalAlign;
4575
4576 let vertical_align = if let Some(dom_id) = cell_node.dom_node_id {
4578 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
4579 let node_state = StyledNodeState::default();
4580
4581 ctx.styled_dom
4582 .css_property_cache
4583 .ptr
4584 .get_vertical_align(node_data, &dom_id, &node_state)
4585 .and_then(|v| v.get_property().copied())
4586 .unwrap_or(StyleVerticalAlign::Top)
4587 } else {
4588 StyleVerticalAlign::Top
4589 };
4590
4591 let content_bounds = inline_result.bounds();
4593 let content_height = content_bounds.height;
4594
4595 let padding = &cell_node.box_props.padding;
4598 let border = &cell_node.box_props.border;
4599 let content_box_height = height
4600 - padding.main_start(writing_mode)
4601 - padding.main_end(writing_mode)
4602 - border.main_start(writing_mode)
4603 - border.main_end(writing_mode);
4604
4605 let align_factor = match vertical_align {
4607 StyleVerticalAlign::Top => 0.0,
4608 StyleVerticalAlign::Middle => 0.5,
4609 StyleVerticalAlign::Bottom => 1.0,
4610 StyleVerticalAlign::Baseline
4612 | StyleVerticalAlign::Sub
4613 | StyleVerticalAlign::Superscript
4614 | StyleVerticalAlign::TextTop
4615 | StyleVerticalAlign::TextBottom => 0.5,
4616 };
4617 let y_offset = (content_box_height - content_height) * align_factor;
4618
4619 debug_info!(
4620 ctx,
4621 "[position_table_cells] Cell {}: vertical-align={:?}, border_box_height={}, \
4622 content_box_height={}, content_height={}, y_offset={}",
4623 cell_info.node_index,
4624 vertical_align,
4625 height,
4626 content_box_height,
4627 content_height,
4628 y_offset
4629 );
4630
4631 if y_offset.abs() > 0.01 {
4633 use std::sync::Arc;
4635
4636 use crate::text3::cache::{PositionedItem, UnifiedLayout};
4637
4638 let adjusted_items: Vec<PositionedItem> = inline_result
4639 .items
4640 .iter()
4641 .map(|item| PositionedItem {
4642 item: item.item.clone(),
4643 position: crate::text3::cache::Point {
4644 x: item.position.x,
4645 y: item.position.y + y_offset,
4646 },
4647 line_index: item.line_index,
4648 })
4649 .collect();
4650
4651 let adjusted_layout = UnifiedLayout {
4652 items: adjusted_items,
4653 overflow: inline_result.overflow.clone(),
4654 };
4655
4656 cell_node.inline_layout_result = Some(CachedInlineLayout::new(
4658 Arc::new(adjusted_layout),
4659 cached_layout.available_width,
4660 cached_layout.has_floats,
4661 ));
4662 }
4663 }
4664
4665 let position = LogicalPosition::from_main_cross(y, x, writing_mode);
4667
4668 positions.insert(cell_info.node_index, position);
4670
4671 debug_log!(
4672 ctx,
4673 "Cell at row={}, col={}: pos=({:.2}, {:.2}), size=({:.2}x{:.2})",
4674 cell_info.row,
4675 cell_info.column,
4676 x,
4677 y,
4678 width,
4679 height
4680 );
4681 }
4682
4683 Ok(positions)
4684}
4685
4686fn collect_and_measure_inline_content<T: ParsedFontTrait>(
4696 ctx: &mut LayoutContext<'_, T>,
4697 text_cache: &mut TextLayoutCache,
4698 tree: &mut LayoutTree,
4699 ifc_root_index: usize,
4700 constraints: &LayoutConstraints,
4701) -> Result<(Vec<InlineContent>, HashMap<ContentIndex, usize>)> {
4702 use crate::solver3::layout_tree::{IfcId, IfcMembership};
4703
4704 debug_ifc_layout!(
4705 ctx,
4706 "collect_and_measure_inline_content: node_index={}",
4707 ifc_root_index
4708 );
4709
4710 let ifc_id = IfcId::unique();
4712
4713 if let Some(ifc_root_node) = tree.get_mut(ifc_root_index) {
4715 ifc_root_node.ifc_id = Some(ifc_id);
4716 }
4717
4718 let mut content = Vec::new();
4719 let mut child_map = HashMap::new();
4721 let mut current_run_index: u32 = 0;
4723
4724 let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
4725
4726 let is_anonymous = ifc_root_node.dom_node_id.is_none();
4728
4729 let ifc_root_dom_id = match ifc_root_node.dom_node_id {
4732 Some(id) => id,
4733 None => {
4734 let parent_dom_id = ifc_root_node
4736 .parent
4737 .and_then(|p| tree.get(p))
4738 .and_then(|n| n.dom_node_id);
4739
4740 if let Some(id) = parent_dom_id {
4741 id
4742 } else {
4743 match ifc_root_node
4745 .children
4746 .iter()
4747 .filter_map(|&child_idx| tree.get(child_idx))
4748 .filter_map(|n| n.dom_node_id)
4749 .next()
4750 {
4751 Some(id) => id,
4752 None => {
4753 debug_warning!(ctx, "IFC root and all ancestors/children have no DOM ID");
4754 return Ok((content, child_map));
4755 }
4756 }
4757 }
4758 }
4759 };
4760
4761 let children: Vec<_> = ifc_root_node.children.clone();
4763 drop(ifc_root_node);
4764
4765 debug_ifc_layout!(
4766 ctx,
4767 "Node {} has {} layout children, is_anonymous={}",
4768 ifc_root_index,
4769 children.len(),
4770 is_anonymous
4771 );
4772
4773 if is_anonymous {
4776 for (item_idx, &child_index) in children.iter().enumerate() {
4778 let content_index = ContentIndex {
4779 run_index: ifc_root_index as u32,
4780 item_index: item_idx as u32,
4781 };
4782
4783 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
4784 let Some(dom_id) = child_node.dom_node_id else {
4785 debug_warning!(
4786 ctx,
4787 "Anonymous IFC child at index {} has no DOM ID",
4788 child_index
4789 );
4790 continue;
4791 };
4792
4793 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
4794
4795 if let NodeType::Text(ref text_content) = node_data.get_node_type() {
4797 debug_info!(
4798 ctx,
4799 "[collect_and_measure_inline_content] OK: Found text node (DOM {:?}) in anonymous wrapper: '{}'",
4800 dom_id,
4801 text_content.as_str()
4802 );
4803 let style = Arc::new(get_style_properties(ctx.styled_dom, dom_id));
4807 let text_items = split_text_for_whitespace(
4808 ctx.styled_dom,
4809 dom_id,
4810 text_content.as_str(),
4811 style,
4812 );
4813 content.extend(text_items);
4814 child_map.insert(content_index, child_index);
4815
4816 drop(child_node);
4818 if let Some(child_node_mut) = tree.get_mut(child_index) {
4819 child_node_mut.ifc_membership = Some(IfcMembership {
4820 ifc_id,
4821 ifc_root_layout_index: ifc_root_index,
4822 run_index: current_run_index,
4823 });
4824 }
4825 current_run_index += 1;
4826
4827 continue;
4828 }
4829
4830 let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
4832 if display != LayoutDisplay::Inline {
4833 let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
4838 let box_props = child_node.box_props.clone();
4839
4840 let styled_node_state = ctx
4841 .styled_dom
4842 .styled_nodes
4843 .as_container()
4844 .get(dom_id)
4845 .map(|n| n.styled_node_state.clone())
4846 .unwrap_or_default();
4847
4848 let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
4850 ctx.styled_dom,
4851 Some(dom_id),
4852 constraints.containing_block_size,
4853 intrinsic_size,
4854 &box_props,
4855 )?;
4856
4857 let writing_mode = get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state)
4858 .unwrap_or_default();
4859
4860 let content_box_size = box_props.inner_size(tentative_size, writing_mode);
4862
4863 let child_constraints = LayoutConstraints {
4865 available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
4866 writing_mode,
4867 bfc_state: None,
4868 text_align: TextAlign::Start,
4869 containing_block_size: constraints.containing_block_size,
4870 available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
4871 };
4872
4873 drop(child_node);
4875
4876 let mut empty_float_cache = std::collections::BTreeMap::new();
4878 let layout_result = layout_formatting_context(
4879 ctx,
4880 tree,
4881 text_cache,
4882 child_index,
4883 &child_constraints,
4884 &mut empty_float_cache,
4885 )?;
4886
4887 let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
4888
4889 let final_height = match css_height.unwrap_or_default() {
4891 LayoutHeight::Auto => {
4892 let content_height = layout_result.output.overflow_size.height;
4893 content_height
4894 + box_props.padding.main_sum(writing_mode)
4895 + box_props.border.main_sum(writing_mode)
4896 }
4897 _ => tentative_size.height,
4898 };
4899
4900 let final_size = LogicalSize::new(tentative_size.width, final_height);
4901
4902 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
4904
4905 let baseline_offset = layout_result.output.baseline.unwrap_or(final_height);
4906
4907 let margin = &box_props.margin;
4910 let margin_box_width = final_size.width + margin.left + margin.right;
4911 let margin_box_height = final_size.height + margin.top + margin.bottom;
4912
4913 let shape_content_index = ContentIndex {
4916 run_index: content.len() as u32,
4917 item_index: 0,
4918 };
4919 content.push(InlineContent::Shape(InlineShape {
4920 shape_def: ShapeDefinition::Rectangle {
4921 size: crate::text3::cache::Size {
4922 width: margin_box_width,
4924 height: margin_box_height,
4925 },
4926 corner_radius: None,
4927 },
4928 fill: None,
4929 stroke: None,
4930 baseline_offset: baseline_offset + margin.top,
4932 source_node_id: Some(dom_id),
4933 }));
4934 child_map.insert(shape_content_index, child_index);
4935 } else {
4936 let span_style = get_style_properties(ctx.styled_dom, dom_id);
4938 collect_inline_span_recursive(
4939 ctx,
4940 tree,
4941 dom_id,
4942 span_style,
4943 &mut content,
4944 &mut child_map,
4945 &children,
4946 constraints,
4947 )?;
4948 }
4949 }
4950
4951 return Ok((content, child_map));
4952 }
4953
4954 let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
4960 let mut list_item_dom_id: Option<NodeId> = None;
4961
4962 if let Some(dom_id) = ifc_root_node.dom_node_id {
4964 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
4965 let node_state = StyledNodeState::default();
4966
4967 if let Some(display_value) =
4968 ctx.styled_dom
4969 .css_property_cache
4970 .ptr
4971 .get_display(node_data, &dom_id, &node_state)
4972 {
4973 if let Some(display) = display_value.get_property() {
4974 use LayoutDisplay;
4975 if *display == LayoutDisplay::ListItem {
4976 debug_ifc_layout!(ctx, "IFC root NodeId({:?}) is list-item", dom_id);
4977 list_item_dom_id = Some(dom_id);
4978 }
4979 }
4980 }
4981 }
4982
4983 if list_item_dom_id.is_none() {
4985 if let Some(parent_idx) = ifc_root_node.parent {
4986 if let Some(parent_node) = tree.get(parent_idx) {
4987 if let Some(parent_dom_id) = parent_node.dom_node_id {
4988 let parent_node_data = &ctx.styled_dom.node_data.as_container()[parent_dom_id];
4989 let parent_node_state = StyledNodeState::default();
4990
4991 if let Some(display_value) = ctx.styled_dom.css_property_cache.ptr.get_display(
4992 parent_node_data,
4993 &parent_dom_id,
4994 &parent_node_state,
4995 ) {
4996 if let Some(display) = display_value.get_property() {
4997 use LayoutDisplay;
4998 if *display == LayoutDisplay::ListItem {
4999 debug_ifc_layout!(
5000 ctx,
5001 "IFC root parent NodeId({:?}) is list-item",
5002 parent_dom_id
5003 );
5004 list_item_dom_id = Some(parent_dom_id);
5005 }
5006 }
5007 }
5008 }
5009 }
5010 }
5011 }
5012
5013 if let Some(list_dom_id) = list_item_dom_id {
5015 debug_ifc_layout!(
5016 ctx,
5017 "Found list-item (NodeId({:?})), generating marker",
5018 list_dom_id
5019 );
5020
5021 let list_item_layout_idx = tree
5023 .nodes
5024 .iter()
5025 .enumerate()
5026 .find(|(_, node)| {
5027 node.dom_node_id == Some(list_dom_id) && node.pseudo_element.is_none()
5028 })
5029 .map(|(idx, _)| idx);
5030
5031 if let Some(list_idx) = list_item_layout_idx {
5032 let list_item_node = tree.get(list_idx).ok_or(LayoutError::InvalidTree)?;
5035 let marker_idx = list_item_node
5036 .children
5037 .iter()
5038 .find(|&&child_idx| {
5039 tree.get(child_idx)
5040 .map(|child| child.pseudo_element == Some(PseudoElement::Marker))
5041 .unwrap_or(false)
5042 })
5043 .copied();
5044
5045 if let Some(marker_idx) = marker_idx {
5046 debug_ifc_layout!(ctx, "Found ::marker pseudo-element at index {}", marker_idx);
5047
5048 let list_dom_id_for_style = tree
5051 .get(marker_idx)
5052 .and_then(|n| n.dom_node_id)
5053 .unwrap_or(list_dom_id);
5054
5055 let list_style_position =
5059 get_list_style_position(ctx.styled_dom, Some(list_dom_id));
5060 let position_outside =
5061 matches!(list_style_position, StyleListStylePosition::Outside);
5062
5063 debug_ifc_layout!(
5064 ctx,
5065 "List marker list-style-position: {:?} (outside={})",
5066 list_style_position,
5067 position_outside
5068 );
5069
5070 let base_style =
5072 Arc::new(get_style_properties(ctx.styled_dom, list_dom_id_for_style));
5073 let marker_segments = generate_list_marker_segments(
5074 tree,
5075 ctx.styled_dom,
5076 marker_idx, ctx.counters,
5078 base_style,
5079 ctx.debug_messages,
5080 );
5081
5082 debug_ifc_layout!(
5083 ctx,
5084 "Generated {} list marker segments",
5085 marker_segments.len()
5086 );
5087
5088 for segment in marker_segments {
5091 content.push(InlineContent::Marker {
5092 run: segment,
5093 position_outside,
5094 });
5095 }
5096 } else {
5097 debug_ifc_layout!(
5098 ctx,
5099 "WARNING: List-item at index {} has no ::marker pseudo-element",
5100 list_idx
5101 );
5102 }
5103 }
5104 }
5105
5106 drop(ifc_root_node);
5107
5108 let node_hier_item = &ctx.styled_dom.node_hierarchy.as_container()[ifc_root_dom_id];
5116 debug_info!(
5117 ctx,
5118 "[collect_and_measure_inline_content] DEBUG: node_hier_item.first_child={:?}, \
5119 last_child={:?}",
5120 node_hier_item.first_child_id(ifc_root_dom_id),
5121 node_hier_item.last_child_id()
5122 );
5123
5124 let dom_children: Vec<NodeId> = ifc_root_dom_id
5125 .az_children(&ctx.styled_dom.node_hierarchy.as_container())
5126 .collect();
5127
5128 let ifc_root_node_data = &ctx.styled_dom.node_data.as_container()[ifc_root_dom_id];
5129
5130 if let NodeType::Text(ref text_content) = ifc_root_node_data.get_node_type() {
5134 let style = Arc::new(get_style_properties(ctx.styled_dom, ifc_root_dom_id));
5135 let text_items = split_text_for_whitespace(
5136 ctx.styled_dom,
5137 ifc_root_dom_id,
5138 text_content.as_str(),
5139 style,
5140 );
5141 content.extend(text_items);
5142 return Ok((content, child_map));
5143 }
5144
5145 let ifc_root_node_type = match ifc_root_node_data.get_node_type() {
5146 NodeType::Div => "Div",
5147 NodeType::Text(_) => "Text",
5148 NodeType::Body => "Body",
5149 _ => "Other",
5150 };
5151
5152 debug_info!(
5153 ctx,
5154 "[collect_and_measure_inline_content] IFC root has {} DOM children",
5155 dom_children.len()
5156 );
5157
5158 for (item_idx, &dom_child_id) in dom_children.iter().enumerate() {
5159 let content_index = ContentIndex {
5160 run_index: ifc_root_index as u32,
5161 item_index: item_idx as u32,
5162 };
5163
5164 let node_data = &ctx.styled_dom.node_data.as_container()[dom_child_id];
5165
5166 if let NodeType::Text(ref text_content) = node_data.get_node_type() {
5168 debug_info!(
5169 ctx,
5170 "[collect_and_measure_inline_content] OK: Found text node (DOM child {:?}): '{}'",
5171 dom_child_id,
5172 text_content.as_str()
5173 );
5174
5175 let style = Arc::new(get_style_properties(ctx.styled_dom, dom_child_id));
5179 let text_items = split_text_for_whitespace(
5180 ctx.styled_dom,
5181 dom_child_id,
5182 text_content.as_str(),
5183 style,
5184 );
5185 content.extend(text_items);
5186
5187 if let Some(&layout_idx) = tree.dom_to_layout.get(&dom_child_id).and_then(|v| v.first()) {
5191 if let Some(text_layout_node) = tree.get_mut(layout_idx) {
5192 text_layout_node.ifc_membership = Some(IfcMembership {
5193 ifc_id,
5194 ifc_root_layout_index: ifc_root_index,
5195 run_index: current_run_index,
5196 });
5197 }
5198 }
5199 current_run_index += 1;
5200
5201 continue;
5202 }
5203
5204 let child_index = children
5206 .iter()
5207 .find(|&&idx| {
5208 tree.get(idx)
5209 .and_then(|n| n.dom_node_id)
5210 .map(|id| id == dom_child_id)
5211 .unwrap_or(false)
5212 })
5213 .copied();
5214
5215 let Some(child_index) = child_index else {
5216 debug_info!(
5217 ctx,
5218 "[collect_and_measure_inline_content] WARN: DOM child {:?} has no layout node",
5219 dom_child_id
5220 );
5221 continue;
5222 };
5223
5224 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5225 let dom_id = child_node.dom_node_id.unwrap();
5227
5228 let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
5229
5230 if display != LayoutDisplay::Inline {
5231 let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
5236 let box_props = child_node.box_props.clone();
5237
5238 let styled_node_state = ctx
5239 .styled_dom
5240 .styled_nodes
5241 .as_container()
5242 .get(dom_id)
5243 .map(|n| n.styled_node_state.clone())
5244 .unwrap_or_default();
5245
5246 let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
5249 ctx.styled_dom,
5250 Some(dom_id),
5251 constraints.containing_block_size,
5252 intrinsic_size,
5253 &box_props,
5254 )?;
5255
5256 let writing_mode =
5257 get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
5258
5259 let content_box_size = box_props.inner_size(tentative_size, writing_mode);
5261
5262 debug_info!(
5263 ctx,
5264 "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
5265 tentative_border_box={:?}, content_box={:?}",
5266 dom_id,
5267 tentative_size,
5268 content_box_size
5269 );
5270
5271 let child_constraints = LayoutConstraints {
5273 available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
5274 writing_mode,
5275 bfc_state: None,
5277 text_align: TextAlign::Start,
5279 containing_block_size: constraints.containing_block_size,
5280 available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
5281 };
5282
5283 drop(child_node);
5285
5286 let mut empty_float_cache = std::collections::BTreeMap::new();
5289 let layout_result = layout_formatting_context(
5290 ctx,
5291 tree,
5292 text_cache,
5293 child_index,
5294 &child_constraints,
5295 &mut empty_float_cache,
5296 )?;
5297
5298 let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
5299
5300 let final_height = match css_height.unwrap_or_default() {
5302 LayoutHeight::Auto => {
5303 let content_height = layout_result.output.overflow_size.height;
5305 content_height
5306 + box_props.padding.main_sum(writing_mode)
5307 + box_props.border.main_sum(writing_mode)
5308 }
5309 _ => tentative_size.height,
5311 };
5312
5313 debug_info!(
5314 ctx,
5315 "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
5316 layout_content_height={}, css_height={:?}, final_border_box_height={}",
5317 dom_id,
5318 layout_result.output.overflow_size.height,
5319 css_height,
5320 final_height
5321 );
5322
5323 let final_size = LogicalSize::new(tentative_size.width, final_height);
5324
5325 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5327
5328 let baseline_from_top = layout_result.output.baseline;
5341 let baseline_offset = match baseline_from_top {
5342 Some(baseline_y) => {
5343 let content_box_top = box_props.padding.top + box_props.border.top;
5346 let baseline_from_border_box_top = baseline_y + content_box_top;
5347 (final_height - baseline_from_border_box_top).max(0.0)
5349 }
5350 None => {
5351 0.0
5353 }
5354 };
5355
5356 debug_info!(
5357 ctx,
5358 "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
5359 baseline_from_top={:?}, final_height={}, baseline_offset_from_bottom={}",
5360 dom_id,
5361 baseline_from_top,
5362 final_height,
5363 baseline_offset
5364 );
5365
5366 let margin = &box_props.margin;
5370 let margin_box_width = final_size.width + margin.left + margin.right;
5371 let margin_box_height = final_size.height + margin.top + margin.bottom;
5372
5373 let shape_content_index = ContentIndex {
5376 run_index: content.len() as u32,
5377 item_index: 0,
5378 };
5379 content.push(InlineContent::Shape(InlineShape {
5380 shape_def: ShapeDefinition::Rectangle {
5381 size: crate::text3::cache::Size {
5382 width: margin_box_width,
5384 height: margin_box_height,
5385 },
5386 corner_radius: None,
5387 },
5388 fill: None,
5389 stroke: None,
5390 baseline_offset: baseline_offset + margin.top,
5392 source_node_id: Some(dom_id),
5393 }));
5394 child_map.insert(shape_content_index, child_index);
5395 } else if let NodeType::Image(image_ref) =
5396 ctx.styled_dom.node_data.as_container()[dom_id].get_node_type()
5397 {
5398 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5403 let box_props = child_node.box_props.clone();
5404
5405 let intrinsic_size = child_node
5407 .intrinsic_sizes
5408 .clone()
5409 .unwrap_or(IntrinsicSizes {
5410 max_content_width: 50.0,
5411 max_content_height: 50.0,
5412 ..Default::default()
5413 });
5414
5415 let styled_node_state = ctx
5417 .styled_dom
5418 .styled_nodes
5419 .as_container()
5420 .get(dom_id)
5421 .map(|n| n.styled_node_state.clone())
5422 .unwrap_or_default();
5423
5424 let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
5426 ctx.styled_dom,
5427 Some(dom_id),
5428 constraints.containing_block_size,
5429 intrinsic_size.clone(),
5430 &box_props,
5431 )?;
5432
5433 drop(child_node);
5435
5436 let final_size = LogicalSize::new(tentative_size.width, tentative_size.height);
5438 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5439
5440 println!("[FC IMAGE] Image node {} final_size: {}x{}",
5441 child_index, final_size.width, final_size.height);
5442
5443 let display_width = if final_size.width > 0.0 {
5445 Some(final_size.width)
5446 } else {
5447 None
5448 };
5449 let display_height = if final_size.height > 0.0 {
5450 Some(final_size.height)
5451 } else {
5452 None
5453 };
5454
5455 content.push(InlineContent::Image(InlineImage {
5456 source: ImageSource::Ref(image_ref.clone()),
5457 intrinsic_size: crate::text3::cache::Size {
5458 width: intrinsic_size.max_content_width,
5459 height: intrinsic_size.max_content_height,
5460 },
5461 display_size: if display_width.is_some() || display_height.is_some() {
5462 Some(crate::text3::cache::Size {
5463 width: display_width.unwrap_or(intrinsic_size.max_content_width),
5464 height: display_height.unwrap_or(intrinsic_size.max_content_height),
5465 })
5466 } else {
5467 None
5468 },
5469 baseline_offset: 0.0,
5471 alignment: crate::text3::cache::VerticalAlign::Baseline,
5472 object_fit: ObjectFit::Fill,
5473 }));
5474 let image_content_index = ContentIndex {
5477 run_index: (content.len() - 1) as u32, item_index: 0,
5479 };
5480 child_map.insert(image_content_index, child_index);
5481 } else {
5482 debug_info!(
5487 ctx,
5488 "[collect_and_measure_inline_content] Found inline span (DOM {:?}), recursing",
5489 dom_id
5490 );
5491
5492 let span_style = get_style_properties(ctx.styled_dom, dom_id);
5493 collect_inline_span_recursive(
5494 ctx,
5495 tree,
5496 dom_id,
5497 span_style,
5498 &mut content,
5499 &mut child_map,
5500 &children,
5501 constraints,
5502 )?;
5503 }
5504 }
5505 Ok((content, child_map))
5506}
5507
5508fn collect_inline_span_recursive<T: ParsedFontTrait>(
5521 ctx: &mut LayoutContext<'_, T>,
5522 tree: &mut LayoutTree,
5523 span_dom_id: NodeId,
5524 span_style: StyleProperties,
5525 content: &mut Vec<InlineContent>,
5526 child_map: &mut HashMap<ContentIndex, usize>,
5527 parent_children: &[usize], constraints: &LayoutConstraints,
5529) -> Result<()> {
5530 debug_info!(
5531 ctx,
5532 "[collect_inline_span_recursive] Processing inline span {:?}",
5533 span_dom_id
5534 );
5535
5536 let span_dom_children: Vec<NodeId> = span_dom_id
5538 .az_children(&ctx.styled_dom.node_hierarchy.as_container())
5539 .collect();
5540
5541 debug_info!(
5542 ctx,
5543 "[collect_inline_span_recursive] Span has {} DOM children",
5544 span_dom_children.len()
5545 );
5546
5547 for &child_dom_id in &span_dom_children {
5548 let node_data = &ctx.styled_dom.node_data.as_container()[child_dom_id];
5549
5550 if let NodeType::Text(ref text_content) = node_data.get_node_type() {
5552 debug_info!(
5553 ctx,
5554 "[collect_inline_span_recursive] ✓ Found text in span: '{}'",
5555 text_content.as_str()
5556 );
5557 content.push(InlineContent::Text(StyledRun {
5558 text: text_content.to_string(),
5559 style: Arc::new(span_style.clone()),
5560 logical_start_byte: 0,
5561 source_node_id: Some(child_dom_id),
5562 }));
5563 continue;
5564 }
5565
5566 let child_display =
5568 get_display_property(ctx.styled_dom, Some(child_dom_id)).unwrap_or_default();
5569
5570 let child_index = parent_children
5572 .iter()
5573 .find(|&&idx| {
5574 tree.get(idx)
5575 .and_then(|n| n.dom_node_id)
5576 .map(|id| id == child_dom_id)
5577 .unwrap_or(false)
5578 })
5579 .copied();
5580
5581 match child_display {
5582 LayoutDisplay::Inline => {
5583 debug_info!(
5585 ctx,
5586 "[collect_inline_span_recursive] Found nested inline span {:?}",
5587 child_dom_id
5588 );
5589 let child_style = get_style_properties(ctx.styled_dom, child_dom_id);
5590 collect_inline_span_recursive(
5591 ctx,
5592 tree,
5593 child_dom_id,
5594 child_style,
5595 content,
5596 child_map,
5597 parent_children,
5598 constraints,
5599 )?;
5600 }
5601 LayoutDisplay::InlineBlock => {
5602 let Some(child_index) = child_index else {
5604 debug_info!(
5605 ctx,
5606 "[collect_inline_span_recursive] WARNING: inline-block {:?} has no layout \
5607 node",
5608 child_dom_id
5609 );
5610 continue;
5611 };
5612
5613 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5614 let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
5615 let width = intrinsic_size.max_content_width;
5616
5617 let styled_node_state = ctx
5618 .styled_dom
5619 .styled_nodes
5620 .as_container()
5621 .get(child_dom_id)
5622 .map(|n| n.styled_node_state.clone())
5623 .unwrap_or_default();
5624 let writing_mode =
5625 get_writing_mode(ctx.styled_dom, child_dom_id, &styled_node_state)
5626 .unwrap_or_default();
5627 let child_constraints = LayoutConstraints {
5628 available_size: LogicalSize::new(width, f32::INFINITY),
5629 writing_mode,
5630 bfc_state: None,
5631 text_align: TextAlign::Start,
5632 containing_block_size: constraints.containing_block_size,
5633 available_width_type: Text3AvailableSpace::Definite(width),
5634 };
5635
5636 drop(child_node);
5637
5638 let mut empty_float_cache = std::collections::BTreeMap::new();
5639 let layout_result = layout_formatting_context(
5640 ctx,
5641 tree,
5642 &mut TextLayoutCache::default(),
5643 child_index,
5644 &child_constraints,
5645 &mut empty_float_cache,
5646 )?;
5647 let final_height = layout_result.output.overflow_size.height;
5648 let final_size = LogicalSize::new(width, final_height);
5649
5650 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5651 let baseline_offset = layout_result.output.baseline.unwrap_or(final_height);
5652
5653 content.push(InlineContent::Shape(InlineShape {
5654 shape_def: ShapeDefinition::Rectangle {
5655 size: crate::text3::cache::Size {
5656 width,
5657 height: final_height,
5658 },
5659 corner_radius: None,
5660 },
5661 fill: None,
5662 stroke: None,
5663 baseline_offset,
5664 source_node_id: Some(child_dom_id),
5665 }));
5666
5667 debug_info!(
5669 ctx,
5670 "[collect_inline_span_recursive] Added inline-block shape {}x{}",
5671 width,
5672 final_height
5673 );
5674 }
5675 _ => {
5676 debug_info!(
5678 ctx,
5679 "[collect_inline_span_recursive] WARNING: Unsupported display type {:?} \
5680 inside inline span",
5681 child_display
5682 );
5683 }
5684 }
5685 }
5686
5687 Ok(())
5688}
5689
5690fn position_floated_child(
5693 _child_index: usize,
5694 child_margin_box_size: LogicalSize,
5695 float_type: LayoutFloat,
5696 constraints: &LayoutConstraints,
5697 _bfc_content_box: LogicalRect,
5698 current_main_offset: f32,
5699 floating_context: &mut FloatingContext,
5700) -> Result<LogicalPosition> {
5701 let wm = constraints.writing_mode;
5702 let child_main_size = child_margin_box_size.main(wm);
5703 let child_cross_size = child_margin_box_size.cross(wm);
5704 let bfc_cross_size = constraints.available_size.cross(wm);
5705 let mut placement_main_offset = current_main_offset;
5706
5707 loop {
5708 let (available_cross_start, available_cross_end) = floating_context
5711 .available_line_box_space(
5712 placement_main_offset,
5713 placement_main_offset + child_main_size,
5714 bfc_cross_size,
5715 wm,
5716 );
5717
5718 let available_cross_width = available_cross_end - available_cross_start;
5719
5720 if child_cross_size <= available_cross_width {
5722 let final_cross_pos = match float_type {
5724 LayoutFloat::Left => available_cross_start,
5725 LayoutFloat::Right => available_cross_end - child_cross_size,
5726 LayoutFloat::None => unreachable!(),
5727 };
5728 let final_pos =
5729 LogicalPosition::from_main_cross(placement_main_offset, final_cross_pos, wm);
5730
5731 let new_float_box = FloatBox {
5732 kind: float_type,
5733 rect: LogicalRect::new(final_pos, child_margin_box_size),
5734 margin: EdgeSizes::default(), };
5736 floating_context.floats.push(new_float_box);
5737 return Ok(final_pos);
5738 } else {
5739 let mut next_main_offset = f32::INFINITY;
5743 for existing_float in &floating_context.floats {
5744 let float_main_start = existing_float.rect.origin.main(wm);
5745 let float_main_end = float_main_start + existing_float.rect.size.main(wm);
5746
5747 if placement_main_offset < float_main_end {
5749 next_main_offset = next_main_offset.min(float_main_end);
5750 }
5751 }
5752
5753 if next_main_offset.is_infinite() {
5754 return Err(LayoutError::PositioningFailed);
5757 }
5758 placement_main_offset = next_main_offset;
5759 }
5760 }
5761}
5762
5763fn get_float_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutFloat {
5767 let Some(id) = dom_id else {
5768 return LayoutFloat::None;
5769 };
5770 let node_data = &styled_dom.node_data.as_container()[id];
5771 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
5772 styled_dom
5773 .css_property_cache
5774 .ptr
5775 .get_float(node_data, &id, node_state)
5776 .and_then(|f| {
5777 f.get_property().map(|inner| match inner {
5778 LayoutFloat::Left => LayoutFloat::Left,
5779 LayoutFloat::Right => LayoutFloat::Right,
5780 LayoutFloat::None => LayoutFloat::None,
5781 })
5782 })
5783 .unwrap_or(LayoutFloat::None)
5784}
5785
5786fn get_clear_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutClear {
5787 let Some(id) = dom_id else {
5788 return LayoutClear::None;
5789 };
5790 let node_data = &styled_dom.node_data.as_container()[id];
5791 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
5792 styled_dom
5793 .css_property_cache
5794 .ptr
5795 .get_clear(node_data, &id, node_state)
5796 .and_then(|c| {
5797 c.get_property().map(|inner| match inner {
5798 LayoutClear::Left => LayoutClear::Left,
5799 LayoutClear::Right => LayoutClear::Right,
5800 LayoutClear::Both => LayoutClear::Both,
5801 LayoutClear::None => LayoutClear::None,
5802 })
5803 })
5804 .unwrap_or(LayoutClear::None)
5805}
5806pub fn check_scrollbar_necessity(
5811 content_size: LogicalSize,
5812 container_size: LogicalSize,
5813 overflow_x: OverflowBehavior,
5814 overflow_y: OverflowBehavior,
5815) -> ScrollbarRequirements {
5816 const EPSILON: f32 = 1.0;
5820
5821 let mut needs_horizontal = match overflow_x {
5822 OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
5823 OverflowBehavior::Scroll => true,
5824 OverflowBehavior::Auto => content_size.width > container_size.width + EPSILON,
5825 };
5826
5827 let mut needs_vertical = match overflow_y {
5828 OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
5829 OverflowBehavior::Scroll => true,
5830 OverflowBehavior::Auto => content_size.height > container_size.height + EPSILON,
5831 };
5832
5833 if needs_vertical && !needs_horizontal && overflow_x == OverflowBehavior::Auto {
5837 if content_size.width > (container_size.width - SCROLLBAR_WIDTH_PX) + EPSILON {
5838 needs_horizontal = true;
5839 }
5840 }
5841 if needs_horizontal && !needs_vertical && overflow_y == OverflowBehavior::Auto {
5842 if content_size.height > (container_size.height - SCROLLBAR_WIDTH_PX) + EPSILON {
5843 needs_vertical = true;
5844 }
5845 }
5846
5847 ScrollbarRequirements {
5848 needs_horizontal,
5849 needs_vertical,
5850 scrollbar_width: if needs_vertical {
5851 SCROLLBAR_WIDTH_PX
5852 } else {
5853 0.0
5854 },
5855 scrollbar_height: if needs_horizontal {
5856 SCROLLBAR_WIDTH_PX
5857 } else {
5858 0.0
5859 },
5860 }
5861}
5862
5863pub(crate) fn collapse_margins(a: f32, b: f32) -> f32 {
5870 if a.is_sign_positive() && b.is_sign_positive() {
5871 a.max(b)
5872 } else if a.is_sign_negative() && b.is_sign_negative() {
5873 a.min(b)
5874 } else {
5875 a + b
5876 }
5877}
5878
5879fn advance_pen_with_margin_collapse(
5899 pen: &mut f32,
5900 last_margin_bottom: f32,
5901 current_margin_top: f32,
5902) -> f32 {
5903 let collapsed_margin = collapse_margins(last_margin_bottom, current_margin_top);
5905
5906 *pen += collapsed_margin;
5908
5909 collapsed_margin
5911}
5912
5913fn has_margin_collapse_blocker(
5930 box_props: &crate::solver3::geometry::BoxProps,
5931 writing_mode: LayoutWritingMode,
5932 check_start: bool, ) -> bool {
5934 if check_start {
5935 let border_start = box_props.border.main_start(writing_mode);
5937 let padding_start = box_props.padding.main_start(writing_mode);
5938 border_start > 0.0 || padding_start > 0.0
5939 } else {
5940 let border_end = box_props.border.main_end(writing_mode);
5942 let padding_end = box_props.padding.main_end(writing_mode);
5943 border_end > 0.0 || padding_end > 0.0
5944 }
5945}
5946
5947fn is_empty_block(node: &LayoutNode) -> bool {
5962 if !node.children.is_empty() {
5970 return false;
5971 }
5972
5973 if node.inline_layout_result.is_some() {
5975 return false;
5976 }
5977
5978 if let Some(size) = node.used_size {
5981 if size.height > 0.0 {
5982 return false;
5983 }
5984 }
5985
5986 true
5988}
5989
5990fn generate_list_marker_text(
5999 tree: &LayoutTree,
6000 styled_dom: &StyledDom,
6001 marker_index: usize,
6002 counters: &BTreeMap<(usize, String), i32>,
6003 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6004) -> String {
6005 use crate::solver3::counters::format_counter;
6006
6007 let marker_node = match tree.get(marker_index) {
6009 Some(n) => n,
6010 None => return String::new(),
6011 };
6012
6013 if marker_node.pseudo_element != Some(PseudoElement::Marker) {
6016 if let Some(msgs) = debug_messages {
6017 msgs.push(LayoutDebugMessage::warning(format!(
6018 "[generate_list_marker_text] WARNING: Node {} is not a ::marker pseudo-element \
6019 (pseudo={:?}, anonymous_type={:?})",
6020 marker_index, marker_node.pseudo_element, marker_node.anonymous_type
6021 )));
6022 }
6023 if marker_node.anonymous_type != Some(AnonymousBoxType::ListItemMarker) {
6025 return String::new();
6026 }
6027 }
6028
6029 let list_item_index = match marker_node.parent {
6031 Some(p) => p,
6032 None => {
6033 if let Some(msgs) = debug_messages {
6034 msgs.push(LayoutDebugMessage::error(
6035 "[generate_list_marker_text] ERROR: Marker has no parent".to_string(),
6036 ));
6037 }
6038 return String::new();
6039 }
6040 };
6041
6042 let list_item_node = match tree.get(list_item_index) {
6043 Some(n) => n,
6044 None => return String::new(),
6045 };
6046
6047 let list_item_dom_id = match list_item_node.dom_node_id {
6048 Some(id) => id,
6049 None => {
6050 if let Some(msgs) = debug_messages {
6051 msgs.push(LayoutDebugMessage::error(
6052 "[generate_list_marker_text] ERROR: List-item has no DOM ID".to_string(),
6053 ));
6054 }
6055 return String::new();
6056 }
6057 };
6058
6059 if let Some(msgs) = debug_messages {
6060 msgs.push(LayoutDebugMessage::info(format!(
6061 "[generate_list_marker_text] marker_index={}, list_item_index={}, \
6062 list_item_dom_id={:?}",
6063 marker_index, list_item_index, list_item_dom_id
6064 )));
6065 }
6066
6067 let list_container_dom_id = if let Some(grandparent_index) = list_item_node.parent {
6069 if let Some(grandparent) = tree.get(grandparent_index) {
6070 grandparent.dom_node_id
6071 } else {
6072 None
6073 }
6074 } else {
6075 None
6076 };
6077
6078 let list_style_type = if let Some(container_id) = list_container_dom_id {
6081 let container_type = get_list_style_type(styled_dom, Some(container_id));
6082 if container_type != StyleListStyleType::default() {
6083 container_type
6084 } else {
6085 get_list_style_type(styled_dom, Some(list_item_dom_id))
6086 }
6087 } else {
6088 get_list_style_type(styled_dom, Some(list_item_dom_id))
6089 };
6090
6091 let counter_value = counters
6095 .get(&(list_item_index, "list-item".to_string()))
6096 .copied()
6097 .unwrap_or_else(|| {
6098 if let Some(msgs) = debug_messages {
6099 msgs.push(LayoutDebugMessage::warning(format!(
6100 "[generate_list_marker_text] WARNING: No counter found for list-item at index \
6101 {}, defaulting to 1",
6102 list_item_index
6103 )));
6104 }
6105 1
6106 });
6107
6108 if let Some(msgs) = debug_messages {
6109 msgs.push(LayoutDebugMessage::info(format!(
6110 "[generate_list_marker_text] counter_value={} for list_item_index={}",
6111 counter_value, list_item_index
6112 )));
6113 }
6114
6115 let marker_text = format_counter(counter_value, list_style_type);
6117
6118 if matches!(
6121 list_style_type,
6122 StyleListStyleType::Decimal
6123 | StyleListStyleType::DecimalLeadingZero
6124 | StyleListStyleType::LowerAlpha
6125 | StyleListStyleType::UpperAlpha
6126 | StyleListStyleType::LowerRoman
6127 | StyleListStyleType::UpperRoman
6128 | StyleListStyleType::LowerGreek
6129 | StyleListStyleType::UpperGreek
6130 ) {
6131 format!("{}. ", marker_text)
6132 } else {
6133 format!("{} ", marker_text)
6134 }
6135}
6136
6137fn generate_list_marker_segments(
6143 tree: &LayoutTree,
6144 styled_dom: &StyledDom,
6145 marker_index: usize,
6146 counters: &BTreeMap<(usize, String), i32>,
6147 base_style: Arc<StyleProperties>,
6148 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6149) -> Vec<StyledRun> {
6150 let marker_text =
6152 generate_list_marker_text(tree, styled_dom, marker_index, counters, debug_messages);
6153 if marker_text.is_empty() {
6154 return Vec::new();
6155 }
6156
6157 if let Some(msgs) = debug_messages {
6158 let font_families: Vec<&str> = match &base_style.font_stack {
6159 crate::text3::cache::FontStack::Stack(selectors) => {
6160 selectors.iter().map(|f| f.family.as_str()).collect()
6161 }
6162 crate::text3::cache::FontStack::Ref(_) => vec!["<embedded-font>"],
6163 };
6164 msgs.push(LayoutDebugMessage::info(format!(
6165 "[generate_list_marker_segments] Marker text: '{}' with font stack: {:?}",
6166 marker_text,
6167 font_families
6168 )));
6169 }
6170
6171 vec![StyledRun {
6174 text: marker_text,
6175 style: base_style,
6176 logical_start_byte: 0,
6177 source_node_id: None,
6178 }]
6179}
6180
6181pub(crate) fn split_text_for_whitespace(
6194 styled_dom: &StyledDom,
6195 dom_id: NodeId,
6196 text: &str,
6197 style: Arc<StyleProperties>,
6198) -> Vec<InlineContent> {
6199 use crate::text3::cache::{BreakType, ClearType, InlineBreak};
6200
6201 let node_hierarchy = styled_dom.node_hierarchy.as_container();
6204 let parent_id = node_hierarchy[dom_id].parent_id();
6205
6206 let white_space = if let Some(parent) = parent_id {
6208 let parent_node_data = &styled_dom.node_data.as_container()[parent];
6209 let styled_nodes = styled_dom.styled_nodes.as_container();
6210 let parent_state = styled_nodes
6211 .get(parent)
6212 .map(|n| n.styled_node_state.clone())
6213 .unwrap_or_default();
6214
6215 styled_dom
6216 .css_property_cache
6217 .ptr
6218 .get_white_space(parent_node_data, &parent, &parent_state)
6219 .and_then(|s| s.get_property().cloned())
6220 .unwrap_or(StyleWhiteSpace::Normal)
6221 } else {
6222 StyleWhiteSpace::Normal
6223 };
6224
6225 let mut result = Vec::new();
6226
6227 match white_space {
6230 StyleWhiteSpace::Pre => {
6231 let mut lines = text.split('\n').peekable();
6233 let mut content_index = 0;
6234
6235 while let Some(line) = lines.next() {
6236 if !line.is_empty() {
6238 result.push(InlineContent::Text(StyledRun {
6239 text: line.to_string(),
6240 style: Arc::clone(&style),
6241 logical_start_byte: 0,
6242 source_node_id: Some(dom_id),
6243 }));
6244 }
6245
6246 if lines.peek().is_some() {
6248 result.push(InlineContent::LineBreak(InlineBreak {
6249 break_type: BreakType::Hard,
6250 clear: ClearType::None,
6251 content_index,
6252 }));
6253 content_index += 1;
6254 }
6255 }
6256 }
6257 StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap => {
6259 if !text.is_empty() {
6261 result.push(InlineContent::Text(StyledRun {
6262 text: text.to_string(),
6263 style,
6264 logical_start_byte: 0,
6265 source_node_id: Some(dom_id),
6266 }));
6267 }
6268 }
6269 }
6270
6271 result
6272}