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_direction_property,
77 get_display_property, get_element_font_size, get_float, get_clear,
78 get_list_style_position, get_list_style_type, get_overflow_x, get_overflow_y,
79 get_parent_font_size, get_root_font_size, get_style_properties,
80 get_text_align, get_vertical_align_property, get_visibility,
81 get_white_space_property, get_writing_mode, MultiValue,
82 },
83 layout_tree::{
84 AnonymousBoxType, CachedInlineLayout, LayoutNode, LayoutTree, PseudoElement,
85 },
86 positioning::get_position_type,
87 scrollbar::ScrollbarRequirements,
88 sizing::extract_text_from_node,
89 taffy_bridge, LayoutContext, LayoutDebugMessage, LayoutError, Result,
90 },
91 text3::cache::{AvailableSpace as Text3AvailableSpace, TextAlign as Text3TextAlign},
92};
93
94pub const DEFAULT_SCROLLBAR_WIDTH_PX: f32 = 16.0;
98
99#[derive(Debug, Clone)]
103pub(crate) struct BfcLayoutResult {
104 pub output: LayoutOutput,
106 pub escaped_top_margin: Option<f32>,
109 pub escaped_bottom_margin: Option<f32>,
112}
113
114impl BfcLayoutResult {
115 pub fn from_output(output: LayoutOutput) -> Self {
116 Self {
117 output,
118 escaped_top_margin: None,
119 escaped_bottom_margin: None,
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum OverflowBehavior {
127 Visible,
128 Hidden,
129 Clip,
130 Scroll,
131 Auto,
132}
133
134impl OverflowBehavior {
135 pub fn is_clipped(&self) -> bool {
136 matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
137 }
138
139 pub fn is_scroll(&self) -> bool {
140 matches!(self, Self::Scroll | Self::Auto)
141 }
142}
143
144#[derive(Debug)]
146pub struct LayoutConstraints<'a> {
147 pub available_size: LogicalSize,
149 pub writing_mode: LayoutWritingMode,
151 pub bfc_state: Option<&'a mut BfcState>,
154 pub text_align: TextAlign,
156 pub containing_block_size: LogicalSize,
159 pub available_width_type: Text3AvailableSpace,
170}
171
172#[derive(Debug, Clone)]
175pub struct BfcState {
176 pub pen: LogicalPosition,
178 pub floats: FloatingContext,
180 pub margins: MarginCollapseContext,
182}
183
184impl BfcState {
185 pub fn new() -> Self {
186 Self {
187 pen: LogicalPosition::zero(),
188 floats: FloatingContext::default(),
189 margins: MarginCollapseContext::default(),
190 }
191 }
192}
193
194#[derive(Debug, Default, Clone)]
196pub struct MarginCollapseContext {
197 pub last_in_flow_margin_bottom: f32,
200}
201
202#[derive(Debug, Default, Clone)]
204pub struct LayoutOutput {
205 pub positions: BTreeMap<usize, LogicalPosition>,
207 pub overflow_size: LogicalSize,
209 pub baseline: Option<f32>,
211}
212
213#[derive(Debug, Clone, Copy, Default)]
215pub enum TextAlign {
216 #[default]
217 Start,
218 End,
219 Center,
220 Justify,
221}
222
223#[derive(Debug, Clone, Copy)]
225struct FloatBox {
226 kind: LayoutFloat,
228 rect: LogicalRect,
230 margin: EdgeSizes,
232}
233
234#[derive(Debug, Default, Clone)]
236pub struct FloatingContext {
237 pub floats: Vec<FloatBox>,
239}
240
241impl FloatingContext {
242 pub fn add_float(&mut self, kind: LayoutFloat, rect: LogicalRect, margin: EdgeSizes) {
244 self.floats.push(FloatBox { kind, rect, margin });
245 }
246
247 pub fn available_line_box_space(
252 &self,
253 main_start: f32,
254 main_end: f32,
255 bfc_cross_size: f32,
256 wm: LayoutWritingMode,
257 ) -> (f32, f32) {
258 let mut available_cross_start = 0.0_f32;
259 let mut available_cross_end = bfc_cross_size;
260
261 for float in &self.floats {
262 let float_main_start = float.rect.origin.main(wm);
264 let float_main_end = float_main_start + float.rect.size.main(wm);
265
266 if main_end > float_main_start && main_start < float_main_end {
268 let float_cross_start = float.rect.origin.cross(wm);
270 let float_cross_end = float_cross_start + float.rect.size.cross(wm);
271
272 if float.kind == LayoutFloat::Left {
273 available_cross_start = available_cross_start.max(float_cross_end);
275 } else {
276 available_cross_end = available_cross_end.min(float_cross_start);
278 }
279 }
280 }
281 (available_cross_start, available_cross_end)
282 }
283
284 pub fn clearance_offset(
286 &self,
287 clear: LayoutClear,
288 current_main_offset: f32,
289 wm: LayoutWritingMode,
290 ) -> f32 {
291 let mut max_end_offset = 0.0_f32;
292
293 let check_left = clear == LayoutClear::Left || clear == LayoutClear::Both;
294 let check_right = clear == LayoutClear::Right || clear == LayoutClear::Both;
295
296 for float in &self.floats {
297 let should_clear_this_float = (check_left && float.kind == LayoutFloat::Left)
298 || (check_right && float.kind == LayoutFloat::Right);
299
300 if should_clear_this_float {
301 let float_margin_box_end = float.rect.origin.main(wm)
304 + float.rect.size.main(wm)
305 + float.margin.main_end(wm);
306 max_end_offset = max_end_offset.max(float_margin_box_end);
307 }
308 }
309
310 if max_end_offset > current_main_offset {
311 max_end_offset
312 } else {
313 current_main_offset
314 }
315 }
316}
317
318struct BfcLayoutState {
320 pen: LogicalPosition,
322 floats: FloatingContext,
323 margins: MarginCollapseContext,
324 writing_mode: LayoutWritingMode,
326}
327
328#[derive(Debug, Default)]
330pub struct LayoutResult {
331 pub positions: Vec<(usize, LogicalPosition)>,
332 pub overflow_size: Option<LogicalSize>,
333 pub baseline_offset: f32,
334}
335
336pub fn layout_formatting_context<T: ParsedFontTrait>(
348 ctx: &mut LayoutContext<'_, T>,
349 tree: &mut LayoutTree,
350 text_cache: &mut crate::font_traits::TextLayoutCache,
351 node_index: usize,
352 constraints: &LayoutConstraints,
353 float_cache: &mut std::collections::BTreeMap<usize, FloatingContext>,
354) -> Result<BfcLayoutResult> {
355 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
356
357 debug_info!(
358 ctx,
359 "[layout_formatting_context] node_index={}, fc={:?}, available_size={:?}",
360 node_index,
361 node.formatting_context,
362 constraints.available_size
363 );
364
365 match node.formatting_context {
366 FormattingContext::Block { .. } => {
367 layout_bfc(ctx, tree, text_cache, node_index, constraints, float_cache)
368 }
369 FormattingContext::Inline => layout_ifc(ctx, text_cache, tree, node_index, constraints)
370 .map(BfcLayoutResult::from_output),
371 FormattingContext::InlineBlock => {
372 let mut temp_float_cache = std::collections::BTreeMap::new();
377 layout_bfc(ctx, tree, text_cache, node_index, constraints, &mut temp_float_cache)
378 }
379 FormattingContext::Table => layout_table_fc(ctx, tree, text_cache, node_index, constraints)
380 .map(BfcLayoutResult::from_output),
381 FormattingContext::Flex | FormattingContext::Grid => {
382 layout_flex_grid(ctx, tree, text_cache, node_index, constraints)
383 }
384 _ => {
385 let mut temp_float_cache = std::collections::BTreeMap::new();
387 layout_bfc(
388 ctx,
389 tree,
390 text_cache,
391 node_index,
392 constraints,
393 &mut temp_float_cache,
394 )
395 }
396 }
397}
398
399fn layout_flex_grid<T: ParsedFontTrait>(
414 ctx: &mut LayoutContext<'_, T>,
415 tree: &mut LayoutTree,
416 text_cache: &mut crate::font_traits::TextLayoutCache,
417 node_index: usize,
418 constraints: &LayoutConstraints,
419) -> Result<BfcLayoutResult> {
420 let available_space = TaffySize {
422 width: AvailableSpace::Definite(constraints.available_size.width),
423 height: AvailableSpace::Definite(constraints.available_size.height),
424 };
425
426 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
427
428 let (explicit_width, has_explicit_width) =
432 resolve_explicit_dimension_width(ctx, node, constraints);
433 let (explicit_height, has_explicit_height) =
434 resolve_explicit_dimension_height(ctx, node, constraints);
435
436 let is_root = node.parent.is_none();
441
442 let effective_width = if has_explicit_width {
448 explicit_width
449 } else if is_root && constraints.available_size.width.is_finite() {
450 Some(constraints.available_size.width)
452 } else {
453 None
454 };
455 let effective_height = if has_explicit_height {
456 explicit_height
457 } else if is_root && constraints.available_size.height.is_finite() {
458 Some(constraints.available_size.height)
460 } else {
461 None
462 };
463 let has_effective_width = effective_width.is_some();
464 let has_effective_height = effective_height.is_some();
465
466 let width_adjustment = node.box_props.border.left
472 + node.box_props.border.right
473 + node.box_props.padding.left
474 + node.box_props.padding.right;
475 let height_adjustment = node.box_props.border.top
476 + node.box_props.border.bottom
477 + node.box_props.padding.top
478 + node.box_props.padding.bottom;
479
480 let adjusted_width = if has_explicit_width {
483 explicit_width.map(|w| w + width_adjustment)
484 } else {
485 effective_width };
487 let adjusted_height = if has_explicit_height {
488 explicit_height.map(|h| h + height_adjustment)
489 } else {
490 effective_height };
492
493 let sizing_mode = if has_effective_width || has_effective_height {
496 taffy::SizingMode::InherentSize
497 } else {
498 taffy::SizingMode::ContentSize
499 };
500
501 let known_dimensions = TaffySize {
502 width: adjusted_width,
503 height: adjusted_height,
504 };
505
506 let parent_size = translate_taffy_size(constraints.containing_block_size);
511
512 let taffy_inputs = LayoutInput {
513 known_dimensions,
514 parent_size,
515 available_space,
516 run_mode: taffy::RunMode::PerformLayout,
517 sizing_mode,
518 axis: taffy::RequestedAxis::Both,
519 vertical_margins_are_collapsible: Line::FALSE,
521 };
522
523 debug_info!(
524 ctx,
525 "CALLING LAYOUT_TAFFY FOR FLEX/GRID FC node_index={:?}",
526 node_index
527 );
528
529 let taffy_output =
530 taffy_bridge::layout_taffy_subtree(ctx, tree, text_cache, node_index, taffy_inputs);
531
532 let mut output = LayoutOutput::default();
534 output.overflow_size = translate_taffy_size_back(taffy_output.content_size);
537
538 let children: Vec<usize> = tree.get(node_index).unwrap().children.clone();
539 for &child_idx in &children {
540 if let Some(child_node) = tree.get(child_idx) {
541 if let Some(pos) = child_node.relative_position {
542 output.positions.insert(child_idx, pos);
543 }
544 }
545 }
546
547 Ok(BfcLayoutResult::from_output(output))
548}
549
550fn resolve_explicit_dimension_width<T: ParsedFontTrait>(
552 ctx: &LayoutContext<'_, T>,
553 node: &LayoutNode,
554 constraints: &LayoutConstraints,
555) -> (Option<f32>, bool) {
556 node.dom_node_id
557 .map(|id| {
558 let width = get_css_width(
559 ctx.styled_dom,
560 id,
561 &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
562 );
563 match width.unwrap_or_default() {
564 LayoutWidth::Auto => (None, false),
565 LayoutWidth::Px(px) => {
566 let pixels = resolve_size_metric(
567 px.metric,
568 px.number.get(),
569 constraints.available_size.width,
570 ctx.viewport_size,
571 );
572 (Some(pixels), true)
573 }
574 LayoutWidth::MinContent | LayoutWidth::MaxContent => (None, false),
575 LayoutWidth::Calc(_) => (None, false), }
577 })
578 .unwrap_or((None, false))
579}
580
581fn resolve_explicit_dimension_height<T: ParsedFontTrait>(
583 ctx: &LayoutContext<'_, T>,
584 node: &LayoutNode,
585 constraints: &LayoutConstraints,
586) -> (Option<f32>, bool) {
587 node.dom_node_id
588 .map(|id| {
589 let height = get_css_height(
590 ctx.styled_dom,
591 id,
592 &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
593 );
594 match height.unwrap_or_default() {
595 LayoutHeight::Auto => (None, false),
596 LayoutHeight::Px(px) => {
597 let pixels = resolve_size_metric(
598 px.metric,
599 px.number.get(),
600 constraints.available_size.height,
601 ctx.viewport_size,
602 );
603 (Some(pixels), true)
604 }
605 LayoutHeight::MinContent | LayoutHeight::MaxContent => (None, false),
606 LayoutHeight::Calc(_) => (None, false), }
608 })
609 .unwrap_or((None, false))
610}
611
612fn position_float(
615 float_ctx: &FloatingContext,
616 float_type: LayoutFloat,
617 size: LogicalSize,
618 margin: &EdgeSizes,
619 current_main_offset: f32,
620 bfc_cross_size: f32,
621 wm: LayoutWritingMode,
622) -> LogicalRect {
623 let mut main_start = current_main_offset;
625
626 let total_main = size.main(wm) + margin.main_start(wm) + margin.main_end(wm);
628 let total_cross = size.cross(wm) + margin.cross_start(wm) + margin.cross_end(wm);
629
630 let cross_start = loop {
632 let (avail_start, avail_end) = float_ctx.available_line_box_space(
633 main_start,
634 main_start + total_main,
635 bfc_cross_size,
636 wm,
637 );
638
639 let available_width = avail_end - avail_start;
640
641 if available_width >= total_cross {
642 if float_type == LayoutFloat::Left {
644 break avail_start + margin.cross_start(wm);
646 } else {
647 break avail_end - total_cross + margin.cross_start(wm);
649 }
650 }
651
652 let next_main = float_ctx
654 .floats
655 .iter()
656 .filter(|f| {
657 let f_main_start = f.rect.origin.main(wm);
658 let f_main_end = f_main_start + f.rect.size.main(wm);
659 f_main_end > main_start && f_main_start < main_start + total_main
660 })
661 .map(|f| f.rect.origin.main(wm) + f.rect.size.main(wm))
662 .max_by(|a, b| a.partial_cmp(b).unwrap());
663
664 if let Some(next) = next_main {
665 main_start = next;
666 } else {
667 if float_type == LayoutFloat::Left {
669 break avail_start + margin.cross_start(wm);
670 } else {
671 break avail_end - total_cross + margin.cross_start(wm);
672 }
673 }
674 };
675
676 LogicalRect {
677 origin: LogicalPosition::from_main_cross(
678 main_start + margin.main_start(wm),
679 cross_start,
680 wm,
681 ),
682 size,
683 }
684}
685
686fn layout_bfc<T: ParsedFontTrait>(
733 ctx: &mut LayoutContext<'_, T>,
734 tree: &mut LayoutTree,
735 text_cache: &mut crate::font_traits::TextLayoutCache,
736 node_index: usize,
737 constraints: &LayoutConstraints,
738 float_cache: &mut std::collections::BTreeMap<usize, FloatingContext>,
739) -> Result<BfcLayoutResult> {
740 let node = tree
741 .get(node_index)
742 .ok_or(LayoutError::InvalidTree)?
743 .clone();
744 let writing_mode = constraints.writing_mode;
745 let mut output = LayoutOutput::default();
746
747 debug_info!(
748 ctx,
749 "\n[layout_bfc] ENTERED for node_index={}, children.len()={}, incoming_bfc_state={}",
750 node_index,
751 node.children.len(),
752 constraints.bfc_state.is_some()
753 );
754
755 let mut float_context = FloatingContext::default();
760
761 let mut children_containing_block_size = if let Some(used_size) = node.used_size {
769 node.box_props.inner_size(used_size, writing_mode)
771 } else {
772 constraints.available_size
775 };
776
777 let scrollbar_reservation = node
782 .dom_node_id
783 .map(|dom_id| {
784 let styled_node_state = ctx
785 .styled_dom
786 .styled_nodes
787 .as_container()
788 .get(dom_id)
789 .map(|s| s.styled_node_state.clone())
790 .unwrap_or_default();
791 let overflow_y =
792 crate::solver3::getters::get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state);
793 use azul_css::props::layout::LayoutOverflow;
794 match overflow_y.unwrap_or_default() {
795 LayoutOverflow::Scroll | LayoutOverflow::Auto => {
796 crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, &styled_node_state)
797 }
798 _ => 0.0,
799 }
800 })
801 .unwrap_or(0.0);
802
803 if scrollbar_reservation > 0.0 {
804 children_containing_block_size.width =
805 (children_containing_block_size.width - scrollbar_reservation).max(0.0);
806 }
807
808 {
826 let mut temp_positions: super::PositionVec = Vec::new();
827 let mut temp_scrollbar_reflow = false;
828
829 for &child_index in &node.children {
830 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
831 let child_dom_id = child_node.dom_node_id;
832
833 let position_type = get_position_type(ctx.styled_dom, child_dom_id);
835 if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
836 continue;
837 }
838
839 crate::solver3::cache::calculate_layout_for_subtree(
843 ctx,
844 tree,
845 text_cache,
846 child_index,
847 LogicalPosition::zero(),
848 children_containing_block_size,
849 &mut temp_positions,
850 &mut temp_scrollbar_reflow,
851 float_cache,
852 crate::solver3::cache::ComputeMode::ComputeSize,
853 )?;
854 }
855 }
856
857 let mut main_pen = 0.0f32;
865 let mut max_cross_size = 0.0f32;
866
867 let mut total_escaped_top_margin = 0.0f32;
871 let mut total_sibling_margins = 0.0f32;
873
874 let mut last_margin_bottom = 0.0f32;
876 let mut is_first_child = true;
877 let mut first_child_index: Option<usize> = None;
878 let mut last_child_index: Option<usize> = None;
879
880 let parent_margin_top = node.box_props.margin.main_start(writing_mode);
882 let parent_margin_bottom = node.box_props.margin.main_end(writing_mode);
883
884 let parent_has_top_blocker = has_margin_collapse_blocker(&node.box_props, writing_mode, true);
886 let parent_has_bottom_blocker =
887 has_margin_collapse_blocker(&node.box_props, writing_mode, false);
888
889 let mut accumulated_top_margin = 0.0f32;
891 let mut top_margin_resolved = false;
892 let mut top_margin_escaped = false;
894
895 let mut has_content = false;
897
898 for &child_index in &node.children {
899 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
900 let child_dom_id = child_node.dom_node_id;
901
902 let position_type = get_position_type(ctx.styled_dom, child_dom_id);
903 if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
904 continue;
905 }
906
907 let is_float = if let Some(node_id) = child_dom_id {
909 let float_type = get_float_property(ctx.styled_dom, Some(node_id));
910
911 if float_type != LayoutFloat::None {
912 let float_size = match child_node.used_size {
914 Some(size) => size,
915 None => {
916 let intrinsic = child_node.intrinsic_sizes.unwrap_or_default();
917 let computed_size = crate::solver3::sizing::calculate_used_size_for_node(
918 ctx.styled_dom,
919 child_dom_id,
920 children_containing_block_size,
921 intrinsic,
922 &child_node.box_props,
923 ctx.viewport_size,
924 )?;
925 if let Some(node_mut) = tree.get_mut(child_index) {
926 node_mut.used_size = Some(computed_size);
927 }
928 computed_size
929 }
930 };
931 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
933 let float_margin = &child_node.box_props.margin;
934
935 let float_y = main_pen + last_margin_bottom;
939
940 debug_info!(
941 ctx,
942 "[layout_bfc] Positioning float: index={}, type={:?}, size={:?}, at Y={} \
943 (main_pen={} + last_margin={})",
944 child_index,
945 float_type,
946 float_size,
947 float_y,
948 main_pen,
949 last_margin_bottom
950 );
951
952 let float_rect = position_float(
954 &float_context,
955 float_type,
956 float_size,
957 float_margin,
958 float_y,
960 constraints.available_size.cross(writing_mode),
961 writing_mode,
962 );
963
964 debug_info!(ctx, "[layout_bfc] Float positioned at: {:?}", float_rect);
965
966 float_context.add_float(float_type, float_rect, *float_margin);
968
969 output.positions.insert(child_index, float_rect.origin);
971
972 debug_info!(
973 ctx,
974 "[layout_bfc] *** FLOAT POSITIONED: child={}, main_pen={} (unchanged - floats \
975 don't advance pen)",
976 child_index,
977 main_pen
978 );
979
980 continue;
983 }
984 false
985 } else {
986 false
987 };
988
989 if is_float {
991 continue;
992 }
993
994 if first_child_index.is_none() {
998 first_child_index = Some(child_index);
999 }
1000 last_child_index = Some(child_index);
1001
1002 let child_size = match child_node.used_size {
1005 Some(size) => size,
1006 None => {
1007 let intrinsic = child_node.intrinsic_sizes.unwrap_or_default();
1009 let child_used_size = crate::solver3::sizing::calculate_used_size_for_node(
1010 ctx.styled_dom,
1011 child_dom_id,
1012 children_containing_block_size,
1013 intrinsic,
1014 &child_node.box_props,
1015 ctx.viewport_size,
1016 )?;
1017 if let Some(node_mut) = tree.get_mut(child_index) {
1019 node_mut.used_size = Some(child_used_size);
1020 }
1021 child_used_size
1022 }
1023 };
1024 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1026 let child_margin = &child_node.box_props.margin;
1027
1028 debug_info!(
1029 ctx,
1030 "[layout_bfc] Child {} margin from box_props: top={}, right={}, bottom={}, left={}",
1031 child_index,
1032 child_margin.top,
1033 child_margin.right,
1034 child_margin.bottom,
1035 child_margin.left
1036 );
1037
1038 let child_margin_top = child_margin.main_start(writing_mode);
1051 let child_margin_bottom = child_margin.main_end(writing_mode);
1052
1053 debug_info!(
1054 ctx,
1055 "[layout_bfc] Child {} final margins: margin_top={}, margin_bottom={}",
1056 child_index,
1057 child_margin_top,
1058 child_margin_bottom
1059 );
1060
1061 let child_has_top_blocker =
1063 has_margin_collapse_blocker(&child_node.box_props, writing_mode, true);
1064 let child_has_bottom_blocker =
1065 has_margin_collapse_blocker(&child_node.box_props, writing_mode, false);
1066
1067 let child_clear = if let Some(node_id) = child_dom_id {
1071 get_clear_property(ctx.styled_dom, Some(node_id))
1072 } else {
1073 LayoutClear::None
1074 };
1075 debug_info!(
1076 ctx,
1077 "[layout_bfc] Child {} clear property: {:?}",
1078 child_index,
1079 child_clear
1080 );
1081
1082 let is_empty = is_empty_block(child_node);
1084
1085 if is_empty
1089 && !child_has_top_blocker
1090 && !child_has_bottom_blocker
1091 && child_clear == LayoutClear::None
1092 {
1093 let self_collapsed = collapse_margins(child_margin_top, child_margin_bottom);
1095
1096 if is_first_child {
1098 is_first_child = false;
1099 if !parent_has_top_blocker {
1101 accumulated_top_margin = collapse_margins(parent_margin_top, self_collapsed);
1102 } else {
1103 if accumulated_top_margin == 0.0 {
1105 accumulated_top_margin = parent_margin_top;
1106 }
1107 main_pen += accumulated_top_margin + self_collapsed;
1108 top_margin_resolved = true;
1109 accumulated_top_margin = 0.0;
1110 }
1111 last_margin_bottom = self_collapsed;
1112 } else {
1113 last_margin_bottom = collapse_margins(last_margin_bottom, self_collapsed);
1115 }
1116
1117 continue;
1119 }
1120
1121 let clearance_applied = if child_clear != LayoutClear::None {
1126 let cleared_offset =
1127 float_context.clearance_offset(child_clear, main_pen, writing_mode);
1128 debug_info!(
1129 ctx,
1130 "[layout_bfc] Child {} clearance check: cleared_offset={}, main_pen={}",
1131 child_index,
1132 cleared_offset,
1133 main_pen
1134 );
1135 if cleared_offset > main_pen {
1136 debug_info!(
1137 ctx,
1138 "[layout_bfc] Applying clearance: child={}, clear={:?}, old_pen={}, new_pen={}",
1139 child_index,
1140 child_clear,
1141 main_pen,
1142 cleared_offset
1143 );
1144 main_pen = cleared_offset;
1145 true } else {
1147 false
1148 }
1149 } else {
1150 false
1151 };
1152
1153 if is_first_child {
1160 is_first_child = false;
1161
1162 if clearance_applied {
1164 main_pen += child_margin_top;
1170 debug_info!(
1171 ctx,
1172 "[layout_bfc] First child {} with CLEARANCE: no collapse, child_margin={}, \
1173 main_pen={}",
1174 child_index,
1175 child_margin_top,
1176 main_pen
1177 );
1178 } else if !parent_has_top_blocker {
1179 accumulated_top_margin = collapse_margins(parent_margin_top, child_margin_top);
1228 top_margin_resolved = true;
1229 top_margin_escaped = true;
1230
1231 total_escaped_top_margin = accumulated_top_margin;
1235
1236 debug_info!(
1238 ctx,
1239 "[layout_bfc] First child {} margin ESCAPES: parent_margin={}, \
1240 child_margin={}, collapsed={}, total_escaped={}",
1241 child_index,
1242 parent_margin_top,
1243 child_margin_top,
1244 accumulated_top_margin,
1245 total_escaped_top_margin
1246 );
1247 } else {
1248 main_pen += child_margin_top;
1299 debug_info!(
1300 ctx,
1301 "[layout_bfc] First child {} BLOCKED: parent_has_blocker={}, advanced by \
1302 child_margin={}, main_pen={}",
1303 child_index,
1304 parent_has_top_blocker,
1305 child_margin_top,
1306 main_pen
1307 );
1308 }
1309 } else {
1310 if !top_margin_resolved {
1316 main_pen += accumulated_top_margin;
1317 top_margin_resolved = true;
1318 debug_info!(
1319 ctx,
1320 "[layout_bfc] RESOLVED top margin for node {} at sibling {}: accumulated={}, \
1321 main_pen={}",
1322 node_index,
1323 child_index,
1324 accumulated_top_margin,
1325 main_pen
1326 );
1327 }
1328
1329 if clearance_applied {
1330 main_pen += child_margin_top;
1332 debug_info!(
1333 ctx,
1334 "[layout_bfc] Child {} with CLEARANCE: no collapse with sibling, \
1335 child_margin_top={}, main_pen={}",
1336 child_index,
1337 child_margin_top,
1338 main_pen
1339 );
1340 } else {
1341 let collapsed = collapse_margins(last_margin_bottom, child_margin_top);
1375 main_pen += collapsed;
1376 total_sibling_margins += collapsed;
1377 debug_info!(
1378 ctx,
1379 "[layout_bfc] Sibling collapse for child {}: last_margin_bottom={}, \
1380 child_margin_top={}, collapsed={}, main_pen={}, total_sibling_margins={}",
1381 child_index,
1382 last_margin_bottom,
1383 child_margin_top,
1384 collapsed,
1385 main_pen,
1386 total_sibling_margins
1387 );
1388 }
1389 }
1390
1391 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1405 let establishes_bfc = establishes_new_bfc(ctx, child_node);
1406
1407 let (cross_start, cross_end, available_cross) = if establishes_bfc {
1409 let (start, end) = float_context.available_line_box_space(
1411 main_pen,
1412 main_pen + child_size.main(writing_mode),
1413 constraints.available_size.cross(writing_mode),
1414 writing_mode,
1415 );
1416 let available = end - start;
1417
1418 debug_info!(
1419 ctx,
1420 "[layout_bfc] Child {} establishes BFC: shrinking to avoid floats, \
1421 cross_range={}..{}, available_cross={}",
1422 child_index,
1423 start,
1424 end,
1425 available
1426 );
1427
1428 (start, end, available)
1429 } else {
1430 let start = 0.0;
1433 let end = constraints.available_size.cross(writing_mode);
1434 let available = end - start;
1435
1436 debug_info!(
1437 ctx,
1438 "[layout_bfc] Child {} is normal flow: overlapping floats at full width, \
1439 available_cross={}",
1440 child_index,
1441 available
1442 );
1443
1444 (start, end, available)
1445 };
1446
1447 let (child_margin_cloned, child_margin_auto, child_used_size, is_inline_fc, child_dom_id_for_debug) = {
1449 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1450 (
1451 child_node.box_props.margin.clone(),
1452 child_node.box_props.margin_auto,
1453 child_node.used_size.unwrap_or_default(),
1454 child_node.formatting_context == FormattingContext::Inline,
1455 child_node.dom_node_id,
1456 )
1457 };
1458 let child_margin = &child_margin_cloned;
1459
1460 debug_info!(
1461 ctx,
1462 "[layout_bfc] Child {} margin_auto: left={}, right={}, top={}, bottom={}",
1463 child_index,
1464 child_margin_auto.left,
1465 child_margin_auto.right,
1466 child_margin_auto.top,
1467 child_margin_auto.bottom
1468 );
1469 debug_info!(
1470 ctx,
1471 "[layout_bfc] Child {} used_size: width={}, height={}",
1472 child_index,
1473 child_used_size.width,
1474 child_used_size.height
1475 );
1476
1477 let (child_cross_pos, mut child_main_pos) = if establishes_bfc {
1485 (
1487 cross_start + child_margin.cross_start(writing_mode),
1488 main_pen,
1489 )
1490 } else {
1491 let available_cross = constraints.available_size.cross(writing_mode);
1493 let child_cross_size = child_used_size.cross(writing_mode);
1494
1495 debug_info!(
1496 ctx,
1497 "[layout_bfc] Child {} centering check: available_cross={}, child_cross_size={}, margin_auto.left={}, margin_auto.right={}",
1498 child_index,
1499 available_cross,
1500 child_cross_size,
1501 child_margin_auto.left,
1502 child_margin_auto.right
1503 );
1504
1505 let cross_pos = if child_margin_auto.left && child_margin_auto.right {
1508 let remaining_space = (available_cross - child_cross_size).max(0.0);
1510 debug_info!(
1511 ctx,
1512 "[layout_bfc] Child {} CENTERING: remaining_space={}, cross_pos={}",
1513 child_index,
1514 remaining_space,
1515 remaining_space / 2.0
1516 );
1517 remaining_space / 2.0
1518 } else if child_margin_auto.left {
1519 let remaining_space = (available_cross - child_cross_size - child_margin.right).max(0.0);
1521 debug_info!(
1522 ctx,
1523 "[layout_bfc] Child {} margin-left:auto only, pushing right: remaining_space={}",
1524 child_index,
1525 remaining_space
1526 );
1527 remaining_space
1528 } else if child_margin_auto.right {
1529 debug_info!(
1531 ctx,
1532 "[layout_bfc] Child {} margin-right:auto only, using left margin={}",
1533 child_index,
1534 child_margin.cross_start(writing_mode)
1535 );
1536 child_margin.cross_start(writing_mode)
1537 } else {
1538 debug_info!(
1540 ctx,
1541 "[layout_bfc] Child {} NO auto margins, using left margin={}",
1542 child_index,
1543 child_margin.cross_start(writing_mode)
1544 );
1545 child_margin.cross_start(writing_mode)
1546 };
1547
1548 (cross_pos, main_pen)
1549 };
1550
1551 let final_pos =
1567 LogicalPosition::from_main_cross(child_main_pos, child_cross_pos, writing_mode);
1568
1569 debug_info!(
1570 ctx,
1571 "[layout_bfc] *** NORMAL FLOW BLOCK POSITIONED: child={}, final_pos={:?}, \
1572 main_pen={}, establishes_bfc={}",
1573 child_index,
1574 final_pos,
1575 main_pen,
1576 establishes_bfc
1577 );
1578
1579 if is_inline_fc && !establishes_bfc {
1582 let floats_for_ifc = float_cache.get(&node_index).unwrap_or(&float_context);
1585
1586 debug_info!(
1587 ctx,
1588 "[layout_bfc] Re-layouting IFC child {} (normal flow) with parent's float context \
1589 at Y={}, child_cross_pos={}",
1590 child_index,
1591 main_pen,
1592 child_cross_pos
1593 );
1594 debug_info!(
1595 ctx,
1596 "[layout_bfc] Using {} floats (from cache: {})",
1597 floats_for_ifc.floats.len(),
1598 float_cache.contains_key(&node_index)
1599 );
1600
1601 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1605 let padding_border_cross = child_node.box_props.padding.cross_start(writing_mode)
1606 + child_node.box_props.border.cross_start(writing_mode);
1607 let padding_border_main = child_node.box_props.padding.main_start(writing_mode)
1608 + child_node.box_props.border.main_start(writing_mode);
1609
1610 let content_box_cross = child_cross_pos + padding_border_cross;
1612 let content_box_main = main_pen + padding_border_main;
1613
1614 debug_info!(
1615 ctx,
1616 "[layout_bfc] Border-box at ({}, {}), Content-box at ({}, {}), \
1617 padding+border=({}, {})",
1618 child_cross_pos,
1619 main_pen,
1620 content_box_cross,
1621 content_box_main,
1622 padding_border_cross,
1623 padding_border_main
1624 );
1625
1626 let mut ifc_floats = FloatingContext::default();
1627 for float_box in &floats_for_ifc.floats {
1628 let float_rel_to_ifc = LogicalRect {
1630 origin: LogicalPosition {
1631 x: float_box.rect.origin.x - content_box_cross,
1632 y: float_box.rect.origin.y - content_box_main,
1633 },
1634 size: float_box.rect.size,
1635 };
1636
1637 debug_info!(
1638 ctx,
1639 "[layout_bfc] Float {:?}: BFC coords = {:?}, IFC-content-relative = {:?}",
1640 float_box.kind,
1641 float_box.rect,
1642 float_rel_to_ifc
1643 );
1644
1645 ifc_floats.add_float(float_box.kind, float_rel_to_ifc, float_box.margin);
1646 }
1647
1648 let mut bfc_state = BfcState {
1650 pen: LogicalPosition::zero(), floats: ifc_floats.clone(),
1652 margins: MarginCollapseContext::default(),
1653 };
1654
1655 debug_info!(
1656 ctx,
1657 "[layout_bfc] Created IFC-relative FloatingContext with {} floats",
1658 ifc_floats.floats.len()
1659 );
1660
1661 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1663 let child_dom_id = child_node.dom_node_id;
1664
1665 let display = get_display_property(ctx.styled_dom, child_dom_id).unwrap_or_default();
1669 let child_content_size = if display == LayoutDisplay::Inline {
1670 LogicalSize::new(
1672 children_containing_block_size.width,
1673 children_containing_block_size.height,
1674 )
1675 } else {
1676 child_node.box_props.inner_size(child_size, writing_mode)
1678 };
1679
1680 debug_info!(
1681 ctx,
1682 "[layout_bfc] IFC child size: border-box={:?}, content-box={:?}",
1683 child_size,
1684 child_content_size
1685 );
1686
1687 let ifc_constraints = LayoutConstraints {
1690 available_size: child_content_size,
1691 bfc_state: Some(&mut bfc_state),
1692 writing_mode,
1693 text_align: constraints.text_align,
1694 containing_block_size: constraints.containing_block_size,
1695 available_width_type: Text3AvailableSpace::Definite(child_content_size.width),
1696 };
1697
1698 let ifc_result = layout_formatting_context(
1701 ctx,
1702 tree,
1703 text_cache,
1704 child_index,
1705 &ifc_constraints,
1706 float_cache,
1707 )?;
1708
1709 debug_info!(
1713 ctx,
1714 "[layout_bfc] IFC child {} re-layouted with float context (text will wrap, box \
1715 stays full width)",
1716 child_index
1717 );
1718
1719 }
1729
1730 output.positions.insert(child_index, final_pos);
1731
1732 main_pen += child_size.main(writing_mode);
1737 has_content = true;
1738
1739 if clearance_applied {
1744 last_margin_bottom = 0.0;
1746 } else {
1747 last_margin_bottom = child_margin_bottom;
1748 }
1749
1750 debug_info!(
1751 ctx,
1752 "[layout_bfc] Child {} positioned at final_pos={:?}, size={:?}, advanced main_pen to \
1753 {}, last_margin_bottom={}, clearance_applied={}",
1754 child_index,
1755 final_pos,
1756 child_size,
1757 main_pen,
1758 last_margin_bottom,
1759 clearance_applied
1760 );
1761
1762 let child_cross_extent =
1764 child_cross_pos + child_size.cross(writing_mode) + child_margin.cross_end(writing_mode);
1765 max_cross_size = max_cross_size.max(child_cross_extent);
1766 }
1767
1768 debug_info!(
1771 ctx,
1772 "[layout_bfc] Storing {} floats in cache for node {}",
1773 float_context.floats.len(),
1774 node_index
1775 );
1776 float_cache.insert(node_index, float_context.clone());
1777
1778 let mut escaped_top_margin = None;
1780 let mut escaped_bottom_margin = None;
1781
1782 if top_margin_escaped {
1784 escaped_top_margin = Some(accumulated_top_margin);
1786 debug_info!(
1787 ctx,
1788 "[layout_bfc] Returning escaped top margin: accumulated={}, node={}",
1789 accumulated_top_margin,
1790 node_index
1791 );
1792 } else if !top_margin_resolved && accumulated_top_margin > 0.0 {
1793 escaped_top_margin = Some(accumulated_top_margin);
1795 debug_info!(
1796 ctx,
1797 "[layout_bfc] Escaping top margin (no content): accumulated={}, node={}",
1798 accumulated_top_margin,
1799 node_index
1800 );
1801 } else if !top_margin_resolved {
1802 escaped_top_margin = Some(accumulated_top_margin);
1804 debug_info!(
1805 ctx,
1806 "[layout_bfc] Escaping top margin (zero, no content): accumulated={}, node={}",
1807 accumulated_top_margin,
1808 node_index
1809 );
1810 } else {
1811 debug_info!(
1812 ctx,
1813 "[layout_bfc] NOT escaping top margin: top_margin_resolved={}, escaped={}, \
1814 accumulated={}, node={}",
1815 top_margin_resolved,
1816 top_margin_escaped,
1817 accumulated_top_margin,
1818 node_index
1819 );
1820 }
1821
1822 if let Some(last_idx) = last_child_index {
1824 let last_child = tree.get(last_idx).ok_or(LayoutError::InvalidTree)?;
1825 let last_has_bottom_blocker =
1826 has_margin_collapse_blocker(&last_child.box_props, writing_mode, false);
1827
1828 debug_info!(
1829 ctx,
1830 "[layout_bfc] Bottom margin for node {}: parent_has_bottom_blocker={}, \
1831 last_has_bottom_blocker={}, last_margin_bottom={}, main_pen_before={}",
1832 node_index,
1833 parent_has_bottom_blocker,
1834 last_has_bottom_blocker,
1835 last_margin_bottom,
1836 main_pen
1837 );
1838
1839 if !parent_has_bottom_blocker && !last_has_bottom_blocker && has_content {
1840 let collapsed_bottom = collapse_margins(parent_margin_bottom, last_margin_bottom);
1842 escaped_bottom_margin = Some(collapsed_bottom);
1843 debug_info!(
1844 ctx,
1845 "[layout_bfc] Bottom margin ESCAPED for node {}: collapsed={}",
1846 node_index,
1847 collapsed_bottom
1848 );
1849 } else {
1851 main_pen += last_margin_bottom;
1853 debug_info!(
1857 ctx,
1858 "[layout_bfc] Bottom margin BLOCKED for node {}: added last_margin_bottom={}, \
1859 main_pen_after={}",
1860 node_index,
1861 last_margin_bottom,
1862 main_pen
1863 );
1864 }
1865 } else {
1866 if !top_margin_resolved {
1868 main_pen += parent_margin_top;
1869 }
1870 main_pen += parent_margin_bottom;
1871 }
1872
1873 let is_root_node = node.parent.is_none();
1876 if is_root_node {
1877 if let Some(top) = escaped_top_margin {
1878 for (_, pos) in output.positions.iter_mut() {
1880 let current_main = pos.main(writing_mode);
1881 *pos = LogicalPosition::from_main_cross(
1882 current_main + top,
1883 pos.cross(writing_mode),
1884 writing_mode,
1885 );
1886 }
1887 main_pen += top;
1888 }
1889 if let Some(bottom) = escaped_bottom_margin {
1890 main_pen += bottom;
1891 }
1892 escaped_top_margin = None;
1894 escaped_bottom_margin = None;
1895 }
1896
1897 let content_box_height = main_pen - total_escaped_top_margin;
1964 output.overflow_size =
1965 LogicalSize::from_main_cross(content_box_height, max_cross_size, writing_mode);
1966
1967 debug_info!(
1968 ctx,
1969 "[layout_bfc] FINAL for node {}: main_pen={}, total_escaped_top={}, \
1970 total_sibling_margins={}, content_box_height={}",
1971 node_index,
1972 main_pen,
1973 total_escaped_top_margin,
1974 total_sibling_margins,
1975 content_box_height
1976 );
1977
1978 output.baseline = None;
1980
1981 if let Some(node_mut) = tree.get_mut(node_index) {
1983 node_mut.escaped_top_margin = escaped_top_margin;
1984 node_mut.escaped_bottom_margin = escaped_bottom_margin;
1985 }
1986
1987 if let Some(node_mut) = tree.get_mut(node_index) {
1988 node_mut.baseline = output.baseline;
1989 }
1990
1991 Ok(BfcLayoutResult {
1992 output,
1993 escaped_top_margin,
1994 escaped_bottom_margin,
1995 })
1996}
1997
1998fn layout_ifc<T: ParsedFontTrait>(
2026 ctx: &mut LayoutContext<'_, T>,
2027 text_cache: &mut crate::font_traits::TextLayoutCache,
2028 tree: &mut LayoutTree,
2029 node_index: usize,
2030 constraints: &LayoutConstraints,
2031) -> Result<LayoutOutput> {
2032 let ifc_start = (ctx.get_system_time_fn.cb)();
2033
2034 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;;
2035 let float_count = constraints
2036 .bfc_state
2037 .as_ref()
2038 .map(|s| s.floats.floats.len())
2039 .unwrap_or(0);
2040 debug_info!(
2041 ctx,
2042 "[layout_ifc] ENTRY: node_index={}, has_bfc_state={}, float_count={}",
2043 node_index,
2044 constraints.bfc_state.is_some(),
2045 float_count
2046 );
2047 debug_ifc_layout!(ctx, "CALLED for node_index={}", node_index);
2048
2049 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
2052 let ifc_root_dom_id = match node.dom_node_id {
2053 Some(id) => id,
2054 None => {
2055 let parent_dom_id = node
2057 .parent
2058 .and_then(|p| tree.get(p))
2059 .and_then(|n| n.dom_node_id);
2060
2061 if let Some(id) = parent_dom_id {
2062 id
2063 } else {
2064 node.children
2066 .iter()
2067 .filter_map(|&child_idx| tree.get(child_idx))
2068 .filter_map(|n| n.dom_node_id)
2069 .next()
2070 .ok_or(LayoutError::InvalidTree)?
2071 }
2072 }
2073 };
2074
2075 debug_ifc_layout!(ctx, "ifc_root_dom_id={:?}", ifc_root_dom_id);
2076
2077 let phase1_start = (ctx.get_system_time_fn.cb)();
2079 let (inline_content, child_map) =
2080 collect_and_measure_inline_content(ctx, text_cache, tree, node_index, constraints)?;
2081 let _phase1_time = (ctx.get_system_time_fn.cb)().duration_since(&phase1_start);
2082
2083 debug_info!(
2084 ctx,
2085 "[layout_ifc] Collected {} inline content items for node {}",
2086 inline_content.len(),
2087 node_index
2088 );
2089 if inline_content.len() > 10 {
2090 let _text_count = inline_content.iter().filter(|i| matches!(i, InlineContent::Text(_))).count();
2091 let _shape_count = inline_content.iter().filter(|i| matches!(i, InlineContent::Shape(_))).count();
2092 }
2093 for (i, item) in inline_content.iter().enumerate() {
2094 match item {
2095 InlineContent::Text(run) => debug_info!(ctx, " [{}] Text: '{}'", i, run.text),
2096 InlineContent::Marker {
2097 run,
2098 position_outside,
2099 } => debug_info!(
2100 ctx,
2101 " [{}] Marker: '{}' (outside={})",
2102 i,
2103 run.text,
2104 position_outside
2105 ),
2106 InlineContent::Shape(_) => debug_info!(ctx, " [{}] Shape", i),
2107 InlineContent::Image(_) => debug_info!(ctx, " [{}] Image", i),
2108 _ => debug_info!(ctx, " [{}] Other", i),
2109 }
2110 }
2111
2112 debug_ifc_layout!(
2113 ctx,
2114 "Collected {} inline content items",
2115 inline_content.len()
2116 );
2117
2118 if inline_content.is_empty() {
2119 debug_warning!(ctx, "inline_content is empty, returning default output!");
2120 return Ok(LayoutOutput::default());
2121 }
2122
2123 let _cached_ifc = tree
2135 .get(node_index)
2136 .and_then(|n| n.inline_layout_result.as_ref());
2137 let text3_constraints =
2145 translate_to_text3_constraints(ctx, constraints, ctx.styled_dom, ifc_root_dom_id);
2146
2147 let cached_constraints = text3_constraints.clone();
2149
2150 debug_info!(
2151 ctx,
2152 "[layout_ifc] CALLING text_cache.layout_flow for node {} with {} exclusions",
2153 node_index,
2154 text3_constraints.shape_exclusions.len()
2155 );
2156
2157 let fragments = vec![LayoutFragment {
2158 id: "main".to_string(),
2159 constraints: text3_constraints,
2160 }];
2161
2162 let phase3_start = (ctx.get_system_time_fn.cb)();
2165 let loaded_fonts = ctx.font_manager.get_loaded_fonts();
2166 let text_layout_result = match text_cache.layout_flow(
2167 &inline_content,
2168 &[],
2169 &fragments,
2170 &ctx.font_manager.font_chain_cache,
2171 &ctx.font_manager.fc_cache,
2172 &loaded_fonts,
2173 ctx.debug_messages,
2174 ) {
2175 Ok(result) => result,
2176 Err(e) => {
2177 debug_warning!(ctx, "Text layout failed: {:?}", e);
2180 debug_warning!(
2181 ctx,
2182 "Continuing with zero-sized layout for node {}",
2183 node_index
2184 );
2185
2186 let mut output = LayoutOutput::default();
2187 output.overflow_size = LogicalSize::new(0.0, 0.0);
2188 return Ok(output);
2189 }
2190 };
2191 let _phase3_time = (ctx.get_system_time_fn.cb)().duration_since(&phase3_start);
2192 let _total_ifc_time = (ctx.get_system_time_fn.cb)().duration_since(&ifc_start);
2193
2194 let mut output = LayoutOutput::default();
2196 let node = tree.get_mut(node_index).ok_or(LayoutError::InvalidTree)?;
2197
2198 debug_ifc_layout!(
2199 ctx,
2200 "text_layout_result has {} fragment_layouts",
2201 text_layout_result.fragment_layouts.len()
2202 );
2203
2204 if let Some(main_frag) = text_layout_result.fragment_layouts.get("main") {
2205 let frag_bounds = main_frag.bounds();
2206 debug_ifc_layout!(
2207 ctx,
2208 "Found 'main' fragment with {} items, bounds={}x{}",
2209 main_frag.items.len(),
2210 frag_bounds.width,
2211 frag_bounds.height
2212 );
2213 debug_ifc_layout!(ctx, "Storing inline_layout_result on node {}", node_index);
2214
2215 let has_floats = constraints
2226 .bfc_state
2227 .as_ref()
2228 .map(|s| !s.floats.floats.is_empty())
2229 .unwrap_or(false);
2230 let current_width_type = constraints.available_width_type;
2231
2232 let should_store = match &node.inline_layout_result {
2233 None => {
2234 debug_info!(
2236 ctx,
2237 "[layout_ifc] Storing NEW inline_layout_result for node {} (width_type={:?}, \
2238 has_floats={})",
2239 node_index,
2240 current_width_type,
2241 has_floats
2242 );
2243 true
2244 }
2245 Some(cached) => {
2246 if cached.should_replace_with(current_width_type, has_floats) {
2248 debug_info!(
2249 ctx,
2250 "[layout_ifc] REPLACING inline_layout_result for node {} (old: \
2251 width={:?}, floats={}) with (new: width={:?}, floats={})",
2252 node_index,
2253 cached.available_width,
2254 cached.has_floats,
2255 current_width_type,
2256 has_floats
2257 );
2258 true
2259 } else {
2260 debug_info!(
2261 ctx,
2262 "[layout_ifc] KEEPING cached inline_layout_result for node {} (cached: \
2263 width={:?}, floats={}, new: width={:?}, floats={})",
2264 node_index,
2265 cached.available_width,
2266 cached.has_floats,
2267 current_width_type,
2268 has_floats
2269 );
2270 false
2271 }
2272 }
2273 };
2274
2275 if should_store {
2276 node.inline_layout_result = Some(CachedInlineLayout::new_with_constraints(
2277 main_frag.clone(),
2278 current_width_type,
2279 has_floats,
2280 cached_constraints,
2281 ));
2282 }
2283
2284 output.overflow_size = LogicalSize::new(frag_bounds.width, frag_bounds.height);
2286 output.baseline = main_frag.last_baseline();
2287 node.baseline = output.baseline;
2288
2289 for positioned_item in &main_frag.items {
2292 if let ShapedItem::Object { source, content, .. } = &positioned_item.item {
2293 if let Some(&child_node_index) = child_map.get(source) {
2294 let new_relative_pos = LogicalPosition {
2296 x: positioned_item.position.x,
2297 y: positioned_item.position.y,
2298 };
2299 output.positions.insert(child_node_index, new_relative_pos);
2300 }
2301 }
2302 }
2303 }
2304
2305 Ok(output)
2306}
2307
2308fn translate_taffy_size(size: LogicalSize) -> TaffySize<Option<f32>> {
2309 TaffySize {
2310 width: Some(size.width),
2311 height: Some(size.height),
2312 }
2313}
2314
2315pub(crate) fn convert_font_style(style: StyleFontStyle) -> crate::font_traits::FontStyle {
2317 match style {
2318 StyleFontStyle::Normal => crate::font_traits::FontStyle::Normal,
2319 StyleFontStyle::Italic => crate::font_traits::FontStyle::Italic,
2320 StyleFontStyle::Oblique => crate::font_traits::FontStyle::Oblique,
2321 }
2322}
2323
2324pub(crate) fn convert_font_weight(weight: StyleFontWeight) -> FcWeight {
2326 match weight {
2327 StyleFontWeight::W100 => FcWeight::Thin,
2328 StyleFontWeight::W200 => FcWeight::ExtraLight,
2329 StyleFontWeight::W300 | StyleFontWeight::Lighter => FcWeight::Light,
2330 StyleFontWeight::Normal => FcWeight::Normal,
2331 StyleFontWeight::W500 => FcWeight::Medium,
2332 StyleFontWeight::W600 => FcWeight::SemiBold,
2333 StyleFontWeight::Bold => FcWeight::Bold,
2334 StyleFontWeight::W800 => FcWeight::ExtraBold,
2335 StyleFontWeight::W900 | StyleFontWeight::Bolder => FcWeight::Black,
2336 }
2337}
2338
2339#[inline]
2346fn resolve_size_metric(
2347 metric: SizeMetric,
2348 value: f32,
2349 containing_block_size: f32,
2350 viewport_size: LogicalSize,
2351) -> f32 {
2352 match metric {
2353 SizeMetric::Px => value,
2354 SizeMetric::Pt => value * PT_TO_PX,
2355 SizeMetric::Percent => value / 100.0 * containing_block_size,
2356 SizeMetric::Em | SizeMetric::Rem => value * DEFAULT_FONT_SIZE,
2357 SizeMetric::Vw => value / 100.0 * viewport_size.width,
2358 SizeMetric::Vh => value / 100.0 * viewport_size.height,
2359 SizeMetric::Vmin => value / 100.0 * viewport_size.width.min(viewport_size.height),
2360 SizeMetric::Vmax => value / 100.0 * viewport_size.width.max(viewport_size.height),
2361 SizeMetric::In => value * 96.0,
2363 SizeMetric::Cm => value * 96.0 / 2.54,
2364 SizeMetric::Mm => value * 96.0 / 25.4,
2365 }
2366}
2367
2368pub fn translate_taffy_size_back(size: TaffySize<f32>) -> LogicalSize {
2369 LogicalSize {
2370 width: size.width,
2371 height: size.height,
2372 }
2373}
2374
2375pub fn translate_taffy_point_back(point: taffy::Point<f32>) -> LogicalPosition {
2376 LogicalPosition {
2377 x: point.x,
2378 y: point.y,
2379 }
2380}
2381
2382fn establishes_new_bfc<T: ParsedFontTrait>(ctx: &LayoutContext<'_, T>, node: &LayoutNode) -> bool {
2397 let Some(dom_id) = node.dom_node_id else {
2398 return false;
2399 };
2400
2401 let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2402
2403 let float_val = get_float(ctx.styled_dom, dom_id, node_state);
2405 if matches!(
2406 float_val,
2407 MultiValue::Exact(LayoutFloat::Left | LayoutFloat::Right)
2408 ) {
2409 return true;
2410 }
2411
2412 let position = crate::solver3::positioning::get_position_type(ctx.styled_dom, Some(dom_id));
2414 if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
2415 return true;
2416 }
2417
2418 let display = get_display_property(ctx.styled_dom, Some(dom_id));
2420 if matches!(
2421 display,
2422 MultiValue::Exact(
2423 LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption
2424 )
2425 ) {
2426 return true;
2427 }
2428
2429 if matches!(display, MultiValue::Exact(LayoutDisplay::FlowRoot)) {
2431 return true;
2432 }
2433
2434 let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, node_state);
2437 let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, node_state);
2438
2439 let creates_bfc_via_overflow = |ov: &MultiValue<LayoutOverflow>| {
2440 matches!(
2441 ov,
2442 &MultiValue::Exact(
2443 LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto
2444 )
2445 )
2446 };
2447
2448 if creates_bfc_via_overflow(&overflow_x) || creates_bfc_via_overflow(&overflow_y) {
2449 return true;
2450 }
2451
2452 if matches!(
2454 node.formatting_context,
2455 FormattingContext::Table | FormattingContext::Flex | FormattingContext::Grid
2456 ) {
2457 return true;
2458 }
2459
2460 false
2462}
2463
2464fn translate_to_text3_constraints<'a, T: ParsedFontTrait>(
2466 ctx: &mut LayoutContext<'_, T>,
2467 constraints: &'a LayoutConstraints<'a>,
2468 styled_dom: &StyledDom,
2469 dom_id: NodeId,
2470) -> UnifiedConstraints {
2471 let mut shape_exclusions = if let Some(ref bfc_state) = constraints.bfc_state {
2473 debug_info!(
2474 ctx,
2475 "[translate_to_text3] dom_id={:?}, converting {} floats to exclusions",
2476 dom_id,
2477 bfc_state.floats.floats.len()
2478 );
2479 bfc_state
2480 .floats
2481 .floats
2482 .iter()
2483 .enumerate()
2484 .map(|(i, float_box)| {
2485 let rect = crate::text3::cache::Rect {
2486 x: float_box.rect.origin.x,
2487 y: float_box.rect.origin.y,
2488 width: float_box.rect.size.width,
2489 height: float_box.rect.size.height,
2490 };
2491 debug_info!(
2492 ctx,
2493 "[translate_to_text3] Exclusion #{}: {:?} at ({}, {}) size {}x{}",
2494 i,
2495 float_box.kind,
2496 rect.x,
2497 rect.y,
2498 rect.width,
2499 rect.height
2500 );
2501 ShapeBoundary::Rectangle(rect)
2502 })
2503 .collect()
2504 } else {
2505 debug_info!(
2506 ctx,
2507 "[translate_to_text3] dom_id={:?}, NO bfc_state - no float exclusions",
2508 dom_id
2509 );
2510 Vec::new()
2511 };
2512
2513 debug_info!(
2514 ctx,
2515 "[translate_to_text3] dom_id={:?}, available_size={}x{}, shape_exclusions.len()={}",
2516 dom_id,
2517 constraints.available_size.width,
2518 constraints.available_size.height,
2519 shape_exclusions.len()
2520 );
2521
2522 let id = dom_id;
2524 let node_data = &styled_dom.node_data.as_container()[id];
2525 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2526
2527 let ref_box_height = if constraints.available_size.height.is_finite() {
2532 constraints.available_size.height
2533 } else {
2534 styled_dom
2538 .css_property_cache
2539 .ptr
2540 .get_height(node_data, &id, node_state)
2541 .and_then(|v| v.get_property())
2542 .and_then(|h| match h {
2543 LayoutHeight::Px(v) => {
2544 match v.metric {
2547 SizeMetric::Px => Some(v.number.get()),
2548 SizeMetric::Pt => Some(v.number.get() * PT_TO_PX),
2549 SizeMetric::In => Some(v.number.get() * 96.0),
2550 SizeMetric::Cm => Some(v.number.get() * 96.0 / 2.54),
2551 SizeMetric::Mm => Some(v.number.get() * 96.0 / 25.4),
2552 _ => None, }
2554 }
2555 _ => None,
2556 })
2557 .unwrap_or(constraints.available_size.width) };
2559
2560 let reference_box = crate::text3::cache::Rect {
2561 x: 0.0,
2562 y: 0.0,
2563 width: constraints.available_size.width,
2564 height: ref_box_height,
2565 };
2566
2567 debug_info!(ctx, "Checking shape-inside for node {:?}", id);
2569 debug_info!(
2570 ctx,
2571 "Reference box: {:?} (available_size height was: {})",
2572 reference_box,
2573 constraints.available_size.height
2574 );
2575
2576 let shape_boundaries = styled_dom
2577 .css_property_cache
2578 .ptr
2579 .get_shape_inside(node_data, &id, node_state)
2580 .and_then(|v| {
2581 debug_info!(ctx, "Got shape-inside value: {:?}", v);
2582 v.get_property()
2583 })
2584 .and_then(|shape_inside| {
2585 debug_info!(ctx, "shape-inside property: {:?}", shape_inside);
2586 if let ShapeInside::Shape(css_shape) = shape_inside {
2587 debug_info!(
2588 ctx,
2589 "Converting CSS shape to ShapeBoundary: {:?}",
2590 css_shape
2591 );
2592 let boundary =
2593 ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
2594 debug_info!(ctx, "Created ShapeBoundary: {:?}", boundary);
2595 Some(vec![boundary])
2596 } else {
2597 debug_info!(ctx, "shape-inside is None");
2598 None
2599 }
2600 })
2601 .unwrap_or_default();
2602
2603 debug_info!(
2604 ctx,
2605 "Final shape_boundaries count: {}",
2606 shape_boundaries.len()
2607 );
2608
2609 debug_info!(ctx, "Checking shape-outside for node {:?}", id);
2611 if let Some(shape_outside_value) = styled_dom
2612 .css_property_cache
2613 .ptr
2614 .get_shape_outside(node_data, &id, node_state)
2615 {
2616 debug_info!(ctx, "Got shape-outside value: {:?}", shape_outside_value);
2617 if let Some(shape_outside) = shape_outside_value.get_property() {
2618 debug_info!(ctx, "shape-outside property: {:?}", shape_outside);
2619 if let ShapeOutside::Shape(css_shape) = shape_outside {
2620 debug_info!(
2621 ctx,
2622 "Converting CSS shape-outside to ShapeBoundary: {:?}",
2623 css_shape
2624 );
2625 let boundary =
2626 ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
2627 debug_info!(ctx, "Created ShapeBoundary (exclusion): {:?}", boundary);
2628 shape_exclusions.push(boundary);
2629 }
2630 }
2631 } else {
2632 debug_info!(ctx, "No shape-outside value found");
2633 }
2634
2635 let writing_mode = get_writing_mode(styled_dom, id, node_state).unwrap_or_default();
2638
2639 let text_align = get_text_align(styled_dom, id, node_state).unwrap_or_default();
2640
2641 let text_justify = styled_dom
2642 .css_property_cache
2643 .ptr
2644 .get_text_justify(node_data, &id, node_state)
2645 .and_then(|s| s.get_property().copied())
2646 .unwrap_or_default();
2647
2648 let font_size = get_element_font_size(styled_dom, id, node_state);
2651
2652 let line_height_value = styled_dom
2653 .css_property_cache
2654 .ptr
2655 .get_line_height(node_data, &id, node_state)
2656 .and_then(|s| s.get_property().cloned())
2657 .unwrap_or_default();
2658
2659 let hyphenation = styled_dom
2660 .css_property_cache
2661 .ptr
2662 .get_hyphens(node_data, &id, node_state)
2663 .and_then(|s| s.get_property().copied())
2664 .unwrap_or_default();
2665
2666 let overflow_behaviour = get_overflow_x(styled_dom, id, node_state).unwrap_or_default();
2667
2668 let vertical_align = match get_vertical_align_property(styled_dom, id, node_state) {
2670 MultiValue::Exact(v) => v,
2671 _ => StyleVerticalAlign::default(),
2672 };
2673
2674 let vertical_align = match vertical_align {
2675 StyleVerticalAlign::Baseline => text3::cache::VerticalAlign::Baseline,
2676 StyleVerticalAlign::Top => text3::cache::VerticalAlign::Top,
2677 StyleVerticalAlign::Middle => text3::cache::VerticalAlign::Middle,
2678 StyleVerticalAlign::Bottom => text3::cache::VerticalAlign::Bottom,
2679 StyleVerticalAlign::Sub => text3::cache::VerticalAlign::Sub,
2680 StyleVerticalAlign::Superscript => text3::cache::VerticalAlign::Super,
2681 StyleVerticalAlign::TextTop => text3::cache::VerticalAlign::TextTop,
2682 StyleVerticalAlign::TextBottom => text3::cache::VerticalAlign::TextBottom,
2683 };
2684 let text_orientation = text3::cache::TextOrientation::default();
2685
2686 let direction = match get_direction_property(styled_dom, id, node_state) {
2688 MultiValue::Exact(d) => Some(match d {
2689 StyleDirection::Ltr => text3::cache::BidiDirection::Ltr,
2690 StyleDirection::Rtl => text3::cache::BidiDirection::Rtl,
2691 }),
2692 _ => None,
2693 };
2694
2695 debug_info!(
2696 ctx,
2697 "dom_id={:?}, available_size={}x{}, setting available_width={}",
2698 dom_id,
2699 constraints.available_size.width,
2700 constraints.available_size.height,
2701 constraints.available_size.width
2702 );
2703
2704 let text_indent = styled_dom
2706 .css_property_cache
2707 .ptr
2708 .get_text_indent(node_data, &id, node_state)
2709 .and_then(|s| s.get_property())
2710 .map(|ti| {
2711 let context = ResolutionContext {
2712 element_font_size: get_element_font_size(styled_dom, id, node_state),
2713 parent_font_size: get_parent_font_size(styled_dom, id, node_state),
2714 root_font_size: get_root_font_size(styled_dom, node_state),
2715 containing_block_size: PhysicalSize::new(constraints.available_size.width, 0.0),
2716 element_size: None,
2717 viewport_size: PhysicalSize::new(0.0, 0.0),
2718 };
2719 ti.inner
2720 .resolve_with_context(&context, PropertyContext::Other)
2721 })
2722 .unwrap_or(0.0);
2723
2724 let columns = styled_dom
2726 .css_property_cache
2727 .ptr
2728 .get_column_count(node_data, &id, node_state)
2729 .and_then(|s| s.get_property())
2730 .map(|cc| match cc {
2731 ColumnCount::Integer(n) => *n,
2732 ColumnCount::Auto => 1,
2733 })
2734 .unwrap_or(1);
2735
2736 let column_gap = styled_dom
2738 .css_property_cache
2739 .ptr
2740 .get_column_gap(node_data, &id, node_state)
2741 .and_then(|s| s.get_property())
2742 .map(|cg| {
2743 let context = ResolutionContext {
2744 element_font_size: get_element_font_size(styled_dom, id, node_state),
2745 parent_font_size: get_parent_font_size(styled_dom, id, node_state),
2746 root_font_size: get_root_font_size(styled_dom, node_state),
2747 containing_block_size: PhysicalSize::new(0.0, 0.0),
2748 element_size: None,
2749 viewport_size: PhysicalSize::new(0.0, 0.0),
2750 };
2751 cg.inner
2752 .resolve_with_context(&context, PropertyContext::Other)
2753 })
2754 .unwrap_or_else(|| {
2755 get_element_font_size(styled_dom, id, node_state)
2757 });
2758
2759 let text_wrap = match get_white_space_property(styled_dom, id, node_state) {
2761 MultiValue::Exact(ws) => match ws {
2762 StyleWhiteSpace::Normal => text3::cache::TextWrap::Wrap,
2763 StyleWhiteSpace::Nowrap => text3::cache::TextWrap::NoWrap,
2764 StyleWhiteSpace::Pre => text3::cache::TextWrap::NoWrap,
2765 StyleWhiteSpace::PreWrap => text3::cache::TextWrap::Wrap,
2766 StyleWhiteSpace::PreLine => text3::cache::TextWrap::Wrap,
2767 StyleWhiteSpace::BreakSpaces => text3::cache::TextWrap::Wrap,
2768 },
2769 _ => text3::cache::TextWrap::Wrap,
2770 };
2771
2772 let initial_letter = styled_dom
2774 .css_property_cache
2775 .ptr
2776 .get_initial_letter(node_data, &id, node_state)
2777 .and_then(|s| s.get_property())
2778 .map(|il| {
2779 use std::num::NonZeroUsize;
2780 let sink = match il.sink {
2781 azul_css::corety::OptionU32::Some(s) => s,
2782 azul_css::corety::OptionU32::None => il.size,
2783 };
2784 text3::cache::InitialLetter {
2785 size: il.size as f32,
2786 sink,
2787 count: NonZeroUsize::new(1).unwrap(),
2788 }
2789 });
2790
2791 let line_clamp = styled_dom
2793 .css_property_cache
2794 .ptr
2795 .get_line_clamp(node_data, &id, node_state)
2796 .and_then(|s| s.get_property())
2797 .and_then(|lc| std::num::NonZeroUsize::new(lc.max_lines));
2798
2799 let hanging_punctuation = styled_dom
2801 .css_property_cache
2802 .ptr
2803 .get_hanging_punctuation(node_data, &id, node_state)
2804 .and_then(|s| s.get_property())
2805 .map(|hp| hp.enabled)
2806 .unwrap_or(false);
2807
2808 let text_combine_upright = styled_dom
2810 .css_property_cache
2811 .ptr
2812 .get_text_combine_upright(node_data, &id, node_state)
2813 .and_then(|s| s.get_property())
2814 .map(|tcu| match tcu {
2815 StyleTextCombineUpright::None => text3::cache::TextCombineUpright::None,
2816 StyleTextCombineUpright::All => text3::cache::TextCombineUpright::All,
2817 StyleTextCombineUpright::Digits(n) => text3::cache::TextCombineUpright::Digits(*n),
2818 });
2819
2820 let exclusion_margin = styled_dom
2822 .css_property_cache
2823 .ptr
2824 .get_exclusion_margin(node_data, &id, node_state)
2825 .and_then(|s| s.get_property())
2826 .map(|em| em.inner.get() as f32)
2827 .unwrap_or(0.0);
2828
2829 let hyphenation_language = styled_dom
2831 .css_property_cache
2832 .ptr
2833 .get_hyphenation_language(node_data, &id, node_state)
2834 .and_then(|s| s.get_property())
2835 .and_then(|hl| {
2836 #[cfg(feature = "text_layout_hyphenation")]
2837 {
2838 use hyphenation::{Language, Load};
2839 match hl.inner.as_str() {
2841 "en-US" | "en" => Some(Language::EnglishUS),
2842 "de-DE" | "de" => Some(Language::German1996),
2843 "fr-FR" | "fr" => Some(Language::French),
2844 "es-ES" | "es" => Some(Language::Spanish),
2845 "it-IT" | "it" => Some(Language::Italian),
2846 "pt-PT" | "pt" => Some(Language::Portuguese),
2847 "nl-NL" | "nl" => Some(Language::Dutch),
2848 "pl-PL" | "pl" => Some(Language::Polish),
2849 "ru-RU" | "ru" => Some(Language::Russian),
2850 "zh-CN" | "zh" => Some(Language::Chinese),
2851 _ => None, }
2853 }
2854 #[cfg(not(feature = "text_layout_hyphenation"))]
2855 {
2856 None::<crate::text3::script::Language>
2857 }
2858 });
2859
2860 UnifiedConstraints {
2861 exclusion_margin,
2862 hyphenation_language,
2863 text_indent,
2864 initial_letter,
2865 line_clamp,
2866 columns,
2867 column_gap,
2868 hanging_punctuation,
2869 text_wrap,
2870 text_combine_upright,
2871 segment_alignment: SegmentAlignment::Total,
2872 overflow: match overflow_behaviour {
2873 LayoutOverflow::Visible => text3::cache::OverflowBehavior::Visible,
2874 LayoutOverflow::Hidden | LayoutOverflow::Clip => text3::cache::OverflowBehavior::Hidden,
2875 LayoutOverflow::Scroll => text3::cache::OverflowBehavior::Scroll,
2876 LayoutOverflow::Auto => text3::cache::OverflowBehavior::Auto,
2877 },
2878 available_width: constraints.available_width_type,
2881 available_height: match overflow_behaviour {
2884 LayoutOverflow::Scroll | LayoutOverflow::Auto => None,
2885 _ => Some(constraints.available_size.height),
2886 },
2887 shape_boundaries, shape_exclusions, writing_mode: Some(match writing_mode {
2890 LayoutWritingMode::HorizontalTb => text3::cache::WritingMode::HorizontalTb,
2891 LayoutWritingMode::VerticalRl => text3::cache::WritingMode::VerticalRl,
2892 LayoutWritingMode::VerticalLr => text3::cache::WritingMode::VerticalLr,
2893 }),
2894 direction, hyphenation: match hyphenation {
2896 StyleHyphens::None => false,
2897 StyleHyphens::Auto => true,
2898 },
2899 text_orientation,
2900 text_align: match text_align {
2901 StyleTextAlign::Start => text3::cache::TextAlign::Start,
2902 StyleTextAlign::End => text3::cache::TextAlign::End,
2903 StyleTextAlign::Left => text3::cache::TextAlign::Left,
2904 StyleTextAlign::Right => text3::cache::TextAlign::Right,
2905 StyleTextAlign::Center => text3::cache::TextAlign::Center,
2906 StyleTextAlign::Justify => text3::cache::TextAlign::Justify,
2907 },
2908 text_justify: match text_justify {
2909 LayoutTextJustify::None => text3::cache::JustifyContent::None,
2910 LayoutTextJustify::Auto => text3::cache::JustifyContent::None,
2911 LayoutTextJustify::InterWord => text3::cache::JustifyContent::InterWord,
2912 LayoutTextJustify::InterCharacter => text3::cache::JustifyContent::InterCharacter,
2913 LayoutTextJustify::Distribute => text3::cache::JustifyContent::Distribute,
2914 },
2915 line_height: line_height_value.inner.normalized() * font_size, vertical_align, }
2918}
2919
2920#[derive(Debug, Clone)]
2925pub struct TableColumnInfo {
2926 pub min_width: f32,
2928 pub max_width: f32,
2930 pub computed_width: Option<f32>,
2932}
2933
2934#[derive(Debug, Clone)]
2936pub struct TableCellInfo {
2937 pub node_index: usize,
2939 pub column: usize,
2941 pub colspan: usize,
2943 pub row: usize,
2945 pub rowspan: usize,
2947}
2948
2949#[derive(Debug)]
2951struct TableLayoutContext {
2952 columns: Vec<TableColumnInfo>,
2954 cells: Vec<TableCellInfo>,
2956 num_rows: usize,
2958 use_fixed_layout: bool,
2960 row_heights: Vec<f32>,
2962 border_collapse: StyleBorderCollapse,
2964 border_spacing: LayoutBorderSpacing,
2966 caption_index: Option<usize>,
2968 collapsed_rows: std::collections::HashSet<usize>,
2971 collapsed_columns: std::collections::HashSet<usize>,
2974}
2975
2976impl TableLayoutContext {
2977 fn new() -> Self {
2978 Self {
2979 columns: Vec::new(),
2980 cells: Vec::new(),
2981 num_rows: 0,
2982 use_fixed_layout: false,
2983 row_heights: Vec::new(),
2984 border_collapse: StyleBorderCollapse::Separate,
2985 border_spacing: LayoutBorderSpacing::default(),
2986 caption_index: None,
2987 collapsed_rows: std::collections::HashSet::new(),
2988 collapsed_columns: std::collections::HashSet::new(),
2989 }
2990 }
2991}
2992
2993#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2995pub enum BorderSource {
2996 Table = 0,
2997 ColumnGroup = 1,
2998 Column = 2,
2999 RowGroup = 3,
3000 Row = 4,
3001 Cell = 5,
3002}
3003
3004#[derive(Debug, Clone)]
3006pub struct BorderInfo {
3007 pub width: f32,
3008 pub style: BorderStyle,
3009 pub color: ColorU,
3010 pub source: BorderSource,
3011}
3012
3013impl BorderInfo {
3014 pub fn new(width: f32, style: BorderStyle, color: ColorU, source: BorderSource) -> Self {
3015 Self {
3016 width,
3017 style,
3018 color,
3019 source,
3020 }
3021 }
3022
3023 pub fn style_priority(style: &BorderStyle) -> u8 {
3026 match style {
3027 BorderStyle::Hidden => 255, BorderStyle::None => 0, BorderStyle::Double => 8,
3030 BorderStyle::Solid => 7,
3031 BorderStyle::Dashed => 6,
3032 BorderStyle::Dotted => 5,
3033 BorderStyle::Ridge => 4,
3034 BorderStyle::Outset => 3,
3035 BorderStyle::Groove => 2,
3036 BorderStyle::Inset => 1,
3037 }
3038 }
3039
3040 pub fn resolve_conflict(a: &BorderInfo, b: &BorderInfo) -> Option<BorderInfo> {
3043 if a.style == BorderStyle::Hidden || b.style == BorderStyle::Hidden {
3045 return None;
3046 }
3047
3048 let a_is_none = a.style == BorderStyle::None;
3050 let b_is_none = b.style == BorderStyle::None;
3051
3052 if a_is_none && b_is_none {
3053 return None;
3054 }
3055 if a_is_none {
3056 return Some(b.clone());
3057 }
3058 if b_is_none {
3059 return Some(a.clone());
3060 }
3061
3062 if a.width > b.width {
3064 return Some(a.clone());
3065 }
3066 if b.width > a.width {
3067 return Some(b.clone());
3068 }
3069
3070 let a_priority = Self::style_priority(&a.style);
3072 let b_priority = Self::style_priority(&b.style);
3073
3074 if a_priority > b_priority {
3075 return Some(a.clone());
3076 }
3077 if b_priority > a_priority {
3078 return Some(b.clone());
3079 }
3080
3081 if a.source > b.source {
3084 return Some(a.clone());
3085 }
3086 if b.source > a.source {
3087 return Some(b.clone());
3088 }
3089
3090 Some(a.clone())
3092 }
3093}
3094
3095fn get_border_info<T: ParsedFontTrait>(
3097 ctx: &LayoutContext<'_, T>,
3098 node: &LayoutNode,
3099 source: BorderSource,
3100) -> (BorderInfo, BorderInfo, BorderInfo, BorderInfo) {
3101 use azul_css::props::{
3102 basic::{
3103 pixel::{PhysicalSize, PropertyContext, ResolutionContext},
3104 ColorU,
3105 },
3106 style::BorderStyle,
3107 };
3108 use get_element_font_size;
3109 use get_parent_font_size;
3110 use get_root_font_size;
3111
3112 let default_border = BorderInfo::new(
3113 0.0,
3114 BorderStyle::None,
3115 ColorU {
3116 r: 0,
3117 g: 0,
3118 b: 0,
3119 a: 0,
3120 },
3121 source,
3122 );
3123
3124 let Some(dom_id) = node.dom_node_id else {
3125 return (
3126 default_border.clone(),
3127 default_border.clone(),
3128 default_border.clone(),
3129 default_border.clone(),
3130 );
3131 };
3132
3133 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3134 let node_state = StyledNodeState::default();
3135 let cache = &ctx.styled_dom.css_property_cache.ptr;
3136
3137 if let Some(ref cc) = cache.compact_cache {
3139 let idx = dom_id.index();
3140
3141 let bts = cc.get_border_top_style(idx);
3143 let brs = cc.get_border_right_style(idx);
3144 let bbs = cc.get_border_bottom_style(idx);
3145 let bls = cc.get_border_left_style(idx);
3146
3147 let make_color = |raw: u32| -> ColorU {
3149 if raw == 0 {
3150 ColorU { r: 0, g: 0, b: 0, a: 0 }
3151 } else {
3152 ColorU {
3153 r: ((raw >> 24) & 0xFF) as u8,
3154 g: ((raw >> 16) & 0xFF) as u8,
3155 b: ((raw >> 8) & 0xFF) as u8,
3156 a: (raw & 0xFF) as u8,
3157 }
3158 }
3159 };
3160
3161 let btc = make_color(cc.get_border_top_color_raw(idx));
3162 let brc = make_color(cc.get_border_right_color_raw(idx));
3163 let bbc = make_color(cc.get_border_bottom_color_raw(idx));
3164 let blc = make_color(cc.get_border_left_color_raw(idx));
3165
3166 let decode_width = |raw: i16| -> f32 {
3168 if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
3169 0.0 } else {
3171 raw as f32 / 10.0
3172 }
3173 };
3174
3175 let btw = decode_width(cc.get_border_top_width_raw(idx));
3176 let brw = decode_width(cc.get_border_right_width_raw(idx));
3177 let bbw = decode_width(cc.get_border_bottom_width_raw(idx));
3178 let blw = decode_width(cc.get_border_left_width_raw(idx));
3179
3180 let top = if bts == BorderStyle::None { default_border.clone() }
3181 else { BorderInfo::new(btw, bts, btc, source) };
3182 let right = if brs == BorderStyle::None { default_border.clone() }
3183 else { BorderInfo::new(brw, brs, brc, source) };
3184 let bottom = if bbs == BorderStyle::None { default_border.clone() }
3185 else { BorderInfo::new(bbw, bbs, bbc, source) };
3186 let left = if bls == BorderStyle::None { default_border.clone() }
3187 else { BorderInfo::new(blw, bls, blc, source) };
3188
3189 return (top, right, bottom, left);
3190 }
3191
3192 let cache = &ctx.styled_dom.css_property_cache.ptr;
3194
3195 let element_font_size = get_element_font_size(ctx.styled_dom, dom_id, &node_state);
3197 let parent_font_size = get_parent_font_size(ctx.styled_dom, dom_id, &node_state);
3198 let root_font_size = get_root_font_size(ctx.styled_dom, &node_state);
3199
3200 let resolution_context = ResolutionContext {
3201 element_font_size,
3202 parent_font_size,
3203 root_font_size,
3204 containing_block_size: PhysicalSize::new(0.0, 0.0),
3206 element_size: None,
3208 viewport_size: PhysicalSize::new(0.0, 0.0),
3209 };
3210
3211 let top = cache
3213 .get_border_top_style(node_data, &dom_id, &node_state)
3214 .and_then(|s| s.get_property())
3215 .map(|style_val| {
3216 let width = cache
3217 .get_border_top_width(node_data, &dom_id, &node_state)
3218 .and_then(|w| w.get_property())
3219 .map(|w| {
3220 w.inner
3221 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
3222 })
3223 .unwrap_or(0.0);
3224 let color = cache
3225 .get_border_top_color(node_data, &dom_id, &node_state)
3226 .and_then(|c| c.get_property())
3227 .map(|c| c.inner)
3228 .unwrap_or(ColorU {
3229 r: 0,
3230 g: 0,
3231 b: 0,
3232 a: 255,
3233 });
3234 BorderInfo::new(width, style_val.inner, color, source)
3235 })
3236 .unwrap_or_else(|| default_border.clone());
3237
3238 let right = cache
3240 .get_border_right_style(node_data, &dom_id, &node_state)
3241 .and_then(|s| s.get_property())
3242 .map(|style_val| {
3243 let width = cache
3244 .get_border_right_width(node_data, &dom_id, &node_state)
3245 .and_then(|w| w.get_property())
3246 .map(|w| {
3247 w.inner
3248 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
3249 })
3250 .unwrap_or(0.0);
3251 let color = cache
3252 .get_border_right_color(node_data, &dom_id, &node_state)
3253 .and_then(|c| c.get_property())
3254 .map(|c| c.inner)
3255 .unwrap_or(ColorU {
3256 r: 0,
3257 g: 0,
3258 b: 0,
3259 a: 255,
3260 });
3261 BorderInfo::new(width, style_val.inner, color, source)
3262 })
3263 .unwrap_or_else(|| default_border.clone());
3264
3265 let bottom = cache
3267 .get_border_bottom_style(node_data, &dom_id, &node_state)
3268 .and_then(|s| s.get_property())
3269 .map(|style_val| {
3270 let width = cache
3271 .get_border_bottom_width(node_data, &dom_id, &node_state)
3272 .and_then(|w| w.get_property())
3273 .map(|w| {
3274 w.inner
3275 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
3276 })
3277 .unwrap_or(0.0);
3278 let color = cache
3279 .get_border_bottom_color(node_data, &dom_id, &node_state)
3280 .and_then(|c| c.get_property())
3281 .map(|c| c.inner)
3282 .unwrap_or(ColorU {
3283 r: 0,
3284 g: 0,
3285 b: 0,
3286 a: 255,
3287 });
3288 BorderInfo::new(width, style_val.inner, color, source)
3289 })
3290 .unwrap_or_else(|| default_border.clone());
3291
3292 let left = cache
3294 .get_border_left_style(node_data, &dom_id, &node_state)
3295 .and_then(|s| s.get_property())
3296 .map(|style_val| {
3297 let width = cache
3298 .get_border_left_width(node_data, &dom_id, &node_state)
3299 .and_then(|w| w.get_property())
3300 .map(|w| {
3301 w.inner
3302 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
3303 })
3304 .unwrap_or(0.0);
3305 let color = cache
3306 .get_border_left_color(node_data, &dom_id, &node_state)
3307 .and_then(|c| c.get_property())
3308 .map(|c| c.inner)
3309 .unwrap_or(ColorU {
3310 r: 0,
3311 g: 0,
3312 b: 0,
3313 a: 255,
3314 });
3315 BorderInfo::new(width, style_val.inner, color, source)
3316 })
3317 .unwrap_or_else(|| default_border.clone());
3318
3319 (top, right, bottom, left)
3320}
3321
3322fn get_table_layout_property<T: ParsedFontTrait>(
3324 ctx: &LayoutContext<'_, T>,
3325 node: &LayoutNode,
3326) -> LayoutTableLayout {
3327 let Some(dom_id) = node.dom_node_id else {
3328 return LayoutTableLayout::Auto;
3329 };
3330
3331 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3332 let node_state = StyledNodeState::default();
3333
3334 ctx.styled_dom
3335 .css_property_cache
3336 .ptr
3337 .get_table_layout(node_data, &dom_id, &node_state)
3338 .and_then(|prop| prop.get_property().copied())
3339 .unwrap_or(LayoutTableLayout::Auto)
3340}
3341
3342fn get_border_collapse_property<T: ParsedFontTrait>(
3344 ctx: &LayoutContext<'_, T>,
3345 node: &LayoutNode,
3346) -> StyleBorderCollapse {
3347 let Some(dom_id) = node.dom_node_id else {
3348 return StyleBorderCollapse::Separate;
3349 };
3350
3351 if let Some(ref cc) = ctx.styled_dom.css_property_cache.ptr.compact_cache {
3353 return cc.get_border_collapse(dom_id.index());
3354 }
3355
3356 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3357 let node_state = StyledNodeState::default();
3358
3359 ctx.styled_dom
3360 .css_property_cache
3361 .ptr
3362 .get_border_collapse(node_data, &dom_id, &node_state)
3363 .and_then(|prop| prop.get_property().copied())
3364 .unwrap_or(StyleBorderCollapse::Separate)
3365}
3366
3367fn get_border_spacing_property<T: ParsedFontTrait>(
3369 ctx: &LayoutContext<'_, T>,
3370 node: &LayoutNode,
3371) -> LayoutBorderSpacing {
3372 if let Some(dom_id) = node.dom_node_id {
3373 if let Some(ref cc) = ctx.styled_dom.css_property_cache.ptr.compact_cache {
3375 let idx = dom_id.index();
3376 let h_raw = cc.get_border_spacing_h_raw(idx);
3377 let v_raw = cc.get_border_spacing_v_raw(idx);
3378 if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
3380 && v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
3381 {
3382 return LayoutBorderSpacing::new_separate(
3383 azul_css::props::basic::pixel::PixelValue::px(h_raw as f32 / 10.0),
3384 azul_css::props::basic::pixel::PixelValue::px(v_raw as f32 / 10.0),
3385 );
3386 }
3387 }
3389
3390 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3391 let node_state = StyledNodeState::default();
3392
3393 if let Some(prop) = ctx.styled_dom.css_property_cache.ptr.get_border_spacing(
3394 node_data,
3395 &dom_id,
3396 &node_state,
3397 ) {
3398 if let Some(value) = prop.get_property() {
3399 return *value;
3400 }
3401 }
3402 }
3403
3404 LayoutBorderSpacing::default() }
3406
3407fn get_caption_side_property<T: ParsedFontTrait>(
3416 ctx: &LayoutContext<'_, T>,
3417 node: &LayoutNode,
3418) -> StyleCaptionSide {
3419 if let Some(dom_id) = node.dom_node_id {
3420 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3421 let node_state = StyledNodeState::default();
3422
3423 if let Some(prop) =
3424 ctx.styled_dom
3425 .css_property_cache
3426 .ptr
3427 .get_caption_side(node_data, &dom_id, &node_state)
3428 {
3429 if let Some(value) = prop.get_property() {
3430 return *value;
3431 }
3432 }
3433 }
3434
3435 StyleCaptionSide::Top }
3437
3438fn is_visibility_collapsed<T: ParsedFontTrait>(
3449 ctx: &LayoutContext<'_, T>,
3450 node: &LayoutNode,
3451) -> bool {
3452 if let Some(dom_id) = node.dom_node_id {
3453 let node_state = StyledNodeState::default();
3454
3455 if let MultiValue::Exact(value) = get_visibility(ctx.styled_dom, dom_id, &node_state) {
3456 return matches!(value, StyleVisibility::Collapse);
3457 }
3458 }
3459
3460 false
3461}
3462
3463fn is_cell_empty(tree: &LayoutTree, cell_index: usize) -> bool {
3482 let cell_node = match tree.get(cell_index) {
3483 Some(node) => node,
3484 None => return true, };
3486
3487 if cell_node.children.is_empty() {
3489 return true;
3490 }
3491
3492 if let Some(ref cached_layout) = cell_node.inline_layout_result {
3494 return cached_layout.layout.items.is_empty();
3498 }
3499
3500 false
3508}
3509
3510pub fn layout_table_fc<T: ParsedFontTrait>(
3512 ctx: &mut LayoutContext<'_, T>,
3513 tree: &mut LayoutTree,
3514 text_cache: &mut crate::font_traits::TextLayoutCache,
3515 node_index: usize,
3516 constraints: &LayoutConstraints,
3517) -> Result<LayoutOutput> {
3518 debug_log!(ctx, "Laying out table");
3519
3520 debug_table_layout!(
3521 ctx,
3522 "node_index={}, available_size={:?}, writing_mode={:?}",
3523 node_index,
3524 constraints.available_size,
3525 constraints.writing_mode
3526 );
3527
3528 let table_node = tree
3538 .get(node_index)
3539 .ok_or(LayoutError::InvalidTree)?
3540 .clone();
3541
3542 let table_border_box_width = if let Some(dom_id) = table_node.dom_node_id {
3545 let intrinsic = table_node.intrinsic_sizes.clone().unwrap_or_default();
3547 let containing_block_size = LogicalSize {
3548 width: constraints.available_size.width,
3549 height: constraints.available_size.height,
3550 };
3551
3552 let table_size = crate::solver3::sizing::calculate_used_size_for_node(
3553 ctx.styled_dom,
3554 Some(dom_id),
3555 containing_block_size,
3556 intrinsic,
3557 &table_node.box_props,
3558 ctx.viewport_size,
3559 )?;
3560
3561 table_size.width
3562 } else {
3563 constraints.available_size.width
3564 };
3565
3566 let table_content_box_width = {
3568 let padding_width = table_node.box_props.padding.left + table_node.box_props.padding.right;
3569 let border_width = table_node.box_props.border.left + table_node.box_props.border.right;
3570 (table_border_box_width - padding_width - border_width).max(0.0)
3571 };
3572
3573 debug_table_layout!(ctx, "Table Layout Debug");
3574 debug_table_layout!(ctx, "Node index: {}", node_index);
3575 debug_table_layout!(
3576 ctx,
3577 "Available size from parent: {:.2} x {:.2}",
3578 constraints.available_size.width,
3579 constraints.available_size.height
3580 );
3581 debug_table_layout!(ctx, "Table border-box width: {:.2}", table_border_box_width);
3582 debug_table_layout!(
3583 ctx,
3584 "Table content-box width: {:.2}",
3585 table_content_box_width
3586 );
3587 debug_table_layout!(
3588 ctx,
3589 "Table padding: L={:.2} R={:.2}",
3590 table_node.box_props.padding.left,
3591 table_node.box_props.padding.right
3592 );
3593 debug_table_layout!(
3594 ctx,
3595 "Table border: L={:.2} R={:.2}",
3596 table_node.box_props.border.left,
3597 table_node.box_props.border.right
3598 );
3599 debug_table_layout!(ctx, "=");
3600
3601 let mut table_ctx = analyze_table_structure(tree, node_index, ctx)?;
3603
3604 let table_layout = get_table_layout_property(ctx, &table_node);
3606 table_ctx.use_fixed_layout = matches!(table_layout, LayoutTableLayout::Fixed);
3607
3608 table_ctx.border_collapse = get_border_collapse_property(ctx, &table_node);
3610 table_ctx.border_spacing = get_border_spacing_property(ctx, &table_node);
3611
3612 debug_log!(
3613 ctx,
3614 "Table layout: {:?}, border-collapse: {:?}, border-spacing: {:?}",
3615 table_layout,
3616 table_ctx.border_collapse,
3617 table_ctx.border_spacing
3618 );
3619
3620 if table_ctx.use_fixed_layout {
3622 debug_table_layout!(
3624 ctx,
3625 "FIXED layout: table_content_box_width={:.2}",
3626 table_content_box_width
3627 );
3628 calculate_column_widths_fixed(ctx, &mut table_ctx, table_content_box_width);
3629 } else {
3630 calculate_column_widths_auto_with_width(
3632 &mut table_ctx,
3633 tree,
3634 text_cache,
3635 ctx,
3636 constraints,
3637 table_content_box_width,
3638 )?;
3639 }
3640
3641 debug_table_layout!(ctx, "After column width calculation:");
3642 debug_table_layout!(ctx, " Number of columns: {}", table_ctx.columns.len());
3643 for (i, col) in table_ctx.columns.iter().enumerate() {
3644 debug_table_layout!(
3645 ctx,
3646 " Column {}: width={:.2}",
3647 i,
3648 col.computed_width.unwrap_or(0.0)
3649 );
3650 }
3651 let total_col_width: f32 = table_ctx
3652 .columns
3653 .iter()
3654 .filter_map(|c| c.computed_width)
3655 .sum();
3656 debug_table_layout!(ctx, " Total column width: {:.2}", total_col_width);
3657
3658 calculate_row_heights(&mut table_ctx, tree, text_cache, ctx, constraints)?;
3660
3661 let mut cell_positions =
3663 position_table_cells(&mut table_ctx, tree, ctx, node_index, constraints)?;
3664
3665 let mut table_width: f32 = table_ctx
3667 .columns
3668 .iter()
3669 .filter_map(|col| col.computed_width)
3670 .sum();
3671 let mut table_height: f32 = table_ctx.row_heights.iter().sum();
3672
3673 debug_table_layout!(
3674 ctx,
3675 "After calculate_row_heights: table_height={:.2}, row_heights={:?}",
3676 table_height,
3677 table_ctx.row_heights
3678 );
3679
3680 if table_ctx.border_collapse == StyleBorderCollapse::Separate {
3682 use get_element_font_size;
3683 use get_parent_font_size;
3684 use get_root_font_size;
3685 use PhysicalSize;
3686 use PropertyContext;
3687 use ResolutionContext;
3688
3689 let styled_dom = ctx.styled_dom;
3690 let table_id = tree.nodes[node_index].dom_node_id.unwrap();
3691 let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
3692
3693 let spacing_context = ResolutionContext {
3694 element_font_size: get_element_font_size(styled_dom, table_id, table_state),
3695 parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
3696 root_font_size: get_root_font_size(styled_dom, table_state),
3697 containing_block_size: PhysicalSize::new(0.0, 0.0),
3698 element_size: None,
3699 viewport_size: PhysicalSize::new(0.0, 0.0),
3701 };
3702
3703 let h_spacing = table_ctx
3704 .border_spacing
3705 .horizontal
3706 .resolve_with_context(&spacing_context, PropertyContext::Other);
3707 let v_spacing = table_ctx
3708 .border_spacing
3709 .vertical
3710 .resolve_with_context(&spacing_context, PropertyContext::Other);
3711
3712 let num_cols = table_ctx.columns.len();
3714 if num_cols > 0 {
3715 table_width += h_spacing * (num_cols + 1) as f32;
3716 }
3717
3718 if table_ctx.num_rows > 0 {
3720 table_height += v_spacing * (table_ctx.num_rows + 1) as f32;
3721 }
3722 }
3723
3724 let caption_side = get_caption_side_property(ctx, &table_node);
3729 let mut caption_height = 0.0;
3730 let mut table_y_offset = 0.0;
3731
3732 if let Some(caption_idx) = table_ctx.caption_index {
3733 debug_log!(
3734 ctx,
3735 "Laying out caption with caption-side: {:?}",
3736 caption_side
3737 );
3738
3739 let caption_constraints = LayoutConstraints {
3741 available_size: LogicalSize {
3742 width: table_width,
3743 height: constraints.available_size.height,
3744 },
3745 writing_mode: constraints.writing_mode,
3746 bfc_state: None, text_align: constraints.text_align,
3748 containing_block_size: constraints.containing_block_size,
3749 available_width_type: Text3AvailableSpace::Definite(table_width),
3750 };
3751
3752 let mut empty_float_cache = std::collections::BTreeMap::new();
3754 let caption_result = layout_formatting_context(
3755 ctx,
3756 tree,
3757 text_cache,
3758 caption_idx,
3759 &caption_constraints,
3760 &mut empty_float_cache,
3761 )?;
3762 caption_height = caption_result.output.overflow_size.height;
3763
3764 let caption_position = match caption_side {
3766 StyleCaptionSide::Top => {
3767 table_y_offset = caption_height;
3769 LogicalPosition { x: 0.0, y: 0.0 }
3770 }
3771 StyleCaptionSide::Bottom => {
3772 LogicalPosition {
3774 x: 0.0,
3775 y: table_height,
3776 }
3777 }
3778 };
3779
3780 cell_positions.insert(caption_idx, caption_position);
3782
3783 debug_log!(
3784 ctx,
3785 "Caption positioned at x={:.2}, y={:.2}, height={:.2}",
3786 caption_position.x,
3787 caption_position.y,
3788 caption_height
3789 );
3790 }
3791
3792 if table_y_offset > 0.0 {
3794 debug_log!(
3795 ctx,
3796 "Adjusting table cells by y offset: {:.2}",
3797 table_y_offset
3798 );
3799
3800 for cell_info in &table_ctx.cells {
3802 if let Some(pos) = cell_positions.get_mut(&cell_info.node_index) {
3803 pos.y += table_y_offset;
3804 }
3805 }
3806 }
3807
3808 let total_height = table_height + caption_height;
3810
3811 debug_table_layout!(ctx, "Final table dimensions:");
3812 debug_table_layout!(ctx, " Content width (columns): {:.2}", table_width);
3813 debug_table_layout!(ctx, " Content height (rows): {:.2}", table_height);
3814 debug_table_layout!(ctx, " Caption height: {:.2}", caption_height);
3815 debug_table_layout!(ctx, " Total height: {:.2}", total_height);
3816 debug_table_layout!(ctx, "End Table Debug");
3817
3818 let output = LayoutOutput {
3820 overflow_size: LogicalSize {
3821 width: table_width,
3822 height: total_height,
3823 },
3824 positions: cell_positions,
3826 baseline: None,
3828 };
3829
3830 Ok(output)
3831}
3832
3833fn analyze_table_structure<T: ParsedFontTrait>(
3835 tree: &LayoutTree,
3836 table_index: usize,
3837 ctx: &mut LayoutContext<'_, T>,
3838) -> Result<TableLayoutContext> {
3839 let mut table_ctx = TableLayoutContext::new();
3840
3841 let table_node = tree.get(table_index).ok_or(LayoutError::InvalidTree)?;
3842
3843 for &child_idx in &table_node.children {
3846 if let Some(child) = tree.get(child_idx) {
3847 if matches!(child.formatting_context, FormattingContext::TableCaption) {
3849 debug_log!(ctx, "Found table caption at index {}", child_idx);
3850 table_ctx.caption_index = Some(child_idx);
3851 continue;
3852 }
3853
3854 if matches!(
3856 child.formatting_context,
3857 FormattingContext::TableColumnGroup
3858 ) {
3859 analyze_table_colgroup(tree, child_idx, &mut table_ctx, ctx)?;
3860 continue;
3861 }
3862
3863 match child.formatting_context {
3865 FormattingContext::TableRow => {
3866 analyze_table_row(tree, child_idx, &mut table_ctx, ctx)?;
3867 }
3868 FormattingContext::TableRowGroup => {
3869 for &row_idx in &child.children {
3871 if let Some(row) = tree.get(row_idx) {
3872 if matches!(row.formatting_context, FormattingContext::TableRow) {
3873 analyze_table_row(tree, row_idx, &mut table_ctx, ctx)?;
3874 }
3875 }
3876 }
3877 }
3878 _ => {}
3879 }
3880 }
3881 }
3882
3883 debug_log!(
3884 ctx,
3885 "Table structure: {} rows, {} columns, {} cells{}",
3886 table_ctx.num_rows,
3887 table_ctx.columns.len(),
3888 table_ctx.cells.len(),
3889 if table_ctx.caption_index.is_some() {
3890 ", has caption"
3891 } else {
3892 ""
3893 }
3894 );
3895
3896 Ok(table_ctx)
3897}
3898
3899fn analyze_table_colgroup<T: ParsedFontTrait>(
3904 tree: &LayoutTree,
3905 colgroup_index: usize,
3906 table_ctx: &mut TableLayoutContext,
3907 ctx: &mut LayoutContext<'_, T>,
3908) -> Result<()> {
3909 let colgroup_node = tree.get(colgroup_index).ok_or(LayoutError::InvalidTree)?;
3910
3911 if is_visibility_collapsed(ctx, colgroup_node) {
3913 debug_log!(
3916 ctx,
3917 "Column group at index {} has visibility:collapse",
3918 colgroup_index
3919 );
3920 }
3921
3922 for &col_idx in &colgroup_node.children {
3924 if let Some(col_node) = tree.get(col_idx) {
3925 if is_visibility_collapsed(ctx, col_node) {
3929 debug_log!(ctx, "Column at index {} has visibility:collapse", col_idx);
3932 }
3933 }
3934 }
3935
3936 Ok(())
3937}
3938
3939fn analyze_table_row<T: ParsedFontTrait>(
3941 tree: &LayoutTree,
3942 row_index: usize,
3943 table_ctx: &mut TableLayoutContext,
3944 ctx: &mut LayoutContext<'_, T>,
3945) -> Result<()> {
3946 let row_node = tree.get(row_index).ok_or(LayoutError::InvalidTree)?;
3947 let row_num = table_ctx.num_rows;
3948 table_ctx.num_rows += 1;
3949
3950 if is_visibility_collapsed(ctx, row_node) {
3952 debug_log!(ctx, "Row {} has visibility:collapse", row_num);
3953 table_ctx.collapsed_rows.insert(row_num);
3954 }
3955
3956 let mut col_index = 0;
3957
3958 for &cell_idx in &row_node.children {
3959 if let Some(cell) = tree.get(cell_idx) {
3960 if matches!(cell.formatting_context, FormattingContext::TableCell) {
3961 let colspan = 1; let rowspan = 1; let cell_info = TableCellInfo {
3966 node_index: cell_idx,
3967 column: col_index,
3968 colspan,
3969 row: row_num,
3970 rowspan,
3971 };
3972
3973 table_ctx.cells.push(cell_info);
3974
3975 let max_col = col_index + colspan;
3977 while table_ctx.columns.len() < max_col {
3978 table_ctx.columns.push(TableColumnInfo {
3979 min_width: 0.0,
3980 max_width: 0.0,
3981 computed_width: None,
3982 });
3983 }
3984
3985 col_index += colspan;
3986 }
3987 }
3988 }
3989
3990 Ok(())
3991}
3992
3993fn calculate_column_widths_fixed<T: ParsedFontTrait>(
4001 ctx: &mut LayoutContext<'_, T>,
4002 table_ctx: &mut TableLayoutContext,
4003 available_width: f32,
4004) {
4005 debug_table_layout!(
4006 ctx,
4007 "calculate_column_widths_fixed: num_cols={}, available_width={:.2}",
4008 table_ctx.columns.len(),
4009 available_width
4010 );
4011
4012 let num_cols = table_ctx.columns.len();
4015 if num_cols == 0 {
4016 return;
4017 }
4018
4019 let num_visible_cols = num_cols - table_ctx.collapsed_columns.len();
4021 if num_visible_cols == 0 {
4022 for col in &mut table_ctx.columns {
4024 col.computed_width = Some(0.0);
4025 }
4026 return;
4027 }
4028
4029 let col_width = available_width / num_visible_cols as f32;
4031 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
4032 if table_ctx.collapsed_columns.contains(&col_idx) {
4033 col.computed_width = Some(0.0);
4034 } else {
4035 col.computed_width = Some(col_width);
4036 }
4037 }
4038}
4039
4040fn measure_cell_min_content_width<T: ParsedFontTrait>(
4042 ctx: &mut LayoutContext<'_, T>,
4043 tree: &mut LayoutTree,
4044 text_cache: &mut crate::font_traits::TextLayoutCache,
4045 cell_index: usize,
4046 constraints: &LayoutConstraints,
4047) -> Result<f32> {
4048 use crate::text3::cache::AvailableSpace;
4054 let min_constraints = LayoutConstraints {
4055 available_size: LogicalSize {
4056 width: AvailableSpace::MinContent.to_f32_for_layout(),
4057 height: f32::INFINITY,
4058 },
4059 writing_mode: constraints.writing_mode,
4060 bfc_state: None, text_align: constraints.text_align,
4062 containing_block_size: constraints.containing_block_size,
4063 available_width_type: Text3AvailableSpace::MinContent,
4066 };
4067
4068 let mut temp_positions: super::PositionVec = Vec::new();
4069 let mut temp_scrollbar_reflow = false;
4070 let mut temp_float_cache = std::collections::BTreeMap::new();
4071
4072 crate::solver3::cache::calculate_layout_for_subtree(
4073 ctx,
4074 tree,
4075 text_cache,
4076 cell_index,
4077 LogicalPosition::zero(),
4078 min_constraints.available_size,
4079 &mut temp_positions,
4080 &mut temp_scrollbar_reflow,
4081 &mut temp_float_cache,
4082 crate::solver3::cache::ComputeMode::ComputeSize,
4084 )?;
4085
4086 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4087 let size = cell_node.used_size.unwrap_or_default();
4088
4089 let padding = &cell_node.box_props.padding;
4091 let border = &cell_node.box_props.border;
4092 let writing_mode = constraints.writing_mode;
4093
4094 let min_width = size.width
4095 + padding.cross_start(writing_mode)
4096 + padding.cross_end(writing_mode)
4097 + border.cross_start(writing_mode)
4098 + border.cross_end(writing_mode);
4099
4100 Ok(min_width)
4101}
4102
4103fn measure_cell_max_content_width<T: ParsedFontTrait>(
4105 ctx: &mut LayoutContext<'_, T>,
4106 tree: &mut LayoutTree,
4107 text_cache: &mut crate::font_traits::TextLayoutCache,
4108 cell_index: usize,
4109 constraints: &LayoutConstraints,
4110) -> Result<f32> {
4111 use crate::text3::cache::AvailableSpace;
4117 let max_constraints = LayoutConstraints {
4118 available_size: LogicalSize {
4119 width: AvailableSpace::MaxContent.to_f32_for_layout(),
4120 height: f32::INFINITY,
4121 },
4122 writing_mode: constraints.writing_mode,
4123 bfc_state: None, text_align: constraints.text_align,
4125 containing_block_size: constraints.containing_block_size,
4126 available_width_type: Text3AvailableSpace::MaxContent,
4129 };
4130
4131 let mut temp_positions: super::PositionVec = Vec::new();
4132 let mut temp_scrollbar_reflow = false;
4133 let mut temp_float_cache = std::collections::BTreeMap::new();
4134
4135 crate::solver3::cache::calculate_layout_for_subtree(
4136 ctx,
4137 tree,
4138 text_cache,
4139 cell_index,
4140 LogicalPosition::zero(),
4141 max_constraints.available_size,
4142 &mut temp_positions,
4143 &mut temp_scrollbar_reflow,
4144 &mut temp_float_cache,
4145 crate::solver3::cache::ComputeMode::ComputeSize,
4147 )?;
4148
4149 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4150 let size = cell_node.used_size.unwrap_or_default();
4151
4152 let padding = &cell_node.box_props.padding;
4154 let border = &cell_node.box_props.border;
4155 let writing_mode = constraints.writing_mode;
4156
4157 let max_width = size.width
4158 + padding.cross_start(writing_mode)
4159 + padding.cross_end(writing_mode)
4160 + border.cross_start(writing_mode)
4161 + border.cross_end(writing_mode);
4162
4163 Ok(max_width)
4164}
4165
4166fn calculate_column_widths_auto<T: ParsedFontTrait>(
4168 table_ctx: &mut TableLayoutContext,
4169 tree: &mut LayoutTree,
4170 text_cache: &mut crate::font_traits::TextLayoutCache,
4171 ctx: &mut LayoutContext<'_, T>,
4172 constraints: &LayoutConstraints,
4173) -> Result<()> {
4174 calculate_column_widths_auto_with_width(
4175 table_ctx,
4176 tree,
4177 text_cache,
4178 ctx,
4179 constraints,
4180 constraints.available_size.width,
4181 )
4182}
4183
4184fn calculate_column_widths_auto_with_width<T: ParsedFontTrait>(
4186 table_ctx: &mut TableLayoutContext,
4187 tree: &mut LayoutTree,
4188 text_cache: &mut crate::font_traits::TextLayoutCache,
4189 ctx: &mut LayoutContext<'_, T>,
4190 constraints: &LayoutConstraints,
4191 table_width: f32,
4192) -> Result<()> {
4193 let num_cols = table_ctx.columns.len();
4195 if num_cols == 0 {
4196 return Ok(());
4197 }
4198
4199 for cell_info in &table_ctx.cells {
4202 if table_ctx.collapsed_columns.contains(&cell_info.column) {
4204 continue;
4205 }
4206
4207 let mut spans_collapsed = false;
4209 for col_offset in 0..cell_info.colspan {
4210 if table_ctx
4211 .collapsed_columns
4212 .contains(&(cell_info.column + col_offset))
4213 {
4214 spans_collapsed = true;
4215 break;
4216 }
4217 }
4218 if spans_collapsed {
4219 continue;
4220 }
4221
4222 let min_width = measure_cell_min_content_width(
4223 ctx,
4224 tree,
4225 text_cache,
4226 cell_info.node_index,
4227 constraints,
4228 )?;
4229
4230 let max_width = measure_cell_max_content_width(
4231 ctx,
4232 tree,
4233 text_cache,
4234 cell_info.node_index,
4235 constraints,
4236 )?;
4237
4238 if cell_info.colspan == 1 {
4240 let col = &mut table_ctx.columns[cell_info.column];
4241 col.min_width = col.min_width.max(min_width);
4242 col.max_width = col.max_width.max(max_width);
4243 } else {
4244 distribute_cell_width_across_columns(
4247 &mut table_ctx.columns,
4248 cell_info.column,
4249 cell_info.colspan,
4250 min_width,
4251 max_width,
4252 &table_ctx.collapsed_columns,
4253 );
4254 }
4255 }
4256
4257 let total_min_width: f32 = table_ctx
4260 .columns
4261 .iter()
4262 .enumerate()
4263 .filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
4264 .map(|(_, c)| c.min_width)
4265 .sum();
4266 let total_max_width: f32 = table_ctx
4267 .columns
4268 .iter()
4269 .enumerate()
4270 .filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
4271 .map(|(_, c)| c.max_width)
4272 .sum();
4273 let available_width = table_width; debug_table_layout!(
4276 ctx,
4277 "calculate_column_widths_auto: min={:.2}, max={:.2}, table_width={:.2}",
4278 total_min_width,
4279 total_max_width,
4280 table_width
4281 );
4282
4283 if !total_max_width.is_finite() || !available_width.is_finite() {
4285 let num_non_collapsed = table_ctx.columns.len() - table_ctx.collapsed_columns.len();
4287 let width_per_column = if num_non_collapsed > 0 {
4288 available_width / num_non_collapsed as f32
4289 } else {
4290 0.0
4291 };
4292
4293 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
4294 if table_ctx.collapsed_columns.contains(&col_idx) {
4295 col.computed_width = Some(0.0);
4296 } else {
4297 col.computed_width = Some(col.min_width.max(width_per_column));
4299 }
4300 }
4301 } else if available_width >= total_max_width {
4302 let excess_width = available_width - total_max_width;
4307
4308 let column_info: Vec<(usize, f32, bool)> = table_ctx
4310 .columns
4311 .iter()
4312 .enumerate()
4313 .map(|(idx, c)| (idx, c.max_width, table_ctx.collapsed_columns.contains(&idx)))
4314 .collect();
4315
4316 let total_weight: f32 = column_info.iter()
4318 .filter(|(_, _, is_collapsed)| !is_collapsed)
4319 .map(|(_, max_w, _)| max_w.max(1.0)) .sum();
4321
4322 let num_non_collapsed = column_info
4323 .iter()
4324 .filter(|(_, _, is_collapsed)| !is_collapsed)
4325 .count();
4326
4327 for (col_idx, max_width, is_collapsed) in column_info {
4329 let col = &mut table_ctx.columns[col_idx];
4330 if is_collapsed {
4331 col.computed_width = Some(0.0);
4332 } else {
4333 let weight_factor = if total_weight > 0.0 {
4335 max_width.max(1.0) / total_weight
4336 } else {
4337 1.0 / num_non_collapsed.max(1) as f32
4339 };
4340
4341 let final_width = max_width + (excess_width * weight_factor);
4342 col.computed_width = Some(final_width);
4343 }
4344 }
4345 } else if available_width >= total_min_width {
4346 let scale = if total_max_width > total_min_width {
4349 (available_width - total_min_width) / (total_max_width - total_min_width)
4350 } else {
4351 0.0 };
4353 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
4354 if table_ctx.collapsed_columns.contains(&col_idx) {
4355 col.computed_width = Some(0.0);
4356 } else {
4357 let interpolated = col.min_width + (col.max_width - col.min_width) * scale;
4358 col.computed_width = Some(interpolated);
4359 }
4360 }
4361 } else {
4362 let scale = available_width / total_min_width;
4364 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
4365 if table_ctx.collapsed_columns.contains(&col_idx) {
4366 col.computed_width = Some(0.0);
4367 } else {
4368 col.computed_width = Some(col.min_width * scale);
4369 }
4370 }
4371 }
4372
4373 Ok(())
4374}
4375
4376fn distribute_cell_width_across_columns(
4378 columns: &mut [TableColumnInfo],
4379 start_col: usize,
4380 colspan: usize,
4381 cell_min_width: f32,
4382 cell_max_width: f32,
4383 collapsed_columns: &std::collections::HashSet<usize>,
4384) {
4385 let end_col = start_col + colspan;
4386 if end_col > columns.len() {
4387 return;
4388 }
4389
4390 let current_min_total: f32 = columns[start_col..end_col]
4392 .iter()
4393 .enumerate()
4394 .filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
4395 .map(|(_, c)| c.min_width)
4396 .sum();
4397 let current_max_total: f32 = columns[start_col..end_col]
4398 .iter()
4399 .enumerate()
4400 .filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
4401 .map(|(_, c)| c.max_width)
4402 .sum();
4403
4404 let num_visible_cols = (start_col..end_col)
4406 .filter(|idx| !collapsed_columns.contains(idx))
4407 .count();
4408
4409 if num_visible_cols == 0 {
4410 return; }
4412
4413 if cell_min_width > current_min_total {
4415 let extra_min = cell_min_width - current_min_total;
4416 let per_col = extra_min / num_visible_cols as f32;
4417 for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
4418 if !collapsed_columns.contains(&(start_col + idx)) {
4419 col.min_width += per_col;
4420 }
4421 }
4422 }
4423
4424 if cell_max_width > current_max_total {
4425 let extra_max = cell_max_width - current_max_total;
4426 let per_col = extra_max / num_visible_cols as f32;
4427 for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
4428 if !collapsed_columns.contains(&(start_col + idx)) {
4429 col.max_width += per_col;
4430 }
4431 }
4432 }
4433}
4434
4435fn layout_cell_for_height<T: ParsedFontTrait>(
4437 ctx: &mut LayoutContext<'_, T>,
4438 tree: &mut LayoutTree,
4439 text_cache: &mut crate::font_traits::TextLayoutCache,
4440 cell_index: usize,
4441 cell_width: f32,
4442 constraints: &LayoutConstraints,
4443) -> Result<f32> {
4444 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4445 let cell_dom_id = cell_node.dom_node_id.ok_or(LayoutError::InvalidTree)?;
4446
4447 let has_text_children = cell_dom_id
4451 .az_children(&ctx.styled_dom.node_hierarchy.as_container())
4452 .any(|child_id| {
4453 let node_data = &ctx.styled_dom.node_data.as_container()[child_id];
4454 matches!(node_data.get_node_type(), NodeType::Text(_))
4455 });
4456
4457 debug_table_layout!(
4458 ctx,
4459 "layout_cell_for_height: cell_index={}, has_text_children={}",
4460 cell_index,
4461 has_text_children
4462 );
4463
4464 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4466 let padding = &cell_node.box_props.padding;
4467 let border = &cell_node.box_props.border;
4468 let writing_mode = constraints.writing_mode;
4469
4470 let content_width = cell_width
4473 - padding.cross_start(writing_mode)
4474 - padding.cross_end(writing_mode)
4475 - border.cross_start(writing_mode)
4476 - border.cross_end(writing_mode);
4477
4478 debug_table_layout!(
4479 ctx,
4480 "Cell width: border_box={:.2}, content_box={:.2}",
4481 cell_width,
4482 content_width
4483 );
4484
4485 let content_height = if has_text_children {
4486 debug_table_layout!(ctx, "Using IFC to measure text content");
4488
4489 let cell_constraints = LayoutConstraints {
4490 available_size: LogicalSize {
4491 width: content_width, height: f32::INFINITY,
4493 },
4494 writing_mode: constraints.writing_mode,
4495 bfc_state: None,
4496 text_align: constraints.text_align,
4497 containing_block_size: constraints.containing_block_size,
4498 available_width_type: Text3AvailableSpace::Definite(content_width),
4501 };
4502
4503 let output = layout_ifc(ctx, text_cache, tree, cell_index, &cell_constraints)?;
4504
4505 debug_table_layout!(
4506 ctx,
4507 "IFC returned height={:.2}",
4508 output.overflow_size.height
4509 );
4510
4511 output.overflow_size.height
4512 } else {
4513 debug_table_layout!(ctx, "Using regular layout for block children");
4515
4516 let cell_constraints = LayoutConstraints {
4517 available_size: LogicalSize {
4518 width: content_width, height: f32::INFINITY,
4520 },
4521 writing_mode: constraints.writing_mode,
4522 bfc_state: None,
4523 text_align: constraints.text_align,
4524 containing_block_size: constraints.containing_block_size,
4525 available_width_type: Text3AvailableSpace::Definite(content_width),
4527 };
4528
4529 let mut temp_positions: super::PositionVec = Vec::new();
4530 let mut temp_scrollbar_reflow = false;
4531 let mut temp_float_cache = std::collections::BTreeMap::new();
4532
4533 crate::solver3::cache::calculate_layout_for_subtree(
4534 ctx,
4535 tree,
4536 text_cache,
4537 cell_index,
4538 LogicalPosition::zero(),
4539 cell_constraints.available_size,
4540 &mut temp_positions,
4541 &mut temp_scrollbar_reflow,
4542 &mut temp_float_cache,
4543 crate::solver3::cache::ComputeMode::PerformLayout,
4545 )?;
4546
4547 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4548 cell_node.used_size.unwrap_or_default().height
4549 };
4550
4551 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4553 let padding = &cell_node.box_props.padding;
4554 let border = &cell_node.box_props.border;
4555 let writing_mode = constraints.writing_mode;
4556
4557 let total_height = content_height
4558 + padding.main_start(writing_mode)
4559 + padding.main_end(writing_mode)
4560 + border.main_start(writing_mode)
4561 + border.main_end(writing_mode);
4562
4563 debug_table_layout!(
4564 ctx,
4565 "Cell total height: cell_index={}, content={:.2}, padding/border={:.2}, total={:.2}",
4566 cell_index,
4567 content_height,
4568 padding.main_start(writing_mode)
4569 + padding.main_end(writing_mode)
4570 + border.main_start(writing_mode)
4571 + border.main_end(writing_mode),
4572 total_height
4573 );
4574
4575 Ok(total_height)
4576}
4577
4578fn calculate_row_heights<T: ParsedFontTrait>(
4580 table_ctx: &mut TableLayoutContext,
4581 tree: &mut LayoutTree,
4582 text_cache: &mut crate::font_traits::TextLayoutCache,
4583 ctx: &mut LayoutContext<'_, T>,
4584 constraints: &LayoutConstraints,
4585) -> Result<()> {
4586 debug_table_layout!(
4587 ctx,
4588 "calculate_row_heights: num_rows={}, available_size={:?}",
4589 table_ctx.num_rows,
4590 constraints.available_size
4591 );
4592
4593 table_ctx.row_heights = vec![0.0; table_ctx.num_rows];
4595
4596 for &row_idx in &table_ctx.collapsed_rows {
4598 if row_idx < table_ctx.row_heights.len() {
4599 table_ctx.row_heights[row_idx] = 0.0;
4600 }
4601 }
4602
4603 for cell_info in &table_ctx.cells {
4605 if table_ctx.collapsed_rows.contains(&cell_info.row) {
4607 continue;
4608 }
4609
4610 let mut cell_width = 0.0;
4612 for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
4613 if let Some(col) = table_ctx.columns.get(col_idx) {
4614 if let Some(width) = col.computed_width {
4615 cell_width += width;
4616 }
4617 }
4618 }
4619
4620 debug_table_layout!(
4621 ctx,
4622 "Cell layout: node_index={}, row={}, col={}, width={:.2}",
4623 cell_info.node_index,
4624 cell_info.row,
4625 cell_info.column,
4626 cell_width
4627 );
4628
4629 let cell_height = layout_cell_for_height(
4631 ctx,
4632 tree,
4633 text_cache,
4634 cell_info.node_index,
4635 cell_width,
4636 constraints,
4637 )?;
4638
4639 debug_table_layout!(
4640 ctx,
4641 "Cell height calculated: node_index={}, height={:.2}",
4642 cell_info.node_index,
4643 cell_height
4644 );
4645
4646 if cell_info.rowspan == 1 {
4648 let current_height = table_ctx.row_heights[cell_info.row];
4649 table_ctx.row_heights[cell_info.row] = current_height.max(cell_height);
4650 }
4651 }
4652
4653 for cell_info in &table_ctx.cells {
4655 if table_ctx.collapsed_rows.contains(&cell_info.row) {
4657 continue;
4658 }
4659
4660 if cell_info.rowspan > 1 {
4661 let mut cell_width = 0.0;
4663 for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
4664 if let Some(col) = table_ctx.columns.get(col_idx) {
4665 if let Some(width) = col.computed_width {
4666 cell_width += width;
4667 }
4668 }
4669 }
4670
4671 let cell_height = layout_cell_for_height(
4673 ctx,
4674 tree,
4675 text_cache,
4676 cell_info.node_index,
4677 cell_width,
4678 constraints,
4679 )?;
4680
4681 let end_row = cell_info.row + cell_info.rowspan;
4683 let current_total: f32 = table_ctx.row_heights[cell_info.row..end_row]
4684 .iter()
4685 .enumerate()
4686 .filter(|(idx, _)| !table_ctx.collapsed_rows.contains(&(cell_info.row + idx)))
4687 .map(|(_, height)| height)
4688 .sum();
4689
4690 if cell_height > current_total {
4693 let extra_height = cell_height - current_total;
4694
4695 let non_collapsed_rows = (cell_info.row..end_row)
4697 .filter(|row_idx| !table_ctx.collapsed_rows.contains(row_idx))
4698 .count();
4699
4700 if non_collapsed_rows > 0 {
4701 let per_row = extra_height / non_collapsed_rows as f32;
4702
4703 for row_idx in cell_info.row..end_row {
4704 if !table_ctx.collapsed_rows.contains(&row_idx) {
4705 table_ctx.row_heights[row_idx] += per_row;
4706 }
4707 }
4708 }
4709 }
4710 }
4711 }
4712
4713 for &row_idx in &table_ctx.collapsed_rows {
4715 if row_idx < table_ctx.row_heights.len() {
4716 table_ctx.row_heights[row_idx] = 0.0;
4717 }
4718 }
4719
4720 Ok(())
4721}
4722
4723fn position_table_cells<T: ParsedFontTrait>(
4725 table_ctx: &mut TableLayoutContext,
4726 tree: &mut LayoutTree,
4727 ctx: &mut LayoutContext<'_, T>,
4728 table_index: usize,
4729 constraints: &LayoutConstraints,
4730) -> Result<BTreeMap<usize, LogicalPosition>> {
4731 debug_log!(ctx, "Positioning table cells in grid");
4732
4733 let mut positions = BTreeMap::new();
4734
4735 let (h_spacing, v_spacing) = if table_ctx.border_collapse == StyleBorderCollapse::Separate {
4737 let styled_dom = ctx.styled_dom;
4738 let table_id = tree.nodes[table_index].dom_node_id.unwrap();
4739 let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
4740
4741 let spacing_context = ResolutionContext {
4742 element_font_size: get_element_font_size(styled_dom, table_id, table_state),
4743 parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
4744 root_font_size: get_root_font_size(styled_dom, table_state),
4745 containing_block_size: PhysicalSize::new(0.0, 0.0),
4746 element_size: None,
4747 viewport_size: PhysicalSize::new(0.0, 0.0), };
4749
4750 let h = table_ctx
4751 .border_spacing
4752 .horizontal
4753 .resolve_with_context(&spacing_context, PropertyContext::Other);
4754
4755 let v = table_ctx
4756 .border_spacing
4757 .vertical
4758 .resolve_with_context(&spacing_context, PropertyContext::Other);
4759
4760 (h, v)
4761 } else {
4762 (0.0, 0.0)
4763 };
4764
4765 debug_log!(
4766 ctx,
4767 "Border spacing: h={:.2}, v={:.2}",
4768 h_spacing,
4769 v_spacing
4770 );
4771
4772 let mut col_positions = vec![0.0; table_ctx.columns.len()];
4774 let mut x_offset = h_spacing; for (i, col) in table_ctx.columns.iter().enumerate() {
4776 col_positions[i] = x_offset;
4777 if let Some(width) = col.computed_width {
4778 x_offset += width + h_spacing; }
4780 }
4781
4782 let mut row_positions = vec![0.0; table_ctx.num_rows];
4784 let mut y_offset = v_spacing; for (i, &height) in table_ctx.row_heights.iter().enumerate() {
4786 row_positions[i] = y_offset;
4787 y_offset += height + v_spacing; }
4789
4790 for cell_info in &table_ctx.cells {
4792 let cell_node = tree
4793 .get_mut(cell_info.node_index)
4794 .ok_or(LayoutError::InvalidTree)?;
4795
4796 let x = col_positions.get(cell_info.column).copied().unwrap_or(0.0);
4798 let y = row_positions.get(cell_info.row).copied().unwrap_or(0.0);
4799
4800 let mut width = 0.0;
4802 debug_info!(
4803 ctx,
4804 "[position_table_cells] Cell {}: calculating width from cols {}..{}",
4805 cell_info.node_index,
4806 cell_info.column,
4807 cell_info.column + cell_info.colspan
4808 );
4809 for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
4810 if let Some(col) = table_ctx.columns.get(col_idx) {
4811 debug_info!(
4812 ctx,
4813 "[position_table_cells] Col {}: computed_width={:?}",
4814 col_idx,
4815 col.computed_width
4816 );
4817 if let Some(col_width) = col.computed_width {
4818 width += col_width;
4819 if col_idx < cell_info.column + cell_info.colspan - 1 {
4821 width += h_spacing;
4822 }
4823 } else {
4824 debug_info!(
4825 ctx,
4826 "[position_table_cells] WARN: Col {} has NO computed_width!",
4827 col_idx
4828 );
4829 }
4830 } else {
4831 debug_info!(
4832 ctx,
4833 "[position_table_cells] WARN: Col {} not found in table_ctx.columns!",
4834 col_idx
4835 );
4836 }
4837 }
4838
4839 let mut height = 0.0;
4840 let end_row = cell_info.row + cell_info.rowspan;
4841 for row_idx in cell_info.row..end_row {
4842 if let Some(&row_height) = table_ctx.row_heights.get(row_idx) {
4843 height += row_height;
4844 if row_idx < end_row - 1 {
4846 height += v_spacing;
4847 }
4848 }
4849 }
4850
4851 let writing_mode = constraints.writing_mode;
4853 debug_info!(
4856 ctx,
4857 "[position_table_cells] Cell {}: BEFORE from_main_cross: width={}, height={}, \
4858 writing_mode={:?}",
4859 cell_info.node_index,
4860 width,
4861 height,
4862 writing_mode
4863 );
4864
4865 cell_node.used_size = Some(LogicalSize::from_main_cross(height, width, writing_mode));
4866
4867 debug_info!(
4868 ctx,
4869 "[position_table_cells] Cell {}: AFTER from_main_cross: used_size={:?}",
4870 cell_info.node_index,
4871 cell_node.used_size
4872 );
4873
4874 debug_info!(
4875 ctx,
4876 "[position_table_cells] Cell {}: setting used_size to {}x{} (row_heights={:?})",
4877 cell_info.node_index,
4878 width,
4879 height,
4880 table_ctx.row_heights
4881 );
4882
4883 if let Some(ref cached_layout) = cell_node.inline_layout_result {
4885 let inline_result = &cached_layout.layout;
4886 use StyleVerticalAlign;
4887
4888 let vertical_align = if let Some(dom_id) = cell_node.dom_node_id {
4890 let node_state = StyledNodeState::default();
4891
4892 match get_vertical_align_property(ctx.styled_dom, dom_id, &node_state) {
4893 MultiValue::Exact(v) => v,
4894 _ => StyleVerticalAlign::Top,
4895 }
4896 } else {
4897 StyleVerticalAlign::Top
4898 };
4899
4900 let content_bounds = inline_result.bounds();
4902 let content_height = content_bounds.height;
4903
4904 let padding = &cell_node.box_props.padding;
4907 let border = &cell_node.box_props.border;
4908 let content_box_height = height
4909 - padding.main_start(writing_mode)
4910 - padding.main_end(writing_mode)
4911 - border.main_start(writing_mode)
4912 - border.main_end(writing_mode);
4913
4914 let align_factor = match vertical_align {
4916 StyleVerticalAlign::Top => 0.0,
4917 StyleVerticalAlign::Middle => 0.5,
4918 StyleVerticalAlign::Bottom => 1.0,
4919 StyleVerticalAlign::Baseline
4921 | StyleVerticalAlign::Sub
4922 | StyleVerticalAlign::Superscript
4923 | StyleVerticalAlign::TextTop
4924 | StyleVerticalAlign::TextBottom => 0.5,
4925 };
4926 let y_offset = (content_box_height - content_height) * align_factor;
4927
4928 debug_info!(
4929 ctx,
4930 "[position_table_cells] Cell {}: vertical-align={:?}, border_box_height={}, \
4931 content_box_height={}, content_height={}, y_offset={}",
4932 cell_info.node_index,
4933 vertical_align,
4934 height,
4935 content_box_height,
4936 content_height,
4937 y_offset
4938 );
4939
4940 if y_offset.abs() > 0.01 {
4942 use std::sync::Arc;
4944
4945 use crate::text3::cache::{PositionedItem, UnifiedLayout};
4946
4947 let adjusted_items: Vec<PositionedItem> = inline_result
4948 .items
4949 .iter()
4950 .map(|item| PositionedItem {
4951 item: item.item.clone(),
4952 position: crate::text3::cache::Point {
4953 x: item.position.x,
4954 y: item.position.y + y_offset,
4955 },
4956 line_index: item.line_index,
4957 })
4958 .collect();
4959
4960 let adjusted_layout = UnifiedLayout {
4961 items: adjusted_items,
4962 overflow: inline_result.overflow.clone(),
4963 };
4964
4965 cell_node.inline_layout_result = Some(CachedInlineLayout::new(
4967 Arc::new(adjusted_layout),
4968 cached_layout.available_width,
4969 cached_layout.has_floats,
4970 ));
4971 }
4972 }
4973
4974 let position = LogicalPosition::from_main_cross(y, x, writing_mode);
4976
4977 positions.insert(cell_info.node_index, position);
4979
4980 debug_log!(
4981 ctx,
4982 "Cell at row={}, col={}: pos=({:.2}, {:.2}), size=({:.2}x{:.2})",
4983 cell_info.row,
4984 cell_info.column,
4985 x,
4986 y,
4987 width,
4988 height
4989 );
4990 }
4991
4992 Ok(positions)
4993}
4994
4995fn collect_and_measure_inline_content<T: ParsedFontTrait>(
5005 ctx: &mut LayoutContext<'_, T>,
5006 text_cache: &mut TextLayoutCache,
5007 tree: &mut LayoutTree,
5008 ifc_root_index: usize,
5009 constraints: &LayoutConstraints,
5010) -> Result<(Vec<InlineContent>, HashMap<ContentIndex, usize>)> {
5011 use crate::solver3::layout_tree::{IfcId, IfcMembership};
5012 use crate::text3::cache::InlineContent;
5013
5014 let result = collect_and_measure_inline_content_impl(ctx, text_cache, tree, ifc_root_index, constraints)?;
5015 Ok(result)
5016}
5017
5018fn collect_and_measure_inline_content_impl<T: ParsedFontTrait>(
5019 ctx: &mut LayoutContext<'_, T>,
5020 text_cache: &mut TextLayoutCache,
5021 tree: &mut LayoutTree,
5022 ifc_root_index: usize,
5023 constraints: &LayoutConstraints,
5024) -> Result<(Vec<InlineContent>, HashMap<ContentIndex, usize>)> {
5025 use crate::solver3::layout_tree::{IfcId, IfcMembership};
5026
5027 debug_ifc_layout!(
5028 ctx,
5029 "collect_and_measure_inline_content: node_index={}",
5030 ifc_root_index
5031 );
5032
5033 let ifc_id = IfcId::unique();
5035
5036 if let Some(ifc_root_node) = tree.get_mut(ifc_root_index) {
5038 ifc_root_node.ifc_id = Some(ifc_id);
5039 }
5040
5041 let mut content = Vec::new();
5042 let mut child_map = HashMap::new();
5044 let mut current_run_index: u32 = 0;
5046
5047 let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
5048
5049 let is_anonymous = ifc_root_node.dom_node_id.is_none();
5051
5052 let ifc_root_dom_id = match ifc_root_node.dom_node_id {
5055 Some(id) => id,
5056 None => {
5057 let parent_dom_id = ifc_root_node
5059 .parent
5060 .and_then(|p| tree.get(p))
5061 .and_then(|n| n.dom_node_id);
5062
5063 if let Some(id) = parent_dom_id {
5064 id
5065 } else {
5066 match ifc_root_node
5068 .children
5069 .iter()
5070 .filter_map(|&child_idx| tree.get(child_idx))
5071 .filter_map(|n| n.dom_node_id)
5072 .next()
5073 {
5074 Some(id) => id,
5075 None => {
5076 debug_warning!(ctx, "IFC root and all ancestors/children have no DOM ID");
5077 return Ok((content, child_map));
5078 }
5079 }
5080 }
5081 }
5082 };
5083
5084 let children: Vec<_> = ifc_root_node.children.clone();
5086 drop(ifc_root_node);
5087
5088 debug_ifc_layout!(
5089 ctx,
5090 "Node {} has {} layout children, is_anonymous={}",
5091 ifc_root_index,
5092 children.len(),
5093 is_anonymous
5094 );
5095
5096 if is_anonymous {
5099 for (item_idx, &child_index) in children.iter().enumerate() {
5101 let content_index = ContentIndex {
5102 run_index: ifc_root_index as u32,
5103 item_index: item_idx as u32,
5104 };
5105
5106 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5107 let Some(dom_id) = child_node.dom_node_id else {
5108 debug_warning!(
5109 ctx,
5110 "Anonymous IFC child at index {} has no DOM ID",
5111 child_index
5112 );
5113 continue;
5114 };
5115
5116 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
5117
5118 if let NodeType::Text(ref text_content) = node_data.get_node_type() {
5120 debug_info!(
5121 ctx,
5122 "[collect_and_measure_inline_content] OK: Found text node (DOM {:?}) in anonymous wrapper: '{}'",
5123 dom_id,
5124 text_content.as_str()
5125 );
5126 let style = Arc::new(get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref()));
5130 let text_items = split_text_for_whitespace(
5131 ctx.styled_dom,
5132 dom_id,
5133 text_content.as_str(),
5134 style,
5135 );
5136 content.extend(text_items);
5137 child_map.insert(content_index, child_index);
5138
5139 drop(child_node);
5141 if let Some(child_node_mut) = tree.get_mut(child_index) {
5142 child_node_mut.ifc_membership = Some(IfcMembership {
5143 ifc_id,
5144 ifc_root_layout_index: ifc_root_index,
5145 run_index: current_run_index,
5146 });
5147 }
5148 current_run_index += 1;
5149
5150 continue;
5151 }
5152
5153 let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
5155
5156 if display != LayoutDisplay::Inline {
5157 let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
5162 let box_props = child_node.box_props.clone();
5163
5164 let styled_node_state = ctx
5165 .styled_dom
5166 .styled_nodes
5167 .as_container()
5168 .get(dom_id)
5169 .map(|n| n.styled_node_state.clone())
5170 .unwrap_or_default();
5171
5172 let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
5174 ctx.styled_dom,
5175 Some(dom_id),
5176 constraints.containing_block_size,
5177 intrinsic_size,
5178 &box_props,
5179 ctx.viewport_size,
5180 )?;
5181
5182 let writing_mode = get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state)
5183 .unwrap_or_default();
5184
5185 let content_box_size = box_props.inner_size(tentative_size, writing_mode);
5187
5188 let child_constraints = LayoutConstraints {
5190 available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
5191 writing_mode,
5192 bfc_state: None,
5193 text_align: TextAlign::Start,
5194 containing_block_size: constraints.containing_block_size,
5195 available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
5196 };
5197
5198 drop(child_node);
5200
5201 let mut empty_float_cache = std::collections::BTreeMap::new();
5203 let layout_result = layout_formatting_context(
5204 ctx,
5205 tree,
5206 text_cache,
5207 child_index,
5208 &child_constraints,
5209 &mut empty_float_cache,
5210 )?;
5211
5212 let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
5213
5214 let final_height = match css_height.unwrap_or_default() {
5216 LayoutHeight::Auto => {
5217 let content_height = layout_result.output.overflow_size.height;
5218 content_height
5219 + box_props.padding.main_sum(writing_mode)
5220 + box_props.border.main_sum(writing_mode)
5221 }
5222 _ => tentative_size.height,
5223 };
5224
5225 let final_size = LogicalSize::new(tentative_size.width, final_height);
5226
5227 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5229
5230 let baseline_offset = layout_result.output.baseline.unwrap_or(final_height);
5231
5232 let margin = &box_props.margin;
5235 let margin_box_width = final_size.width + margin.left + margin.right;
5236 let margin_box_height = final_size.height + margin.top + margin.bottom;
5237
5238 let shape_content_index = ContentIndex {
5241 run_index: content.len() as u32,
5242 item_index: 0,
5243 };
5244 content.push(InlineContent::Shape(InlineShape {
5245 shape_def: ShapeDefinition::Rectangle {
5246 size: crate::text3::cache::Size {
5247 width: margin_box_width,
5249 height: margin_box_height,
5250 },
5251 corner_radius: None,
5252 },
5253 fill: None,
5254 stroke: None,
5255 baseline_offset: baseline_offset + margin.top,
5257 alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, dom_id),
5258 source_node_id: Some(dom_id),
5259 }));
5260 child_map.insert(shape_content_index, child_index);
5261 } else {
5262 let span_style = get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref());
5264 collect_inline_span_recursive(
5265 ctx,
5266 tree,
5267 dom_id,
5268 span_style,
5269 &mut content,
5270 &mut child_map,
5271 &children,
5272 constraints,
5273 )?;
5274 }
5275 }
5276
5277 return Ok((content, child_map));
5278 }
5279
5280 let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
5286 let mut list_item_dom_id: Option<NodeId> = None;
5287
5288 if let Some(dom_id) = ifc_root_node.dom_node_id {
5290 use crate::solver3::getters::get_display_property;
5291 if let MultiValue::Exact(display) = get_display_property(ctx.styled_dom, Some(dom_id)) {
5292 use LayoutDisplay;
5293 if display == LayoutDisplay::ListItem {
5294 debug_ifc_layout!(ctx, "IFC root NodeId({:?}) is list-item", dom_id);
5295 list_item_dom_id = Some(dom_id);
5296 }
5297 }
5298 }
5299
5300 if list_item_dom_id.is_none() {
5302 if let Some(parent_idx) = ifc_root_node.parent {
5303 if let Some(parent_node) = tree.get(parent_idx) {
5304 if let Some(parent_dom_id) = parent_node.dom_node_id {
5305 use crate::solver3::getters::get_display_property;
5306 if let MultiValue::Exact(display) = get_display_property(ctx.styled_dom, Some(parent_dom_id)) {
5307 use LayoutDisplay;
5308 if display == LayoutDisplay::ListItem {
5309 debug_ifc_layout!(
5310 ctx,
5311 "IFC root parent NodeId({:?}) is list-item",
5312 parent_dom_id
5313 );
5314 list_item_dom_id = Some(parent_dom_id);
5315 }
5316 }
5317 }
5318 }
5319 }
5320 }
5321
5322 if let Some(list_dom_id) = list_item_dom_id {
5324 debug_ifc_layout!(
5325 ctx,
5326 "Found list-item (NodeId({:?})), generating marker",
5327 list_dom_id
5328 );
5329
5330 let list_item_layout_idx = tree
5332 .nodes
5333 .iter()
5334 .enumerate()
5335 .find(|(_, node)| {
5336 node.dom_node_id == Some(list_dom_id) && node.pseudo_element.is_none()
5337 })
5338 .map(|(idx, _)| idx);
5339
5340 if let Some(list_idx) = list_item_layout_idx {
5341 let list_item_node = tree.get(list_idx).ok_or(LayoutError::InvalidTree)?;
5344 let marker_idx = list_item_node
5345 .children
5346 .iter()
5347 .find(|&&child_idx| {
5348 tree.get(child_idx)
5349 .map(|child| child.pseudo_element == Some(PseudoElement::Marker))
5350 .unwrap_or(false)
5351 })
5352 .copied();
5353
5354 if let Some(marker_idx) = marker_idx {
5355 debug_ifc_layout!(ctx, "Found ::marker pseudo-element at index {}", marker_idx);
5356
5357 let list_dom_id_for_style = tree
5360 .get(marker_idx)
5361 .and_then(|n| n.dom_node_id)
5362 .unwrap_or(list_dom_id);
5363
5364 let list_style_position =
5368 get_list_style_position(ctx.styled_dom, Some(list_dom_id));
5369 let position_outside =
5370 matches!(list_style_position, StyleListStylePosition::Outside);
5371
5372 debug_ifc_layout!(
5373 ctx,
5374 "List marker list-style-position: {:?} (outside={})",
5375 list_style_position,
5376 position_outside
5377 );
5378
5379 let base_style =
5381 Arc::new(get_style_properties(ctx.styled_dom, list_dom_id_for_style, ctx.system_style.as_ref()));
5382 let marker_segments = generate_list_marker_segments(
5383 tree,
5384 ctx.styled_dom,
5385 marker_idx, ctx.counters,
5387 base_style,
5388 ctx.debug_messages,
5389 );
5390
5391 debug_ifc_layout!(
5392 ctx,
5393 "Generated {} list marker segments",
5394 marker_segments.len()
5395 );
5396
5397 for segment in marker_segments {
5400 content.push(InlineContent::Marker {
5401 run: segment,
5402 position_outside,
5403 });
5404 }
5405 } else {
5406 debug_ifc_layout!(
5407 ctx,
5408 "WARNING: List-item at index {} has no ::marker pseudo-element",
5409 list_idx
5410 );
5411 }
5412 }
5413 }
5414
5415 drop(ifc_root_node);
5416
5417 let node_hier_item = &ctx.styled_dom.node_hierarchy.as_container()[ifc_root_dom_id];
5425 debug_info!(
5426 ctx,
5427 "[collect_and_measure_inline_content] DEBUG: node_hier_item.first_child={:?}, \
5428 last_child={:?}",
5429 node_hier_item.first_child_id(ifc_root_dom_id),
5430 node_hier_item.last_child_id()
5431 );
5432
5433 let dom_children: Vec<NodeId> = ifc_root_dom_id
5434 .az_children(&ctx.styled_dom.node_hierarchy.as_container())
5435 .collect();
5436
5437 let ifc_root_node_data = &ctx.styled_dom.node_data.as_container()[ifc_root_dom_id];
5438
5439 if let NodeType::Text(ref text_content) = ifc_root_node_data.get_node_type() {
5443 let style = Arc::new(get_style_properties(ctx.styled_dom, ifc_root_dom_id, ctx.system_style.as_ref()));
5444 let text_items = split_text_for_whitespace(
5445 ctx.styled_dom,
5446 ifc_root_dom_id,
5447 text_content.as_str(),
5448 style,
5449 );
5450 content.extend(text_items);
5451 return Ok((content, child_map));
5452 }
5453
5454 let ifc_root_node_type = match ifc_root_node_data.get_node_type() {
5455 NodeType::Div => "Div",
5456 NodeType::Text(_) => "Text",
5457 NodeType::Body => "Body",
5458 _ => "Other",
5459 };
5460
5461 debug_info!(
5462 ctx,
5463 "[collect_and_measure_inline_content] IFC root has {} DOM children",
5464 dom_children.len()
5465 );
5466
5467 for (item_idx, &dom_child_id) in dom_children.iter().enumerate() {
5468 let content_index = ContentIndex {
5469 run_index: ifc_root_index as u32,
5470 item_index: item_idx as u32,
5471 };
5472
5473 let node_data = &ctx.styled_dom.node_data.as_container()[dom_child_id];
5474
5475 if let NodeType::Text(ref text_content) = node_data.get_node_type() {
5477 debug_info!(
5478 ctx,
5479 "[collect_and_measure_inline_content] OK: Found text node (DOM child {:?}): '{}'",
5480 dom_child_id,
5481 text_content.as_str()
5482 );
5483
5484 let style = Arc::new(get_style_properties(ctx.styled_dom, dom_child_id, ctx.system_style.as_ref()));
5488 let text_items = split_text_for_whitespace(
5489 ctx.styled_dom,
5490 dom_child_id,
5491 text_content.as_str(),
5492 style,
5493 );
5494 content.extend(text_items);
5495
5496 if let Some(&layout_idx) = tree.dom_to_layout.get(&dom_child_id).and_then(|v| v.first()) {
5500 if let Some(text_layout_node) = tree.get_mut(layout_idx) {
5501 text_layout_node.ifc_membership = Some(IfcMembership {
5502 ifc_id,
5503 ifc_root_layout_index: ifc_root_index,
5504 run_index: current_run_index,
5505 });
5506 }
5507 }
5508 current_run_index += 1;
5509
5510 continue;
5511 }
5512
5513 let child_index = children
5515 .iter()
5516 .find(|&&idx| {
5517 tree.get(idx)
5518 .and_then(|n| n.dom_node_id)
5519 .map(|id| id == dom_child_id)
5520 .unwrap_or(false)
5521 })
5522 .copied();
5523
5524 let Some(child_index) = child_index else {
5525 debug_info!(
5526 ctx,
5527 "[collect_and_measure_inline_content] WARN: DOM child {:?} has no layout node",
5528 dom_child_id
5529 );
5530 continue;
5531 };
5532
5533 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5534 let dom_id = child_node.dom_node_id.unwrap();
5536
5537 let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
5538 if display != LayoutDisplay::Inline {
5539 let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
5544 let box_props = child_node.box_props.clone();
5545
5546 let styled_node_state = ctx
5547 .styled_dom
5548 .styled_nodes
5549 .as_container()
5550 .get(dom_id)
5551 .map(|n| n.styled_node_state.clone())
5552 .unwrap_or_default();
5553
5554 let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
5557 ctx.styled_dom,
5558 Some(dom_id),
5559 constraints.containing_block_size,
5560 intrinsic_size,
5561 &box_props,
5562 ctx.viewport_size,
5563 )?;
5564
5565 let writing_mode =
5566 get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
5567
5568 let content_box_size = box_props.inner_size(tentative_size, writing_mode);
5570
5571 debug_info!(
5572 ctx,
5573 "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
5574 tentative_border_box={:?}, content_box={:?}",
5575 dom_id,
5576 tentative_size,
5577 content_box_size
5578 );
5579
5580 let child_constraints = LayoutConstraints {
5582 available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
5583 writing_mode,
5584 bfc_state: None,
5586 text_align: TextAlign::Start,
5588 containing_block_size: constraints.containing_block_size,
5589 available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
5590 };
5591
5592 drop(child_node);
5594
5595 let mut empty_float_cache = std::collections::BTreeMap::new();
5598 let layout_result = layout_formatting_context(
5599 ctx,
5600 tree,
5601 text_cache,
5602 child_index,
5603 &child_constraints,
5604 &mut empty_float_cache,
5605 )?;
5606
5607 let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
5608
5609 let final_height = match css_height.clone().unwrap_or_default() {
5611 LayoutHeight::Auto => {
5612 let content_height = layout_result.output.overflow_size.height;
5614 content_height
5615 + box_props.padding.main_sum(writing_mode)
5616 + box_props.border.main_sum(writing_mode)
5617 }
5618 _ => tentative_size.height,
5620 };
5621
5622 debug_info!(
5623 ctx,
5624 "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
5625 layout_content_height={}, css_height={:?}, final_border_box_height={}",
5626 dom_id,
5627 layout_result.output.overflow_size.height,
5628 css_height,
5629 final_height
5630 );
5631
5632 let final_size = LogicalSize::new(tentative_size.width, final_height);
5633
5634 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5636
5637 let baseline_from_top = layout_result.output.baseline;
5650 let baseline_offset = match baseline_from_top {
5651 Some(baseline_y) => {
5652 let content_box_top = box_props.padding.top + box_props.border.top;
5655 let baseline_from_border_box_top = baseline_y + content_box_top;
5656 (final_height - baseline_from_border_box_top).max(0.0)
5658 }
5659 None => {
5660 0.0
5662 }
5663 };
5664
5665 debug_info!(
5666 ctx,
5667 "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
5668 baseline_from_top={:?}, final_height={}, baseline_offset_from_bottom={}",
5669 dom_id,
5670 baseline_from_top,
5671 final_height,
5672 baseline_offset
5673 );
5674
5675 let margin = &box_props.margin;
5679 let margin_box_width = final_size.width + margin.left + margin.right;
5680 let margin_box_height = final_size.height + margin.top + margin.bottom;
5681
5682 let shape_content_index = ContentIndex {
5685 run_index: content.len() as u32,
5686 item_index: 0,
5687 };
5688 content.push(InlineContent::Shape(InlineShape {
5689 shape_def: ShapeDefinition::Rectangle {
5690 size: crate::text3::cache::Size {
5691 width: margin_box_width,
5693 height: margin_box_height,
5694 },
5695 corner_radius: None,
5696 },
5697 fill: None,
5698 stroke: None,
5699 baseline_offset: baseline_offset + margin.top,
5701 alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, dom_id),
5702 source_node_id: Some(dom_id),
5703 }));
5704 child_map.insert(shape_content_index, child_index);
5705 } else if let NodeType::Image(image_ref) =
5706 ctx.styled_dom.node_data.as_container()[dom_id].get_node_type()
5707 {
5708 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5713 let box_props = child_node.box_props.clone();
5714
5715 let intrinsic_size = child_node
5717 .intrinsic_sizes
5718 .clone()
5719 .unwrap_or(IntrinsicSizes {
5720 max_content_width: 50.0,
5721 max_content_height: 50.0,
5722 ..Default::default()
5723 });
5724
5725 let styled_node_state = ctx
5727 .styled_dom
5728 .styled_nodes
5729 .as_container()
5730 .get(dom_id)
5731 .map(|n| n.styled_node_state.clone())
5732 .unwrap_or_default();
5733
5734 let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
5736 ctx.styled_dom,
5737 Some(dom_id),
5738 constraints.containing_block_size,
5739 intrinsic_size.clone(),
5740 &box_props,
5741 ctx.viewport_size,
5742 )?;
5743
5744 drop(child_node);
5746
5747 let final_size = LogicalSize::new(tentative_size.width, tentative_size.height);
5749 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5750
5751 let display_width = if final_size.width > 0.0 {
5753 Some(final_size.width)
5754 } else {
5755 None
5756 };
5757 let display_height = if final_size.height > 0.0 {
5758 Some(final_size.height)
5759 } else {
5760 None
5761 };
5762
5763 content.push(InlineContent::Image(InlineImage {
5764 source: ImageSource::Ref(image_ref.clone()),
5765 intrinsic_size: crate::text3::cache::Size {
5766 width: intrinsic_size.max_content_width,
5767 height: intrinsic_size.max_content_height,
5768 },
5769 display_size: if display_width.is_some() || display_height.is_some() {
5770 Some(crate::text3::cache::Size {
5771 width: display_width.unwrap_or(intrinsic_size.max_content_width),
5772 height: display_height.unwrap_or(intrinsic_size.max_content_height),
5773 })
5774 } else {
5775 None
5776 },
5777 baseline_offset: 0.0,
5779 alignment: crate::text3::cache::VerticalAlign::Baseline,
5780 object_fit: ObjectFit::Fill,
5781 }));
5782 let image_content_index = ContentIndex {
5785 run_index: (content.len() - 1) as u32, item_index: 0,
5787 };
5788 child_map.insert(image_content_index, child_index);
5789 } else {
5790 debug_info!(
5795 ctx,
5796 "[collect_and_measure_inline_content] Found inline span (DOM {:?}), recursing",
5797 dom_id
5798 );
5799
5800 let span_style = get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref());
5801 collect_inline_span_recursive(
5802 ctx,
5803 tree,
5804 dom_id,
5805 span_style,
5806 &mut content,
5807 &mut child_map,
5808 &children,
5809 constraints,
5810 )?;
5811 }
5812 }
5813 Ok((content, child_map))
5814}
5815
5816fn collect_inline_span_recursive<T: ParsedFontTrait>(
5829 ctx: &mut LayoutContext<'_, T>,
5830 tree: &mut LayoutTree,
5831 span_dom_id: NodeId,
5832 span_style: StyleProperties,
5833 content: &mut Vec<InlineContent>,
5834 child_map: &mut HashMap<ContentIndex, usize>,
5835 parent_children: &[usize], constraints: &LayoutConstraints,
5837) -> Result<()> {
5838 debug_info!(
5839 ctx,
5840 "[collect_inline_span_recursive] Processing inline span {:?}",
5841 span_dom_id
5842 );
5843
5844 let span_dom_children: Vec<NodeId> = span_dom_id
5846 .az_children(&ctx.styled_dom.node_hierarchy.as_container())
5847 .collect();
5848
5849 debug_info!(
5850 ctx,
5851 "[collect_inline_span_recursive] Span has {} DOM children",
5852 span_dom_children.len()
5853 );
5854
5855 for &child_dom_id in &span_dom_children {
5856 let node_data = &ctx.styled_dom.node_data.as_container()[child_dom_id];
5857
5858 if let NodeType::Text(ref text_content) = node_data.get_node_type() {
5861 debug_info!(
5862 ctx,
5863 "[collect_inline_span_recursive] ✓ Found text in span: '{}'",
5864 text_content.as_str()
5865 );
5866 let text_items = split_text_for_whitespace(
5868 ctx.styled_dom,
5869 child_dom_id,
5870 text_content.as_str(),
5871 Arc::new(span_style.clone()),
5872 );
5873 content.extend(text_items);
5874 continue;
5875 }
5876
5877 let child_display =
5879 get_display_property(ctx.styled_dom, Some(child_dom_id)).unwrap_or_default();
5880
5881 let child_index = parent_children
5883 .iter()
5884 .find(|&&idx| {
5885 tree.get(idx)
5886 .and_then(|n| n.dom_node_id)
5887 .map(|id| id == child_dom_id)
5888 .unwrap_or(false)
5889 })
5890 .copied();
5891
5892 match child_display {
5893 LayoutDisplay::Inline => {
5894 debug_info!(
5896 ctx,
5897 "[collect_inline_span_recursive] Found nested inline span {:?}",
5898 child_dom_id
5899 );
5900 let child_style = get_style_properties(ctx.styled_dom, child_dom_id, ctx.system_style.as_ref());
5901 collect_inline_span_recursive(
5902 ctx,
5903 tree,
5904 child_dom_id,
5905 child_style,
5906 content,
5907 child_map,
5908 parent_children,
5909 constraints,
5910 )?;
5911 }
5912 LayoutDisplay::InlineBlock => {
5913 let Some(child_index) = child_index else {
5915 debug_info!(
5916 ctx,
5917 "[collect_inline_span_recursive] WARNING: inline-block {:?} has no layout \
5918 node",
5919 child_dom_id
5920 );
5921 continue;
5922 };
5923
5924 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5925 let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
5926 let width = intrinsic_size.max_content_width;
5927
5928 let styled_node_state = ctx
5929 .styled_dom
5930 .styled_nodes
5931 .as_container()
5932 .get(child_dom_id)
5933 .map(|n| n.styled_node_state.clone())
5934 .unwrap_or_default();
5935 let writing_mode =
5936 get_writing_mode(ctx.styled_dom, child_dom_id, &styled_node_state)
5937 .unwrap_or_default();
5938 let child_constraints = LayoutConstraints {
5939 available_size: LogicalSize::new(width, f32::INFINITY),
5940 writing_mode,
5941 bfc_state: None,
5942 text_align: TextAlign::Start,
5943 containing_block_size: constraints.containing_block_size,
5944 available_width_type: Text3AvailableSpace::Definite(width),
5945 };
5946
5947 drop(child_node);
5948
5949 let mut empty_float_cache = std::collections::BTreeMap::new();
5950 let layout_result = layout_formatting_context(
5951 ctx,
5952 tree,
5953 &mut TextLayoutCache::default(),
5954 child_index,
5955 &child_constraints,
5956 &mut empty_float_cache,
5957 )?;
5958 let final_height = layout_result.output.overflow_size.height;
5959 let final_size = LogicalSize::new(width, final_height);
5960
5961 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5962 let baseline_offset = layout_result.output.baseline.unwrap_or(final_height);
5963
5964 content.push(InlineContent::Shape(InlineShape {
5965 shape_def: ShapeDefinition::Rectangle {
5966 size: crate::text3::cache::Size {
5967 width,
5968 height: final_height,
5969 },
5970 corner_radius: None,
5971 },
5972 fill: None,
5973 stroke: None,
5974 baseline_offset,
5975 alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
5976 source_node_id: Some(child_dom_id),
5977 }));
5978
5979 debug_info!(
5981 ctx,
5982 "[collect_inline_span_recursive] Added inline-block shape {}x{}",
5983 width,
5984 final_height
5985 );
5986 }
5987 _ => {
5988 debug_info!(
5990 ctx,
5991 "[collect_inline_span_recursive] WARNING: Unsupported display type {:?} \
5992 inside inline span",
5993 child_display
5994 );
5995 }
5996 }
5997 }
5998
5999 Ok(())
6000}
6001
6002fn position_floated_child(
6005 _child_index: usize,
6006 child_margin_box_size: LogicalSize,
6007 float_type: LayoutFloat,
6008 constraints: &LayoutConstraints,
6009 _bfc_content_box: LogicalRect,
6010 current_main_offset: f32,
6011 floating_context: &mut FloatingContext,
6012) -> Result<LogicalPosition> {
6013 let wm = constraints.writing_mode;
6014 let child_main_size = child_margin_box_size.main(wm);
6015 let child_cross_size = child_margin_box_size.cross(wm);
6016 let bfc_cross_size = constraints.available_size.cross(wm);
6017 let mut placement_main_offset = current_main_offset;
6018
6019 loop {
6020 let (available_cross_start, available_cross_end) = floating_context
6023 .available_line_box_space(
6024 placement_main_offset,
6025 placement_main_offset + child_main_size,
6026 bfc_cross_size,
6027 wm,
6028 );
6029
6030 let available_cross_width = available_cross_end - available_cross_start;
6031
6032 if child_cross_size <= available_cross_width {
6034 let final_cross_pos = match float_type {
6036 LayoutFloat::Left => available_cross_start,
6037 LayoutFloat::Right => available_cross_end - child_cross_size,
6038 LayoutFloat::None => unreachable!(),
6039 };
6040 let final_pos =
6041 LogicalPosition::from_main_cross(placement_main_offset, final_cross_pos, wm);
6042
6043 let new_float_box = FloatBox {
6044 kind: float_type,
6045 rect: LogicalRect::new(final_pos, child_margin_box_size),
6046 margin: EdgeSizes::default(), };
6048 floating_context.floats.push(new_float_box);
6049 return Ok(final_pos);
6050 } else {
6051 let mut next_main_offset = f32::INFINITY;
6055 for existing_float in &floating_context.floats {
6056 let float_main_start = existing_float.rect.origin.main(wm);
6057 let float_main_end = float_main_start + existing_float.rect.size.main(wm);
6058
6059 if placement_main_offset < float_main_end {
6061 next_main_offset = next_main_offset.min(float_main_end);
6062 }
6063 }
6064
6065 if next_main_offset.is_infinite() {
6066 return Err(LayoutError::PositioningFailed);
6069 }
6070 placement_main_offset = next_main_offset;
6071 }
6072 }
6073}
6074
6075fn get_float_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutFloat {
6079 let Some(id) = dom_id else {
6080 return LayoutFloat::None;
6081 };
6082 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
6083 get_float(styled_dom, id, node_state).unwrap_or(LayoutFloat::None)
6084}
6085
6086fn get_clear_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutClear {
6087 let Some(id) = dom_id else {
6088 return LayoutClear::None;
6089 };
6090 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
6091 get_clear(styled_dom, id, node_state).unwrap_or(LayoutClear::None)
6092}
6093pub fn check_scrollbar_necessity(
6098 content_size: LogicalSize,
6099 container_size: LogicalSize,
6100 overflow_x: OverflowBehavior,
6101 overflow_y: OverflowBehavior,
6102 scrollbar_width_px: f32,
6103) -> ScrollbarRequirements {
6104 const EPSILON: f32 = 1.0;
6108
6109 let mut needs_horizontal = match overflow_x {
6114 OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
6115 OverflowBehavior::Scroll => true,
6116 OverflowBehavior::Auto => content_size.width > container_size.width + EPSILON,
6117 };
6118
6119 let mut needs_vertical = match overflow_y {
6120 OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
6121 OverflowBehavior::Scroll => true,
6122 OverflowBehavior::Auto => content_size.height > container_size.height + EPSILON,
6123 };
6124
6125 if scrollbar_width_px > 0.0 {
6130 if needs_vertical && !needs_horizontal && overflow_x == OverflowBehavior::Auto {
6131 if content_size.width > (container_size.width - scrollbar_width_px) + EPSILON {
6132 needs_horizontal = true;
6133 }
6134 }
6135 if needs_horizontal && !needs_vertical && overflow_y == OverflowBehavior::Auto {
6136 if content_size.height > (container_size.height - scrollbar_width_px) + EPSILON {
6137 needs_vertical = true;
6138 }
6139 }
6140 }
6141
6142 ScrollbarRequirements {
6143 needs_horizontal,
6144 needs_vertical,
6145 scrollbar_width: if needs_vertical {
6146 scrollbar_width_px
6147 } else {
6148 0.0
6149 },
6150 scrollbar_height: if needs_horizontal {
6151 scrollbar_width_px
6152 } else {
6153 0.0
6154 },
6155 }
6156}
6157
6158pub(crate) fn collapse_margins(a: f32, b: f32) -> f32 {
6165 if a.is_sign_positive() && b.is_sign_positive() {
6166 a.max(b)
6167 } else if a.is_sign_negative() && b.is_sign_negative() {
6168 a.min(b)
6169 } else {
6170 a + b
6171 }
6172}
6173
6174fn advance_pen_with_margin_collapse(
6194 pen: &mut f32,
6195 last_margin_bottom: f32,
6196 current_margin_top: f32,
6197) -> f32 {
6198 let collapsed_margin = collapse_margins(last_margin_bottom, current_margin_top);
6200
6201 *pen += collapsed_margin;
6203
6204 collapsed_margin
6206}
6207
6208fn has_margin_collapse_blocker(
6225 box_props: &crate::solver3::geometry::BoxProps,
6226 writing_mode: LayoutWritingMode,
6227 check_start: bool, ) -> bool {
6229 if check_start {
6230 let border_start = box_props.border.main_start(writing_mode);
6232 let padding_start = box_props.padding.main_start(writing_mode);
6233 border_start > 0.0 || padding_start > 0.0
6234 } else {
6235 let border_end = box_props.border.main_end(writing_mode);
6237 let padding_end = box_props.padding.main_end(writing_mode);
6238 border_end > 0.0 || padding_end > 0.0
6239 }
6240}
6241
6242fn is_empty_block(node: &LayoutNode) -> bool {
6257 if !node.children.is_empty() {
6265 return false;
6266 }
6267
6268 if node.inline_layout_result.is_some() {
6270 return false;
6271 }
6272
6273 if let Some(size) = node.used_size {
6276 if size.height > 0.0 {
6277 return false;
6278 }
6279 }
6280
6281 true
6283}
6284
6285fn generate_list_marker_text(
6294 tree: &LayoutTree,
6295 styled_dom: &StyledDom,
6296 marker_index: usize,
6297 counters: &BTreeMap<(usize, String), i32>,
6298 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6299) -> String {
6300 use crate::solver3::counters::format_counter;
6301
6302 let marker_node = match tree.get(marker_index) {
6304 Some(n) => n,
6305 None => return String::new(),
6306 };
6307
6308 if marker_node.pseudo_element != Some(PseudoElement::Marker) {
6311 if let Some(msgs) = debug_messages {
6312 msgs.push(LayoutDebugMessage::warning(format!(
6313 "[generate_list_marker_text] WARNING: Node {} is not a ::marker pseudo-element \
6314 (pseudo={:?}, anonymous_type={:?})",
6315 marker_index, marker_node.pseudo_element, marker_node.anonymous_type
6316 )));
6317 }
6318 if marker_node.anonymous_type != Some(AnonymousBoxType::ListItemMarker) {
6320 return String::new();
6321 }
6322 }
6323
6324 let list_item_index = match marker_node.parent {
6326 Some(p) => p,
6327 None => {
6328 if let Some(msgs) = debug_messages {
6329 msgs.push(LayoutDebugMessage::error(
6330 "[generate_list_marker_text] ERROR: Marker has no parent".to_string(),
6331 ));
6332 }
6333 return String::new();
6334 }
6335 };
6336
6337 let list_item_node = match tree.get(list_item_index) {
6338 Some(n) => n,
6339 None => return String::new(),
6340 };
6341
6342 let list_item_dom_id = match list_item_node.dom_node_id {
6343 Some(id) => id,
6344 None => {
6345 if let Some(msgs) = debug_messages {
6346 msgs.push(LayoutDebugMessage::error(
6347 "[generate_list_marker_text] ERROR: List-item has no DOM ID".to_string(),
6348 ));
6349 }
6350 return String::new();
6351 }
6352 };
6353
6354 if let Some(msgs) = debug_messages {
6355 msgs.push(LayoutDebugMessage::info(format!(
6356 "[generate_list_marker_text] marker_index={}, list_item_index={}, \
6357 list_item_dom_id={:?}",
6358 marker_index, list_item_index, list_item_dom_id
6359 )));
6360 }
6361
6362 let list_container_dom_id = if let Some(grandparent_index) = list_item_node.parent {
6364 if let Some(grandparent) = tree.get(grandparent_index) {
6365 grandparent.dom_node_id
6366 } else {
6367 None
6368 }
6369 } else {
6370 None
6371 };
6372
6373 let list_style_type = if let Some(container_id) = list_container_dom_id {
6376 let container_type = get_list_style_type(styled_dom, Some(container_id));
6377 if container_type != StyleListStyleType::default() {
6378 container_type
6379 } else {
6380 get_list_style_type(styled_dom, Some(list_item_dom_id))
6381 }
6382 } else {
6383 get_list_style_type(styled_dom, Some(list_item_dom_id))
6384 };
6385
6386 let counter_value = counters
6390 .get(&(list_item_index, "list-item".to_string()))
6391 .copied()
6392 .unwrap_or_else(|| {
6393 if let Some(msgs) = debug_messages {
6394 msgs.push(LayoutDebugMessage::warning(format!(
6395 "[generate_list_marker_text] WARNING: No counter found for list-item at index \
6396 {}, defaulting to 1",
6397 list_item_index
6398 )));
6399 }
6400 1
6401 });
6402
6403 if let Some(msgs) = debug_messages {
6404 msgs.push(LayoutDebugMessage::info(format!(
6405 "[generate_list_marker_text] counter_value={} for list_item_index={}",
6406 counter_value, list_item_index
6407 )));
6408 }
6409
6410 let marker_text = format_counter(counter_value, list_style_type);
6412
6413 if matches!(
6416 list_style_type,
6417 StyleListStyleType::Decimal
6418 | StyleListStyleType::DecimalLeadingZero
6419 | StyleListStyleType::LowerAlpha
6420 | StyleListStyleType::UpperAlpha
6421 | StyleListStyleType::LowerRoman
6422 | StyleListStyleType::UpperRoman
6423 | StyleListStyleType::LowerGreek
6424 | StyleListStyleType::UpperGreek
6425 ) {
6426 format!("{}. ", marker_text)
6427 } else {
6428 format!("{} ", marker_text)
6429 }
6430}
6431
6432fn generate_list_marker_segments(
6438 tree: &LayoutTree,
6439 styled_dom: &StyledDom,
6440 marker_index: usize,
6441 counters: &BTreeMap<(usize, String), i32>,
6442 base_style: Arc<StyleProperties>,
6443 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6444) -> Vec<StyledRun> {
6445 let marker_text =
6447 generate_list_marker_text(tree, styled_dom, marker_index, counters, debug_messages);
6448 if marker_text.is_empty() {
6449 return Vec::new();
6450 }
6451
6452 if let Some(msgs) = debug_messages {
6453 let font_families: Vec<&str> = match &base_style.font_stack {
6454 crate::text3::cache::FontStack::Stack(selectors) => {
6455 selectors.iter().map(|f| f.family.as_str()).collect()
6456 }
6457 crate::text3::cache::FontStack::Ref(_) => vec!["<embedded-font>"],
6458 };
6459 msgs.push(LayoutDebugMessage::info(format!(
6460 "[generate_list_marker_segments] Marker text: '{}' with font stack: {:?}",
6461 marker_text,
6462 font_families
6463 )));
6464 }
6465
6466 vec![StyledRun {
6469 text: marker_text,
6470 style: base_style,
6471 logical_start_byte: 0,
6472 source_node_id: None,
6473 }]
6474}
6475
6476pub(crate) fn split_text_for_whitespace(
6489 styled_dom: &StyledDom,
6490 dom_id: NodeId,
6491 text: &str,
6492 style: Arc<StyleProperties>,
6493) -> Vec<InlineContent> {
6494 use crate::text3::cache::{BreakType, ClearType, InlineBreak};
6495
6496 let node_hierarchy = styled_dom.node_hierarchy.as_container();
6499 let parent_id = node_hierarchy[dom_id].parent_id();
6500
6501 let white_space = if let Some(parent) = parent_id {
6503 let styled_nodes = styled_dom.styled_nodes.as_container();
6504 let parent_state = styled_nodes
6505 .get(parent)
6506 .map(|n| n.styled_node_state.clone())
6507 .unwrap_or_default();
6508
6509 match get_white_space_property(styled_dom, parent, &parent_state) {
6510 MultiValue::Exact(ws) => ws,
6511 _ => StyleWhiteSpace::Normal,
6512 }
6513 } else {
6514 StyleWhiteSpace::Normal
6515 };
6516
6517 let mut result = Vec::new();
6518
6519 match white_space {
6522 StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => {
6523 let mut lines = text.split('\n').peekable();
6527 let mut content_index = 0;
6528
6529 while let Some(line) = lines.next() {
6530 let mut tab_parts = line.split('\t').peekable();
6532 while let Some(part) = tab_parts.next() {
6533 if !part.is_empty() {
6535 result.push(InlineContent::Text(StyledRun {
6536 text: part.to_string(),
6537 style: Arc::clone(&style),
6538 logical_start_byte: 0,
6539 source_node_id: Some(dom_id),
6540 }));
6541 }
6542
6543 if tab_parts.peek().is_some() {
6545 result.push(InlineContent::Tab { style: Arc::clone(&style) });
6546 }
6547 }
6548
6549 if lines.peek().is_some() {
6551 result.push(InlineContent::LineBreak(InlineBreak {
6552 break_type: BreakType::Hard,
6553 clear: ClearType::None,
6554 content_index,
6555 }));
6556 content_index += 1;
6557 }
6558 }
6559 }
6560 StyleWhiteSpace::PreLine => {
6561 let mut lines = text.split('\n').peekable();
6563 let mut content_index = 0;
6564
6565 while let Some(line) = lines.next() {
6566 let collapsed: String = line.split_whitespace().collect::<Vec<_>>().join(" ");
6568
6569 if !collapsed.is_empty() {
6570 result.push(InlineContent::Text(StyledRun {
6571 text: collapsed,
6572 style: Arc::clone(&style),
6573 logical_start_byte: 0,
6574 source_node_id: Some(dom_id),
6575 }));
6576 }
6577
6578 if lines.peek().is_some() {
6580 result.push(InlineContent::LineBreak(InlineBreak {
6581 break_type: BreakType::Hard,
6582 clear: ClearType::None,
6583 content_index,
6584 }));
6585 content_index += 1;
6586 }
6587 }
6588 }
6589 StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap => {
6590 let collapsed: String = text
6604 .chars()
6605 .map(|c| if c.is_whitespace() { ' ' } else { c })
6606 .collect::<String>()
6607 .split(' ')
6608 .filter(|s| !s.is_empty())
6609 .collect::<Vec<_>>()
6610 .join(" ");
6611
6612 let final_text = if collapsed.is_empty() && !text.is_empty() {
6616 " ".to_string()
6619 } else if !collapsed.is_empty() {
6620 let had_leading = text.chars().next().map(|c| c.is_whitespace()).unwrap_or(false);
6622 let had_trailing = text.chars().last().map(|c| c.is_whitespace()).unwrap_or(false);
6623
6624 let mut result = String::new();
6625 if had_leading { result.push(' '); }
6626 result.push_str(&collapsed);
6627 if had_trailing && !had_leading { result.push(' '); }
6628 else if had_trailing && had_leading && collapsed.is_empty() { }
6629 else if had_trailing { result.push(' '); }
6630 result
6631 } else {
6632 collapsed
6633 };
6634
6635 if !final_text.is_empty() {
6636 result.push(InlineContent::Text(StyledRun {
6637 text: final_text,
6638 style,
6639 logical_start_byte: 0,
6640 source_node_id: Some(dom_id),
6641 }));
6642 }
6643 }
6644 }
6645
6646 result
6647}