1use std::{
4 collections::{BTreeMap, HashMap},
5 sync::Arc,
6};
7
8use azul_core::{
9 dom::{FormattingContext, NodeId, NodeType},
10 geom::{LogicalPosition, LogicalRect, LogicalSize},
11 resources::RendererResources,
12 styled_dom::{StyledDom, StyledNodeState},
13};
14use azul_css::{
15 css::CssPropertyValue,
16 props::{
17 basic::{
18 font::{StyleFontStyle, StyleFontWeight},
19 pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
20 ColorU, PhysicalSize, PropertyContext, ResolutionContext, SizeMetric,
21 },
22 layout::{
23 ColumnCount, ColumnWidth, LayoutBorderSpacing, LayoutClear, LayoutDisplay, LayoutFloat,
24 LayoutHeight, LayoutJustifyContent, LayoutOverflow, LayoutPosition, LayoutTableLayout,
25 LayoutTextJustify, LayoutWidth, LayoutWritingMode, ShapeInside, ShapeOutside,
26 StyleBorderCollapse, StyleCaptionSide, StyleEmptyCells,
27 },
28 property::CssProperty,
29 style::{
30 BorderStyle, StyleDirection, StyleHyphens, StyleLineBreak, StyleListStylePosition,
31 StyleListStyleType, StyleOverflowWrap, StyleTextAlign, StyleTextAlignLast,
32 StyleTextBoxTrim, StyleTextCombineUpright, StyleTextOrientation, StyleUnicodeBidi,
33 StyleVerticalAlign, StyleVisibility, StyleWhiteSpace, StyleWordBreak,
34 },
35 },
36};
37use rust_fontconfig::FcWeight;
38use taffy::{AvailableSpace, LayoutInput, Line, Size as TaffySize};
39
40#[cfg(feature = "text_layout")]
41use crate::text3;
42use crate::{
43 debug_ifc_layout, debug_info, debug_log, debug_table_layout, debug_warning,
44 font_traits::{
45 ContentIndex, FontLoaderTrait, ImageSource, InlineContent, InlineImage, InlineShape,
46 LayoutFragment, ObjectFit, ParsedFontTrait, SegmentAlignment, ShapeBoundary,
47 ShapeDefinition, ShapedItem, Size, StyleProperties, StyledRun, TextLayoutCache,
48 UnifiedConstraints,
49 },
50 solver3::{
51 geometry::{BoxProps, EdgeSizes, IntrinsicSizes},
52 getters::{
53 get_css_border_bottom_width, get_css_border_top_width, get_css_box_sizing,
54 get_css_height, get_css_padding_bottom, get_css_padding_top,
55 get_css_width, get_direction_property, get_unicode_bidi_property,
56 get_display_property, get_element_font_size, get_float, get_clear,
57 get_list_style_position, get_list_style_type, get_overflow_x, get_overflow_y,
58 get_parent_font_size, get_root_font_size, get_style_properties,
59 get_text_align, get_text_box_trim_property, get_text_orientation_property,
60 get_vertical_align_property, get_visibility, get_white_space_property,
61 get_writing_mode, MultiValue,
62 },
63 layout_tree::{
64 AnonymousBoxType, CachedInlineLayout, LayoutNode, LayoutNodeHot, LayoutNodeWarm, LayoutNodeCold, LayoutTree, PseudoElement,
65 },
66 positioning::get_position_type,
67 scrollbar::ScrollbarRequirements,
68 sizing::extract_text_from_node,
69 taffy_bridge, LayoutContext, LayoutDebugMessage, LayoutError, Result,
70 },
71 text3::cache::{
72 AvailableSpace as Text3AvailableSpace, BreakType, ClearType, InlineBreak,
73 TextAlign as Text3TextAlign,
74 },
75};
76
77pub const DEFAULT_SCROLLBAR_WIDTH_PX: f32 = 16.0;
82
83#[derive(Debug, Clone)]
87pub(crate) struct BfcLayoutResult {
88 pub output: LayoutOutput,
90 pub escaped_top_margin: Option<f32>,
93 pub escaped_bottom_margin: Option<f32>,
96}
97
98impl BfcLayoutResult {
99 pub(crate) const fn from_output(output: LayoutOutput) -> Self {
100 Self {
101 output,
102 escaped_top_margin: None,
103 escaped_bottom_margin: None,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum OverflowBehavior {
111 Visible,
112 Hidden,
113 Clip,
114 Scroll,
115 Auto,
116}
117
118impl OverflowBehavior {
119 #[must_use] pub const fn is_clipped(&self) -> bool {
120 matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
121 }
122
123 #[must_use] pub const fn is_scroll(&self) -> bool {
124 matches!(self, Self::Scroll | Self::Auto)
125 }
126}
127
128#[derive(Debug)]
130pub struct LayoutConstraints<'a> {
131 pub available_size: LogicalSize,
133 pub writing_mode: LayoutWritingMode,
135 pub writing_mode_ctx: super::geometry::WritingModeContext,
139 pub bfc_state: Option<&'a mut BfcState>,
142 pub text_align: TextAlign,
144 pub containing_block_size: LogicalSize,
147 pub available_width_type: Text3AvailableSpace,
158}
159
160#[derive(Debug, Clone)]
163pub struct BfcState {
164 pub pen: LogicalPosition,
166 pub floats: FloatingContext,
168 pub margins: MarginCollapseContext,
170}
171
172impl Default for BfcState {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178impl BfcState {
179 #[must_use] pub fn new() -> Self {
180 Self {
181 pen: LogicalPosition::zero(),
182 floats: FloatingContext::default(),
183 margins: MarginCollapseContext::default(),
184 }
185 }
186}
187
188#[derive(Copy, Debug, Default, Clone)]
190pub struct MarginCollapseContext {
191 pub last_in_flow_margin_bottom: f32,
194}
195
196#[derive(Debug, Default, Clone)]
198pub struct LayoutOutput {
199 pub positions: BTreeMap<usize, LogicalPosition>,
201 pub overflow_size: LogicalSize,
203 pub baseline: Option<f32>,
206}
207
208#[derive(Debug, Clone, Copy, Default)]
210pub enum TextAlign {
211 #[default]
212 Start,
213 End,
214 Center,
215 Justify,
216}
217
218#[derive(Debug, Clone, Copy)]
220struct FloatBox {
221 kind: LayoutFloat,
223 rect: LogicalRect,
225 margin: EdgeSizes,
227}
228
229#[derive(Debug, Default, Clone)]
234pub struct FloatingContext {
235 pub floats: Vec<FloatBox>,
237}
238
239impl FloatingContext {
240 pub fn add_float(&mut self, kind: LayoutFloat, rect: LogicalRect, margin: EdgeSizes) {
242 self.floats.push(FloatBox { kind, rect, margin });
243 }
244
245 #[must_use] pub fn available_line_box_space(
259 &self,
260 main_start: f32,
261 main_end: f32,
262 bfc_cross_size: f32,
263 wm: LayoutWritingMode,
264 ) -> (f32, f32) {
265 let mut available_cross_start = 0.0_f32;
266 let mut available_cross_end = bfc_cross_size;
267
268 for float in &self.floats {
269 let float_main_start = float.rect.origin.main(wm) - float.margin.main_start(wm);
271 let float_main_end = float_main_start + float.rect.size.main(wm)
272 + float.margin.main_start(wm) + float.margin.main_end(wm);
273
274 if main_end > float_main_start && main_start < float_main_end {
276 let float_cross_start = float.rect.origin.cross(wm) - float.margin.cross_start(wm);
279 let float_cross_end = float_cross_start + float.rect.size.cross(wm)
280 + float.margin.cross_start(wm) + float.margin.cross_end(wm);
281
282 if float.kind == LayoutFloat::Left {
285 available_cross_start = available_cross_start.max(float_cross_end);
287 } else {
288 available_cross_end = available_cross_end.min(float_cross_start);
290 }
291 }
292 }
293 (available_cross_start, available_cross_end)
294 }
295
296 #[must_use] pub fn clearance_offset(
316 &self,
317 clear: LayoutClear,
318 current_main_offset: f32,
319 wm: LayoutWritingMode,
320 ) -> f32 {
321 let mut max_end_offset = 0.0_f32;
322
323 let check_left = clear == LayoutClear::Left || clear == LayoutClear::Both;
324 let check_right = clear == LayoutClear::Right || clear == LayoutClear::Both;
325
326 for float in &self.floats {
327 let should_clear_this_float = (check_left && float.kind == LayoutFloat::Left)
328 || (check_right && float.kind == LayoutFloat::Right);
329
330 if should_clear_this_float {
331 let float_margin_box_end = float.rect.origin.main(wm)
334 + float.rect.size.main(wm)
335 + float.margin.main_end(wm);
336 max_end_offset = max_end_offset.max(float_margin_box_end);
337 }
338 }
339
340 if max_end_offset > current_main_offset {
341 max_end_offset
342 } else {
343 current_main_offset
344 }
345 }
346}
347
348struct BfcLayoutState {
350 pen: LogicalPosition,
352 floats: FloatingContext,
353 margins: MarginCollapseContext,
354 writing_mode: LayoutWritingMode,
356}
357
358#[allow(clippy::implicit_hasher)] pub fn layout_formatting_context<T: ParsedFontTrait>(
376 ctx: &mut LayoutContext<'_, T>,
377 tree: &mut LayoutTree,
378 text_cache: &mut TextLayoutCache,
379 node_index: usize,
380 constraints: &LayoutConstraints<'_>,
381 float_cache: &mut HashMap<usize, FloatingContext>,
382) -> Result<BfcLayoutResult> {
383 #[cfg(feature = "web_lift")]
386 unsafe { crate::az_mark(((0x609E0 + (node_index & 7) * 4)) as u32, (0xC0DE0042) as u32); }
387 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
388 #[cfg(feature = "web_lift")]
392 unsafe { crate::az_mark(((0x60B80 + (node_index & 7) * 4)) as u32, ((node as *const _ as usize) as u32) as u32); }
393
394 #[cfg(feature = "web_lift")]
404 {
405 let force_ifc = node
406 .dom_node_id
407 .map_or(false, |dom_id| {
408 crate::solver3::layout_tree::has_only_inline_children(ctx.styled_dom, dom_id)
409 });
410 if force_ifc {
411 unsafe { crate::az_mark(((0x60BA0 + (node_index & 7) * 4)) as u32, (0xC0DE1FC0) as u32); }
412 return layout_ifc(ctx, text_cache, tree, node_index, constraints)
413 .map(BfcLayoutResult::from_output);
414 }
415 }
416
417 #[cfg(feature = "web_lift")]
423 unsafe {
424 let fc_disc = match node.formatting_context {
425 FormattingContext::Block { .. } => 1u32,
426 FormattingContext::Inline => 2,
427 FormattingContext::InlineBlock => 3,
428 FormattingContext::Flex => 4,
429 FormattingContext::Grid => 5,
430 FormattingContext::Table => 6,
431 FormattingContext::TableCell => 7,
432 FormattingContext::TableCaption => 8,
433 _ => 0,
434 };
435 crate::az_mark(((0x609A0 + (node_index & 7) * 4)) as u32, (fc_disc | 0xC0DE0000) as u32);
436 }
437
438 debug_info!(
439 ctx,
440 "[layout_formatting_context] node_index={}, fc={:?}, available_size={:?}",
441 node_index,
442 node.formatting_context,
443 constraints.available_size
444 );
445
446 match node.formatting_context {
450 FormattingContext::Block { .. } => {
451 #[cfg(feature = "web_lift")]
452 unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0001) as u32); }
453 layout_bfc(ctx, tree, text_cache, node_index, constraints, float_cache)
454 }
455 FormattingContext::Inline => {
457 #[cfg(feature = "web_lift")]
458 unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0002) as u32); }
459 layout_ifc(ctx, text_cache, tree, node_index, constraints)
460 .map(BfcLayoutResult::from_output)
461 }
462 FormattingContext::InlineBlock => {
463 #[cfg(feature = "web_lift")]
464 unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0003) as u32); }
465 let mut temp_float_cache = HashMap::new();
473 layout_bfc(ctx, tree, text_cache, node_index, constraints, &mut temp_float_cache)
474 }
475 FormattingContext::Table => {
477 #[cfg(feature = "web_lift")]
478 unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0006) as u32); }
479 layout_table_fc(ctx, tree, text_cache, node_index, constraints)
480 .map(BfcLayoutResult::from_output)
481 }
482 FormattingContext::Flex | FormattingContext::Grid => {
486 #[cfg(feature = "web_lift")]
487 unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0004) as u32); }
488 layout_flex_grid(ctx, tree, text_cache, node_index, constraints)
489 }
490 FormattingContext::TableCell | FormattingContext::TableCaption => {
492 #[cfg(feature = "web_lift")]
493 unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0007) as u32); }
494 let mut temp_float_cache = HashMap::new();
495 layout_bfc(ctx, tree, text_cache, node_index, constraints, &mut temp_float_cache)
496 }
497 _ => {
498 #[cfg(feature = "web_lift")]
503 unsafe {
504 crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0009) as u32);
505 let disc: u8 = core::ptr::read_volatile((&node.formatting_context) as *const FormattingContext as *const u8);
506 crate::az_mark(((0x60B40 + (node_index & 7) * 4)) as u32, (0xC0DE0000 | (disc as u32)) as u32);
507 }
508 let mut temp_float_cache = HashMap::new();
510 layout_bfc(
511 ctx,
512 tree,
513 text_cache,
514 node_index,
515 constraints,
516 &mut temp_float_cache,
517 )
518 }
519 }
520}
521
522#[allow(clippy::too_many_lines)] fn layout_flex_grid<T: ParsedFontTrait>(
543 ctx: &mut LayoutContext<'_, T>,
544 tree: &mut LayoutTree,
545 text_cache: &mut TextLayoutCache,
546 node_index: usize,
547 constraints: &LayoutConstraints<'_>,
548) -> Result<BfcLayoutResult> {
549 let available_space = TaffySize {
551 width: AvailableSpace::Definite(constraints.available_size.width),
552 height: AvailableSpace::Definite(constraints.available_size.height),
553 };
554
555 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
556
557 let (explicit_width, has_explicit_width) =
565 resolve_explicit_dimension_width(ctx, node, constraints);
566 let (explicit_height, has_explicit_height) =
567 resolve_explicit_dimension_height(ctx, node, constraints);
568
569 let is_root = node.parent.is_none();
574
575 let bp = node.box_props.unpack();
576 let width_adjustment = bp.border.left
577 + bp.border.right
578 + bp.padding.left
579 + bp.padding.right;
580 let height_adjustment = bp.border.top
581 + bp.border.bottom
582 + bp.padding.top
583 + bp.padding.bottom;
584
585 let root_border_box = node.used_size;
592
593 let effective_width = if has_explicit_width {
594 explicit_width
595 } else if is_root {
596 root_border_box.as_ref().map(|s| s.width).or_else(|| {
597 if constraints.available_size.width.is_finite() {
598 Some(constraints.available_size.width + width_adjustment)
600 } else {
601 None
602 }
603 })
604 } else {
605 node.used_size.as_ref().map(|s| s.width)
617 };
618 let effective_height = if has_explicit_height {
619 explicit_height
620 } else if is_root {
621 root_border_box.as_ref().map(|s| s.height).or_else(|| {
622 if constraints.available_size.height.is_finite() {
623 Some(constraints.available_size.height + height_adjustment)
624 } else {
625 None
626 }
627 })
628 } else {
629 None
630 };
631 let has_effective_width = effective_width.is_some();
632 let has_effective_height = effective_height.is_some();
633
634 let adjusted_width = if has_explicit_width && !is_root {
642 explicit_width.map(|w| w + width_adjustment)
643 } else if has_explicit_width && is_root {
644 explicit_width
645 } else {
646 effective_width
647 };
648 let adjusted_height = if has_explicit_height && !is_root {
649 explicit_height.map(|h| h + height_adjustment)
650 } else if has_explicit_height && is_root {
651 explicit_height
652 } else {
653 effective_height
654 };
655
656 let sizing_mode = if has_effective_width || has_effective_height {
659 taffy::SizingMode::InherentSize
660 } else {
661 taffy::SizingMode::ContentSize
662 };
663
664 let known_dimensions = TaffySize {
665 width: adjusted_width,
666 height: adjusted_height,
667 };
668
669 let parent_size = translate_taffy_size(constraints.containing_block_size);
674
675 let taffy_inputs = LayoutInput {
676 known_dimensions,
677 parent_size,
678 available_space,
679 run_mode: taffy::RunMode::PerformLayout,
680 sizing_mode,
681 axis: taffy::RequestedAxis::Both,
682 vertical_margins_are_collapsible: Line::FALSE,
684 };
685
686 debug_info!(
687 ctx,
688 "CALLING LAYOUT_TAFFY FOR FLEX/GRID FC node_index={:?}",
689 node_index
690 );
691
692 if is_root {
698 if let (Some(aw), Some(ah)) = (adjusted_width, adjusted_height) {
699 if let Some(node_mut) = tree.get_mut(node_index) {
700 node_mut.used_size = Some(LogicalSize::new(aw, ah));
701 }
702 }
703 }
704
705 let border_left = bp.border.left;
707 let border_top = bp.border.top;
708
709 let taffy_output =
710 taffy_bridge::layout_taffy_subtree(ctx, tree, text_cache, node_index, taffy_inputs);
711
712 if !is_root {
722 let container_bb = translate_taffy_size_back(taffy_output.size);
723 if let Some(node_mut) = tree.get_mut(node_index) {
724 node_mut.used_size = Some(container_bb);
725 }
726 }
727
728 let mut output = LayoutOutput::default();
730 let raw = translate_taffy_size_back(taffy_output.content_size);
739 output.overflow_size = LogicalSize::new(
740 (raw.width - border_left).max(0.0),
741 (raw.height - border_top).max(0.0),
742 );
743
744 let children: Vec<usize> = tree.children(node_index).to_vec();
745 for &child_idx in &children {
746 if let Some(warm_node) = tree.warm(child_idx) {
747 if let Some(pos) = warm_node.relative_position {
748 output.positions.insert(child_idx, pos);
749 }
750 }
751 }
752
753 Ok(BfcLayoutResult::from_output(output))
754}
755
756#[derive(Clone, Copy)]
759enum Axis {
760 Width,
761 Height,
762}
763
764fn border_box_to_content<T: ParsedFontTrait>(
777 ctx: &LayoutContext<'_, T>,
778 node: &LayoutNodeHot,
779 id: NodeId,
780 node_state: &StyledNodeState,
781 resolved: f32,
782 is_percentage: bool,
783 axis: Axis,
784) -> f32 {
785 if is_percentage {
786 return resolved;
787 }
788 let is_border_box = matches!(
789 get_css_box_sizing(ctx.styled_dom, id, node_state),
790 MultiValue::Exact(azul_css::props::layout::LayoutBoxSizing::BorderBox)
791 );
792 if !is_border_box {
793 return resolved;
794 }
795 let bp = node.box_props.unpack();
796 let adjustment = match axis {
797 Axis::Width => bp.border.left + bp.border.right + bp.padding.left + bp.padding.right,
798 Axis::Height => bp.border.top + bp.border.bottom + bp.padding.top + bp.padding.bottom,
799 };
800 (resolved - adjustment).max(0.0)
801}
802
803fn resolve_explicit_dimension_width<T: ParsedFontTrait>(
804 ctx: &LayoutContext<'_, T>,
805 node: &LayoutNodeHot,
806 constraints: &LayoutConstraints<'_>,
807) -> (Option<f32>, bool) {
808 node.dom_node_id
809 .map_or((None, false), |id| {
810 let width = get_css_width(
811 ctx.styled_dom,
812 id,
813 &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
814 );
815 match width.unwrap_or_default() {
816 LayoutWidth::Auto
817 | LayoutWidth::MinContent
818 | LayoutWidth::MaxContent
819 | LayoutWidth::FitContent(_) => (None, false),
820 LayoutWidth::Px(px) => {
821 let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
822 let pixels = resolve_size_metric(
823 px.metric,
824 px.number.get(),
825 constraints.available_size.width,
826 ctx.viewport_size,
827 get_element_font_size(ctx.styled_dom, id, node_state),
828 get_root_font_size(ctx.styled_dom, node_state),
829 );
830 let content_px = border_box_to_content(
831 ctx, node, id, node_state, pixels, px.metric == SizeMetric::Percent, Axis::Width,
832 );
833 (Some(content_px), true)
834 }
835 LayoutWidth::Calc(items) => {
836 let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
837 let em = get_element_font_size(ctx.styled_dom, id, node_state);
838 let calc_ctx = super::calc::CalcResolveContext {
839 items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
840 };
841 let px = super::calc::evaluate_calc(&calc_ctx, constraints.available_size.width);
842 let content_px = border_box_to_content(
843 ctx, node, id, node_state, px, false, Axis::Width,
844 );
845 (Some(content_px), true)
846 }
847 }
848 })
849}
850
851fn resolve_explicit_dimension_height<T: ParsedFontTrait>(
853 ctx: &LayoutContext<'_, T>,
854 node: &LayoutNodeHot,
855 constraints: &LayoutConstraints<'_>,
856) -> (Option<f32>, bool) {
857 node.dom_node_id
858 .map_or((None, false), |id| {
859 let height = get_css_height(
860 ctx.styled_dom,
861 id,
862 &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
863 );
864 match height.unwrap_or_default() {
865 LayoutHeight::Auto
866 | LayoutHeight::MinContent
867 | LayoutHeight::MaxContent
868 | LayoutHeight::FitContent(_) => (None, false),
869 LayoutHeight::Px(px) => {
870 let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
871 let pixels = resolve_size_metric(
872 px.metric,
873 px.number.get(),
874 constraints.available_size.height,
875 ctx.viewport_size,
876 get_element_font_size(ctx.styled_dom, id, node_state),
877 get_root_font_size(ctx.styled_dom, node_state),
878 );
879 let content_px = border_box_to_content(
885 ctx, node, id, node_state, pixels, px.metric == SizeMetric::Percent, Axis::Height,
886 );
887 (Some(content_px), true)
888 }
889 LayoutHeight::Calc(items) => {
890 let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
891 let em = get_element_font_size(ctx.styled_dom, id, node_state);
892 let calc_ctx = super::calc::CalcResolveContext {
893 items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
894 };
895 let px = super::calc::evaluate_calc(&calc_ctx, constraints.available_size.height);
896 let content_px = border_box_to_content(
897 ctx, node, id, node_state, px, false, Axis::Height,
898 );
899 (Some(content_px), true)
900 }
901 }
902 })
903}
904
905fn position_float(
919 float_ctx: &FloatingContext,
920 float_type: LayoutFloat,
921 size: LogicalSize,
922 margin: &EdgeSizes,
923 current_main_offset: f32,
924 bfc_cross_size: f32,
925 wm: LayoutWritingMode,
926) -> LogicalRect {
927 let mut main_start = current_main_offset;
929
930 let total_main = size.main(wm) + margin.main_start(wm) + margin.main_end(wm);
932 let total_cross = size.cross(wm) + margin.cross_start(wm) + margin.cross_end(wm);
933
934 let cross_start = loop {
937 let (avail_start, avail_end) = float_ctx.available_line_box_space(
938 main_start,
939 main_start + total_main,
940 bfc_cross_size,
941 wm,
942 );
943
944 let available_width = avail_end - avail_start;
945
946 if available_width >= total_cross {
947 if float_type == LayoutFloat::Left {
950 break avail_start + margin.cross_start(wm);
953 }
954 break avail_end - total_cross + margin.cross_start(wm);
956 }
957
958 let next_main = float_ctx
961 .floats
962 .iter()
963 .filter(|f| {
964 let f_main_start = f.rect.origin.main(wm) - f.margin.main_start(wm);
965 let f_main_end = f_main_start + f.rect.size.main(wm)
966 + f.margin.main_start(wm) + f.margin.main_end(wm);
967 f_main_end > main_start && f_main_start < main_start + total_main
968 })
969 .map(|f| f.rect.origin.main(wm) + f.rect.size.main(wm) + f.margin.main_end(wm))
970 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
971
972 if let Some(next) = next_main {
973 main_start = next;
974 } else {
975 if float_type == LayoutFloat::Left {
977 break avail_start + margin.cross_start(wm);
978 }
979 break avail_end - total_cross + margin.cross_start(wm);
980 }
981 };
982
983 LogicalRect {
984 origin: LogicalPosition::from_main_cross(
985 main_start + margin.main_start(wm),
986 cross_start,
987 wm,
988 ),
989 size,
990 }
991}
992
993#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn layout_bfc<T: ParsedFontTrait>(
1042 ctx: &mut LayoutContext<'_, T>,
1043 tree: &mut LayoutTree,
1044 text_cache: &mut TextLayoutCache,
1045 node_index: usize,
1046 constraints: &LayoutConstraints<'_>,
1047 float_cache: &mut HashMap<usize, FloatingContext>,
1048) -> Result<BfcLayoutResult> {
1049 let node = tree
1050 .get(node_index)
1051 .ok_or(LayoutError::InvalidTree)?
1052 .clone();
1053 let writing_mode = constraints.writing_mode;
1055 let mut output = LayoutOutput::default();
1056
1057 debug_info!(
1058 ctx,
1059 "\n[layout_bfc] ENTERED for node_index={}, children.len()={}, incoming_bfc_state={}",
1060 node_index,
1061 tree.children(node_index).len(),
1062 constraints.bfc_state.is_some()
1063 );
1064
1065 let mut float_context = FloatingContext::default();
1070
1071 let mut children_containing_block_size = node.used_size.map_or_else(
1089 || constraints.available_size,
1092 |used_size| {
1093 let inner = node.box_props.inner_size(used_size, writing_mode);
1101 let height_is_auto = tree
1102 .warm(node_index)
1103 .is_none_or(|w| w.computed_style.height.is_none());
1104 if height_is_auto {
1105 LogicalSize::new(inner.width, constraints.available_size.height)
1106 } else {
1107 inner
1108 }
1109 },
1110 );
1111
1112 let scrollbar_reservation = node
1125 .dom_node_id
1126 .map_or(0.0, |dom_id| {
1127 let styled_node_state = ctx
1128 .styled_dom
1129 .styled_nodes
1130 .as_container()
1131 .get(dom_id)
1132 .map(|s| s.styled_node_state)
1133 .unwrap_or_default();
1134 let overflow_y =
1135 get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state);
1136 match overflow_y.unwrap_or_default() {
1137 LayoutOverflow::Scroll => {
1138 crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, &styled_node_state)
1139 }
1140 LayoutOverflow::Auto => {
1141 let already_needs = tree.warm(node_index)
1142 .and_then(|w| w.scrollbar_info.as_ref())
1143 .is_some_and(|s| s.needs_vertical);
1144 if already_needs {
1145 crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, &styled_node_state)
1146 } else {
1147 0.0
1148 }
1149 }
1150 _ => 0.0,
1151 }
1152 });
1153
1154 if scrollbar_reservation > 0.0 {
1155 children_containing_block_size.width =
1156 (children_containing_block_size.width - scrollbar_reservation).max(0.0);
1157 }
1158
1159 {
1177 let mut temp_positions: super::PositionVec = Vec::new();
1178 let mut temp_scrollbar_reflow = false;
1179
1180 let bfc_children = tree.children(node_index).to_vec();
1181 #[cfg(feature = "web_lift")]
1186 unsafe { crate::az_mark(((0x60A00 + (node_index & 7) * 4)) as u32, (bfc_children.len() as u32 | 0xC0DE0000) as u32); }
1187 for &child_index in &bfc_children {
1188 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1189 let child_dom_id = child_node.dom_node_id;
1190
1191 let position_type = get_position_type(ctx.styled_dom, child_dom_id);
1198 if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
1199 continue;
1200 }
1201
1202 #[cfg(feature = "web_lift")]
1207 unsafe { crate::az_mark(((0x60A40 + (child_index & 7) * 4)) as u32, (0xC0DE0000 | (child_index as u32 & 0xffff)) as u32); }
1208 crate::solver3::cache::calculate_layout_for_subtree(
1209 ctx,
1210 tree,
1211 text_cache,
1212 child_index,
1213 LogicalPosition::zero(),
1214 children_containing_block_size,
1215 &mut temp_positions,
1216 &mut temp_scrollbar_reflow,
1217 float_cache,
1218 crate::solver3::cache::ComputeMode::ComputeSize,
1219 )?;
1220 }
1221 }
1222
1223 let mut main_pen = 0.0f32;
1232 let mut max_cross_size = 0.0f32;
1233
1234 let mut total_escaped_top_margin = 0.0f32;
1238 let mut total_sibling_margins = 0.0f32;
1240
1241 let mut last_margin_bottom = 0.0f32;
1243 let mut is_first_child = true;
1244 let mut first_child_index: Option<usize> = None;
1245 let mut last_child_index: Option<usize> = None;
1246
1247 let node_bp = node.box_props.unpack();
1249 let parent_margin_top = node_bp.margin.main_start(writing_mode);
1250 let parent_margin_bottom = node_bp.margin.main_end(writing_mode);
1251
1252 let establishes_own_bfc = establishes_new_bfc(ctx, &node, tree.cold(node_index));
1258 let is_bfc_root = node.parent.is_none() || establishes_own_bfc;
1259
1260 let parent_has_top_blocker = establishes_own_bfc
1264 || has_margin_collapse_blocker(&node_bp, writing_mode, true);
1265 let parent_has_bottom_blocker = establishes_own_bfc
1266 || has_margin_collapse_blocker(&node_bp, writing_mode, false);
1267
1268 let mut accumulated_top_margin = 0.0f32;
1270 let mut top_margin_resolved = false;
1271 let mut top_margin_escaped = false;
1273
1274 let mut has_content = false;
1276
1277 let pos_children = tree.children(node_index).to_vec();
1279 for &child_index in &pos_children {
1280 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1281 let child_dom_id = child_node.dom_node_id;
1282
1283 let position_type = get_position_type(ctx.styled_dom, child_dom_id);
1286 if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
1287 continue;
1288 }
1289
1290 if let Some(node_id) = child_dom_id {
1294 let float_type = get_float_property(ctx.styled_dom, Some(node_id));
1295
1296 if float_type != LayoutFloat::None {
1297 let float_size = if let Some(size) = child_node.used_size { size } else {
1299 let intrinsic = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
1300 let child_bp = child_node.box_props.unpack();
1301 let computed_size = crate::solver3::sizing::calculate_used_size_for_node(
1302 ctx.styled_dom,
1303 child_dom_id,
1304 &children_containing_block_size,
1305 intrinsic,
1306 &child_bp,
1307 &ctx.viewport_size,
1308 )?;
1309 if let Some(node_mut) = tree.get_mut(child_index) {
1310 node_mut.used_size = Some(computed_size);
1311 }
1312 computed_size
1313 };
1314 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1316 let child_bp2 = child_node.box_props.unpack();
1317 let float_margin = &child_bp2.margin;
1318
1319 let float_clear = get_clear_property(ctx.styled_dom, Some(node_id));
1322 let float_y = if float_clear == LayoutClear::None {
1323 main_pen + last_margin_bottom
1326 } else {
1327 float_context.clearance_offset(float_clear, main_pen + last_margin_bottom, writing_mode)
1328 };
1329
1330 debug_info!(
1331 ctx,
1332 "[layout_bfc] Positioning float: index={}, type={:?}, size={:?}, at Y={} \
1333 (main_pen={} + last_margin={})",
1334 child_index,
1335 float_type,
1336 float_size,
1337 float_y,
1338 main_pen,
1339 last_margin_bottom
1340 );
1341
1342 let float_rect = position_float(
1344 &float_context,
1345 float_type,
1346 float_size,
1347 float_margin,
1348 float_y,
1350 constraints.available_size.cross(writing_mode),
1351 writing_mode,
1352 );
1353
1354 debug_info!(ctx, "[layout_bfc] Float positioned at: {:?}", float_rect);
1355
1356 float_context.add_float(float_type, float_rect, *float_margin);
1358
1359 output.positions.insert(child_index, float_rect.origin);
1361
1362 debug_info!(
1363 ctx,
1364 "[layout_bfc] *** FLOAT POSITIONED: child={}, main_pen={} (unchanged - floats \
1365 don't advance pen)",
1366 child_index,
1367 main_pen
1368 );
1369
1370 continue;
1373 }
1374 }
1375
1376 if first_child_index.is_none() {
1383 first_child_index = Some(child_index);
1384 }
1385 last_child_index = Some(child_index);
1386
1387 let child_size = if let Some(size) = child_node.used_size { size } else {
1390 let intrinsic = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
1392 let child_used_size = crate::solver3::sizing::calculate_used_size_for_node(
1393 ctx.styled_dom,
1394 child_dom_id,
1395 &children_containing_block_size,
1396 intrinsic,
1397 &child_node.box_props.unpack(),
1398 &ctx.viewport_size,
1399 )?;
1400 if let Some(node_mut) = tree.get_mut(child_index) {
1402 node_mut.used_size = Some(child_used_size);
1403 }
1404 child_used_size
1405 };
1406 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1408 let child_bp = child_node.box_props.unpack();
1409 let child_margin = &child_bp.margin;
1410
1411 debug_info!(
1412 ctx,
1413 "[layout_bfc] Child {} margin from box_props: top={}, right={}, bottom={}, left={}",
1414 child_index,
1415 child_margin.top,
1416 child_margin.right,
1417 child_margin.bottom,
1418 child_margin.left
1419 );
1420
1421 let child_own_margin_top = child_margin.main_start(writing_mode);
1423 let child_own_margin_bottom = child_margin.main_end(writing_mode);
1424
1425 let child_escaped_top = if has_margin_collapse_blocker(&child_bp, writing_mode, true) { None } else {
1430 tree.warm(child_index).and_then(|w| w.escaped_top_margin)
1431 };
1432 let child_escaped_bottom = if has_margin_collapse_blocker(&child_bp, writing_mode, false) { None } else {
1433 tree.warm(child_index).and_then(|w| w.escaped_bottom_margin)
1434 };
1435
1436 let child_margin_top = child_escaped_top.unwrap_or(child_own_margin_top);
1437 let child_margin_bottom = child_escaped_bottom.unwrap_or(child_own_margin_bottom);
1438
1439 debug_info!(
1440 ctx,
1441 "[layout_bfc] Child {} final margins: margin_top={}, margin_bottom={}",
1442 child_index,
1443 child_margin_top,
1444 child_margin_bottom
1445 );
1446
1447 let child_has_top_blocker =
1449 has_margin_collapse_blocker(&child_bp, writing_mode, true);
1450 let child_has_bottom_blocker =
1451 has_margin_collapse_blocker(&child_bp, writing_mode, false);
1452
1453 let child_clear = if let Some(node_id) = child_dom_id {
1458 get_clear_property(ctx.styled_dom, Some(node_id))
1459 } else {
1460 LayoutClear::None
1461 };
1462 debug_info!(
1463 ctx,
1464 "[layout_bfc] Child {} clear property: {:?}",
1465 child_index,
1466 child_clear
1467 );
1468
1469 let is_empty = is_empty_block(tree, child_index);
1471
1472 if is_empty
1476 && !child_has_top_blocker
1477 && !child_has_bottom_blocker
1478 && child_clear == LayoutClear::None
1479 {
1480 let self_collapsed = collapse_margins(child_margin_top, child_margin_bottom);
1482
1483 if is_first_child {
1485 is_first_child = false;
1486 if parent_has_top_blocker {
1488 if accumulated_top_margin == 0.0 {
1490 accumulated_top_margin = parent_margin_top;
1491 }
1492 main_pen += accumulated_top_margin + self_collapsed;
1493 top_margin_resolved = true;
1494 accumulated_top_margin = 0.0;
1495 } else {
1496 accumulated_top_margin = collapse_margins(parent_margin_top, self_collapsed);
1497 }
1498 last_margin_bottom = self_collapsed;
1499 } else {
1500 last_margin_bottom = collapse_margins(last_margin_bottom, self_collapsed);
1502 }
1503
1504 continue;
1506 }
1507
1508 let clearance_applied = if child_clear == LayoutClear::None {
1524 false
1525 } else {
1526 let hypothetical = main_pen + collapse_margins(last_margin_bottom, child_margin_top);
1527 let cleared_position =
1528 float_context.clearance_offset(child_clear, hypothetical, writing_mode);
1529 debug_info!(
1530 ctx,
1531 "[layout_bfc] Child {} clearance check: cleared_position={}, hypothetical={} (main_pen={} + collapse({}, {}))",
1532 child_index,
1533 cleared_position,
1534 hypothetical,
1535 main_pen,
1536 last_margin_bottom,
1537 child_margin_top
1538 );
1539 if cleared_position > hypothetical {
1540 debug_info!(
1541 ctx,
1542 "[layout_bfc] Applying clearance: child={}, clear={:?}, old_pen={}, new_pen={}",
1543 child_index,
1544 child_clear,
1545 main_pen,
1546 cleared_position
1547 );
1548 main_pen = cleared_position;
1549 true } else {
1551 false
1552 }
1553 };
1554
1555 if is_first_child {
1562 is_first_child = false;
1563
1564 if clearance_applied {
1566 debug_info!(
1572 ctx,
1573 "[layout_bfc] First child {} with CLEARANCE: no collapse, child_margin={}, \
1574 main_pen={}",
1575 child_index,
1576 child_margin_top,
1577 main_pen
1578 );
1579 } else if !parent_has_top_blocker {
1580 accumulated_top_margin = collapse_margins(parent_margin_top, child_margin_top);
1629 top_margin_resolved = true;
1630 top_margin_escaped = true;
1631
1632 total_escaped_top_margin = accumulated_top_margin;
1636
1637 debug_info!(
1639 ctx,
1640 "[layout_bfc] First child {} margin ESCAPES: parent_margin={}, \
1641 child_margin={}, collapsed={}, total_escaped={}",
1642 child_index,
1643 parent_margin_top,
1644 child_margin_top,
1645 accumulated_top_margin,
1646 total_escaped_top_margin
1647 );
1648 } else {
1649 main_pen += child_margin_top;
1700 debug_info!(
1701 ctx,
1702 "[layout_bfc] First child {} BLOCKED: parent_has_blocker={}, advanced by \
1703 child_margin={}, main_pen={}",
1704 child_index,
1705 parent_has_top_blocker,
1706 child_margin_top,
1707 main_pen
1708 );
1709 }
1710 } else {
1711 if !top_margin_resolved {
1717 main_pen += accumulated_top_margin;
1718 top_margin_resolved = true;
1719 debug_info!(
1720 ctx,
1721 "[layout_bfc] RESOLVED top margin for node {} at sibling {}: accumulated={}, \
1722 main_pen={}",
1723 node_index,
1724 child_index,
1725 accumulated_top_margin,
1726 main_pen
1727 );
1728 }
1729
1730 if clearance_applied {
1731 debug_info!(
1736 ctx,
1737 "[layout_bfc] Child {} with CLEARANCE: no collapse with sibling, \
1738 child_margin_top={}, main_pen={}",
1739 child_index,
1740 child_margin_top,
1741 main_pen
1742 );
1743 } else {
1744 let collapsed = collapse_margins(last_margin_bottom, child_margin_top);
1778 main_pen += collapsed;
1779 total_sibling_margins += collapsed;
1780 debug_info!(
1781 ctx,
1782 "[layout_bfc] Sibling collapse for child {}: last_margin_bottom={}, \
1783 child_margin_top={}, collapsed={}, main_pen={}, total_sibling_margins={}",
1784 child_index,
1785 last_margin_bottom,
1786 child_margin_top,
1787 collapsed,
1788 main_pen,
1789 total_sibling_margins
1790 );
1791 }
1792 }
1793
1794 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1814 let avoids_floats = establishes_new_bfc(ctx, child_node, tree.cold(child_index))
1815 || is_block_level_replaced(ctx, child_node);
1816
1817 let (cross_start, cross_end, available_cross) = if avoids_floats {
1819 let child_cross_needed = child_size.cross(writing_mode);
1821 let bfc_cross = constraints.available_size.cross(writing_mode);
1822
1823 let (mut start, mut end) = float_context.available_line_box_space(
1824 main_pen,
1825 main_pen + child_size.main(writing_mode),
1826 bfc_cross,
1827 writing_mode,
1828 );
1829 let mut available = end - start;
1830
1831 if available < child_cross_needed && !float_context.floats.is_empty() {
1835 let clear_to = float_context.floats.iter()
1836 .filter(|f| {
1837 let f_main_start = f.rect.origin.main(writing_mode) - f.margin.main_start(writing_mode);
1838 let f_main_end = f_main_start + f.rect.size.main(writing_mode)
1839 + f.margin.main_start(writing_mode) + f.margin.main_end(writing_mode);
1840 f_main_end > main_pen && f_main_start < main_pen + child_size.main(writing_mode)
1841 })
1842 .map(|f| {
1843 f.rect.origin.main(writing_mode) + f.rect.size.main(writing_mode)
1844 + f.margin.main_end(writing_mode)
1845 })
1846 .fold(main_pen, f32::max);
1847
1848 if clear_to > main_pen {
1849 main_pen = clear_to;
1850 let (s, e) = float_context.available_line_box_space(
1851 main_pen,
1852 main_pen + child_size.main(writing_mode),
1853 bfc_cross,
1854 writing_mode,
1855 );
1856 start = s;
1857 end = e;
1858 available = end - start;
1859 }
1860 }
1861
1862 debug_info!(
1863 ctx,
1864 "[layout_bfc] Child {} avoids floats: shrinking to avoid floats, \
1865 cross_range={}..{}, available_cross={}",
1866 child_index,
1867 start,
1868 end,
1869 available
1870 );
1871
1872 (start, end, available)
1873 } else {
1874 let start = 0.0;
1877 let end = constraints.available_size.cross(writing_mode);
1878 let available = end - start;
1879
1880 debug_info!(
1881 ctx,
1882 "[layout_bfc] Child {} is normal flow: overlapping floats at full width, \
1883 available_cross={}",
1884 child_index,
1885 available
1886 );
1887
1888 (start, end, available)
1889 };
1890
1891 let (child_margin_cloned, child_margin_auto, child_used_size, is_inline_fc, child_dom_id_for_debug) = {
1893 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1894 let cbp = child_node.box_props.unpack();
1895 (
1896 cbp.margin,
1897 cbp.margin_auto,
1898 child_node.used_size.unwrap_or_default(),
1899 child_node.formatting_context == FormattingContext::Inline,
1900 child_node.dom_node_id,
1901 )
1902 };
1903 let child_margin = &child_margin_cloned;
1904
1905 debug_info!(
1906 ctx,
1907 "[layout_bfc] Child {} margin_auto: left={}, right={}, top={}, bottom={}",
1908 child_index,
1909 child_margin_auto.left,
1910 child_margin_auto.right,
1911 child_margin_auto.top,
1912 child_margin_auto.bottom
1913 );
1914 debug_info!(
1915 ctx,
1916 "[layout_bfc] Child {} used_size: width={}, height={}",
1917 child_index,
1918 child_used_size.width,
1919 child_used_size.height
1920 );
1921
1922 let (child_cross_pos, mut child_main_pos) = if avoids_floats {
1930 let cross_pos = if child_margin_auto.left && child_margin_auto.right {
1934 let remaining = (available_cross - child_used_size.cross(writing_mode)).max(0.0);
1935 debug_info!(
1936 ctx,
1937 "[layout_bfc] Child {} BFC + margin:auto centering: available={}, size={}, offset={}",
1938 child_index, available_cross, child_used_size.cross(writing_mode), remaining / 2.0
1939 );
1940 cross_start + remaining / 2.0
1941 } else if child_margin_auto.left {
1942 let remaining = (available_cross - child_used_size.cross(writing_mode) - child_margin.right).max(0.0);
1943 cross_start + remaining
1944 } else {
1945 cross_start + child_margin.cross_start(writing_mode)
1946 };
1947 (cross_pos, main_pen)
1948 } else {
1949 let available_cross = constraints.available_size.cross(writing_mode);
1951 let child_cross_size = child_used_size.cross(writing_mode);
1952
1953 debug_info!(
1954 ctx,
1955 "[layout_bfc] Child {} centering check: available_cross={}, child_cross_size={}, margin_auto.left={}, margin_auto.right={}",
1956 child_index,
1957 available_cross,
1958 child_cross_size,
1959 child_margin_auto.left,
1960 child_margin_auto.right
1961 );
1962
1963 let cross_pos = if child_margin_auto.left && child_margin_auto.right {
1971 let remaining_space = (available_cross - child_cross_size).max(0.0);
1973 debug_info!(
1974 ctx,
1975 "[layout_bfc] Child {} CENTERING: remaining_space={}, cross_pos={}",
1976 child_index,
1977 remaining_space,
1978 remaining_space / 2.0
1979 );
1980 remaining_space / 2.0
1981 } else if child_margin_auto.left {
1982 let remaining_space = (available_cross - child_cross_size - child_margin.right).max(0.0);
1984 debug_info!(
1985 ctx,
1986 "[layout_bfc] Child {} margin-left:auto only, pushing right: remaining_space={}",
1987 child_index,
1988 remaining_space
1989 );
1990 remaining_space
1991 } else if child_margin_auto.right {
1992 debug_info!(
1994 ctx,
1995 "[layout_bfc] Child {} margin-right:auto only, using left margin={}",
1996 child_index,
1997 child_margin.cross_start(writing_mode)
1998 );
1999 child_margin.cross_start(writing_mode)
2000 } else {
2001 let is_rtl = tree.get(node_index)
2006 .and_then(|n| n.dom_node_id)
2007 .is_some_and(|cb_dom_id| {
2008 let node_state = ctx.styled_dom.styled_nodes.as_container()
2009 .get(cb_dom_id)
2010 .map(|s| s.styled_node_state)
2011 .unwrap_or_default();
2012 matches!(
2013 get_direction_property(ctx.styled_dom, cb_dom_id, &node_state),
2014 MultiValue::Exact(StyleDirection::Rtl)
2015 )
2016 });
2017 let cross_pos = if is_rtl {
2018 available_cross - child_cross_size - child_margin.cross_end(writing_mode)
2020 } else {
2021 child_margin.cross_start(writing_mode)
2023 };
2024 debug_info!(
2025 ctx,
2026 "[layout_bfc] Child {} NO auto margins (over-constrained), is_rtl={}, cross_pos={}",
2027 child_index,
2028 is_rtl,
2029 cross_pos
2030 );
2031 cross_pos
2032 };
2033
2034 (cross_pos, main_pen)
2035 };
2036
2037 let final_pos =
2053 LogicalPosition::from_main_cross(child_main_pos, child_cross_pos, writing_mode);
2054
2055 debug_info!(
2056 ctx,
2057 "[layout_bfc] *** NORMAL FLOW BLOCK POSITIONED: child={}, final_pos={:?}, \
2058 main_pen={}, avoids_floats={}",
2059 child_index,
2060 final_pos,
2061 main_pen,
2062 avoids_floats
2063 );
2064
2065 if is_inline_fc && !avoids_floats {
2068 let floats_for_ifc = float_cache.get(&node_index).unwrap_or(&float_context);
2071
2072 debug_info!(
2073 ctx,
2074 "[layout_bfc] Re-layouting IFC child {} (normal flow) with parent's float context \
2075 at Y={}, child_cross_pos={}",
2076 child_index,
2077 main_pen,
2078 child_cross_pos
2079 );
2080 debug_info!(
2081 ctx,
2082 "[layout_bfc] Using {} floats (from cache: {})",
2083 floats_for_ifc.floats.len(),
2084 float_cache.contains_key(&node_index)
2085 );
2086
2087 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
2091 let cbp = child_node.box_props.unpack();
2092 let padding_border_cross = cbp.padding.cross_start(writing_mode)
2093 + cbp.border.cross_start(writing_mode);
2094 let padding_border_main = cbp.padding.main_start(writing_mode)
2095 + cbp.border.main_start(writing_mode);
2096
2097 let content_box_cross = child_cross_pos + padding_border_cross;
2099 let content_box_main = main_pen + padding_border_main;
2100
2101 debug_info!(
2102 ctx,
2103 "[layout_bfc] Border-box at ({}, {}), Content-box at ({}, {}), \
2104 padding+border=({}, {})",
2105 child_cross_pos,
2106 main_pen,
2107 content_box_cross,
2108 content_box_main,
2109 padding_border_cross,
2110 padding_border_main
2111 );
2112
2113 let mut ifc_floats = FloatingContext::default();
2114 for float_box in &floats_for_ifc.floats {
2115 let float_rel_to_ifc = LogicalRect {
2117 origin: LogicalPosition {
2118 x: float_box.rect.origin.x - content_box_cross,
2119 y: float_box.rect.origin.y - content_box_main,
2120 },
2121 size: float_box.rect.size,
2122 };
2123
2124 debug_info!(
2125 ctx,
2126 "[layout_bfc] Float {:?}: BFC coords = {:?}, IFC-content-relative = {:?}",
2127 float_box.kind,
2128 float_box.rect,
2129 float_rel_to_ifc
2130 );
2131
2132 ifc_floats.add_float(float_box.kind, float_rel_to_ifc, float_box.margin);
2133 }
2134
2135 let mut bfc_state = BfcState {
2137 pen: LogicalPosition::zero(), floats: ifc_floats.clone(),
2139 margins: MarginCollapseContext::default(),
2140 };
2141
2142 debug_info!(
2143 ctx,
2144 "[layout_bfc] Created IFC-relative FloatingContext with {} floats",
2145 ifc_floats.floats.len()
2146 );
2147
2148 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
2150 let child_dom_id = child_node.dom_node_id;
2151
2152 let display = get_display_property(ctx.styled_dom, child_dom_id).unwrap_or_default();
2157 let child_content_size = if display == LayoutDisplay::Inline {
2158 LogicalSize::new(
2160 children_containing_block_size.width,
2161 children_containing_block_size.height,
2162 )
2163 } else {
2164 child_node.box_props.inner_size(child_size, writing_mode)
2166 };
2167
2168 debug_info!(
2169 ctx,
2170 "[layout_bfc] IFC child size: border-box={:?}, content-box={:?}",
2171 child_size,
2172 child_content_size
2173 );
2174
2175 let ifc_constraints = LayoutConstraints {
2178 available_size: child_content_size,
2179 bfc_state: Some(&mut bfc_state),
2180 writing_mode,
2181 writing_mode_ctx: constraints.writing_mode_ctx,
2182 text_align: constraints.text_align,
2183 containing_block_size: constraints.containing_block_size,
2184 available_width_type: Text3AvailableSpace::Definite(child_content_size.width),
2185 };
2186
2187 let ifc_result = layout_formatting_context(
2190 ctx,
2191 tree,
2192 text_cache,
2193 child_index,
2194 &ifc_constraints,
2195 float_cache,
2196 )?;
2197
2198 debug_info!(
2202 ctx,
2203 "[layout_bfc] IFC child {} re-layouted with float context (text will wrap, box \
2204 stays full width)",
2205 child_index
2206 );
2207
2208 }
2218
2219 output.positions.insert(child_index, final_pos);
2220
2221 main_pen += child_size.main(writing_mode);
2225 has_content = true;
2226
2227 last_margin_bottom = child_margin_bottom;
2235
2236 debug_info!(
2237 ctx,
2238 "[layout_bfc] Child {} positioned at final_pos={:?}, size={:?}, advanced main_pen to \
2239 {}, last_margin_bottom={}, clearance_applied={}",
2240 child_index,
2241 final_pos,
2242 child_size,
2243 main_pen,
2244 last_margin_bottom,
2245 clearance_applied
2246 );
2247
2248 let child_cross_extent =
2250 child_cross_pos + child_size.cross(writing_mode) + child_margin.cross_end(writing_mode);
2251 max_cross_size = max_cross_size.max(child_cross_extent);
2252 }
2253
2254 debug_info!(
2257 ctx,
2258 "[layout_bfc] Storing {} floats in cache for node {}",
2259 float_context.floats.len(),
2260 node_index
2261 );
2262 float_cache.insert(node_index, float_context.clone());
2263
2264 let mut escaped_top_margin = None;
2266 let mut escaped_bottom_margin = None;
2267
2268 if top_margin_escaped {
2270 escaped_top_margin = Some(accumulated_top_margin);
2272 debug_info!(
2273 ctx,
2274 "[layout_bfc] Returning escaped top margin: accumulated={}, node={}",
2275 accumulated_top_margin,
2276 node_index
2277 );
2278 } else if !top_margin_resolved && accumulated_top_margin > 0.0 {
2279 escaped_top_margin = Some(accumulated_top_margin);
2281 debug_info!(
2282 ctx,
2283 "[layout_bfc] Escaping top margin (no content): accumulated={}, node={}",
2284 accumulated_top_margin,
2285 node_index
2286 );
2287 } else {
2288 debug_info!(
2291 ctx,
2292 "[layout_bfc] NOT escaping top margin: top_margin_resolved={}, escaped={}, \
2293 accumulated={}, node={}",
2294 top_margin_resolved,
2295 top_margin_escaped,
2296 accumulated_top_margin,
2297 node_index
2298 );
2299 }
2300
2301 if let Some(last_idx) = last_child_index {
2303 let last_child = tree.get(last_idx).ok_or(LayoutError::InvalidTree)?;
2304 let last_child_bp = last_child.box_props.unpack();
2305 let last_has_bottom_blocker =
2306 has_margin_collapse_blocker(&last_child_bp, writing_mode, false);
2307
2308 debug_info!(
2309 ctx,
2310 "[layout_bfc] Bottom margin for node {}: parent_has_bottom_blocker={}, \
2311 last_has_bottom_blocker={}, last_margin_bottom={}, main_pen_before={}",
2312 node_index,
2313 parent_has_bottom_blocker,
2314 last_has_bottom_blocker,
2315 last_margin_bottom,
2316 main_pen
2317 );
2318
2319 if !parent_has_bottom_blocker && !last_has_bottom_blocker && has_content {
2320 let collapsed_bottom = collapse_margins(parent_margin_bottom, last_margin_bottom);
2323 escaped_bottom_margin = Some(collapsed_bottom);
2324 debug_info!(
2325 ctx,
2326 "[layout_bfc] Bottom margin ESCAPED for node {}: collapsed={}",
2327 node_index,
2328 collapsed_bottom
2329 );
2330 } else if !parent_has_bottom_blocker && has_content {
2332 debug_info!(
2344 ctx,
2345 "[layout_bfc] Bottom margin of blocked last child ESCAPES content box for node \
2346 {}: last_margin_bottom={} (not added to height)",
2347 node_index,
2348 last_margin_bottom
2349 );
2350 } else {
2351 main_pen += last_margin_bottom;
2353 debug_info!(
2357 ctx,
2358 "[layout_bfc] Bottom margin BLOCKED for node {}: added last_margin_bottom={}, \
2359 main_pen_after={}",
2360 node_index,
2361 last_margin_bottom,
2362 main_pen
2363 );
2364 }
2365 } else {
2366 if !top_margin_resolved {
2368 main_pen += parent_margin_top;
2369 }
2370 main_pen += parent_margin_bottom;
2371 }
2372
2373 let is_root_node = node.parent.is_none();
2376 if is_root_node {
2377 if let Some(top) = escaped_top_margin {
2378 for pos in output.positions.values_mut() {
2380 let current_main = pos.main(writing_mode);
2381 *pos = LogicalPosition::from_main_cross(
2382 current_main + top,
2383 pos.cross(writing_mode),
2384 writing_mode,
2385 );
2386 }
2387 main_pen += top;
2388 }
2389 if let Some(bottom) = escaped_bottom_margin {
2390 main_pen += bottom;
2391 }
2392 escaped_top_margin = None;
2394 escaped_bottom_margin = None;
2395 }
2396
2397 let mut content_box_height = if is_root_node {
2478 main_pen - total_escaped_top_margin - escaped_bottom_margin.unwrap_or(0.0)
2482 } else {
2483 main_pen
2492 };
2493
2494 if is_bfc_root {
2500 for float_box in &float_context.floats {
2501 let float_bottom_margin_edge = float_box.rect.origin.main(writing_mode)
2502 + float_box.rect.size.main(writing_mode)
2503 + float_box.margin.main_end(writing_mode);
2504 if float_bottom_margin_edge > content_box_height {
2505 content_box_height = float_bottom_margin_edge;
2506 }
2507 }
2508 }
2509
2510 output.overflow_size =
2513 LogicalSize::from_main_cross(content_box_height, max_cross_size, writing_mode);
2514
2515 debug_info!(
2516 ctx,
2517 "[layout_bfc] FINAL for node {}: main_pen={}, total_escaped_top={}, \
2518 total_sibling_margins={}, content_box_height={}",
2519 node_index,
2520 main_pen,
2521 total_escaped_top_margin,
2522 total_sibling_margins,
2523 content_box_height
2524 );
2525
2526 output.baseline = None;
2531
2532 if let Some(warm_mut) = tree.warm_mut(node_index) {
2534 warm_mut.escaped_top_margin = escaped_top_margin;
2535 warm_mut.escaped_bottom_margin = escaped_bottom_margin;
2536 }
2537
2538 if let Some(warm_mut) = tree.warm_mut(node_index) {
2539 warm_mut.baseline = output.baseline;
2540 }
2541
2542 Ok(BfcLayoutResult {
2543 output,
2544 escaped_top_margin,
2545 escaped_bottom_margin,
2546 })
2547}
2548
2549#[allow(clippy::field_reassign_with_default)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn layout_ifc<T: ParsedFontTrait>(
2589 ctx: &mut LayoutContext<'_, T>,
2590 text_cache: &mut TextLayoutCache,
2591 tree: &mut LayoutTree,
2592 node_index: usize,
2593 constraints: &LayoutConstraints<'_>,
2594) -> Result<LayoutOutput> {
2595 unsafe { crate::az_mark(0x60704_u32, (0x20u32)); }
2596 #[cfg(feature = "web_lift")]
2601 unsafe {
2602 let slot = (node_index & 7) * 4;
2603 crate::az_mark(((0x60900 + slot)) as u32, (tree.nodes.len() as u32) as u32);
2604 crate::az_mark(((0x60920 + slot)) as u32, ((&*tree as *const LayoutTree as usize) as u32) as u32);
2605 }
2606 let float_count = constraints
2607 .bfc_state
2608 .as_ref()
2609 .map_or(0, |s| s.floats.floats.len());
2610 debug_info!(
2611 ctx,
2612 "[layout_ifc] ENTRY: node_index={}, has_bfc_state={}, float_count={}",
2613 node_index,
2614 constraints.bfc_state.is_some(),
2615 float_count
2616 );
2617 debug_ifc_layout!(ctx, "CALLED for node_index={}", node_index);
2618
2619 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
2624 let ifc_root_dom_id = if let Some(id) = node.dom_node_id { id } else {
2625 let parent_dom_id = node
2627 .parent
2628 .and_then(|p| tree.get(p))
2629 .and_then(|n| n.dom_node_id);
2630
2631 if let Some(id) = parent_dom_id {
2632 id
2633 } else {
2634 tree.children(node_index)
2636 .iter()
2637 .filter_map(|&child_idx| tree.get(child_idx)).find_map(|n| n.dom_node_id)
2638 .ok_or(LayoutError::InvalidTree)?
2639 }
2640 };
2641
2642 debug_ifc_layout!(ctx, "ifc_root_dom_id={:?}", ifc_root_dom_id);
2643
2644 let collect_result = collect_and_measure_inline_content(
2648 ctx,
2649 text_cache,
2650 tree,
2651 node_index,
2652 constraints,
2653 );
2654 #[cfg(feature = "web_lift")]
2656 unsafe {
2657 crate::az_mark((0x60680) as u32, (collect_result.as_ref().map(|(c, _)| c.len()).unwrap_or(0) as u32) as u32);
2658 crate::az_mark((0x60684) as u32, (if collect_result.is_ok() { 0xC0DE0680u32 } else { 0x000000EEu32 }) as u32);
2659 }
2660 let (inline_content, child_map) = collect_result?;
2661
2662 let text3_constraints =
2675 translate_to_text3_constraints(ctx, constraints, ctx.styled_dom, ifc_root_dom_id);
2676
2677 let current_content_hash = {
2678 use std::hash::{Hash, Hasher};
2679 let mut h = std::collections::hash_map::DefaultHasher::new();
2680 inline_content.hash(&mut h);
2681 text3_constraints.text_align.hash(&mut h);
2683 text3_constraints.text_align_last.hash(&mut h);
2684 text3_constraints.white_space_mode.hash(&mut h);
2685 text3_constraints.direction.hash(&mut h);
2686 text3_constraints.columns.hash(&mut h);
2687 text3_constraints.text_indent.to_bits().hash(&mut h);
2688 match text3_constraints.line_height {
2689 text3::cache::LineHeight::Normal => 0u64.hash(&mut h),
2690 text3::cache::LineHeight::Px(v) => {
2691 1u64.hash(&mut h);
2692 v.to_bits().hash(&mut h);
2693 }
2694 }
2695 h.finish()
2696 };
2697
2698 debug_info!(
2699 ctx,
2700 "[layout_ifc] Collected {} inline content items for node {}",
2701 inline_content.len(),
2702 node_index
2703 );
2704 for (i, item) in inline_content.iter().enumerate() {
2705 match item {
2706 InlineContent::Text(run) => debug_info!(ctx, " [{}] Text: '{}'", i, run.text),
2707 InlineContent::Marker {
2708 run,
2709 position_outside,
2710 } => debug_info!(
2711 ctx,
2712 " [{}] Marker: '{}' (outside={})",
2713 i,
2714 run.text,
2715 position_outside
2716 ),
2717 InlineContent::Shape(_) => debug_info!(ctx, " [{}] Shape", i),
2718 InlineContent::Image(_) => debug_info!(ctx, " [{}] Image", i),
2719 _ => debug_info!(ctx, " [{}] Other", i),
2720 }
2721 }
2722
2723 debug_ifc_layout!(
2724 ctx,
2725 "Collected {} inline content items",
2726 inline_content.len()
2727 );
2728
2729 if inline_content.is_empty() {
2730 debug_warning!(ctx, "inline_content is empty, returning default output!");
2731 if let Some(warm_node) = tree.warm_mut(node_index) {
2741 warm_node.inline_layout_result = None;
2742 }
2743 return Ok(LayoutOutput::default());
2744 }
2745
2746 {
2752 let cached_ifc = tree
2753 .warm(node_index)
2754 .and_then(|n| n.inline_layout_result.as_ref());
2755
2756 let resize_has_floats = constraints
2764 .bfc_state
2765 .as_ref()
2766 .is_some_and(|s| !s.floats.floats.is_empty());
2767 let cached_ifc = cached_ifc
2771 .filter(|c| c.is_valid_for(constraints.available_width_type, resize_has_floats))
2772 .filter(|c| c.inline_content_hash == current_content_hash);
2773
2774 if let Some(cached) = cached_ifc {
2775 if let Some(ref line_breaks) = cached.line_breaks {
2776 let old_advances: Vec<f32> = cached.item_metrics.iter()
2778 .map(|m| m.advance_width)
2779 .collect();
2780
2781 let result = text3::cache::try_incremental_relayout(
2791 &[], &old_advances,
2793 &old_advances, line_breaks,
2795 );
2796
2797 if matches!(result, text3::cache::IncrementalRelayoutResult::GlyphSwap) {
2798 debug_info!(ctx, "[layout_ifc] Phase 2d: GlyphSwap — reusing cached layout");
2800 let main_frag = &cached.layout;
2801 let frag_bounds = main_frag.bounds();
2802 let mut output = LayoutOutput::default();
2803 output.overflow_size = LogicalSize::new(frag_bounds.width, frag_bounds.height);
2804 output.baseline = main_frag.last_baseline();
2805 for positioned_item in &main_frag.items {
2807 if let ShapedItem::Object { source, .. } = &positioned_item.item {
2808 if let Some(&child_node_index) = child_map.get(source) {
2809 output.positions.insert(child_node_index, LogicalPosition {
2810 x: positioned_item.position.x,
2811 y: positioned_item.position.y,
2812 });
2813 }
2814 }
2815 }
2816 return Ok(output);
2817 }
2818 }
2820 }
2821 }
2822
2823 let cached_constraints = text3_constraints.clone();
2827
2828 debug_info!(
2829 ctx,
2830 "[layout_ifc] CALLING text_cache.layout_flow for node {} with {} exclusions",
2831 node_index,
2832 text3_constraints.shape_exclusions.len()
2833 );
2834
2835 let fragments = vec![LayoutFragment {
2836 id: "main".to_string(),
2837 constraints: text3_constraints,
2838 }];
2839
2840 let loaded_fonts = ctx.font_manager.get_loaded_fonts();
2843 let text_layout_result = match text_cache.layout_flow(
2844 &inline_content,
2845 &[],
2846 &fragments,
2847 &ctx.font_manager.font_chain_cache,
2848 &ctx.font_manager.fc_cache,
2849 &loaded_fonts,
2850 ctx.debug_messages,
2851 ) {
2852 Ok(result) => {
2853 #[cfg(feature = "web_lift")]
2855 unsafe { crate::az_mark((0x60688) as u32, (0xC0DE0688u32) as u32); }
2856 result
2857 }
2858 Err(e) => {
2859 #[cfg(feature = "web_lift")]
2861 unsafe {
2862 crate::az_mark((0x60688) as u32, (0x000000EEu32) as u32);
2863 crate::az_mark((0x6068C) as u32, (*(&e as *const _ as *const u8)) as u32);
2867 }
2868 debug_warning!(ctx, "Text layout failed: {:?}", e);
2871 debug_warning!(
2872 ctx,
2873 "Continuing with zero-sized layout for node {}",
2874 node_index
2875 );
2876
2877 let mut output = LayoutOutput::default();
2878 output.overflow_size = LogicalSize::new(0.0, 0.0);
2879 return Ok(output);
2880 }
2881 };
2882 let mut output = LayoutOutput::default();
2884
2885 debug_ifc_layout!(
2886 ctx,
2887 "text_layout_result has {} fragment_layouts",
2888 text_layout_result.fragment_layouts.len()
2889 );
2890
2891 if let Some(main_frag) = text_layout_result.fragment_layouts.get("main") {
2892 let frag_bounds = main_frag.bounds();
2893 debug_ifc_layout!(
2894 ctx,
2895 "Found 'main' fragment with {} items, bounds={}x{}",
2896 main_frag.items.len(),
2897 frag_bounds.width,
2898 frag_bounds.height
2899 );
2900 debug_ifc_layout!(ctx, "Storing inline_layout_result on node {}", node_index);
2901
2902 let has_floats = constraints
2913 .bfc_state
2914 .as_ref()
2915 .is_some_and(|s| !s.floats.floats.is_empty());
2916 let current_width_type = constraints.available_width_type;
2917
2918 let warm_node = tree.warm_mut(node_index).ok_or(LayoutError::InvalidTree)?;
2919
2920 let should_store = match &warm_node.inline_layout_result {
2921 None => {
2922 debug_info!(
2924 ctx,
2925 "[layout_ifc] Storing NEW inline_layout_result for node {} (width_type={:?}, \
2926 has_floats={})",
2927 node_index,
2928 current_width_type,
2929 has_floats
2930 );
2931 true
2932 }
2933 Some(cached) => {
2934 if cached.should_replace_with(current_width_type, has_floats)
2936 || cached.inline_content_hash != current_content_hash
2937 {
2938 debug_info!(
2942 ctx,
2943 "[layout_ifc] REPLACING inline_layout_result for node {} (old: \
2944 width={:?}, floats={}) with (new: width={:?}, floats={})",
2945 node_index,
2946 cached.available_width,
2947 cached.has_floats,
2948 current_width_type,
2949 has_floats
2950 );
2951 true
2952 } else {
2953 debug_info!(
2954 ctx,
2955 "[layout_ifc] KEEPING cached inline_layout_result for node {} (cached: \
2956 width={:?}, floats={}, new: width={:?}, floats={})",
2957 node_index,
2958 cached.available_width,
2959 cached.has_floats,
2960 current_width_type,
2961 has_floats
2962 );
2963 false
2964 }
2965 }
2966 };
2967
2968 if should_store {
2969 let mut cil = CachedInlineLayout::new_with_constraints(
2970 main_frag.clone(),
2971 current_width_type,
2972 has_floats,
2973 cached_constraints.clone(),
2974 );
2975 cil.inline_content_hash = current_content_hash;
2979 warm_node.inline_layout_result = Some(cil);
2980 }
2981
2982 output.overflow_size = LogicalSize::new(frag_bounds.width, frag_bounds.height);
2986 output.baseline = main_frag.last_baseline();
2987 warm_node.baseline = output.baseline;
2988
2989 let ifc_node_state = &ctx.styled_dom.styled_nodes.as_container()[ifc_root_dom_id].styled_node_state;
2996 let text_box_trim = {
2999 let skip = ctx.styled_dom
3000 .css_property_cache
3001 .ptr
3002 .compact_cache
3003 .as_ref()
3004 .is_some_and(|cc| cc.dom_declared_flags & azul_css::compact_cache::DOM_HAS_TEXT_BOX_TRIM == 0);
3005 if skip {
3006 StyleTextBoxTrim::None
3007 } else {
3008 get_text_box_trim_property(ctx.styled_dom, ifc_root_dom_id, ifc_node_state)
3009 .unwrap_or(StyleTextBoxTrim::None)
3010 }
3011 };
3012
3013 if text_box_trim != StyleTextBoxTrim::None && !main_frag.items.is_empty() {
3014 let half_leading = (cached_constraints.resolved_line_height()
3016 - (cached_constraints.strut_ascent + cached_constraints.strut_descent))
3017 / 2.0;
3018 let half_leading = half_leading.max(0.0);
3019
3020 let has_pad_or_border_top = match get_css_padding_top(ctx.styled_dom, ifc_root_dom_id, ifc_node_state) {
3022 MultiValue::Exact(pv) => pv.number.get() != 0.0,
3023 _ => false,
3024 } || match get_css_border_top_width(ctx.styled_dom, ifc_root_dom_id, ifc_node_state) {
3025 MultiValue::Exact(pv) => pv.number.get() != 0.0,
3026 _ => false,
3027 };
3028
3029 let has_pad_or_border_bottom = match get_css_padding_bottom(ctx.styled_dom, ifc_root_dom_id, ifc_node_state) {
3031 MultiValue::Exact(pv) => pv.number.get() != 0.0,
3032 _ => false,
3033 } || match get_css_border_bottom_width(ctx.styled_dom, ifc_root_dom_id, ifc_node_state) {
3034 MultiValue::Exact(pv) => pv.number.get() != 0.0,
3035 _ => false,
3036 };
3037
3038 let trim_start = matches!(text_box_trim, StyleTextBoxTrim::TrimStart | StyleTextBoxTrim::TrimBoth)
3039 && !has_pad_or_border_top;
3040 let trim_end = matches!(text_box_trim, StyleTextBoxTrim::TrimEnd | StyleTextBoxTrim::TrimBoth)
3041 && !has_pad_or_border_bottom;
3042
3043 let mut height_reduction = 0.0;
3044 if trim_start && half_leading > 0.0 {
3045 height_reduction += half_leading;
3046 }
3047 if trim_end && half_leading > 0.0 {
3048 height_reduction += half_leading;
3049 }
3050
3051 if height_reduction > 0.0 {
3052 output.overflow_size.height = (output.overflow_size.height - height_reduction).max(0.0);
3053 }
3054 }
3055
3056 for positioned_item in &main_frag.items {
3059 if let ShapedItem::Object { source, content, .. } = &positioned_item.item {
3060 if let Some(&child_node_index) = child_map.get(source) {
3061 let new_relative_pos = LogicalPosition {
3063 x: positioned_item.position.x,
3064 y: positioned_item.position.y,
3065 };
3066 output.positions.insert(child_node_index, new_relative_pos);
3067 }
3068 }
3069 }
3070 }
3071
3072 #[cfg(feature = "web_lift")]
3076 unsafe {
3077 crate::az_mark((0x60670) as u32, (output.overflow_size.width.to_bits()) as u32);
3078 crate::az_mark((0x60674) as u32, (output.overflow_size.height.to_bits()) as u32);
3079 crate::az_mark((0x60678) as u32, (output.positions.len() as u32) as u32);
3080 crate::az_mark((0x6067C) as u32, (0xC0DE0132u32) as u32);
3081 }
3082
3083 Ok(output)
3084}
3085
3086const fn translate_taffy_size(size: LogicalSize) -> TaffySize<Option<f32>> {
3087 TaffySize {
3088 width: Some(size.width),
3089 height: Some(size.height),
3090 }
3091}
3092
3093#[must_use] pub const fn convert_font_style(style: StyleFontStyle) -> crate::font_traits::FontStyle {
3095 match style {
3096 StyleFontStyle::Normal => crate::font_traits::FontStyle::Normal,
3097 StyleFontStyle::Italic => crate::font_traits::FontStyle::Italic,
3098 StyleFontStyle::Oblique => crate::font_traits::FontStyle::Oblique,
3099 }
3100}
3101
3102#[must_use] pub const fn convert_font_weight(weight: StyleFontWeight) -> FcWeight {
3104 match weight {
3105 StyleFontWeight::W100 => FcWeight::Thin,
3106 StyleFontWeight::W200 => FcWeight::ExtraLight,
3107 StyleFontWeight::W300 | StyleFontWeight::Lighter => FcWeight::Light,
3108 StyleFontWeight::Normal => FcWeight::Normal,
3109 StyleFontWeight::W500 => FcWeight::Medium,
3110 StyleFontWeight::W600 => FcWeight::SemiBold,
3111 StyleFontWeight::Bold => FcWeight::Bold,
3112 StyleFontWeight::W800 => FcWeight::ExtraBold,
3113 StyleFontWeight::W900 | StyleFontWeight::Bolder => FcWeight::Black,
3114 }
3115}
3116
3117#[inline]
3126fn resolve_size_metric(
3127 metric: SizeMetric,
3128 value: f32,
3129 containing_block_size: f32,
3130 viewport_size: LogicalSize,
3131 element_font_size: f32,
3132 root_font_size: f32,
3133) -> f32 {
3134 match metric {
3135 SizeMetric::Px => value,
3136 SizeMetric::Pt => value * PT_TO_PX,
3137 SizeMetric::Percent => value / 100.0 * containing_block_size,
3138 SizeMetric::Em => value * element_font_size,
3139 SizeMetric::Rem => value * root_font_size,
3140 SizeMetric::Vw => value / 100.0 * viewport_size.width,
3141 SizeMetric::Vh => value / 100.0 * viewport_size.height,
3142 SizeMetric::Vmin => value / 100.0 * viewport_size.width.min(viewport_size.height),
3143 SizeMetric::Vmax => value / 100.0 * viewport_size.width.max(viewport_size.height),
3144 SizeMetric::In => value * super::calc::PX_PER_INCH,
3145 SizeMetric::Cm => value * super::calc::PX_PER_INCH / super::calc::CM_PER_INCH,
3146 SizeMetric::Mm => value * super::calc::PX_PER_INCH / super::calc::MM_PER_INCH,
3147 }
3148}
3149
3150#[must_use] pub const fn translate_taffy_size_back(size: TaffySize<f32>) -> LogicalSize {
3151 LogicalSize {
3152 width: size.width,
3153 height: size.height,
3154 }
3155}
3156
3157#[must_use] pub const fn translate_taffy_point_back(point: taffy::Point<f32>) -> LogicalPosition {
3158 LogicalPosition {
3159 x: point.x,
3160 y: point.y,
3161 }
3162}
3163
3164fn establishes_new_bfc<T: ParsedFontTrait>(ctx: &LayoutContext<'_, T>, node: &LayoutNodeHot, cold: Option<&LayoutNodeCold>) -> bool {
3184 if cold.and_then(|c| c.anonymous_type) == Some(AnonymousBoxType::TableWrapper) {
3189 return true;
3190 }
3191 let Some(dom_id) = node.dom_node_id else {
3192 return false;
3193 };
3194
3195 let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
3196
3197 let float_val = get_float(ctx.styled_dom, dom_id, node_state);
3199 if matches!(
3200 float_val,
3201 MultiValue::Exact(LayoutFloat::Left | LayoutFloat::Right)
3202 ) {
3203 return true;
3204 }
3205
3206 let position = get_position_type(ctx.styled_dom, Some(dom_id));
3208 if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
3209 return true;
3210 }
3211
3212 let display = get_display_property(ctx.styled_dom, Some(dom_id));
3214 if matches!(
3215 display,
3216 MultiValue::Exact(
3217 LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption
3218 )
3219 ) {
3220 return true;
3221 }
3222
3223 if matches!(display, MultiValue::Exact(LayoutDisplay::FlowRoot)) {
3226 return true;
3227 }
3228
3229 let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, node_state);
3238 let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, node_state);
3239
3240 let creates_bfc_via_overflow = |ov: &MultiValue<LayoutOverflow>| {
3241 matches!(
3242 ov,
3243 &MultiValue::Exact(
3244 LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto
3245 )
3246 )
3247 };
3248
3249 if creates_bfc_via_overflow(&overflow_x) || creates_bfc_via_overflow(&overflow_y) {
3250 return true;
3251 }
3252
3253 if matches!(
3256 node.formatting_context,
3257 FormattingContext::Table | FormattingContext::Flex | FormattingContext::Grid
3258 ) {
3259 return true;
3260 }
3261
3262 {
3269 let hierarchy = ctx.styled_dom.node_hierarchy.as_container();
3270 if let Some(parent_dom_id) = hierarchy[dom_id].parent_id() {
3271 let parent_display = get_display_property(ctx.styled_dom, Some(parent_dom_id));
3272 if matches!(
3273 parent_display,
3274 MultiValue::Exact(
3275 LayoutDisplay::Flex
3276 | LayoutDisplay::InlineFlex
3277 | LayoutDisplay::Grid
3278 | LayoutDisplay::InlineGrid
3279 )
3280 ) {
3281 return true;
3282 }
3283 }
3284 }
3285
3286 {
3290 let hierarchy = ctx.styled_dom.node_hierarchy.as_container();
3291 if let Some(parent_dom_id) = hierarchy[dom_id].parent_id() {
3292 let parent_state = &ctx.styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
3293 let child_wm = get_writing_mode(ctx.styled_dom, dom_id, node_state).unwrap_or_default();
3294 let parent_wm = get_writing_mode(ctx.styled_dom, parent_dom_id, parent_state).unwrap_or_default();
3295 if child_wm != parent_wm {
3296 return true;
3297 }
3298 }
3299 }
3300
3301 false
3304}
3305
3306fn is_block_level_replaced<T: ParsedFontTrait>(ctx: &LayoutContext<'_, T>, node: &LayoutNodeHot) -> bool {
3311 let Some(dom_id) = node.dom_node_id else {
3312 return false;
3313 };
3314
3315 let display = get_display_property(ctx.styled_dom, Some(dom_id));
3317 let is_block_level = matches!(
3318 display,
3319 MultiValue::Exact(LayoutDisplay::Block | LayoutDisplay::ListItem | LayoutDisplay::FlowRoot)
3320 );
3321
3322 if !is_block_level {
3323 return false;
3324 }
3325
3326 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3328 matches!(
3329 node_data.get_node_type(),
3330 NodeType::Image(_)
3331 )
3332}
3333
3334#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn translate_to_text3_constraints<'a, T: ParsedFontTrait>(
3338 ctx: &mut LayoutContext<'_, T>,
3339 constraints: &'a LayoutConstraints<'a>,
3340 styled_dom: &StyledDom,
3341 dom_id: NodeId,
3342) -> UnifiedConstraints {
3343 use azul_css::compact_cache::{
3344 DOM_HAS_SHAPE_INSIDE, DOM_HAS_SHAPE_OUTSIDE, DOM_HAS_TEXT_JUSTIFY,
3345 DOM_HAS_TEXT_INDENT, DOM_HAS_COLUMN_COUNT, DOM_HAS_COLUMN_GAP,
3346 DOM_HAS_COLUMN_WIDTH,
3347 DOM_HAS_INITIAL_LETTER, DOM_HAS_INITIAL_LETTER_ALIGN,
3348 DOM_HAS_LINE_CLAMP, DOM_HAS_HANGING_PUNCTUATION,
3349 DOM_HAS_TEXT_COMBINE_UPRIGHT, DOM_HAS_EXCLUSION_MARGIN,
3350 DOM_HAS_SHAPE_MARGIN,
3351 DOM_HAS_HYPHENATION_LANGUAGE, DOM_HAS_UNICODE_BIDI,
3352 DOM_HAS_HYPHENS, DOM_HAS_WORD_BREAK, DOM_HAS_OVERFLOW_WRAP,
3353 DOM_HAS_LINE_BREAK, DOM_HAS_TEXT_ALIGN_LAST, DOM_HAS_LINE_HEIGHT,
3354 };
3355 unsafe { crate::az_mark(0x60704_u32, (0x30u32)); }
3356 let dom_declared = styled_dom
3361 .css_property_cache
3362 .ptr
3363 .compact_cache
3364 .as_ref()
3365 .map_or(!0u32, |cc| cc.dom_declared_flags);
3366
3367 let mut shape_exclusions = if let Some(ref bfc_state) = constraints.bfc_state {
3369 debug_info!(
3370 ctx,
3371 "[translate_to_text3] dom_id={:?}, converting {} floats to exclusions",
3372 dom_id,
3373 bfc_state.floats.floats.len()
3374 );
3375 bfc_state
3376 .floats
3377 .floats
3378 .iter()
3379 .enumerate()
3380 .map(|(i, float_box)| {
3381 let rect = text3::cache::Rect {
3382 x: float_box.rect.origin.x,
3383 y: float_box.rect.origin.y,
3384 width: float_box.rect.size.width,
3385 height: float_box.rect.size.height,
3386 };
3387 debug_info!(
3388 ctx,
3389 "[translate_to_text3] Exclusion #{}: {:?} at ({}, {}) size {}x{}",
3390 i,
3391 float_box.kind,
3392 rect.x,
3393 rect.y,
3394 rect.width,
3395 rect.height
3396 );
3397 ShapeBoundary::Rectangle(rect)
3398 })
3399 .collect()
3400 } else {
3401 debug_info!(
3402 ctx,
3403 "[translate_to_text3] dom_id={:?}, NO bfc_state - no float exclusions",
3404 dom_id
3405 );
3406 Vec::new()
3407 };
3408
3409 debug_info!(
3410 ctx,
3411 "[translate_to_text3] dom_id={:?}, available_size={}x{}, shape_exclusions.len()={}",
3412 dom_id,
3413 constraints.available_size.width,
3414 constraints.available_size.height,
3415 shape_exclusions.len()
3416 );
3417
3418 let id = dom_id;
3420 let node_data = &styled_dom.node_data.as_container()[id];
3421 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3422
3423 let ref_box_height = if constraints.available_size.height.is_finite() {
3428 constraints.available_size.height
3429 } else {
3430 styled_dom
3434 .css_property_cache
3435 .ptr
3436 .get_height(node_data, &id, node_state)
3437 .and_then(|v| v.get_property())
3438 .and_then(|h| match h {
3439 LayoutHeight::Px(v) => {
3440 match v.metric {
3443 SizeMetric::Px => Some(v.number.get()),
3444 SizeMetric::Pt => Some(v.number.get() * PT_TO_PX),
3445 SizeMetric::In => Some(v.number.get() * super::calc::PX_PER_INCH),
3446 SizeMetric::Cm => Some(v.number.get() * super::calc::PX_PER_INCH / super::calc::CM_PER_INCH),
3447 SizeMetric::Mm => Some(v.number.get() * super::calc::PX_PER_INCH / super::calc::MM_PER_INCH),
3448 _ => None, }
3450 }
3451 _ => None,
3452 })
3453 .unwrap_or(constraints.available_size.width) };
3455
3456 let reference_box = text3::cache::Rect {
3457 x: 0.0,
3458 y: 0.0,
3459 width: constraints.available_size.width,
3460 height: ref_box_height,
3461 };
3462
3463 debug_info!(ctx, "Checking shape-inside for node {:?}", id);
3465 debug_info!(
3466 ctx,
3467 "Reference box: {:?} (available_size height was: {})",
3468 reference_box,
3469 constraints.available_size.height
3470 );
3471
3472 let shape_boundaries = if dom_declared & DOM_HAS_SHAPE_INSIDE != 0 {
3473 styled_dom
3474 .css_property_cache
3475 .ptr
3476 .get_shape_inside(node_data, &id, node_state)
3477 .and_then(|v| {
3478 debug_info!(ctx, "Got shape-inside value: {:?}", v);
3479 v.get_property()
3480 })
3481 .and_then(|shape_inside| {
3482 debug_info!(ctx, "shape-inside property: {:?}", shape_inside);
3483 if let ShapeInside::Shape(css_shape) = shape_inside {
3484 debug_info!(
3485 ctx,
3486 "Converting CSS shape to ShapeBoundary: {:?}",
3487 css_shape
3488 );
3489 let boundary =
3490 ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
3491 debug_info!(ctx, "Created ShapeBoundary: {:?}", boundary);
3492 Some(vec![boundary])
3493 } else {
3494 debug_info!(ctx, "shape-inside is None");
3495 None
3496 }
3497 })
3498 .unwrap_or_default()
3499 } else {
3500 Vec::new()
3501 };
3502
3503 debug_info!(
3504 ctx,
3505 "Final shape_boundaries count: {}",
3506 shape_boundaries.len()
3507 );
3508
3509 debug_info!(ctx, "Checking shape-outside for node {:?}", id);
3511 if dom_declared & DOM_HAS_SHAPE_OUTSIDE != 0 {
3512 if let Some(shape_outside_value) = styled_dom
3513 .css_property_cache
3514 .ptr
3515 .get_shape_outside(node_data, &id, node_state)
3516 {
3517 debug_info!(ctx, "Got shape-outside value: {:?}", shape_outside_value);
3518 if let Some(shape_outside) = shape_outside_value.get_property() {
3519 debug_info!(ctx, "shape-outside property: {:?}", shape_outside);
3520 if let ShapeOutside::Shape(css_shape) = shape_outside {
3521 debug_info!(
3522 ctx,
3523 "Converting CSS shape-outside to ShapeBoundary: {:?}",
3524 css_shape
3525 );
3526 let boundary =
3527 ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
3528 debug_info!(ctx, "Created ShapeBoundary (exclusion): {:?}", boundary);
3529 shape_exclusions.push(boundary);
3530 }
3531 }
3532 } else {
3533 debug_info!(ctx, "No shape-outside value found");
3534 }
3535 }
3536
3537 let writing_mode = get_writing_mode(styled_dom, id, node_state).unwrap_or_default();
3540
3541 let text_align = get_text_align(styled_dom, id, node_state).unwrap_or_default();
3542
3543 let text_justify = if dom_declared & DOM_HAS_TEXT_JUSTIFY != 0 {
3544 styled_dom
3545 .css_property_cache
3546 .ptr
3547 .get_text_justify(node_data, &id, node_state)
3548 .and_then(|s| s.get_property().copied())
3549 .unwrap_or_default()
3550 } else {
3551 LayoutTextJustify::default()
3552 };
3553
3554 let font_size = get_element_font_size(styled_dom, id, node_state);
3557
3558 let line_height_value = if dom_declared & DOM_HAS_LINE_HEIGHT != 0 {
3559 styled_dom
3560 .css_property_cache
3561 .ptr
3562 .get_line_height(node_data, &id, node_state)
3563 .and_then(|s| s.get_property().copied())
3564 .unwrap_or_default()
3565 } else {
3566 azul_css::props::style::text::StyleLineHeight::default()
3567 };
3568
3569 let hyphenation = if dom_declared & DOM_HAS_HYPHENS != 0 {
3570 styled_dom
3571 .css_property_cache
3572 .ptr
3573 .get_hyphens(node_data, &id, node_state)
3574 .and_then(|s| s.get_property().copied())
3575 .unwrap_or_default()
3576 } else {
3577 StyleHyphens::default()
3578 };
3579
3580 let word_break_css = if dom_declared & DOM_HAS_WORD_BREAK != 0 {
3581 styled_dom
3582 .css_property_cache
3583 .ptr
3584 .get_word_break(node_data, &id, node_state)
3585 .and_then(|s| s.get_property().copied())
3586 .unwrap_or_default()
3587 } else {
3588 StyleWordBreak::default()
3589 };
3590
3591 let overflow_wrap_css = if dom_declared & DOM_HAS_OVERFLOW_WRAP != 0 {
3592 styled_dom
3593 .css_property_cache
3594 .ptr
3595 .get_overflow_wrap(node_data, &id, node_state)
3596 .and_then(|s| s.get_property().copied())
3597 .unwrap_or_default()
3598 } else {
3599 StyleOverflowWrap::default()
3600 };
3601
3602 let line_break_css = if dom_declared & DOM_HAS_LINE_BREAK != 0 {
3603 styled_dom
3604 .css_property_cache
3605 .ptr
3606 .get_line_break(node_data, &id, node_state)
3607 .and_then(|s| s.get_property().copied())
3608 .unwrap_or_default()
3609 } else {
3610 StyleLineBreak::default()
3611 };
3612
3613 let text_align_last_css = if dom_declared & DOM_HAS_TEXT_ALIGN_LAST != 0 {
3614 styled_dom
3615 .css_property_cache
3616 .ptr
3617 .get_text_align_last(node_data, &id, node_state)
3618 .and_then(|s| s.get_property().copied())
3619 .unwrap_or_default()
3620 } else {
3621 StyleTextAlignLast::default()
3622 };
3623
3624 let overflow_behaviour = get_overflow_x(styled_dom, id, node_state).unwrap_or_default();
3625
3626 let vertical_align = match get_vertical_align_property(styled_dom, id, node_state) {
3645 MultiValue::Exact(v) => v,
3646 _ => StyleVerticalAlign::default(),
3647 };
3648
3649 let vertical_align = match vertical_align {
3651 StyleVerticalAlign::Baseline => text3::cache::VerticalAlign::Baseline,
3652 StyleVerticalAlign::Top => text3::cache::VerticalAlign::Top,
3653 StyleVerticalAlign::Middle => text3::cache::VerticalAlign::Middle,
3654 StyleVerticalAlign::Bottom => text3::cache::VerticalAlign::Bottom,
3655 StyleVerticalAlign::Sub => text3::cache::VerticalAlign::Sub,
3656 StyleVerticalAlign::Superscript => text3::cache::VerticalAlign::Super,
3659 StyleVerticalAlign::TextTop => text3::cache::VerticalAlign::TextTop,
3660 StyleVerticalAlign::TextBottom => text3::cache::VerticalAlign::TextBottom,
3661 StyleVerticalAlign::Percentage(p) => {
3663 let lh_n = line_height_value.inner.normalized();
3664 let resolved_lh = if lh_n < 0.0 { -lh_n } else { lh_n * font_size };
3665 let offset = p.normalized() * resolved_lh;
3666 text3::cache::VerticalAlign::Offset(offset)
3667 }
3668 StyleVerticalAlign::Length(l) => {
3670 let offset = super::calc::resolve_pixel_value_with_viewport(
3673 &l,
3674 0.0,
3675 font_size,
3676 font_size,
3677 ctx.viewport_size.width,
3678 ctx.viewport_size.height,
3679 );
3680 text3::cache::VerticalAlign::Offset(offset)
3681 }
3682 };
3683 let text_orientation = match get_text_orientation_property(styled_dom, id, node_state) {
3688 MultiValue::Exact(o) => match o {
3689 StyleTextOrientation::Mixed => text3::cache::TextOrientation::Mixed,
3690 StyleTextOrientation::Upright => text3::cache::TextOrientation::Upright,
3691 StyleTextOrientation::Sideways => text3::cache::TextOrientation::Sideways,
3693 },
3694 _ => text3::cache::TextOrientation::default(),
3695 };
3696
3697 let direction = match constraints.writing_mode {
3707 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr
3708 if matches!(text_orientation, text3::cache::TextOrientation::Upright) =>
3709 {
3710 Some(text3::cache::BidiDirection::Ltr)
3711 }
3712 _ => match get_direction_property(styled_dom, id, node_state) {
3713 MultiValue::Exact(d) => Some(match d {
3714 StyleDirection::Ltr => text3::cache::BidiDirection::Ltr,
3715 StyleDirection::Rtl => text3::cache::BidiDirection::Rtl,
3716 }),
3717 _ => None,
3718 },
3719 };
3720
3721 let unicode_bidi_val = if dom_declared & DOM_HAS_UNICODE_BIDI != 0 {
3724 match get_unicode_bidi_property(styled_dom, id, node_state) {
3725 MultiValue::Exact(u) => match u {
3726 StyleUnicodeBidi::Normal => text3::cache::UnicodeBidi::Normal,
3727 StyleUnicodeBidi::Embed => text3::cache::UnicodeBidi::Embed,
3728 StyleUnicodeBidi::Isolate => text3::cache::UnicodeBidi::Isolate,
3729 StyleUnicodeBidi::BidiOverride => text3::cache::UnicodeBidi::BidiOverride,
3730 StyleUnicodeBidi::IsolateOverride => text3::cache::UnicodeBidi::IsolateOverride,
3731 StyleUnicodeBidi::Plaintext => text3::cache::UnicodeBidi::Plaintext,
3732 },
3733 _ => text3::cache::UnicodeBidi::Normal,
3734 }
3735 } else {
3736 text3::cache::UnicodeBidi::Normal
3737 };
3738
3739 debug_info!(
3740 ctx,
3741 "dom_id={:?}, available_size={}x{}, setting available_width={}",
3742 dom_id,
3743 constraints.available_size.width,
3744 constraints.available_size.height,
3745 constraints.available_size.width
3746 );
3747
3748 let text_indent_prop = if dom_declared & DOM_HAS_TEXT_INDENT != 0 {
3753 styled_dom
3754 .css_property_cache
3755 .ptr
3756 .get_text_indent(node_data, &id, node_state)
3757 .and_then(|s| s.get_property().copied())
3758 } else {
3759 None
3760 };
3761 let is_intrinsic_sizing = matches!(
3762 constraints.available_width_type,
3763 Text3AvailableSpace::MinContent | Text3AvailableSpace::MaxContent
3764 );
3765 let text_indent = text_indent_prop
3767 .map_or(0.0, |ti| {
3768 if is_intrinsic_sizing && ti.inner.to_percent().is_some() {
3771 return 0.0;
3772 }
3773 let context = ResolutionContext {
3774 element_font_size: get_element_font_size(styled_dom, id, node_state),
3775 parent_font_size: get_parent_font_size(styled_dom, id, node_state),
3776 root_font_size: get_root_font_size(styled_dom, node_state),
3777 containing_block_size: PhysicalSize::new(constraints.available_size.width, 0.0),
3778 element_size: None,
3779 viewport_size: PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
3780 };
3781 ti.inner
3782 .resolve_with_context(&context, PropertyContext::Other)
3783 });
3784 let text_indent_each_line = text_indent_prop.is_some_and(|ti| ti.each_line);
3785 let text_indent_hanging = text_indent_prop.is_some_and(|ti| ti.hanging);
3786
3787 let column_resolve_ctx = ResolutionContext {
3790 element_font_size: get_element_font_size(styled_dom, id, node_state),
3791 parent_font_size: get_parent_font_size(styled_dom, id, node_state),
3792 root_font_size: get_root_font_size(styled_dom, node_state),
3793 containing_block_size: PhysicalSize::new(0.0, 0.0),
3794 element_size: None,
3795 viewport_size: PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
3796 };
3797
3798 macro_rules! declared_prop {
3801 ($bit:expr, $getter:ident) => {
3802 if dom_declared & $bit != 0 {
3803 styled_dom
3804 .css_property_cache
3805 .ptr
3806 .$getter(node_data, &id, node_state)
3807 .and_then(|s| s.get_property())
3808 } else {
3809 None
3810 }
3811 };
3812 }
3813
3814 let column_gap = declared_prop!(DOM_HAS_COLUMN_GAP, get_column_gap)
3816 .map(|cg| {
3817 cg.inner
3818 .resolve_with_context(&column_resolve_ctx, PropertyContext::Other)
3819 })
3820 .unwrap_or_else(|| get_element_font_size(styled_dom, id, node_state));
3821
3822 let column_width =
3824 declared_prop!(DOM_HAS_COLUMN_WIDTH, get_column_width).and_then(|cw| match cw {
3825 ColumnWidth::Auto => None,
3826 ColumnWidth::Length(px) => {
3827 Some(px.resolve_with_context(&column_resolve_ctx, PropertyContext::Other))
3828 }
3829 });
3830
3831 let explicit_column_count =
3833 declared_prop!(DOM_HAS_COLUMN_COUNT, get_column_count).copied();
3834
3835 let columns = match (explicit_column_count, column_width) {
3838 (Some(ColumnCount::Integer(n)), _) => n,
3839 (_, Some(cw)) if cw > 0.0 => {
3840 let avail = constraints.available_size.width;
3841 ((avail + column_gap) / (cw + column_gap)).floor().max(1.0) as u32
3842 }
3843 _ => 1,
3844 };
3845
3846 let resolved_ws = match get_white_space_property(styled_dom, id, node_state) {
3849 MultiValue::Exact(ws) => ws,
3850 _ => StyleWhiteSpace::Normal,
3851 };
3852 let text_wrap = match resolved_ws {
3853 StyleWhiteSpace::Normal
3854 | StyleWhiteSpace::PreWrap
3855 | StyleWhiteSpace::PreLine
3856 | StyleWhiteSpace::BreakSpaces => text3::cache::TextWrap::Wrap,
3857 StyleWhiteSpace::Nowrap | StyleWhiteSpace::Pre => text3::cache::TextWrap::NoWrap,
3858 };
3859 let white_space_mode = match resolved_ws {
3860 StyleWhiteSpace::Normal => text3::cache::WhiteSpaceMode::Normal,
3861 StyleWhiteSpace::Nowrap => text3::cache::WhiteSpaceMode::Nowrap,
3862 StyleWhiteSpace::Pre => text3::cache::WhiteSpaceMode::Pre,
3863 StyleWhiteSpace::PreWrap => text3::cache::WhiteSpaceMode::PreWrap,
3864 StyleWhiteSpace::PreLine => text3::cache::WhiteSpaceMode::PreLine,
3865 StyleWhiteSpace::BreakSpaces => text3::cache::WhiteSpaceMode::BreakSpaces,
3866 };
3867
3868 let initial_letter_align = if dom_declared & DOM_HAS_INITIAL_LETTER_ALIGN != 0 {
3887 styled_dom
3888 .css_property_cache
3889 .ptr
3890 .get_initial_letter_align(node_data, &id, node_state)
3891 .and_then(|s| s.get_property())
3892 .map_or(text3::cache::InitialLetterAlign::Auto, |a| match a {
3893 azul_css::props::style::text::StyleInitialLetterAlign::Auto => text3::cache::InitialLetterAlign::Auto,
3894 azul_css::props::style::text::StyleInitialLetterAlign::Alphabetic => text3::cache::InitialLetterAlign::Alphabetic,
3895 azul_css::props::style::text::StyleInitialLetterAlign::Hanging => text3::cache::InitialLetterAlign::Hanging,
3896 azul_css::props::style::text::StyleInitialLetterAlign::Ideographic => text3::cache::InitialLetterAlign::Ideographic,
3897 })
3898 } else {
3899 text3::cache::InitialLetterAlign::Auto
3900 };
3901 let initial_letter = if dom_declared & DOM_HAS_INITIAL_LETTER != 0 {
3910 styled_dom
3911 .css_property_cache
3912 .ptr
3913 .get_initial_letter(node_data, &id, node_state)
3914 .and_then(|s| s.get_property())
3915 .map(|il| {
3916 use std::num::NonZeroUsize;
3917 let sink = match il.sink {
3918 azul_css::corety::OptionU32::Some(s) => s,
3919 azul_css::corety::OptionU32::None => il.size, };
3921 text3::cache::InitialLetter {
3922 size: il.size as f32,
3923 sink,
3924 count: NonZeroUsize::new(1).unwrap(),
3925 align: initial_letter_align,
3926 }
3927 })
3928 } else {
3929 None
3930 };
3931
3932 if let Some(ref il) = initial_letter {
3937 let lh_n = line_height_value.inner.normalized();
3938 let computed_line_height = if lh_n < 0.0 { -lh_n } else { lh_n * font_size };
3939 let (letter_w, letter_h) = layout_initial_letter(
3940 il.size,
3941 il.sink,
3942 constraints.available_size.width,
3943 computed_line_height,
3944 );
3945 if letter_w > 0.0 && letter_h > 0.0 {
3946 shape_exclusions.push(ShapeBoundary::Rectangle(text3::cache::Rect {
3949 x: 0.0,
3950 y: 0.0,
3951 width: letter_w,
3952 height: letter_h,
3953 }));
3954 }
3955 }
3956
3957 let line_clamp = if dom_declared & DOM_HAS_LINE_CLAMP != 0 {
3959 styled_dom
3960 .css_property_cache
3961 .ptr
3962 .get_line_clamp(node_data, &id, node_state)
3963 .and_then(|s| s.get_property())
3964 .and_then(|lc| std::num::NonZeroUsize::new(lc.max_lines))
3965 } else {
3966 None
3967 };
3968
3969 let hanging_punctuation = if dom_declared & DOM_HAS_HANGING_PUNCTUATION != 0 {
3971 styled_dom
3972 .css_property_cache
3973 .ptr
3974 .get_hanging_punctuation(node_data, &id, node_state)
3975 .and_then(|s| s.get_property())
3976 .is_some_and(azul_css::props::style::StyleHangingPunctuation::is_enabled)
3977 } else {
3978 false
3979 };
3980
3981 let text_combine_upright = if dom_declared & DOM_HAS_TEXT_COMBINE_UPRIGHT != 0 {
3987 styled_dom
3988 .css_property_cache
3989 .ptr
3990 .get_text_combine_upright(node_data, &id, node_state)
3991 .and_then(|s| s.get_property())
3992 .map(|tcu| match tcu {
3994 StyleTextCombineUpright::None => text3::cache::TextCombineUpright::None,
3995 StyleTextCombineUpright::All => text3::cache::TextCombineUpright::All,
3996 StyleTextCombineUpright::Digits(n) => text3::cache::TextCombineUpright::Digits(*n),
3997 })
3998 } else {
3999 None
4000 };
4001
4002 let exclusion_margin_base = if dom_declared & DOM_HAS_EXCLUSION_MARGIN != 0 {
4008 styled_dom
4009 .css_property_cache
4010 .ptr
4011 .get_exclusion_margin(node_data, &id, node_state)
4012 .and_then(|s| s.get_property())
4013 .map_or(0.0, |em| em.inner.get())
4014 } else {
4015 0.0
4016 };
4017
4018 let shape_margin = if dom_declared & DOM_HAS_SHAPE_MARGIN != 0 {
4019 styled_dom
4020 .css_property_cache
4021 .ptr
4022 .get_shape_margin(node_data, &id, node_state)
4023 .and_then(|s| s.get_property())
4024 .map_or(0.0, |sm| sm.inner.number.get())
4025 } else {
4026 0.0
4027 };
4028
4029 let exclusion_margin = exclusion_margin_base + shape_margin;
4030
4031 let hyphenation_language = if dom_declared & DOM_HAS_HYPHENATION_LANGUAGE != 0 {
4033 styled_dom
4034 .css_property_cache
4035 .ptr
4036 .get_hyphenation_language(node_data, &id, node_state)
4037 .and_then(|s| s.get_property())
4038 .and_then(|hl| {
4039 #[cfg(feature = "text_layout_hyphenation")]
4040 {
4041 use hyphenation::{Language, Load};
4042 match hl.inner.as_str() {
4044 "en-US" | "en" => Some(Language::EnglishUS),
4045 "de-DE" | "de" => Some(Language::German1996),
4046 "fr-FR" | "fr" => Some(Language::French),
4047 "es-ES" | "es" => Some(Language::Spanish),
4048 "it-IT" | "it" => Some(Language::Italian),
4049 "pt-PT" | "pt" => Some(Language::Portuguese),
4050 "nl-NL" | "nl" => Some(Language::Dutch),
4051 "pl-PL" | "pl" => Some(Language::Polish),
4052 "ru-RU" | "ru" => Some(Language::Russian),
4053 "zh-CN" | "zh" => Some(Language::Chinese),
4054 _ => None, }
4056 }
4057 #[cfg(not(feature = "text_layout_hyphenation"))]
4058 {
4059 None::<crate::text3::script::Language>
4060 }
4061 })
4062 } else {
4063 None
4064 };
4065
4066 UnifiedConstraints {
4067 exclusion_margin,
4068 hyphenation_language,
4069 text_indent,
4070 text_indent_each_line,
4071 text_indent_hanging,
4072 initial_letter,
4073 line_clamp,
4074 columns,
4075 column_gap,
4076 hanging_punctuation,
4077 text_wrap,
4078 white_space_mode,
4079 text_combine_upright,
4080 segment_alignment: SegmentAlignment::Total,
4081 overflow: match overflow_behaviour {
4082 LayoutOverflow::Visible => text3::cache::OverflowBehavior::Visible,
4083 LayoutOverflow::Hidden | LayoutOverflow::Clip => text3::cache::OverflowBehavior::Hidden,
4084 LayoutOverflow::Scroll => text3::cache::OverflowBehavior::Scroll,
4085 LayoutOverflow::Auto => text3::cache::OverflowBehavior::Auto,
4086 },
4087 available_width: constraints.available_width_type,
4090 available_height: match overflow_behaviour {
4093 LayoutOverflow::Scroll | LayoutOverflow::Auto => None,
4094 _ => Some(constraints.available_size.height),
4095 },
4096 shape_boundaries, shape_exclusions, writing_mode: Some(match writing_mode {
4099 LayoutWritingMode::HorizontalTb => text3::cache::WritingMode::HorizontalTb,
4100 LayoutWritingMode::VerticalRl => text3::cache::WritingMode::VerticalRl,
4101 LayoutWritingMode::VerticalLr => text3::cache::WritingMode::VerticalLr,
4102 }),
4103 direction, unicode_bidi: unicode_bidi_val,
4105 hyphenation: match hyphenation {
4107 StyleHyphens::None => text3::cache::Hyphens::None,
4108 StyleHyphens::Manual => text3::cache::Hyphens::Manual,
4109 StyleHyphens::Auto => text3::cache::Hyphens::Auto,
4110 },
4111 text_orientation,
4112 text_align: match text_align {
4117 StyleTextAlign::Start => text3::cache::TextAlign::Start,
4118 StyleTextAlign::End => text3::cache::TextAlign::End,
4119 StyleTextAlign::Left => text3::cache::TextAlign::Left,
4120 StyleTextAlign::Right => text3::cache::TextAlign::Right,
4121 StyleTextAlign::Center => text3::cache::TextAlign::Center,
4122 StyleTextAlign::Justify => text3::cache::TextAlign::Justify,
4123 },
4124 text_justify: match text_justify {
4127 LayoutTextJustify::None => text3::cache::JustifyContent::None,
4128 LayoutTextJustify::Auto | LayoutTextJustify::InterWord => {
4129 text3::cache::JustifyContent::InterWord
4130 }
4131 LayoutTextJustify::InterCharacter | LayoutTextJustify::Distribute => {
4133 text3::cache::JustifyContent::InterCharacter
4134 }
4135 },
4136 line_height: if dom_declared & DOM_HAS_LINE_HEIGHT == 0 {
4143 text3::cache::LineHeight::Normal
4144 } else {
4145 text3::cache::LineHeight::Px({
4146 let n = line_height_value.inner.normalized();
4147 if n < 0.0 { -n } else { n * font_size }
4148 })
4149 },
4150 strut_ascent: font_size * 0.8,
4161 strut_descent: font_size * 0.2,
4162 strut_x_height: font_size * 0.5, ch_width: font_size * 0.5,
4164 vertical_align,
4165 overflow_wrap: if word_break_css == StyleWordBreak::BreakWord {
4168 text3::cache::OverflowWrap::Anywhere
4170 } else {
4171 match overflow_wrap_css {
4172 StyleOverflowWrap::Normal => text3::cache::OverflowWrap::Normal,
4173 StyleOverflowWrap::Anywhere => text3::cache::OverflowWrap::Anywhere,
4174 StyleOverflowWrap::BreakWord => text3::cache::OverflowWrap::BreakWord,
4175 }
4176 },
4177 text_align_last: match text_align_last_css {
4178 StyleTextAlignLast::Auto => text3::cache::TextAlign::default(),
4179 StyleTextAlignLast::Start => text3::cache::TextAlign::Start,
4180 StyleTextAlignLast::End => text3::cache::TextAlign::End,
4181 StyleTextAlignLast::Left => text3::cache::TextAlign::Left,
4182 StyleTextAlignLast::Right => text3::cache::TextAlign::Right,
4183 StyleTextAlignLast::Center => text3::cache::TextAlign::Center,
4184 StyleTextAlignLast::Justify => text3::cache::TextAlign::Justify,
4185 },
4186 word_break: match word_break_css {
4188 StyleWordBreak::Normal | StyleWordBreak::BreakWord => text3::cache::WordBreak::Normal,
4189 StyleWordBreak::BreakAll => text3::cache::WordBreak::BreakAll,
4190 StyleWordBreak::KeepAll => text3::cache::WordBreak::KeepAll,
4191 },
4192 line_break: match line_break_css {
4202 StyleLineBreak::Auto => text3::cache::LineBreakStrictness::Auto,
4203 StyleLineBreak::Loose => text3::cache::LineBreakStrictness::Loose,
4204 StyleLineBreak::Normal => text3::cache::LineBreakStrictness::Normal,
4205 StyleLineBreak::Strict => text3::cache::LineBreakStrictness::Strict,
4206 StyleLineBreak::Anywhere => text3::cache::LineBreakStrictness::Anywhere,
4207 },
4208 }
4209}
4210
4211#[derive(Copy, Debug, Clone)]
4219pub struct TableColumnInfo {
4220 pub min_width: f32,
4222 pub max_width: f32,
4224 pub computed_width: Option<f32>,
4226}
4227
4228#[derive(Copy, Debug, Clone)]
4230pub struct TableCellInfo {
4231 pub node_index: usize,
4233 pub column: usize,
4235 pub colspan: usize,
4237 pub row: usize,
4239 pub rowspan: usize,
4241}
4242
4243#[derive(Debug)]
4245struct TableLayoutContext {
4246 columns: Vec<TableColumnInfo>,
4248 cells: Vec<TableCellInfo>,
4250 num_rows: usize,
4252 use_fixed_layout: bool,
4254 row_heights: Vec<f32>,
4256 row_baselines: Vec<f32>,
4258 border_collapse: StyleBorderCollapse,
4261 border_spacing: LayoutBorderSpacing,
4263 caption_index: Option<usize>,
4265 collapsed_rows: std::collections::HashSet<usize>,
4269 collapsed_columns: std::collections::HashSet<usize>,
4272 hidden_empty_rows: std::collections::HashSet<usize>,
4274 row_node_indices: Vec<usize>,
4276 col_occupied: Vec<usize>,
4281}
4282
4283impl TableLayoutContext {
4284 fn new() -> Self {
4285 Self {
4286 columns: Vec::new(),
4287 cells: Vec::new(),
4288 num_rows: 0,
4289 use_fixed_layout: false,
4290 row_heights: Vec::new(),
4291 row_baselines: Vec::new(),
4292 border_collapse: StyleBorderCollapse::Separate,
4293 border_spacing: LayoutBorderSpacing::default(),
4294 caption_index: None,
4295 collapsed_rows: std::collections::HashSet::new(),
4296 collapsed_columns: std::collections::HashSet::new(),
4297 hidden_empty_rows: std::collections::HashSet::new(),
4298 row_node_indices: Vec::new(),
4299 col_occupied: Vec::new(),
4300 }
4301 }
4302}
4303
4304#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
4308pub enum BorderSource {
4309 Table = 0,
4310 ColumnGroup = 1,
4311 Column = 2,
4312 RowGroup = 3,
4313 Row = 4,
4314 Cell = 5,
4315}
4316
4317#[derive(Copy, Debug, Clone)]
4319pub struct BorderInfo {
4320 pub width: f32,
4321 pub style: BorderStyle,
4322 pub color: ColorU,
4323 pub source: BorderSource,
4324}
4325
4326impl BorderInfo {
4327 #[must_use] pub const fn new(width: f32, style: BorderStyle, color: ColorU, source: BorderSource) -> Self {
4328 Self {
4329 width,
4330 style,
4331 color,
4332 source,
4333 }
4334 }
4335
4336 #[must_use] pub const fn style_priority(style: &BorderStyle) -> u8 {
4340 match style {
4341 BorderStyle::Hidden => 255, BorderStyle::None => 0, BorderStyle::Double => 8,
4344 BorderStyle::Solid => 7,
4345 BorderStyle::Dashed => 6,
4346 BorderStyle::Dotted => 5,
4347 BorderStyle::Ridge => 4,
4348 BorderStyle::Outset => 3,
4349 BorderStyle::Groove => 2,
4350 BorderStyle::Inset => 1,
4351 }
4352 }
4353
4354 #[must_use] pub fn resolve_conflict(a: &Self, b: &Self) -> Option<Self> {
4365 if a.style == BorderStyle::Hidden || b.style == BorderStyle::Hidden {
4367 return None;
4368 }
4369
4370 let a_is_none = a.style == BorderStyle::None;
4372 let b_is_none = b.style == BorderStyle::None;
4373
4374 if a_is_none && b_is_none {
4375 return None;
4376 }
4377 if a_is_none {
4378 return Some(*b);
4379 }
4380 if b_is_none {
4381 return Some(*a);
4382 }
4383
4384 if a.width > b.width {
4386 return Some(*a);
4387 }
4388 if b.width > a.width {
4389 return Some(*b);
4390 }
4391
4392 let a_priority = Self::style_priority(&a.style);
4394 let b_priority = Self::style_priority(&b.style);
4395
4396 if a_priority > b_priority {
4397 return Some(*a);
4398 }
4399 if b_priority > a_priority {
4400 return Some(*b);
4401 }
4402
4403 if a.source > b.source {
4406 return Some(*a);
4407 }
4408 if b.source > a.source {
4409 return Some(*b);
4410 }
4411
4412 Some(*a)
4414 }
4415}
4416
4417#[allow(clippy::similar_names)] #[allow(clippy::too_many_lines)] fn get_border_info<T: ParsedFontTrait>(
4421 ctx: &LayoutContext<'_, T>,
4422 node: &LayoutNodeHot,
4423 source: BorderSource,
4424) -> (BorderInfo, BorderInfo, BorderInfo, BorderInfo) {
4425 use azul_css::props::{
4426 basic::{
4427 pixel::{PhysicalSize, PropertyContext, ResolutionContext},
4428 ColorU,
4429 },
4430 style::BorderStyle,
4431 };
4432 use get_element_font_size;
4433 use get_parent_font_size;
4434 use get_root_font_size;
4435
4436 let default_border = BorderInfo::new(
4437 0.0,
4438 BorderStyle::None,
4439 ColorU {
4440 r: 0,
4441 g: 0,
4442 b: 0,
4443 a: 0,
4444 },
4445 source,
4446 );
4447
4448 let Some(dom_id) = node.dom_node_id else {
4449 return (
4450 default_border,
4451 default_border,
4452 default_border,
4453 default_border,
4454 );
4455 };
4456
4457 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
4458 let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4459 let cache = &ctx.styled_dom.css_property_cache.ptr;
4460
4461 if let Some(ref cc) = cache.compact_cache {
4463 let idx = dom_id.index();
4464
4465 let bts = cc.get_border_top_style(idx);
4467 let brs = cc.get_border_right_style(idx);
4468 let bbs = cc.get_border_bottom_style(idx);
4469 let bls = cc.get_border_left_style(idx);
4470
4471 let make_color = |raw: u32| -> ColorU {
4473 if raw == 0 {
4474 ColorU { r: 0, g: 0, b: 0, a: 0 }
4475 } else {
4476 ColorU {
4477 r: ((raw >> 24) & 0xFF) as u8,
4478 g: ((raw >> 16) & 0xFF) as u8,
4479 b: ((raw >> 8) & 0xFF) as u8,
4480 a: (raw & 0xFF) as u8,
4481 }
4482 }
4483 };
4484
4485 let btc = make_color(cc.get_border_top_color_raw(idx));
4486 let brc = make_color(cc.get_border_right_color_raw(idx));
4487 let bbc = make_color(cc.get_border_bottom_color_raw(idx));
4488 let blc = make_color(cc.get_border_left_color_raw(idx));
4489
4490 let decode_width = |raw: i16| -> f32 {
4492 if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
4493 0.0 } else {
4495 f32::from(raw) / 10.0
4496 }
4497 };
4498
4499 let btw = decode_width(cc.get_border_top_width_raw(idx));
4500 let brw = decode_width(cc.get_border_right_width_raw(idx));
4501 let bbw = decode_width(cc.get_border_bottom_width_raw(idx));
4502 let blw = decode_width(cc.get_border_left_width_raw(idx));
4503
4504 let top = if bts == BorderStyle::None { default_border }
4505 else { BorderInfo::new(btw, bts, btc, source) };
4506 let right = if brs == BorderStyle::None { default_border }
4507 else { BorderInfo::new(brw, brs, brc, source) };
4508 let bottom = if bbs == BorderStyle::None { default_border }
4509 else { BorderInfo::new(bbw, bbs, bbc, source) };
4510 let left = if bls == BorderStyle::None { default_border }
4511 else { BorderInfo::new(blw, bls, blc, source) };
4512
4513 return (top, right, bottom, left);
4514 }
4515
4516 let cache = &ctx.styled_dom.css_property_cache.ptr;
4518
4519 let element_font_size = get_element_font_size(ctx.styled_dom, dom_id, &node_state);
4521 let parent_font_size = get_parent_font_size(ctx.styled_dom, dom_id, &node_state);
4522 let root_font_size = get_root_font_size(ctx.styled_dom, &node_state);
4523
4524 let resolution_context = ResolutionContext {
4525 element_font_size,
4526 parent_font_size,
4527 root_font_size,
4528 containing_block_size: PhysicalSize::new(0.0, 0.0),
4530 element_size: None,
4532 viewport_size: PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
4533 };
4534
4535 let top = cache
4537 .get_border_top_style(node_data, &dom_id, &node_state)
4538 .and_then(|s| s.get_property())
4539 .map_or_else(|| default_border, |style_val| {
4540 let width = cache
4541 .get_border_top_width(node_data, &dom_id, &node_state)
4542 .and_then(|w| w.get_property())
4543 .map_or(0.0, |w| {
4544 w.inner
4545 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
4546 });
4547 let color = cache
4548 .get_border_top_color(node_data, &dom_id, &node_state)
4549 .and_then(|c| c.get_property())
4550 .map_or(ColorU {
4551 r: 0,
4552 g: 0,
4553 b: 0,
4554 a: 255,
4555 }, |c| c.inner);
4556 BorderInfo::new(width, style_val.inner, color, source)
4557 });
4558
4559 let right = cache
4561 .get_border_right_style(node_data, &dom_id, &node_state)
4562 .and_then(|s| s.get_property())
4563 .map_or_else(|| default_border, |style_val| {
4564 let width = cache
4565 .get_border_right_width(node_data, &dom_id, &node_state)
4566 .and_then(|w| w.get_property())
4567 .map_or(0.0, |w| {
4568 w.inner
4569 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
4570 });
4571 let color = cache
4572 .get_border_right_color(node_data, &dom_id, &node_state)
4573 .and_then(|c| c.get_property())
4574 .map_or(ColorU {
4575 r: 0,
4576 g: 0,
4577 b: 0,
4578 a: 255,
4579 }, |c| c.inner);
4580 BorderInfo::new(width, style_val.inner, color, source)
4581 });
4582
4583 let bottom = cache
4585 .get_border_bottom_style(node_data, &dom_id, &node_state)
4586 .and_then(|s| s.get_property())
4587 .map_or_else(|| default_border, |style_val| {
4588 let width = cache
4589 .get_border_bottom_width(node_data, &dom_id, &node_state)
4590 .and_then(|w| w.get_property())
4591 .map_or(0.0, |w| {
4592 w.inner
4593 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
4594 });
4595 let color = cache
4596 .get_border_bottom_color(node_data, &dom_id, &node_state)
4597 .and_then(|c| c.get_property())
4598 .map_or(ColorU {
4599 r: 0,
4600 g: 0,
4601 b: 0,
4602 a: 255,
4603 }, |c| c.inner);
4604 BorderInfo::new(width, style_val.inner, color, source)
4605 });
4606
4607 let left = cache
4609 .get_border_left_style(node_data, &dom_id, &node_state)
4610 .and_then(|s| s.get_property())
4611 .map_or_else(|| default_border, |style_val| {
4612 let width = cache
4613 .get_border_left_width(node_data, &dom_id, &node_state)
4614 .and_then(|w| w.get_property())
4615 .map_or(0.0, |w| {
4616 w.inner
4617 .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
4618 });
4619 let color = cache
4620 .get_border_left_color(node_data, &dom_id, &node_state)
4621 .and_then(|c| c.get_property())
4622 .map_or(ColorU {
4623 r: 0,
4624 g: 0,
4625 b: 0,
4626 a: 255,
4627 }, |c| c.inner);
4628 BorderInfo::new(width, style_val.inner, color, source)
4629 });
4630
4631 (top, right, bottom, left)
4632}
4633
4634fn get_table_layout_property<T: ParsedFontTrait>(
4637 ctx: &LayoutContext<'_, T>,
4638 node: &LayoutNodeHot,
4639) -> LayoutTableLayout {
4640 let Some(dom_id) = node.dom_node_id else {
4641 return LayoutTableLayout::Auto;
4642 };
4643
4644 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
4645 let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4646
4647 ctx.styled_dom
4648 .css_property_cache
4649 .ptr
4650 .get_table_layout(node_data, &dom_id, &node_state)
4651 .and_then(|prop| prop.get_property().copied())
4652 .unwrap_or(LayoutTableLayout::Auto)
4653}
4654
4655fn get_border_collapse_property<T: ParsedFontTrait>(
4657 ctx: &LayoutContext<'_, T>,
4658 node: &LayoutNodeHot,
4659) -> StyleBorderCollapse {
4660 let Some(dom_id) = node.dom_node_id else {
4661 return StyleBorderCollapse::Separate;
4662 };
4663
4664 if let Some(ref cc) = ctx.styled_dom.css_property_cache.ptr.compact_cache {
4666 return cc.get_border_collapse(dom_id.index());
4667 }
4668
4669 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
4670 let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4671
4672 ctx.styled_dom
4673 .css_property_cache
4674 .ptr
4675 .get_border_collapse(node_data, &dom_id, &node_state)
4676 .and_then(|prop| prop.get_property().copied())
4677 .unwrap_or(StyleBorderCollapse::Separate)
4678}
4679
4680fn get_border_spacing_property<T: ParsedFontTrait>(
4682 ctx: &LayoutContext<'_, T>,
4683 node: &LayoutNodeHot,
4684) -> LayoutBorderSpacing {
4685 if let Some(dom_id) = node.dom_node_id {
4686 if let Some(ref cc) = ctx.styled_dom.css_property_cache.ptr.compact_cache {
4688 let idx = dom_id.index();
4689 let h_raw = cc.get_border_spacing_h_raw(idx);
4690 let v_raw = cc.get_border_spacing_v_raw(idx);
4691 if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
4693 && v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
4694 {
4695 return LayoutBorderSpacing::new_separate(
4696 azul_css::props::basic::pixel::PixelValue::px(f32::from(h_raw) / 10.0),
4697 azul_css::props::basic::pixel::PixelValue::px(f32::from(v_raw) / 10.0),
4698 );
4699 }
4700 }
4702
4703 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
4704 let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4705
4706 if let Some(prop) = ctx.styled_dom.css_property_cache.ptr.get_border_spacing(
4707 node_data,
4708 &dom_id,
4709 &node_state,
4710 ) {
4711 if let Some(value) = prop.get_property() {
4712 return *value;
4713 }
4714 }
4715 }
4716
4717 LayoutBorderSpacing::default() }
4719
4720fn get_empty_cells_property<T: ParsedFontTrait>(
4723 ctx: &LayoutContext<'_, T>,
4724 node: &LayoutNodeHot,
4725) -> StyleEmptyCells {
4726 let Some(dom_id) = node.dom_node_id else {
4727 return StyleEmptyCells::Show;
4728 };
4729
4730 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
4731 let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4732
4733 ctx.styled_dom
4734 .css_property_cache
4735 .ptr
4736 .get_empty_cells(node_data, &dom_id, &node_state)
4737 .and_then(|prop| prop.get_property().copied())
4738 .unwrap_or(StyleEmptyCells::Show)
4739}
4740
4741fn get_caption_side_property<T: ParsedFontTrait>(
4750 ctx: &LayoutContext<'_, T>,
4751 node: &LayoutNodeHot,
4752) -> StyleCaptionSide {
4753 if let Some(dom_id) = node.dom_node_id {
4754 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
4755 let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4756
4757 if let Some(prop) =
4758 ctx.styled_dom
4759 .css_property_cache
4760 .ptr
4761 .get_caption_side(node_data, &dom_id, &node_state)
4762 {
4763 if let Some(value) = prop.get_property() {
4764 return *value;
4765 }
4766 }
4767 }
4768
4769 StyleCaptionSide::Top }
4771
4772fn is_visibility_collapsed<T: ParsedFontTrait>(
4788 ctx: &LayoutContext<'_, T>,
4789 node: &LayoutNodeHot,
4790) -> bool {
4791 if let Some(dom_id) = node.dom_node_id {
4792 let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4793
4794 if let MultiValue::Exact(value) = get_visibility(ctx.styled_dom, dom_id, &node_state) {
4795 return matches!(value, StyleVisibility::Collapse);
4796 }
4797 }
4798
4799 false
4800}
4801
4802fn is_cell_empty(tree: &LayoutTree, cell_index: usize) -> bool {
4824 if tree.get(cell_index).is_none() {
4825 return true; }
4827
4828 if tree.children(cell_index).is_empty() {
4830 return true;
4831 }
4832
4833 if let Some(warm_node) = tree.warm(cell_index) {
4835 if let Some(ref cached_layout) = warm_node.inline_layout_result {
4836 return cached_layout.layout.items.is_empty();
4840 }
4841 }
4842
4843 false
4851}
4852
4853#[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn layout_table_fc<T: ParsedFontTrait>(
4865 ctx: &mut LayoutContext<'_, T>,
4866 tree: &mut LayoutTree,
4867 text_cache: &mut TextLayoutCache,
4868 node_index: usize,
4869 constraints: &LayoutConstraints<'_>,
4870) -> Result<LayoutOutput> {
4871 debug_log!(ctx, "Laying out table");
4872
4873 debug_table_layout!(
4874 ctx,
4875 "node_index={}, available_size={:?}, writing_mode={:?}",
4876 node_index,
4877 constraints.available_size,
4878 constraints.writing_mode
4879 );
4880
4881 let table_node = tree
4891 .get(node_index)
4892 .ok_or(LayoutError::InvalidTree)?
4893 .clone();
4894
4895 let table_border_box_width = if let Some(dom_id) = table_node.dom_node_id {
4898 let intrinsic = tree.warm(node_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
4900 let containing_block_size = LogicalSize {
4901 width: constraints.available_size.width,
4902 height: constraints.available_size.height,
4903 };
4904
4905 let table_bp = table_node.box_props.unpack();
4906 let table_size = crate::solver3::sizing::calculate_used_size_for_node(
4907 ctx.styled_dom,
4908 Some(dom_id),
4909 &containing_block_size,
4910 intrinsic,
4911 &table_bp,
4912 &ctx.viewport_size,
4913 )?;
4914
4915 table_size.width
4916 } else {
4917 constraints.available_size.width
4918 };
4919
4920 let tbp = table_node.box_props.unpack();
4922 let table_content_box_width = {
4923 let padding_width = tbp.padding.left + tbp.padding.right;
4924 let border_width = tbp.border.left + tbp.border.right;
4925 (table_border_box_width - padding_width - border_width).max(0.0)
4926 };
4927
4928 debug_table_layout!(ctx, "Table Layout Debug");
4929 debug_table_layout!(ctx, "Node index: {}", node_index);
4930 debug_table_layout!(
4931 ctx,
4932 "Available size from parent: {:.2} x {:.2}",
4933 constraints.available_size.width,
4934 constraints.available_size.height
4935 );
4936 debug_table_layout!(ctx, "Table border-box width: {:.2}", table_border_box_width);
4937 debug_table_layout!(
4938 ctx,
4939 "Table content-box width: {:.2}",
4940 table_content_box_width
4941 );
4942 debug_table_layout!(
4943 ctx,
4944 "Table padding: L={:.2} R={:.2}",
4945 tbp.padding.left,
4946 tbp.padding.right
4947 );
4948 debug_table_layout!(
4949 ctx,
4950 "Table border: L={:.2} R={:.2}",
4951 tbp.border.left,
4952 tbp.border.right
4953 );
4954 debug_table_layout!(ctx, "=");
4955
4956 let mut table_ctx = analyze_table_structure(tree, node_index, ctx)?;
4958
4959 let table_layout = get_table_layout_property(ctx, &table_node);
4963 table_ctx.use_fixed_layout = matches!(table_layout, LayoutTableLayout::Fixed);
4964
4965 table_ctx.border_collapse = get_border_collapse_property(ctx, &table_node);
4968 table_ctx.border_spacing = get_border_spacing_property(ctx, &table_node);
4969
4970 debug_log!(
4971 ctx,
4972 "Table layout: {:?}, border-collapse: {:?}, border-spacing: {:?}",
4973 table_layout,
4974 table_ctx.border_collapse,
4975 table_ctx.border_spacing
4976 );
4977
4978 if table_ctx.use_fixed_layout {
4981 debug_table_layout!(
4983 ctx,
4984 "FIXED layout: table_content_box_width={:.2}",
4985 table_content_box_width
4986 );
4987 calculate_column_widths_fixed(ctx, tree, &mut table_ctx, table_content_box_width);
4988 } else {
4989 calculate_column_widths_auto_with_width(
4991 &mut table_ctx,
4992 tree,
4993 text_cache,
4994 ctx,
4995 constraints,
4996 table_content_box_width,
4997 )?;
4998 }
4999
5000 debug_table_layout!(ctx, "After column width calculation:");
5001 debug_table_layout!(ctx, " Number of columns: {}", table_ctx.columns.len());
5002 for (i, col) in table_ctx.columns.iter().enumerate() {
5003 debug_table_layout!(
5004 ctx,
5005 " Column {}: width={:.2}",
5006 i,
5007 col.computed_width.unwrap_or(0.0)
5008 );
5009 }
5010 let total_col_width: f32 = table_ctx
5011 .columns
5012 .iter()
5013 .filter_map(|c| c.computed_width)
5014 .sum();
5015 debug_table_layout!(ctx, " Total column width: {:.2}", total_col_width);
5016
5017 calculate_row_heights(&mut table_ctx, tree, text_cache, ctx, constraints)?;
5019
5020 let mut cell_positions =
5022 position_table_cells(&table_ctx, tree, ctx, node_index, constraints)?;
5023
5024 let mut table_width: f32 = table_ctx
5026 .columns
5027 .iter()
5028 .filter_map(|col| col.computed_width)
5029 .sum();
5030 let mut table_height: f32 = table_ctx.row_heights.iter().sum();
5031
5032 debug_table_layout!(
5033 ctx,
5034 "After calculate_row_heights: table_height={:.2}, row_heights={:?}",
5035 table_height,
5036 table_ctx.row_heights
5037 );
5038
5039 if table_ctx.border_collapse == StyleBorderCollapse::Separate {
5046 use get_element_font_size;
5047 use get_parent_font_size;
5048 use get_root_font_size;
5049 use PhysicalSize;
5050 use PropertyContext;
5051 use ResolutionContext;
5052
5053 let styled_dom = ctx.styled_dom;
5054 let (h_spacing, v_spacing) = if let Some(table_id) = tree.nodes[node_index].dom_node_id {
5058 let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
5059
5060 let spacing_context = ResolutionContext {
5061 element_font_size: get_element_font_size(styled_dom, table_id, table_state),
5062 parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
5063 root_font_size: get_root_font_size(styled_dom, table_state),
5064 containing_block_size: PhysicalSize::new(0.0, 0.0),
5065 element_size: None,
5066 viewport_size: PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
5067 };
5068
5069 let h_spacing = table_ctx
5070 .border_spacing
5071 .horizontal
5072 .resolve_with_context(&spacing_context, PropertyContext::Other)
5073 .max(0.0);
5074 let v_spacing = table_ctx
5075 .border_spacing
5076 .vertical
5077 .resolve_with_context(&spacing_context, PropertyContext::Other)
5078 .max(0.0);
5079 (h_spacing, v_spacing)
5080 } else {
5081 (0.0f32, 0.0f32)
5082 };
5083
5084 let num_cols = table_ctx.columns.len();
5086 if num_cols > 0 {
5087 table_width += h_spacing * (num_cols + 1) as f32;
5088 }
5089
5090 if table_ctx.num_rows > 0 {
5092 let full_spacings = (table_ctx.num_rows + 1) as f32;
5093 let hidden_empty_count = table_ctx.hidden_empty_rows.len() as f32;
5095 table_height += v_spacing * (full_spacings - hidden_empty_count);
5096 }
5097 }
5098
5099 let caption_side = get_caption_side_property(ctx, &table_node);
5106 let mut caption_height = 0.0;
5107 let mut table_y_offset = 0.0;
5108
5109 if let Some(caption_idx) = table_ctx.caption_index {
5110 debug_log!(
5111 ctx,
5112 "Laying out caption with caption-side: {:?}",
5113 caption_side
5114 );
5115
5116 let caption_constraints = LayoutConstraints {
5118 available_size: LogicalSize {
5119 width: table_width,
5120 height: constraints.available_size.height,
5121 },
5122 writing_mode: constraints.writing_mode,
5123 writing_mode_ctx: constraints.writing_mode_ctx,
5124 bfc_state: None, text_align: constraints.text_align,
5126 containing_block_size: constraints.containing_block_size,
5127 available_width_type: Text3AvailableSpace::Definite(table_width),
5128 };
5129
5130 let mut empty_float_cache = HashMap::new();
5132 let caption_result = layout_formatting_context(
5133 ctx,
5134 tree,
5135 text_cache,
5136 caption_idx,
5137 &caption_constraints,
5138 &mut empty_float_cache,
5139 )?;
5140 caption_height = caption_result.output.overflow_size.height;
5141
5142 let caption_position = match caption_side {
5143 StyleCaptionSide::Top => {
5144 table_y_offset = caption_height;
5146 LogicalPosition { x: 0.0, y: 0.0 }
5147 }
5148 StyleCaptionSide::Bottom => {
5149 LogicalPosition {
5151 x: 0.0,
5152 y: table_height,
5153 }
5154 }
5155 };
5156
5157 cell_positions.insert(caption_idx, caption_position);
5159
5160 debug_log!(
5161 ctx,
5162 "Caption positioned at x={:.2}, y={:.2}, height={:.2}",
5163 caption_position.x,
5164 caption_position.y,
5165 caption_height
5166 );
5167 }
5168
5169 if table_y_offset > 0.0 {
5171 debug_log!(
5172 ctx,
5173 "Adjusting table cells by y offset: {:.2}",
5174 table_y_offset
5175 );
5176
5177 for cell_info in &table_ctx.cells {
5179 if let Some(pos) = cell_positions.get_mut(&cell_info.node_index) {
5180 pos.y += table_y_offset;
5181 }
5182 }
5183 }
5184
5185 let total_height = table_height + caption_height;
5186
5187 debug_table_layout!(ctx, "Final table dimensions:");
5188 debug_table_layout!(ctx, " Content width (columns): {:.2}", table_width);
5189 debug_table_layout!(ctx, " Content height (rows): {:.2}", table_height);
5190 debug_table_layout!(ctx, " Caption height: {:.2}", caption_height);
5191 debug_table_layout!(ctx, " Total height: {:.2}", total_height);
5192 debug_table_layout!(ctx, "End Table Debug");
5193
5194 let table_baseline = table_ctx
5203 .row_baselines
5204 .first()
5205 .copied()
5206 .and_then(|row0_baseline| {
5207 table_ctx
5208 .cells
5209 .iter()
5210 .find(|c| c.row == 0)
5211 .and_then(|c| cell_positions.get(&c.node_index))
5212 .map(|pos| pos.y + row0_baseline)
5213 });
5214
5215 let output = LayoutOutput {
5218 overflow_size: LogicalSize {
5219 width: table_width,
5220 height: total_height,
5221 },
5222 positions: cell_positions,
5224 baseline: table_baseline,
5226 };
5227
5228 Ok(output)
5229}
5230
5231fn analyze_table_structure<T: ParsedFontTrait>(
5234 tree: &LayoutTree,
5235 table_index: usize,
5236 ctx: &mut LayoutContext<'_, T>,
5237) -> Result<TableLayoutContext> {
5238 let mut table_ctx = TableLayoutContext::new();
5239
5240 let table_node = tree.get(table_index).ok_or(LayoutError::InvalidTree)?;
5241
5242 for &child_idx in tree.children(table_index) {
5246 if let Some(child) = tree.get(child_idx) {
5247 if matches!(child.formatting_context, FormattingContext::TableCaption) {
5249 debug_log!(ctx, "Found table caption at index {}", child_idx);
5250 table_ctx.caption_index = Some(child_idx);
5251 continue;
5252 }
5253
5254 if matches!(
5256 child.formatting_context,
5257 FormattingContext::TableColumnGroup
5258 ) {
5259 analyze_table_colgroup(tree, child_idx, &table_ctx, ctx)?;
5260 continue;
5261 }
5262
5263 match child.formatting_context {
5265 FormattingContext::TableRow => {
5266 analyze_table_row(tree, child_idx, &mut table_ctx, ctx)?;
5267 }
5268 FormattingContext::TableRowGroup => {
5269 for &row_idx in tree.children(child_idx) {
5271 if let Some(row) = tree.get(row_idx) {
5272 if matches!(row.formatting_context, FormattingContext::TableRow) {
5273 analyze_table_row(tree, row_idx, &mut table_ctx, ctx)?;
5274 }
5275 }
5276 }
5277 }
5278 _ => {}
5279 }
5280 }
5281 }
5282
5283 debug_log!(
5284 ctx,
5285 "Table structure: {} rows, {} columns, {} cells{}",
5286 table_ctx.num_rows,
5287 table_ctx.columns.len(),
5288 table_ctx.cells.len(),
5289 if table_ctx.caption_index.is_some() {
5290 ", has caption"
5291 } else {
5292 ""
5293 }
5294 );
5295
5296 Ok(table_ctx)
5297}
5298
5299fn analyze_table_colgroup<T: ParsedFontTrait>(
5304 tree: &LayoutTree,
5305 colgroup_index: usize,
5306 table_ctx: &TableLayoutContext,
5307 ctx: &mut LayoutContext<'_, T>,
5308) -> Result<()> {
5309 let colgroup_node = tree.get(colgroup_index).ok_or(LayoutError::InvalidTree)?;
5310
5311 if is_visibility_collapsed(ctx, colgroup_node) {
5313 debug_log!(
5316 ctx,
5317 "Column group at index {} has visibility:collapse",
5318 colgroup_index
5319 );
5320 }
5321
5322 for &col_idx in tree.children(colgroup_index) {
5324 if let Some(col_node) = tree.get(col_idx) {
5325 if is_visibility_collapsed(ctx, col_node) {
5329 debug_log!(ctx, "Column at index {} has visibility:collapse", col_idx);
5332 }
5333 }
5334 }
5335
5336 Ok(())
5337}
5338
5339#[allow(clippy::cast_sign_loss)] fn get_cell_spans(styled_dom: &StyledDom, dom_id: NodeId) -> (usize, usize) {
5346 let mut colspan = 1usize;
5347 let mut rowspan = 1usize;
5348 let node_data = &styled_dom.node_data.as_container()[dom_id];
5349 for attr in node_data.attributes().as_ref() {
5350 match attr {
5351 azul_core::dom::AttributeType::ColSpan(n) => colspan = (*n).clamp(1, 1000) as usize,
5354 azul_core::dom::AttributeType::RowSpan(n) => rowspan = (*n).clamp(1, 65534) as usize,
5355 _ => {}
5356 }
5357 }
5358 (colspan, rowspan)
5359}
5360
5361fn analyze_table_row<T: ParsedFontTrait>(
5364 tree: &LayoutTree,
5365 row_index: usize,
5366 table_ctx: &mut TableLayoutContext,
5367 ctx: &mut LayoutContext<'_, T>,
5368) -> Result<()> {
5369 let row_node = tree.get(row_index).ok_or(LayoutError::InvalidTree)?;
5371 let row_num = table_ctx.num_rows;
5372 table_ctx.num_rows += 1;
5373 if table_ctx.row_node_indices.len() <= row_num {
5375 table_ctx.row_node_indices.resize(row_num + 1, 0);
5376 }
5377 table_ctx.row_node_indices[row_num] = row_index;
5378
5379 if is_visibility_collapsed(ctx, row_node) {
5381 debug_log!(ctx, "Row {} has visibility:collapse", row_num);
5382 table_ctx.collapsed_rows.insert(row_num);
5383 }
5384
5385 let mut col_index = 0;
5386
5387 for &cell_idx in tree.children(row_index) {
5388 if let Some(cell) = tree.get(cell_idx) {
5389 if matches!(cell.formatting_context, FormattingContext::TableCell) {
5390 let (colspan, rowspan) = cell
5392 .dom_node_id
5393 .map_or((1, 1), |dom_id| get_cell_spans(ctx.styled_dom, dom_id));
5394
5395 while table_ctx
5399 .col_occupied
5400 .get(col_index)
5401 .is_some_and(|&n| n > 0)
5402 {
5403 col_index += 1;
5404 }
5405
5406 let cell_info = TableCellInfo {
5407 node_index: cell_idx,
5408 column: col_index,
5409 colspan,
5410 row: row_num,
5411 rowspan,
5412 };
5413
5414 table_ctx.cells.push(cell_info);
5415
5416 let max_col = col_index + colspan;
5418 while table_ctx.columns.len() < max_col {
5419 table_ctx.columns.push(TableColumnInfo {
5420 min_width: 0.0,
5421 max_width: 0.0,
5422 computed_width: None,
5423 });
5424 }
5425
5426 if rowspan > 1 {
5430 if table_ctx.col_occupied.len() < max_col {
5431 table_ctx.col_occupied.resize(max_col, 0);
5432 }
5433 for occ in &mut table_ctx.col_occupied[col_index..max_col] {
5434 *occ = rowspan;
5435 }
5436 }
5437
5438 col_index += colspan;
5439 }
5440 }
5441 }
5442
5443 for occ in &mut table_ctx.col_occupied {
5445 *occ = occ.saturating_sub(1);
5446 }
5447
5448 Ok(())
5449}
5450
5451#[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines)] fn calculate_column_widths_fixed<T: ParsedFontTrait>(
5471 ctx: &mut LayoutContext<'_, T>,
5472 tree: &LayoutTree,
5473 table_ctx: &mut TableLayoutContext,
5474 available_width: f32,
5475) {
5476 debug_table_layout!(
5477 ctx,
5478 "calculate_column_widths_fixed: num_cols={}, available_width={:.2}",
5479 table_ctx.columns.len(),
5480 available_width
5481 );
5482
5483 let num_cols = table_ctx.columns.len();
5484 if num_cols == 0 {
5485 return;
5486 }
5487
5488 let num_visible_cols = num_cols - table_ctx.collapsed_columns.len();
5489 if num_visible_cols == 0 {
5490 for col in &mut table_ctx.columns {
5491 col.computed_width = Some(0.0);
5492 }
5493 return;
5494 }
5495
5496 let mut col_has_width = vec![false; num_cols];
5500
5501 for cell_info in &table_ctx.cells {
5502 if cell_info.row != 0 {
5503 continue; }
5505 if table_ctx.collapsed_columns.contains(&cell_info.column) {
5506 continue;
5507 }
5508
5509 let Some(dom_id) = tree.get(cell_info.node_index).and_then(|n| n.dom_node_id) else {
5511 continue;
5512 };
5513
5514 let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
5515 let css_width = get_css_width(ctx.styled_dom, dom_id, node_state);
5516
5517 let explicit_px = match css_width.unwrap_or_default() {
5518 LayoutWidth::Px(px) => {
5519 resolve_size_metric(
5520 px.metric,
5521 px.number.get(),
5522 available_width,
5523 ctx.viewport_size,
5524 get_element_font_size(ctx.styled_dom, dom_id, node_state),
5525 get_root_font_size(ctx.styled_dom, node_state),
5526 )
5527 }
5528 LayoutWidth::Auto | LayoutWidth::MinContent | LayoutWidth::MaxContent
5529 | LayoutWidth::Calc(_) | LayoutWidth::FitContent(_) => continue,
5530 };
5531
5532 if cell_info.colspan == 1 {
5533 table_ctx.columns[cell_info.column].computed_width = Some(explicit_px);
5534 col_has_width[cell_info.column] = true;
5535 } else {
5536 let mut visible_span_count = 0;
5537 for offset in 0..cell_info.colspan {
5538 let col_idx = cell_info.column + offset;
5539 if col_idx < num_cols && !table_ctx.collapsed_columns.contains(&col_idx) {
5540 visible_span_count += 1;
5541 }
5542 }
5543 if visible_span_count > 0 {
5544 let per_col = explicit_px / visible_span_count as f32;
5545 for offset in 0..cell_info.colspan {
5546 let col_idx = cell_info.column + offset;
5547 if col_idx < num_cols
5548 && !table_ctx.collapsed_columns.contains(&col_idx)
5549 && !col_has_width[col_idx]
5550 {
5551 table_ctx.columns[col_idx].computed_width = Some(per_col);
5552 col_has_width[col_idx] = true;
5553 }
5554 }
5555 }
5556 }
5557 }
5558
5559 let used_width: f32 = table_ctx.columns.iter().enumerate()
5560 .filter(|(idx, _)| col_has_width[*idx] && !table_ctx.collapsed_columns.contains(idx))
5561 .filter_map(|(_, c)| c.computed_width)
5562 .sum();
5563 let remaining_width = (available_width - used_width).max(0.0);
5564 let num_remaining = table_ctx.columns.iter().enumerate()
5565 .filter(|(idx, _)| !col_has_width[*idx] && !table_ctx.collapsed_columns.contains(idx))
5566 .count();
5567
5568 if num_remaining > 0 {
5569 let width_per_remaining = remaining_width / num_remaining as f32;
5570 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
5571 if table_ctx.collapsed_columns.contains(&col_idx) {
5572 col.computed_width = Some(0.0);
5573 } else if !col_has_width[col_idx] {
5574 col.computed_width = Some(width_per_remaining);
5575 }
5576 }
5577 }
5578
5579 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
5581 if table_ctx.collapsed_columns.contains(&col_idx) {
5582 col.computed_width = Some(0.0);
5583 }
5584 }
5585
5586 let total_col_width: f32 = table_ctx.columns.iter()
5587 .filter_map(|c| c.computed_width)
5588 .sum();
5589 if available_width > total_col_width && num_visible_cols > 0 {
5590 let extra = available_width - total_col_width;
5591 let extra_per_col = extra / num_visible_cols as f32;
5592 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
5593 if !table_ctx.collapsed_columns.contains(&col_idx) {
5594 if let Some(ref mut w) = col.computed_width {
5595 *w += extra_per_col;
5596 }
5597 }
5598 }
5599 }
5600}
5601
5602fn clear_subtree_cache(
5609 tree: &LayoutTree,
5610 cache_map: &mut crate::solver3::cache::LayoutCacheMap,
5611 root: usize,
5612) {
5613 if root < cache_map.entries.len() {
5614 cache_map.entries[root].clear();
5615 }
5616 let child_ids: Vec<usize> = tree.children(root).to_vec();
5617 for child in child_ids {
5618 clear_subtree_cache(tree, cache_map, child);
5619 }
5620}
5621
5622fn measure_cell_content_width<T: ParsedFontTrait>(
5628 ctx: &mut LayoutContext<'_, T>,
5629 tree: &mut LayoutTree,
5630 text_cache: &mut TextLayoutCache,
5631 cell_index: usize,
5632 constraints: &LayoutConstraints<'_>,
5633 sizing_mode: text3::cache::AvailableSpace,
5634) -> Result<f32> {
5635 let width_type = match sizing_mode {
5636 text3::cache::AvailableSpace::MinContent => Text3AvailableSpace::MinContent,
5637 text3::cache::AvailableSpace::MaxContent => Text3AvailableSpace::MaxContent,
5638 text3::cache::AvailableSpace::Definite(w) => Text3AvailableSpace::Definite(w),
5639 };
5640 let cell_constraints = LayoutConstraints {
5641 available_size: LogicalSize {
5642 width: sizing_mode.to_f32_for_layout(),
5643 height: f32::INFINITY,
5644 },
5645 writing_mode: constraints.writing_mode,
5646 writing_mode_ctx: constraints.writing_mode_ctx,
5647 bfc_state: None,
5648 text_align: constraints.text_align,
5649 containing_block_size: constraints.containing_block_size,
5650 available_width_type: width_type,
5651 };
5652
5653 let mut temp_positions: super::PositionVec = Vec::new();
5654 let mut temp_scrollbar_reflow = false;
5655 let mut temp_float_cache = HashMap::new();
5656
5657 clear_subtree_cache(tree, &mut ctx.cache_map, cell_index);
5663
5664 crate::solver3::cache::calculate_layout_for_subtree(
5665 ctx,
5666 tree,
5667 text_cache,
5668 cell_index,
5669 LogicalPosition::zero(),
5670 cell_constraints.available_size,
5671 &mut temp_positions,
5672 &mut temp_scrollbar_reflow,
5673 &mut temp_float_cache,
5674 crate::solver3::cache::ComputeMode::ComputeSize,
5675 )?;
5676
5677 let cell_bp = tree.get(cell_index)
5678 .ok_or(LayoutError::InvalidTree)?
5679 .box_props.unpack();
5680 let padding = &cell_bp.padding;
5681 let border = &cell_bp.border;
5682 let wm = constraints.writing_mode;
5683
5684 let content_width = tree.warm(cell_index)
5689 .and_then(|w| w.overflow_content_size)
5690 .map_or_else(|| {
5691 tree.get(cell_index)
5692 .and_then(|n| n.used_size)
5693 .map_or(0.0, |s| s.width)
5694 }, |s| s.width);
5695
5696 Ok(content_width
5697 + padding.cross_start(wm) + padding.cross_end(wm)
5698 + border.cross_start(wm) + border.cross_end(wm))
5699}
5700
5701fn measure_cell_min_content_width<T: ParsedFontTrait>(
5703 ctx: &mut LayoutContext<'_, T>,
5704 tree: &mut LayoutTree,
5705 text_cache: &mut TextLayoutCache,
5706 cell_index: usize,
5707 constraints: &LayoutConstraints<'_>,
5708) -> Result<f32> {
5709 measure_cell_content_width(
5710 ctx, tree, text_cache, cell_index, constraints,
5711 text3::cache::AvailableSpace::MinContent,
5712 )
5713}
5714
5715fn measure_cell_max_content_width<T: ParsedFontTrait>(
5717 ctx: &mut LayoutContext<'_, T>,
5718 tree: &mut LayoutTree,
5719 text_cache: &mut TextLayoutCache,
5720 cell_index: usize,
5721 constraints: &LayoutConstraints<'_>,
5722) -> Result<f32> {
5723 measure_cell_content_width(
5724 ctx, tree, text_cache, cell_index, constraints,
5725 text3::cache::AvailableSpace::MaxContent,
5726 )
5727}
5728
5729fn calculate_column_widths_auto<T: ParsedFontTrait>(
5731 table_ctx: &mut TableLayoutContext,
5732 tree: &mut LayoutTree,
5733 text_cache: &mut TextLayoutCache,
5734 ctx: &mut LayoutContext<'_, T>,
5735 constraints: &LayoutConstraints<'_>,
5736) -> Result<()> {
5737 calculate_column_widths_auto_with_width(
5738 table_ctx,
5739 tree,
5740 text_cache,
5741 ctx,
5742 constraints,
5743 constraints.available_size.width,
5744 )
5745}
5746
5747#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines)] fn calculate_column_widths_auto_with_width<T: ParsedFontTrait>(
5758 table_ctx: &mut TableLayoutContext,
5759 tree: &mut LayoutTree,
5760 text_cache: &mut TextLayoutCache,
5761 ctx: &mut LayoutContext<'_, T>,
5762 constraints: &LayoutConstraints<'_>,
5763 table_width: f32,
5764) -> Result<()> {
5765 let num_cols = table_ctx.columns.len();
5767 if num_cols == 0 {
5768 return Ok(());
5769 }
5770
5771 for cell_info in &table_ctx.cells {
5774 if table_ctx.collapsed_columns.contains(&cell_info.column) {
5776 continue;
5777 }
5778
5779 let mut spans_collapsed = false;
5781 for col_offset in 0..cell_info.colspan {
5782 if table_ctx
5783 .collapsed_columns
5784 .contains(&(cell_info.column + col_offset))
5785 {
5786 spans_collapsed = true;
5787 break;
5788 }
5789 }
5790 if spans_collapsed {
5791 continue;
5792 }
5793
5794 let min_width = measure_cell_min_content_width(
5795 ctx,
5796 tree,
5797 text_cache,
5798 cell_info.node_index,
5799 constraints,
5800 )?;
5801
5802 let max_width = measure_cell_max_content_width(
5803 ctx,
5804 tree,
5805 text_cache,
5806 cell_info.node_index,
5807 constraints,
5808 )?;
5809
5810 if cell_info.colspan == 1 {
5812 let col = &mut table_ctx.columns[cell_info.column];
5813 col.min_width = col.min_width.max(min_width);
5814 col.max_width = col.max_width.max(max_width);
5815 } else {
5816 distribute_cell_width_across_columns(
5819 &mut table_ctx.columns,
5820 cell_info.column,
5821 cell_info.colspan,
5822 min_width,
5823 max_width,
5824 &table_ctx.collapsed_columns,
5825 );
5826 }
5827 }
5828
5829 let total_min_width: f32 = table_ctx
5832 .columns
5833 .iter()
5834 .enumerate()
5835 .filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
5836 .map(|(_, c)| c.min_width)
5837 .sum();
5838 let total_max_width: f32 = table_ctx
5839 .columns
5840 .iter()
5841 .enumerate()
5842 .filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
5843 .map(|(_, c)| c.max_width)
5844 .sum();
5845 let available_width = table_width; debug_table_layout!(
5848 ctx,
5849 "calculate_column_widths_auto: min={:.2}, max={:.2}, table_width={:.2}",
5850 total_min_width,
5851 total_max_width,
5852 table_width
5853 );
5854
5855 if !total_max_width.is_finite() || !available_width.is_finite() {
5857 let num_non_collapsed = table_ctx.columns.len() - table_ctx.collapsed_columns.len();
5859 let width_per_column = if num_non_collapsed > 0 {
5860 available_width / num_non_collapsed as f32
5861 } else {
5862 0.0
5863 };
5864
5865 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
5866 if table_ctx.collapsed_columns.contains(&col_idx) {
5867 col.computed_width = Some(0.0);
5868 } else {
5869 col.computed_width = Some(col.min_width.max(width_per_column));
5871 }
5872 }
5873 } else if available_width >= total_max_width {
5874 let excess_width = available_width - total_max_width;
5879
5880 let column_info: Vec<(usize, f32, bool)> = table_ctx
5882 .columns
5883 .iter()
5884 .enumerate()
5885 .map(|(idx, c)| (idx, c.max_width, table_ctx.collapsed_columns.contains(&idx)))
5886 .collect();
5887
5888 let total_weight: f32 = column_info.iter()
5890 .filter(|(_, _, is_collapsed)| !is_collapsed)
5891 .map(|(_, max_w, _)| max_w.max(1.0)) .sum();
5893
5894 let num_non_collapsed = column_info
5895 .iter()
5896 .filter(|(_, _, is_collapsed)| !is_collapsed)
5897 .count();
5898
5899 for (col_idx, max_width, is_collapsed) in column_info {
5901 let col = &mut table_ctx.columns[col_idx];
5902 if is_collapsed {
5903 col.computed_width = Some(0.0);
5904 } else {
5905 let weight_factor = if total_weight > 0.0 {
5907 max_width.max(1.0) / total_weight
5908 } else {
5909 1.0 / num_non_collapsed.max(1) as f32
5911 };
5912
5913 let final_width = max_width + (excess_width * weight_factor);
5914 col.computed_width = Some(final_width);
5915 }
5916 }
5917 } else if available_width >= total_min_width {
5918 let scale = if total_max_width > total_min_width {
5921 (available_width - total_min_width) / (total_max_width - total_min_width)
5922 } else {
5923 0.0 };
5925 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
5926 if table_ctx.collapsed_columns.contains(&col_idx) {
5927 col.computed_width = Some(0.0);
5928 } else {
5929 let interpolated = col.min_width + (col.max_width - col.min_width) * scale;
5930 col.computed_width = Some(interpolated);
5931 }
5932 }
5933 } else {
5934 for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
5938 if table_ctx.collapsed_columns.contains(&col_idx) {
5939 col.computed_width = Some(0.0);
5940 } else {
5941 col.computed_width = Some(col.min_width);
5942 }
5943 }
5944 }
5945
5946 Ok(())
5947}
5948
5949#[allow(clippy::cast_precision_loss)] fn distribute_cell_width_across_columns(
5952 columns: &mut [TableColumnInfo],
5953 start_col: usize,
5954 colspan: usize,
5955 cell_min_width: f32,
5956 cell_max_width: f32,
5957 collapsed_columns: &std::collections::HashSet<usize>,
5958) {
5959 let end_col = start_col + colspan;
5960 if end_col > columns.len() {
5961 return;
5962 }
5963
5964 let current_min_total: f32 = columns[start_col..end_col]
5966 .iter()
5967 .enumerate()
5968 .filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
5969 .map(|(_, c)| c.min_width)
5970 .sum();
5971 let current_max_total: f32 = columns[start_col..end_col]
5972 .iter()
5973 .enumerate()
5974 .filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
5975 .map(|(_, c)| c.max_width)
5976 .sum();
5977
5978 let num_visible_cols = (start_col..end_col)
5980 .filter(|idx| !collapsed_columns.contains(idx))
5981 .count();
5982
5983 if num_visible_cols == 0 {
5984 return; }
5986
5987 if cell_min_width > current_min_total {
5989 let extra_min = cell_min_width - current_min_total;
5990 let per_col = extra_min / num_visible_cols as f32;
5991 for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
5992 if !collapsed_columns.contains(&(start_col + idx)) {
5993 col.min_width += per_col;
5994 }
5995 }
5996 }
5997
5998 if cell_max_width > current_max_total {
5999 let extra_max = cell_max_width - current_max_total;
6000 let per_col = extra_max / num_visible_cols as f32;
6001 for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
6002 if !collapsed_columns.contains(&(start_col + idx)) {
6003 col.max_width += per_col;
6004 }
6005 }
6006 }
6007}
6008
6009#[allow(clippy::too_many_lines)] fn layout_cell_for_height<T: ParsedFontTrait>(
6012 ctx: &mut LayoutContext<'_, T>,
6013 tree: &mut LayoutTree,
6014 text_cache: &mut TextLayoutCache,
6015 cell_index: usize,
6016 cell_width: f32,
6017 constraints: &LayoutConstraints<'_>,
6018) -> Result<f32> {
6019 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
6020 let cell_dom_id = cell_node.dom_node_id.ok_or(LayoutError::InvalidTree)?;
6021
6022 let has_text_children = cell_dom_id
6026 .az_children(&ctx.styled_dom.node_hierarchy.as_container())
6027 .any(|child_id| {
6028 let node_data = &ctx.styled_dom.node_data.as_container()[child_id];
6029 matches!(node_data.get_node_type(), NodeType::Text(_))
6030 });
6031
6032 debug_table_layout!(
6033 ctx,
6034 "layout_cell_for_height: cell_index={}, has_text_children={}",
6035 cell_index,
6036 has_text_children
6037 );
6038
6039 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
6041 let cell_bp = cell_node.box_props.unpack();
6042 let padding = &cell_bp.padding;
6043 let border = &cell_bp.border;
6044 let writing_mode = constraints.writing_mode;
6045
6046 let content_width = cell_width
6049 - padding.cross_start(writing_mode)
6050 - padding.cross_end(writing_mode)
6051 - border.cross_start(writing_mode)
6052 - border.cross_end(writing_mode);
6053
6054 debug_table_layout!(
6055 ctx,
6056 "Cell width: border_box={:.2}, content_box={:.2}",
6057 cell_width,
6058 content_width
6059 );
6060
6061 let content_height = if has_text_children {
6062 debug_table_layout!(ctx, "Using IFC to measure text content");
6064
6065 let cell_constraints = LayoutConstraints {
6066 available_size: LogicalSize {
6067 width: content_width, height: f32::INFINITY,
6069 },
6070 writing_mode: constraints.writing_mode,
6071 writing_mode_ctx: constraints.writing_mode_ctx,
6072 bfc_state: None,
6073 text_align: constraints.text_align,
6074 containing_block_size: constraints.containing_block_size,
6075 available_width_type: Text3AvailableSpace::Definite(content_width),
6078 };
6079
6080 let output = layout_ifc(ctx, text_cache, tree, cell_index, &cell_constraints)?;
6081
6082 let cell_children: Vec<usize> = tree.children(cell_index).to_vec();
6086 for child_idx in cell_children {
6087 if let Some(warm) = tree.warm_mut(child_idx) {
6088 warm.inline_layout_result = None;
6089 }
6090 }
6091
6092 debug_table_layout!(
6093 ctx,
6094 "IFC returned height={:.2}",
6095 output.overflow_size.height
6096 );
6097
6098 output.overflow_size.height
6099 } else {
6100 debug_table_layout!(ctx, "Using regular layout for block children");
6102
6103 let cell_constraints = LayoutConstraints {
6104 available_size: LogicalSize {
6105 width: content_width, height: f32::INFINITY,
6107 },
6108 writing_mode: constraints.writing_mode,
6109 writing_mode_ctx: constraints.writing_mode_ctx,
6110 bfc_state: None,
6111 text_align: constraints.text_align,
6112 containing_block_size: constraints.containing_block_size,
6113 available_width_type: Text3AvailableSpace::Definite(content_width),
6115 };
6116
6117 let mut temp_positions: super::PositionVec = Vec::new();
6118 let mut temp_scrollbar_reflow = false;
6119 let mut temp_float_cache = HashMap::new();
6120
6121 crate::solver3::cache::calculate_layout_for_subtree(
6122 ctx,
6123 tree,
6124 text_cache,
6125 cell_index,
6126 LogicalPosition::zero(),
6127 cell_constraints.available_size,
6128 &mut temp_positions,
6129 &mut temp_scrollbar_reflow,
6130 &mut temp_float_cache,
6131 crate::solver3::cache::ComputeMode::PerformLayout,
6133 )?;
6134
6135 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
6136 cell_node.used_size.unwrap_or_default().height
6137 };
6138
6139 let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
6141 let cell_bp = cell_node.box_props.unpack();
6142 let padding = &cell_bp.padding;
6143 let border = &cell_bp.border;
6144 let writing_mode = constraints.writing_mode;
6145
6146 let total_height = content_height
6147 + padding.main_start(writing_mode)
6148 + padding.main_end(writing_mode)
6149 + border.main_start(writing_mode)
6150 + border.main_end(writing_mode);
6151
6152 debug_table_layout!(
6153 ctx,
6154 "Cell total height: cell_index={}, content={:.2}, padding/border={:.2}, total={:.2}",
6155 cell_index,
6156 content_height,
6157 padding.main_start(writing_mode)
6158 + padding.main_end(writing_mode)
6159 + border.main_start(writing_mode)
6160 + border.main_end(writing_mode),
6161 total_height
6162 );
6163
6164 Ok(total_height)
6165}
6166
6167fn compute_cell_baseline(cell_index: usize, tree: &LayoutTree) -> f32 {
6173 let Some(cell_node) = tree.get(cell_index) else {
6174 return 0.0;
6175 };
6176
6177 let cell_bp = cell_node.box_props.unpack();
6178
6179 if let Some(warm_node) = tree.warm(cell_index) {
6182 if let Some(ref cached_layout) = warm_node.inline_layout_result {
6183 let inline_result = &cached_layout.layout;
6184 if let Some(first_item) = inline_result.items.first() {
6186 let (item_ascent, _) = text3::cache::get_item_vertical_metrics_approx(&first_item.item);
6187 let padding_top = cell_bp.padding.top;
6188 let border_top = cell_bp.border.top;
6189 return padding_top + border_top + first_item.position.y + item_ascent;
6190 }
6191 }
6192 }
6193
6194 let children = tree.children(cell_index);
6196 for &child_idx in children {
6197 if child_idx < tree.nodes.len() {
6198 if let Some(child_warm) = tree.warm(child_idx) {
6199 if child_warm.inline_layout_result.is_some() {
6200 let child_baseline = compute_cell_baseline(child_idx, tree);
6201 let padding_top = cell_bp.padding.top;
6202 let border_top = cell_bp.border.top;
6203 return padding_top + border_top + child_baseline;
6204 }
6205 }
6206 }
6207 }
6208
6209 let used_size = cell_node.used_size.unwrap_or_default();
6211 let padding_bottom = cell_bp.padding.bottom;
6212 let border_bottom = cell_bp.border.bottom;
6213 used_size.height - padding_bottom - border_bottom
6214}
6215
6216#[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn calculate_row_heights<T: ParsedFontTrait>(
6225 table_ctx: &mut TableLayoutContext,
6226 tree: &mut LayoutTree,
6227 text_cache: &mut TextLayoutCache,
6228 ctx: &mut LayoutContext<'_, T>,
6229 constraints: &LayoutConstraints<'_>,
6230) -> Result<()> {
6231 debug_table_layout!(
6232 ctx,
6233 "calculate_row_heights: num_rows={}, available_size={:?}",
6234 table_ctx.num_rows,
6235 constraints.available_size
6236 );
6237
6238 table_ctx.row_heights = vec![0.0; table_ctx.num_rows];
6241 table_ctx.row_baselines = vec![0.0; table_ctx.num_rows];
6242
6243 for &row_idx in &table_ctx.collapsed_rows {
6245 if row_idx < table_ctx.row_heights.len() {
6246 table_ctx.row_heights[row_idx] = 0.0;
6247 }
6248 }
6249
6250 for cell_info in &table_ctx.cells {
6254 if table_ctx.collapsed_rows.contains(&cell_info.row) {
6256 continue;
6257 }
6258
6259 let mut cell_width = 0.0;
6261 for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
6262 if let Some(col) = table_ctx.columns.get(col_idx) {
6263 if let Some(width) = col.computed_width {
6264 cell_width += width;
6265 }
6266 }
6267 }
6268
6269 debug_table_layout!(
6270 ctx,
6271 "Cell layout: node_index={}, row={}, col={}, width={:.2}",
6272 cell_info.node_index,
6273 cell_info.row,
6274 cell_info.column,
6275 cell_width
6276 );
6277
6278 let cell_height = layout_cell_for_height(
6280 ctx,
6281 tree,
6282 text_cache,
6283 cell_info.node_index,
6284 cell_width,
6285 constraints,
6286 )?;
6287
6288 debug_table_layout!(
6289 ctx,
6290 "Cell height calculated: node_index={}, height={:.2}",
6291 cell_info.node_index,
6292 cell_height
6293 );
6294
6295 if cell_info.rowspan == 1 {
6297 let current_height = table_ctx.row_heights[cell_info.row];
6298 table_ctx.row_heights[cell_info.row] = current_height.max(cell_height);
6299 }
6300
6301 if cell_info.rowspan == 1 {
6305 let cell_baseline = compute_cell_baseline(cell_info.node_index, tree);
6306 let current_baseline = table_ctx.row_baselines[cell_info.row];
6307 table_ctx.row_baselines[cell_info.row] = current_baseline.max(cell_baseline);
6308 }
6309 }
6310
6311 for cell_info in &table_ctx.cells {
6314 if table_ctx.collapsed_rows.contains(&cell_info.row) {
6316 continue;
6317 }
6318
6319 if cell_info.rowspan > 1 {
6320 let mut cell_width = 0.0;
6322 for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
6323 if let Some(col) = table_ctx.columns.get(col_idx) {
6324 if let Some(width) = col.computed_width {
6325 cell_width += width;
6326 }
6327 }
6328 }
6329
6330 let cell_height = layout_cell_for_height(
6332 ctx,
6333 tree,
6334 text_cache,
6335 cell_info.node_index,
6336 cell_width,
6337 constraints,
6338 )?;
6339
6340 let end_row = (cell_info.row + cell_info.rowspan).min(table_ctx.row_heights.len());
6345 let current_total: f32 = table_ctx.row_heights[cell_info.row..end_row]
6346 .iter()
6347 .enumerate()
6348 .filter(|(idx, _)| !table_ctx.collapsed_rows.contains(&(cell_info.row + idx)))
6349 .map(|(_, height)| height)
6350 .sum();
6351
6352 if cell_height > current_total {
6355 let extra_height = cell_height - current_total;
6356
6357 let non_collapsed_rows = (cell_info.row..end_row)
6359 .filter(|row_idx| !table_ctx.collapsed_rows.contains(row_idx))
6360 .count();
6361
6362 if non_collapsed_rows > 0 {
6363 let per_row = extra_height / non_collapsed_rows as f32;
6364
6365 for row_idx in cell_info.row..end_row {
6366 if !table_ctx.collapsed_rows.contains(&row_idx) {
6367 table_ctx.row_heights[row_idx] += per_row;
6368 }
6369 }
6370 }
6371 }
6372 }
6373 }
6374
6375 for &row_idx in &table_ctx.collapsed_rows {
6377 if row_idx < table_ctx.row_heights.len() {
6378 table_ctx.row_heights[row_idx] = 0.0;
6379 }
6380 }
6381
6382 if table_ctx.border_collapse == StyleBorderCollapse::Separate {
6388 for row_idx in 0..table_ctx.num_rows {
6389 if table_ctx.collapsed_rows.contains(&row_idx) {
6390 continue;
6391 }
6392 let row_cells: Vec<usize> = table_ctx
6394 .cells
6395 .iter()
6396 .filter(|c| c.row == row_idx && c.rowspan == 1)
6397 .map(|c| c.node_index)
6398 .collect();
6399 if row_cells.is_empty() {
6400 continue;
6401 }
6402 let all_hidden_empty = row_cells.iter().all(|&cell_idx| {
6405 tree.get(cell_idx).is_none_or(|cell_node| {
6406 let ec = get_empty_cells_property(ctx, cell_node);
6407 ec == StyleEmptyCells::Hide && is_cell_empty(tree, cell_idx)
6408 })
6409 });
6410 if all_hidden_empty {
6411 table_ctx.row_heights[row_idx] = 0.0;
6412 table_ctx.hidden_empty_rows.insert(row_idx);
6413 }
6414 }
6415 }
6416
6417 Ok(())
6418}
6419
6420#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn position_table_cells<T: ParsedFontTrait>(
6425 table_ctx: &TableLayoutContext,
6426 tree: &mut LayoutTree,
6427 ctx: &mut LayoutContext<'_, T>,
6428 table_index: usize,
6429 constraints: &LayoutConstraints<'_>,
6430) -> Result<BTreeMap<usize, LogicalPosition>> {
6431 debug_log!(ctx, "Positioning table cells in grid");
6432
6433 let mut positions = BTreeMap::new();
6434
6435 let (h_spacing, v_spacing) = if table_ctx.border_collapse == StyleBorderCollapse::Separate {
6442 let styled_dom = ctx.styled_dom;
6443 if let Some(table_id) = tree.nodes[table_index].dom_node_id {
6447 let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
6448
6449 let spacing_context = ResolutionContext {
6450 element_font_size: get_element_font_size(styled_dom, table_id, table_state),
6451 parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
6452 root_font_size: get_root_font_size(styled_dom, table_state),
6453 containing_block_size: PhysicalSize::new(0.0, 0.0),
6454 element_size: None,
6455 viewport_size: PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height),
6456 };
6457
6458 let h = table_ctx
6459 .border_spacing
6460 .horizontal
6461 .resolve_with_context(&spacing_context, PropertyContext::Other)
6462 .max(0.0);
6463
6464 let v = table_ctx
6465 .border_spacing
6466 .vertical
6467 .resolve_with_context(&spacing_context, PropertyContext::Other)
6468 .max(0.0);
6469
6470 (h, v)
6471 } else {
6472 (0.0, 0.0)
6473 }
6474 } else {
6475 (0.0, 0.0)
6476 };
6477
6478 debug_log!(
6479 ctx,
6480 "Border spacing: h={:.2}, v={:.2}",
6481 h_spacing,
6482 v_spacing
6483 );
6484
6485 let mut col_positions = vec![0.0; table_ctx.columns.len()];
6487 let mut x_offset = h_spacing; for (i, col) in table_ctx.columns.iter().enumerate() {
6489 col_positions[i] = x_offset;
6490 if let Some(width) = col.computed_width {
6491 if table_ctx.collapsed_columns.contains(&i) {
6493 } else {
6495 x_offset += width + h_spacing; }
6497 }
6498 }
6499
6500 let mut row_positions = vec![0.0; table_ctx.num_rows];
6502 let mut y_offset = v_spacing; for (i, &height) in table_ctx.row_heights.iter().enumerate() {
6504 row_positions[i] = y_offset;
6505 if table_ctx.collapsed_rows.contains(&i) {
6507 } else if table_ctx.hidden_empty_rows.contains(&i) {
6509 y_offset += height; } else {
6513 y_offset += height + v_spacing; }
6515 }
6516
6517 {
6520 let total_col_width: f32 = table_ctx.columns.iter().map(|c| c.computed_width.unwrap_or(0.0)).sum::<f32>()
6521 + h_spacing * (table_ctx.columns.len().max(1) - 1) as f32
6522 + h_spacing * 2.0; for (i, &row_y) in row_positions.iter().enumerate() {
6524 if let Some(&row_node_idx) = table_ctx.row_node_indices.get(i) {
6525 let row_height = table_ctx.row_heights.get(i).copied().unwrap_or(0.0);
6526 if let Some(row_node) = tree.get_mut(row_node_idx) {
6527 row_node.used_size = Some(LogicalSize {
6528 width: total_col_width,
6529 height: row_height,
6530 });
6531 }
6532 }
6536 }
6537 }
6538
6539 for cell_info in &table_ctx.cells {
6541 let precomputed_cell_baseline = compute_cell_baseline(cell_info.node_index, tree);
6542
6543 let cell_node = tree
6544 .get_mut(cell_info.node_index)
6545 .ok_or(LayoutError::InvalidTree)?;
6546
6547 let x = col_positions.get(cell_info.column).copied().unwrap_or(0.0);
6549 let y = row_positions.get(cell_info.row).copied().unwrap_or(0.0);
6550
6551 let mut width = 0.0;
6553 debug_info!(
6554 ctx,
6555 "[position_table_cells] Cell {}: calculating width from cols {}..{}",
6556 cell_info.node_index,
6557 cell_info.column,
6558 cell_info.column + cell_info.colspan
6559 );
6560 for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
6561 if let Some(col) = table_ctx.columns.get(col_idx) {
6562 debug_info!(
6563 ctx,
6564 "[position_table_cells] Col {}: computed_width={:?}",
6565 col_idx,
6566 col.computed_width
6567 );
6568 if let Some(col_width) = col.computed_width {
6569 width += col_width;
6570 if col_idx < cell_info.column + cell_info.colspan - 1 {
6572 width += h_spacing;
6573 }
6574 } else {
6575 debug_info!(
6576 ctx,
6577 "[position_table_cells] WARN: Col {} has NO computed_width!",
6578 col_idx
6579 );
6580 }
6581 } else {
6582 debug_info!(
6583 ctx,
6584 "[position_table_cells] WARN: Col {} not found in table_ctx.columns!",
6585 col_idx
6586 );
6587 }
6588 }
6589
6590 let mut height = 0.0;
6591 let end_row = cell_info.row + cell_info.rowspan;
6592 for row_idx in cell_info.row..end_row {
6593 if let Some(&row_height) = table_ctx.row_heights.get(row_idx) {
6594 height += row_height;
6595 if row_idx < end_row - 1 {
6597 height += v_spacing;
6598 }
6599 }
6600 }
6601
6602 let writing_mode = constraints.writing_mode;
6604 debug_info!(
6607 ctx,
6608 "[position_table_cells] Cell {}: BEFORE from_main_cross: width={}, height={}, \
6609 writing_mode={:?}",
6610 cell_info.node_index,
6611 width,
6612 height,
6613 writing_mode
6614 );
6615
6616 cell_node.used_size = Some(LogicalSize::from_main_cross(height, width, writing_mode));
6617
6618 debug_info!(
6619 ctx,
6620 "[position_table_cells] Cell {}: AFTER from_main_cross: used_size={:?}",
6621 cell_info.node_index,
6622 cell_node.used_size
6623 );
6624
6625 debug_info!(
6626 ctx,
6627 "[position_table_cells] Cell {}: setting used_size to {}x{} (row_heights={:?})",
6628 cell_info.node_index,
6629 width,
6630 height,
6631 table_ctx.row_heights
6632 );
6633
6634 let cell_dom_node_id = cell_node.dom_node_id;
6636 let cell_box_props = cell_node.box_props.unpack();
6637 drop(cell_node);
6638
6639 let vertical_align_adjustment = if let Some(warm_node) = tree.warm(cell_info.node_index) {
6647 if let Some(ref cached_layout) = warm_node.inline_layout_result {
6648 let inline_result = &cached_layout.layout;
6649
6650 let vertical_align = if let Some(dom_id) = cell_dom_node_id {
6652 let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
6653
6654 match get_vertical_align_property(ctx.styled_dom, dom_id, &node_state) {
6655 MultiValue::Exact(v) => v,
6656 _ => StyleVerticalAlign::Baseline,
6657 }
6658 } else {
6659 StyleVerticalAlign::Baseline
6660 };
6661
6662 let content_bounds = inline_result.bounds();
6664 let content_height = content_bounds.height;
6665
6666 let padding = &cell_box_props.padding;
6669 let border = &cell_box_props.border;
6670 let content_box_height = height
6671 - padding.main_start(writing_mode)
6672 - padding.main_end(writing_mode)
6673 - border.main_start(writing_mode)
6674 - border.main_end(writing_mode);
6675
6676 let y_offset = match vertical_align {
6681 StyleVerticalAlign::Top => 0.0,
6682 StyleVerticalAlign::Middle => (content_box_height - content_height) * 0.5,
6683 StyleVerticalAlign::Bottom => content_box_height - content_height,
6684 StyleVerticalAlign::Baseline
6687 | StyleVerticalAlign::Sub
6688 | StyleVerticalAlign::Superscript
6689 | StyleVerticalAlign::TextTop
6690 | StyleVerticalAlign::TextBottom
6691 | StyleVerticalAlign::Percentage(_)
6692 | StyleVerticalAlign::Length(_) => {
6693 let row_baseline = table_ctx.row_baselines.get(cell_info.row).copied().unwrap_or(0.0);
6694 (row_baseline - precomputed_cell_baseline).max(0.0)
6695 }
6696 };
6697
6698 debug_info!(
6699 ctx,
6700 "[position_table_cells] Cell {}: vertical-align={:?}, border_box_height={}, \
6701 content_box_height={}, content_height={}, y_offset={}",
6702 cell_info.node_index,
6703 vertical_align,
6704 height,
6705 content_box_height,
6706 content_height,
6707 y_offset
6708 );
6709
6710 if y_offset.abs() > 0.01 {
6711 Some((y_offset, cached_layout.available_width, cached_layout.has_floats))
6712 } else {
6713 None
6714 }
6715 } else {
6716 None
6717 }
6718 } else {
6719 None
6720 };
6721
6722 if let Some((y_offset, available_width, has_floats)) = vertical_align_adjustment {
6724 if let Some(warm_mut) = tree.warm_mut(cell_info.node_index) {
6725 if let Some(ref cached_layout) = warm_mut.inline_layout_result {
6726 use std::sync::Arc;
6727 use crate::text3::cache::{PositionedItem, UnifiedLayout};
6728
6729 let adjusted_items: Vec<PositionedItem> = cached_layout.layout
6730 .items
6731 .iter()
6732 .map(|item| PositionedItem {
6733 item: item.item.clone(),
6734 position: text3::cache::Point {
6735 x: item.position.x,
6736 y: item.position.y + y_offset,
6737 },
6738 line_index: item.line_index,
6739 })
6740 .collect();
6741
6742 let adjusted_layout = UnifiedLayout {
6743 items: adjusted_items,
6744 overflow: cached_layout.layout.overflow.clone(),
6745 };
6746
6747 let mut cil = CachedInlineLayout::new(
6749 Arc::new(adjusted_layout),
6750 available_width,
6751 has_floats,
6752 );
6753 cil.inline_content_hash = cached_layout.inline_content_hash;
6756 warm_mut.inline_layout_result = Some(cil);
6757 }
6758 }
6759 }
6760
6761 let cell_has_inline = tree
6770 .warm(cell_info.node_index)
6771 .is_some_and(|w| w.inline_layout_result.is_some());
6772 if !cell_has_inline {
6773 let vertical_align = cell_dom_node_id.map_or(StyleVerticalAlign::Baseline, |dom_id| {
6774 let node_state =
6775 ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
6776 match get_vertical_align_property(ctx.styled_dom, dom_id, &node_state) {
6777 MultiValue::Exact(v) => v,
6778 _ => StyleVerticalAlign::Baseline,
6779 }
6780 });
6781
6782 let factor = match vertical_align {
6785 StyleVerticalAlign::Middle => 0.5,
6786 StyleVerticalAlign::Bottom => 1.0,
6787 _ => 0.0,
6788 };
6789 if factor > 0.0 {
6790 let children: Vec<usize> = tree.children(cell_info.node_index).to_vec();
6791 let mut content_height = 0.0f32;
6795 let mut inflow: Vec<usize> = Vec::new();
6796 for &c in &children {
6797 let dom_id = tree.get(c).and_then(|n| n.dom_node_id);
6798 if matches!(
6799 get_position_type(ctx.styled_dom, dom_id),
6800 LayoutPosition::Absolute | LayoutPosition::Fixed
6801 ) {
6802 continue; }
6804 let top = tree
6805 .warm(c)
6806 .and_then(|w| w.relative_position)
6807 .map_or(0.0, |p| p.y);
6808 let h = tree.get(c).and_then(|n| n.used_size).map_or(0.0, |s| s.height);
6809 content_height = content_height.max(top + h);
6810 inflow.push(c);
6811 }
6812 let content_box_height = height
6813 - cell_box_props.padding.main_start(writing_mode)
6814 - cell_box_props.padding.main_end(writing_mode)
6815 - cell_box_props.border.main_start(writing_mode)
6816 - cell_box_props.border.main_end(writing_mode);
6817 let y_offset = (content_box_height - content_height) * factor;
6818 if y_offset > 0.01 {
6819 for &c in &inflow {
6820 if let Some(w) = tree.warm_mut(c) {
6821 if let Some(pos) = w.relative_position.as_mut() {
6822 pos.y += y_offset;
6823 }
6824 }
6825 }
6826 }
6827 }
6828 }
6829
6830 let position = LogicalPosition::from_main_cross(y, x, writing_mode);
6832
6833 positions.insert(cell_info.node_index, position);
6835
6836 debug_log!(
6837 ctx,
6838 "Cell at row={}, col={}: pos=({:.2}, {:.2}), size=({:.2}x{:.2})",
6839 cell_info.row,
6840 cell_info.column,
6841 x,
6842 y,
6843 width,
6844 height
6845 );
6846 }
6847
6848 Ok(positions)
6849}
6850
6851fn collect_and_measure_inline_content<T: ParsedFontTrait>(
6862 ctx: &mut LayoutContext<'_, T>,
6863 text_cache: &mut TextLayoutCache,
6864 tree: &mut LayoutTree,
6865 ifc_root_index: usize,
6866 constraints: &LayoutConstraints<'_>,
6867) -> Result<(Vec<InlineContent>, HashMap<ContentIndex, usize>)> {
6868 use crate::solver3::layout_tree::{IfcId, IfcMembership};
6869 use crate::text3::cache::InlineContent;
6870
6871 let mut content = Vec::new();
6872 let mut child_map = HashMap::new();
6873 collect_and_measure_inline_content_impl(
6874 ctx,
6875 text_cache,
6876 tree,
6877 ifc_root_index,
6878 constraints,
6879 &mut content,
6880 &mut child_map,
6881 )?;
6882 Ok((content, child_map))
6883}
6884
6885#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn collect_and_measure_inline_content_impl<T: ParsedFontTrait>(
6888 ctx: &mut LayoutContext<'_, T>,
6889 text_cache: &mut TextLayoutCache,
6890 tree: &mut LayoutTree,
6891 ifc_root_index: usize,
6892 constraints: &LayoutConstraints<'_>,
6893 content: &mut Vec<InlineContent>,
6894 child_map: &mut HashMap<ContentIndex, usize>,
6895) -> Result<()> {
6896 use crate::solver3::layout_tree::{IfcId, IfcMembership};
6897
6898 debug_ifc_layout!(
6899 ctx,
6900 "collect_and_measure_inline_content: node_index={}",
6901 ifc_root_index
6902 );
6903
6904 let ifc_id = IfcId::unique();
6906
6907 if let Some(cold_node) = tree.cold_mut(ifc_root_index) {
6909 cold_node.ifc_id = Some(ifc_id);
6910 }
6911
6912 let mut current_run_index: u32 = 0;
6918
6919 #[cfg(feature = "web_lift")]
6923 unsafe {
6924 crate::az_mark((0x60690) as u32, (content as *const _ as usize as u32) as u32);
6925 crate::az_mark((0x60694) as u32, (ifc_root_index as u32 | 0xC0DE0000u32) as u32);
6926 crate::az_mark((0x606A8) as u32, (tree.nodes.len() as u32) as u32);
6927 crate::az_mark((0x606AC) as u32, (tree.get(ifc_root_index).is_some() as u32 | 0xC0DE0000u32) as u32);
6928 crate::az_mark((0x606B0) as u32, (tree.root as u32) as u32);
6929 let slot = (ifc_root_index & 7) * 4;
6932 crate::az_mark(((0x60940 + slot)) as u32, (tree.nodes.len() as u32) as u32);
6933 crate::az_mark(((0x60960 + slot)) as u32, ((&*tree as *const LayoutTree as usize) as u32) as u32);
6934 }
6935
6936 let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
6937 #[cfg(feature = "web_lift")]
6939 unsafe { crate::az_mark((0x606A4) as u32, (0x0000_6449u32) as u32); }
6940
6941 let is_anonymous = ifc_root_node.dom_node_id.is_none();
6943
6944 let ifc_root_dom_id = if let Some(id) = ifc_root_node.dom_node_id { id } else {
6947 let parent_dom_id = ifc_root_node
6949 .parent
6950 .and_then(|p| tree.get(p))
6951 .and_then(|n| n.dom_node_id);
6952
6953 if let Some(id) = parent_dom_id {
6954 id
6955 } else {
6956 if let Some(id) = tree.children(ifc_root_index)
6958 .iter()
6959 .filter_map(|&child_idx| tree.get(child_idx)).find_map(|n| n.dom_node_id) { id } else {
6960 debug_warning!(ctx, "IFC root and all ancestors/children have no DOM ID");
6961 return Ok(());
6962 }
6963 }
6964 };
6965
6966 let children: Vec<_> = tree.children(ifc_root_index).to_vec();
6968 drop(ifc_root_node);
6969
6970 debug_ifc_layout!(
6971 ctx,
6972 "Node {} has {} layout children, is_anonymous={}",
6973 ifc_root_index,
6974 children.len(),
6975 is_anonymous
6976 );
6977
6978 if is_anonymous {
6981 for (item_idx, &child_index) in children.iter().enumerate() {
6983 let content_index = ContentIndex {
6984 run_index: ifc_root_index as u32,
6985 item_index: item_idx as u32,
6986 };
6987
6988 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
6989 let Some(dom_id) = child_node.dom_node_id else {
6990 debug_warning!(
6991 ctx,
6992 "Anonymous IFC child at index {} has no DOM ID",
6993 child_index
6994 );
6995 continue;
6996 };
6997
6998 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
6999
7000 if let NodeType::Text(ref text_content) = node_data.get_node_type() {
7002 debug_info!(
7003 ctx,
7004 "[collect_and_measure_inline_content] OK: Found text node (DOM {:?}) in anonymous wrapper: '{}'",
7005 dom_id,
7006 text_content.as_str()
7007 );
7008 let style = Arc::new(get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
7011 let text_items = split_text_for_whitespace(
7012 ctx.styled_dom,
7013 dom_id,
7014 text_content.as_str(),
7015 &style,
7016 );
7017 content.extend(text_items);
7018 child_map.insert(content_index, child_index);
7019
7020 drop(child_node);
7022 if let Some(warm_mut) = tree.warm_mut(child_index) {
7023 warm_mut.ifc_membership = Some(IfcMembership {
7024 ifc_id,
7025 ifc_root_layout_index: ifc_root_index,
7026 run_index: current_run_index,
7027 });
7028 }
7029 current_run_index += 1;
7030
7031 continue;
7032 }
7033
7034 if matches!(node_data.get_node_type(), NodeType::Br) {
7038 content.push(InlineContent::LineBreak(InlineBreak {
7039 break_type: BreakType::Hard,
7040 clear: ClearType::None,
7041 content_index: content.len(),
7042 }));
7043 continue;
7044 }
7045
7046 if matches!(
7049 get_position_type(ctx.styled_dom, Some(dom_id)),
7050 LayoutPosition::Absolute | LayoutPosition::Fixed
7051 ) {
7052 continue;
7053 }
7054
7055 let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
7057
7058 if display == LayoutDisplay::Inline {
7059 let span_style = get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
7061 collect_inline_span_recursive(
7062 ctx,
7063 tree,
7064 dom_id,
7065 &span_style,
7066 content,
7067 &children,
7068 constraints,
7069 )?;
7070 } else {
7071 let intrinsic_size = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
7077 let box_props = child_node.box_props.unpack();
7078
7079 let styled_node_state = ctx
7080 .styled_dom
7081 .styled_nodes
7082 .as_container()
7083 .get(dom_id)
7084 .map(|n| n.styled_node_state)
7085 .unwrap_or_default();
7086
7087 let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
7089 ctx.styled_dom,
7090 Some(dom_id),
7091 &constraints.containing_block_size,
7092 intrinsic_size,
7093 &box_props,
7094 &ctx.viewport_size,
7095 )?;
7096
7097 let writing_mode = get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state)
7098 .unwrap_or_default();
7099
7100 let content_box_size = box_props.inner_size(tentative_size, writing_mode);
7102
7103 let child_wm_ctx = super::geometry::WritingModeContext::new(
7105 writing_mode,
7106 get_direction_property(ctx.styled_dom, dom_id, &styled_node_state)
7107 .unwrap_or_default(),
7108 get_text_orientation_property(ctx.styled_dom, dom_id, &styled_node_state)
7109 .unwrap_or_default(),
7110 );
7111 let child_constraints = LayoutConstraints {
7112 available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
7113 writing_mode,
7114 writing_mode_ctx: child_wm_ctx,
7115 bfc_state: None,
7116 text_align: TextAlign::Start,
7117 containing_block_size: constraints.containing_block_size,
7118 available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
7119 };
7120
7121 drop(child_node);
7123
7124 let mut empty_float_cache = HashMap::new();
7126 let layout_result = layout_formatting_context(
7127 ctx,
7128 tree,
7129 text_cache,
7130 child_index,
7131 &child_constraints,
7132 &mut empty_float_cache,
7133 )?;
7134
7135 let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
7136
7137 let is_replaced_atomic = {
7141 let nd = &ctx.styled_dom.node_data.as_container()[dom_id];
7142 matches!(nd.get_node_type(), NodeType::Image(_)) || nd.is_virtual_view_node()
7143 };
7144 let final_height = match css_height.unwrap_or_default() {
7146 LayoutHeight::Auto if !is_replaced_atomic => {
7147 let content_height = layout_result.output.overflow_size.height;
7148 content_height
7149 + box_props.padding.main_sum(writing_mode)
7150 + box_props.border.main_sum(writing_mode)
7151 }
7152 _ => tentative_size.height,
7153 };
7154
7155 let final_size = LogicalSize::new(tentative_size.width, final_height);
7156
7157 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
7159
7160 let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
7163 let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
7164 let overflow_is_visible = matches!(
7165 (overflow_x, overflow_y),
7166 (LayoutOverflow::Visible, LayoutOverflow::Visible)
7167 );
7168 let baseline_offset = if overflow_is_visible {
7169 layout_result.output.baseline.unwrap_or(final_height)
7170 } else {
7171 final_height
7172 };
7173
7174 let margin = &box_props.margin;
7177 let margin_box_width = final_size.width + margin.left + margin.right;
7178 let margin_box_height = final_size.height + margin.top + margin.bottom;
7179
7180 let shape_content_index = ContentIndex {
7183 run_index: content.len() as u32,
7184 item_index: 0,
7185 };
7186 content.push(InlineContent::Shape(InlineShape {
7187 shape_def: ShapeDefinition::Rectangle {
7188 size: crate::text3::cache::Size {
7189 width: margin_box_width,
7191 height: margin_box_height,
7192 },
7193 corner_radius: None,
7194 },
7195 fill: None,
7196 stroke: None,
7197 baseline_offset: baseline_offset + margin.top,
7199 alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, dom_id),
7200 source_node_id: Some(dom_id),
7201 }));
7202 child_map.insert(shape_content_index, child_index);
7203 }
7204 }
7205
7206 return Ok(());
7207 }
7208
7209 let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
7215 #[cfg(feature = "web_lift")]
7217 unsafe { crate::az_mark((0x606A4) as u32, (0x0000_6706u32) as u32); }
7218 let mut list_item_dom_id: Option<NodeId> = None;
7219
7220 if let Some(dom_id) = ifc_root_node.dom_node_id {
7222 use crate::solver3::getters::get_display_property;
7223 if let MultiValue::Exact(display) = get_display_property(ctx.styled_dom, Some(dom_id)) {
7224 use LayoutDisplay;
7225 if display == LayoutDisplay::ListItem {
7226 debug_ifc_layout!(ctx, "IFC root NodeId({:?}) is list-item", dom_id);
7227 list_item_dom_id = Some(dom_id);
7228 }
7229 }
7230 }
7231
7232 if list_item_dom_id.is_none() {
7234 if let Some(parent_idx) = ifc_root_node.parent {
7235 if let Some(parent_node) = tree.get(parent_idx) {
7236 if let Some(parent_dom_id) = parent_node.dom_node_id {
7237 use crate::solver3::getters::get_display_property;
7238 if let MultiValue::Exact(display) = get_display_property(ctx.styled_dom, Some(parent_dom_id)) {
7239 use LayoutDisplay;
7240 if display == LayoutDisplay::ListItem {
7241 debug_ifc_layout!(
7242 ctx,
7243 "IFC root parent NodeId({:?}) is list-item",
7244 parent_dom_id
7245 );
7246 list_item_dom_id = Some(parent_dom_id);
7247 }
7248 }
7249 }
7250 }
7251 }
7252 }
7253
7254 if let Some(list_dom_id) = list_item_dom_id {
7256 debug_ifc_layout!(
7257 ctx,
7258 "Found list-item (NodeId({:?})), generating marker",
7259 list_dom_id
7260 );
7261
7262 let list_item_layout_idx = tree
7264 .nodes
7265 .iter()
7266 .enumerate()
7267 .find(|(idx, node)| {
7268 node.dom_node_id == Some(list_dom_id) && tree.warm(*idx).and_then(|w| w.pseudo_element).is_none()
7269 })
7270 .map(|(idx, _)| idx);
7271
7272 if let Some(list_idx) = list_item_layout_idx {
7273 let marker_idx = tree.children(list_idx)
7276 .iter()
7277 .find(|&&child_idx| {
7278 tree.warm(child_idx)
7279 .is_some_and(|w| w.pseudo_element == Some(PseudoElement::Marker))
7280 })
7281 .copied();
7282
7283 if let Some(marker_idx) = marker_idx {
7284 debug_ifc_layout!(ctx, "Found ::marker pseudo-element at index {}", marker_idx);
7285
7286 let list_dom_id_for_style = tree
7289 .get(marker_idx)
7290 .and_then(|n| n.dom_node_id)
7291 .unwrap_or(list_dom_id);
7292
7293 let list_style_position =
7297 get_list_style_position(ctx.styled_dom, Some(list_dom_id));
7298 let position_outside =
7299 matches!(list_style_position, StyleListStylePosition::Outside);
7300
7301 debug_ifc_layout!(
7302 ctx,
7303 "List marker list-style-position: {:?} (outside={})",
7304 list_style_position,
7305 position_outside
7306 );
7307
7308 let base_style =
7310 Arc::new(get_style_properties(ctx.styled_dom, list_dom_id_for_style, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
7311 let marker_segments = generate_list_marker_segments(
7312 tree,
7313 ctx.styled_dom,
7314 marker_idx, ctx.counters,
7316 base_style,
7317 ctx.debug_messages,
7318 );
7319
7320 debug_ifc_layout!(
7321 ctx,
7322 "Generated {} list marker segments",
7323 marker_segments.len()
7324 );
7325
7326 for segment in marker_segments {
7329 content.push(InlineContent::Marker {
7330 run: segment,
7331 position_outside,
7332 });
7333 }
7334 } else {
7335 debug_ifc_layout!(
7336 ctx,
7337 "WARNING: List-item at index {} has no ::marker pseudo-element",
7338 list_idx
7339 );
7340 }
7341 }
7342 }
7343
7344 drop(ifc_root_node);
7345
7346 let node_hier_item = &ctx.styled_dom.node_hierarchy.as_container()[ifc_root_dom_id];
7354 debug_info!(
7355 ctx,
7356 "[collect_and_measure_inline_content] DEBUG: node_hier_item.first_child={:?}, \
7357 last_child={:?}",
7358 node_hier_item.first_child_id(ifc_root_dom_id),
7359 node_hier_item.last_child_id()
7360 );
7361
7362 let ifc_root_node_data = &ctx.styled_dom.node_data.as_container()[ifc_root_dom_id];
7363
7364 if let NodeType::Text(ref text_content) = ifc_root_node_data.get_node_type() {
7367 let style = Arc::new(get_style_properties(ctx.styled_dom, ifc_root_dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
7368 let text_items = split_text_for_whitespace(
7369 ctx.styled_dom,
7370 ifc_root_dom_id,
7371 text_content.as_str(),
7372 &style,
7373 );
7374 content.extend(text_items);
7375 return Ok(());
7376 }
7377
7378 let _ifc_root_node_type = match ifc_root_node_data.get_node_type() {
7379 NodeType::Div => "Div",
7380 NodeType::Text(_) => "Text",
7381 NodeType::Body => "Body",
7382 _ => "Other",
7383 };
7384
7385 let dom_children: Vec<NodeId> = ifc_root_dom_id
7391 .az_children(&ctx.styled_dom.node_hierarchy.as_container())
7392 .collect();
7393 #[cfg(feature = "web_lift")]
7404 let dom_children_len = unsafe {
7405 crate::az_mark((0x606B4) as u32, (dom_children.len() as u32) as u32);
7406 crate::az_mark((0x606A4) as u32, (0x0000_6863u32) as u32);
7407 crate::az_mark_read(0x606B4) as usize
7408 };
7409 #[cfg(not(feature = "web_lift"))]
7410 let dom_children_len = dom_children.len();
7411
7412 for item_idx in 0..dom_children_len {
7413 let dom_child_id = unsafe { *dom_children.get_unchecked(item_idx) };
7414 let content_index = ContentIndex {
7415 run_index: ifc_root_index as u32,
7416 item_index: item_idx as u32,
7417 };
7418
7419 let node_data = &ctx.styled_dom.node_data.as_container()[dom_child_id];
7420 #[cfg(feature = "web_lift")]
7422 unsafe {
7423 if item_idx == 0 {
7424 crate::az_mark((0x606B8) as u32, (match node_data.get_node_type() {
7425 NodeType::Text(_) => 0xC0DE_7E70u32,
7426 NodeType::Div => 0xC0DE_D11Fu32,
7427 NodeType::Body => 0xC0DE_B0D1u32,
7428 _ => 0xC0DE_0000u32,
7429 }) as u32);
7430 }
7431 crate::az_mark((0x606A4) as u32, (0x0000_6896u32) as u32);
7432 }
7433
7434 if let NodeType::Text(ref text_content) = node_data.get_node_type() {
7436 debug_info!(
7437 ctx,
7438 "[collect_and_measure_inline_content] OK: Found text node (DOM child {:?}): '{}'",
7439 dom_child_id,
7440 text_content.as_str()
7441 );
7442
7443 let style = Arc::new(get_style_properties(ctx.styled_dom, dom_child_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
7447 let text_items = split_text_for_whitespace(
7448 ctx.styled_dom,
7449 dom_child_id,
7450 text_content.as_str(),
7451 &style,
7452 );
7453 content.extend(text_items);
7454 #[cfg(feature = "web_lift")]
7456 unsafe {
7457 crate::az_mark((0x606A4) as u32, (0x0000_6905u32) as u32);
7458 crate::az_mark((0x606BC) as u32, (content.len() as u32) as u32);
7459 }
7460
7461 if let Some(&layout_idx) = tree.dom_to_layout.get(&dom_child_id).and_then(|v| v.first()) {
7465 if let Some(warm_mut) = tree.warm_mut(layout_idx) {
7466 warm_mut.ifc_membership = Some(IfcMembership {
7467 ifc_id,
7468 ifc_root_layout_index: ifc_root_index,
7469 run_index: current_run_index,
7470 });
7471 }
7472 }
7473 current_run_index += 1;
7474
7475 continue;
7476 }
7477
7478 if matches!(node_data.get_node_type(), NodeType::Br) {
7481 content.push(InlineContent::LineBreak(InlineBreak {
7482 break_type: BreakType::Hard,
7483 clear: ClearType::None,
7484 content_index: content.len(),
7485 }));
7486 continue;
7487 }
7488
7489 let child_index = children
7491 .iter()
7492 .find(|&&idx| {
7493 tree.get(idx)
7494 .and_then(|n| n.dom_node_id)
7495 .is_some_and(|id| id == dom_child_id)
7496 })
7497 .copied();
7498
7499 let Some(child_index) = child_index else {
7500 debug_info!(
7501 ctx,
7502 "[collect_and_measure_inline_content] WARN: DOM child {:?} has no layout node",
7503 dom_child_id
7504 );
7505 continue;
7506 };
7507
7508 #[cfg(feature = "web_lift")]
7510 unsafe { crate::az_mark((0x606A4) as u32, (0x0000_6942u32) as u32); }
7511 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
7512 let dom_id = child_node.dom_node_id.unwrap();
7514
7515 if matches!(
7523 get_position_type(ctx.styled_dom, Some(dom_id)),
7524 LayoutPosition::Absolute | LayoutPosition::Fixed
7525 ) {
7526 continue;
7527 }
7528
7529 let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
7530 if display != LayoutDisplay::Inline {
7531 let intrinsic_size = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
7536 let box_props = child_node.box_props.unpack();
7537
7538 let styled_node_state = ctx
7539 .styled_dom
7540 .styled_nodes
7541 .as_container()
7542 .get(dom_id)
7543 .map(|n| n.styled_node_state)
7544 .unwrap_or_default();
7545
7546 let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
7549 ctx.styled_dom,
7550 Some(dom_id),
7551 &constraints.containing_block_size,
7552 intrinsic_size,
7553 &box_props,
7554 &ctx.viewport_size,
7555 )?;
7556
7557 let writing_mode =
7558 get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
7559
7560 let content_box_size = box_props.inner_size(tentative_size, writing_mode);
7562
7563 debug_info!(
7564 ctx,
7565 "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
7566 tentative_border_box={:?}, content_box={:?}",
7567 dom_id,
7568 tentative_size,
7569 content_box_size
7570 );
7571
7572 let child_wm_ctx = super::geometry::WritingModeContext::new(
7574 writing_mode,
7575 get_direction_property(ctx.styled_dom, dom_id, &styled_node_state)
7576 .unwrap_or_default(),
7577 get_text_orientation_property(ctx.styled_dom, dom_id, &styled_node_state)
7578 .unwrap_or_default(),
7579 );
7580 let child_constraints = LayoutConstraints {
7581 available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
7582 writing_mode,
7583 writing_mode_ctx: child_wm_ctx,
7584 bfc_state: None,
7586 text_align: TextAlign::Start,
7588 containing_block_size: constraints.containing_block_size,
7589 available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
7590 };
7591
7592 drop(child_node);
7594
7595 let mut empty_float_cache = HashMap::new();
7598 let layout_result = layout_formatting_context(
7599 ctx,
7600 tree,
7601 text_cache,
7602 child_index,
7603 &child_constraints,
7604 &mut empty_float_cache,
7605 )?;
7606
7607 let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
7608
7609 let is_replaced_atomic = {
7614 let nd = &ctx.styled_dom.node_data.as_container()[dom_id];
7615 matches!(nd.get_node_type(), NodeType::Image(_)) || nd.is_virtual_view_node()
7616 };
7617 let final_height = match css_height.clone().unwrap_or_default() {
7619 LayoutHeight::Auto if !is_replaced_atomic => {
7620 let content_height = layout_result.output.overflow_size.height;
7622 content_height
7623 + box_props.padding.main_sum(writing_mode)
7624 + box_props.border.main_sum(writing_mode)
7625 }
7626 _ => tentative_size.height,
7629 };
7630
7631 debug_info!(
7632 ctx,
7633 "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
7634 layout_content_height={}, css_height={:?}, final_border_box_height={}",
7635 dom_id,
7636 layout_result.output.overflow_size.height,
7637 css_height,
7638 final_height
7639 );
7640
7641 let final_size = LogicalSize::new(tentative_size.width, final_height);
7642
7643 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
7645
7646 let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
7661 let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
7662 let overflow_is_visible = matches!(
7663 (overflow_x, overflow_y),
7664 (LayoutOverflow::Visible, LayoutOverflow::Visible)
7665 );
7666 let baseline_from_top = layout_result.output.baseline;
7667 let baseline_offset = match baseline_from_top {
7668 Some(baseline_y) if overflow_is_visible => {
7669 let content_box_top = box_props.padding.top + box_props.border.top;
7672 let baseline_from_border_box_top = baseline_y + content_box_top;
7673 (final_height - baseline_from_border_box_top).max(0.0)
7675 }
7676 _ => {
7677 0.0
7679 }
7680 };
7681
7682 debug_info!(
7683 ctx,
7684 "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
7685 baseline_from_top={:?}, final_height={}, baseline_offset_from_bottom={}",
7686 dom_id,
7687 baseline_from_top,
7688 final_height,
7689 baseline_offset
7690 );
7691
7692 let margin = &box_props.margin;
7696 let margin_box_width = final_size.width + margin.left + margin.right;
7697 let margin_box_height = final_size.height + margin.top + margin.bottom;
7698
7699 let shape_content_index = ContentIndex {
7702 run_index: content.len() as u32,
7703 item_index: 0,
7704 };
7705 content.push(InlineContent::Shape(InlineShape {
7707 shape_def: ShapeDefinition::Rectangle {
7708 size: crate::text3::cache::Size {
7709 width: margin_box_width,
7711 height: margin_box_height,
7712 },
7713 corner_radius: None,
7714 },
7715 fill: None,
7716 stroke: None,
7717 baseline_offset: baseline_offset + margin.top,
7719 alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, dom_id),
7720 source_node_id: Some(dom_id),
7721 }));
7722 child_map.insert(shape_content_index, child_index);
7723 } else if let NodeType::Image(image_ref) =
7724 ctx.styled_dom.node_data.as_container()[dom_id].get_node_type()
7725 {
7726 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
7732 let box_props = child_node.box_props.unpack();
7733
7734 let intrinsic_size = tree.warm(child_index)
7736 .and_then(|w| w.intrinsic_sizes)
7737 .unwrap_or_else(|| IntrinsicSizes {
7738 max_content_width: 50.0,
7739 max_content_height: 50.0,
7740 ..Default::default()
7741 });
7742
7743 let styled_node_state = ctx
7745 .styled_dom
7746 .styled_nodes
7747 .as_container()
7748 .get(dom_id)
7749 .map(|n| n.styled_node_state)
7750 .unwrap_or_default();
7751
7752 let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
7754 ctx.styled_dom,
7755 Some(dom_id),
7756 &constraints.containing_block_size,
7757 intrinsic_size,
7758 &box_props,
7759 &ctx.viewport_size,
7760 )?;
7761
7762 drop(child_node);
7764
7765 let final_size = LogicalSize::new(tentative_size.width, tentative_size.height);
7767 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
7768
7769 let display_width = if final_size.width > 0.0 {
7771 Some(final_size.width)
7772 } else {
7773 None
7774 };
7775 let display_height = if final_size.height > 0.0 {
7776 Some(final_size.height)
7777 } else {
7778 None
7779 };
7780
7781 content.push(InlineContent::Image(InlineImage {
7782 source: ImageSource::Ref(image_ref.as_ref().clone()),
7783 intrinsic_size: crate::text3::cache::Size {
7784 width: intrinsic_size.max_content_width,
7785 height: intrinsic_size.max_content_height,
7786 },
7787 display_size: if display_width.is_some() || display_height.is_some() {
7788 Some(crate::text3::cache::Size {
7789 width: display_width.unwrap_or(intrinsic_size.max_content_width),
7790 height: display_height.unwrap_or(intrinsic_size.max_content_height),
7791 })
7792 } else {
7793 None
7794 },
7795 baseline_offset: 0.0,
7797 alignment: text3::cache::VerticalAlign::Baseline,
7798 object_fit: ObjectFit::Fill,
7799 }));
7800 let image_content_index = ContentIndex {
7803 run_index: (content.len() - 1) as u32, item_index: 0,
7805 };
7806 child_map.insert(image_content_index, child_index);
7807 } else {
7808 debug_info!(
7813 ctx,
7814 "[collect_and_measure_inline_content] Found inline span (DOM {:?}), recursing",
7815 dom_id
7816 );
7817
7818 let span_style = get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
7819 collect_inline_span_recursive(
7820 ctx,
7821 tree,
7822 dom_id,
7823 &span_style,
7824 content,
7825 &children,
7826 constraints,
7827 )?;
7828 }
7829 }
7830 #[cfg(feature = "web_lift")]
7832 unsafe {
7833 crate::az_mark((0x60698) as u32, (content.len() as u32) as u32);
7834 crate::az_mark((0x6069C) as u32, (0xC0DE069Cu32) as u32);
7835 }
7836 Ok(())
7837}
7838
7839#[allow(clippy::too_many_lines)] fn collect_inline_span_recursive<T: ParsedFontTrait>(
7857 ctx: &mut LayoutContext<'_, T>,
7858 tree: &mut LayoutTree,
7859 span_dom_id: NodeId,
7860 span_style: &StyleProperties,
7861 content: &mut Vec<InlineContent>,
7862 parent_children: &[usize], constraints: &LayoutConstraints<'_>,
7864) -> Result<()> {
7865 debug_info!(
7866 ctx,
7867 "[collect_inline_span_recursive] Processing inline span {:?}",
7868 span_dom_id
7869 );
7870
7871 let span_dom_children: Vec<NodeId> = span_dom_id
7873 .az_children(&ctx.styled_dom.node_hierarchy.as_container())
7874 .collect();
7875
7876 debug_info!(
7877 ctx,
7878 "[collect_inline_span_recursive] Span has {} DOM children",
7879 span_dom_children.len()
7880 );
7881
7882 if span_dom_children.is_empty() {
7885 let node_state = &ctx.styled_dom.styled_nodes.as_container()[span_dom_id].styled_node_state;
7886 let font_size = get_element_font_size(ctx.styled_dom, span_dom_id, node_state);
7887
7888 let line_height_value = crate::solver3::getters::get_line_height_value(
7889 ctx.styled_dom, span_dom_id, node_state
7890 );
7891 let line_height = line_height_value
7892 .map_or(text3::cache::LineHeight::Normal, |v| {
7893 let n = v.inner.normalized();
7896 let px = if n < 0.0 { -n } else { n * font_size };
7897 text3::cache::LineHeight::Px(px)
7898 });
7899
7900 let cb_width = constraints.containing_block_size.main(constraints.writing_mode);
7901 let padding_top = get_css_padding_top(ctx.styled_dom, span_dom_id, node_state)
7902 .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
7903 let padding_bottom = get_css_padding_bottom(ctx.styled_dom, span_dom_id, node_state)
7904 .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
7905 let padding_left = crate::solver3::getters::get_css_padding_left(ctx.styled_dom, span_dom_id, node_state)
7906 .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
7907 let padding_right = crate::solver3::getters::get_css_padding_right(ctx.styled_dom, span_dom_id, node_state)
7908 .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
7909 let border_top = get_css_border_top_width(ctx.styled_dom, span_dom_id, node_state)
7910 .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
7911 let border_bottom = get_css_border_bottom_width(ctx.styled_dom, span_dom_id, node_state)
7912 .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
7913 let border_left = crate::solver3::getters::get_css_border_left_width(ctx.styled_dom, span_dom_id, node_state)
7914 .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
7915 let border_right = crate::solver3::getters::get_css_border_right_width(ctx.styled_dom, span_dom_id, node_state)
7916 .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
7917 let margin_left = crate::solver3::getters::get_css_margin_left(ctx.styled_dom, span_dom_id, node_state)
7918 .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
7919 let margin_right = crate::solver3::getters::get_css_margin_right(ctx.styled_dom, span_dom_id, node_state)
7920 .exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
7921
7922 let resolved_line_height = line_height.resolve(font_size, 0.0, 0.0, 0.0, 0);
7923 let total_height = resolved_line_height + padding_top + padding_bottom + border_top + border_bottom;
7924 let total_width = margin_left + padding_left + border_left
7925 + border_right + padding_right + margin_right;
7926
7927 content.push(InlineContent::Shape(InlineShape {
7928 shape_def: ShapeDefinition::Rectangle {
7929 size: crate::text3::cache::Size {
7930 width: total_width,
7931 height: total_height,
7932 },
7933 corner_radius: None,
7934 },
7935 fill: None,
7936 stroke: None,
7937 baseline_offset: 0.0,
7938 alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, span_dom_id),
7939 source_node_id: Some(span_dom_id),
7940 }));
7941
7942 return Ok(());
7943 }
7944
7945 for &child_dom_id in &span_dom_children {
7946 let node_data = &ctx.styled_dom.node_data.as_container()[child_dom_id];
7947
7948 if let NodeType::Text(ref text_content) = node_data.get_node_type() {
7950 debug_info!(
7951 ctx,
7952 "[collect_inline_span_recursive] ✓ Found text in span: '{}'",
7953 text_content.as_str()
7954 );
7955 let text_items = split_text_for_whitespace(
7956 ctx.styled_dom,
7957 child_dom_id,
7958 text_content.as_str(),
7959 &Arc::new(span_style.clone()),
7960 );
7961 content.extend(text_items);
7962 continue;
7963 }
7964
7965 if matches!(node_data.get_node_type(), NodeType::Br) {
7967 content.push(InlineContent::LineBreak(InlineBreak {
7968 break_type: BreakType::Hard,
7969 clear: ClearType::None,
7970 content_index: content.len(),
7971 }));
7972 continue;
7973 }
7974
7975 if matches!(
7979 get_position_type(ctx.styled_dom, Some(child_dom_id)),
7980 LayoutPosition::Absolute | LayoutPosition::Fixed
7981 ) {
7982 continue;
7983 }
7984
7985 let child_display =
7987 get_display_property(ctx.styled_dom, Some(child_dom_id)).unwrap_or_default();
7988
7989 let child_index = parent_children
7991 .iter()
7992 .find(|&&idx| {
7993 tree.get(idx)
7994 .and_then(|n| n.dom_node_id)
7995 .is_some_and(|id| id == child_dom_id)
7996 })
7997 .copied();
7998
7999 match child_display {
8000 LayoutDisplay::Inline => {
8001 debug_info!(
8003 ctx,
8004 "[collect_inline_span_recursive] Found nested inline span {:?}",
8005 child_dom_id
8006 );
8007 let child_style = get_style_properties(ctx.styled_dom, child_dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
8008 collect_inline_span_recursive(
8009 ctx,
8010 tree,
8011 child_dom_id,
8012 &child_style,
8013 content,
8014 parent_children,
8015 constraints,
8016 )?;
8017 }
8018 LayoutDisplay::InlineBlock => {
8019 let Some(child_index) = child_index else {
8021 debug_info!(
8022 ctx,
8023 "[collect_inline_span_recursive] WARNING: inline-block {:?} has no layout \
8024 node",
8025 child_dom_id
8026 );
8027 continue;
8028 };
8029
8030 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
8031 let intrinsic_size = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
8032 let width = intrinsic_size.max_content_width;
8033
8034 let styled_node_state = ctx
8035 .styled_dom
8036 .styled_nodes
8037 .as_container()
8038 .get(child_dom_id)
8039 .map(|n| n.styled_node_state)
8040 .unwrap_or_default();
8041 let writing_mode =
8042 get_writing_mode(ctx.styled_dom, child_dom_id, &styled_node_state)
8043 .unwrap_or_default();
8044 let child_wm_ctx = super::geometry::WritingModeContext::new(
8045 writing_mode,
8046 get_direction_property(ctx.styled_dom, child_dom_id, &styled_node_state)
8047 .unwrap_or_default(),
8048 get_text_orientation_property(ctx.styled_dom, child_dom_id, &styled_node_state)
8049 .unwrap_or_default(),
8050 );
8051 let child_constraints = LayoutConstraints {
8052 available_size: LogicalSize::new(width, f32::INFINITY),
8053 writing_mode,
8054 writing_mode_ctx: child_wm_ctx,
8055 bfc_state: None,
8056 text_align: TextAlign::Start,
8057 containing_block_size: constraints.containing_block_size,
8058 available_width_type: Text3AvailableSpace::Definite(width),
8059 };
8060
8061 drop(child_node);
8062
8063 let mut empty_float_cache = HashMap::new();
8064 let layout_result = layout_formatting_context(
8065 ctx,
8066 tree,
8067 &mut TextLayoutCache::default(),
8068 child_index,
8069 &child_constraints,
8070 &mut empty_float_cache,
8071 )?;
8072 let final_height = layout_result.output.overflow_size.height;
8073 let final_size = LogicalSize::new(width, final_height);
8074
8075 tree.get_mut(child_index).unwrap().used_size = Some(final_size);
8076
8077 let overflow_x = get_overflow_x(ctx.styled_dom, child_dom_id, &styled_node_state).unwrap_or_default();
8079 let overflow_y = get_overflow_y(ctx.styled_dom, child_dom_id, &styled_node_state).unwrap_or_default();
8080 let overflow_is_visible = matches!(
8081 (overflow_x, overflow_y),
8082 (LayoutOverflow::Visible, LayoutOverflow::Visible)
8083 );
8084 let baseline_offset = if overflow_is_visible {
8085 layout_result.output.baseline.unwrap_or(final_height)
8086 } else {
8087 final_height
8088 };
8089
8090 content.push(InlineContent::Shape(InlineShape {
8091 shape_def: ShapeDefinition::Rectangle {
8092 size: crate::text3::cache::Size {
8093 width,
8094 height: final_height,
8095 },
8096 corner_radius: None,
8097 },
8098 fill: None,
8099 stroke: None,
8100 baseline_offset,
8101 alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
8102 source_node_id: Some(child_dom_id),
8103 }));
8104
8105 debug_info!(
8107 ctx,
8108 "[collect_inline_span_recursive] Added inline-block shape {}x{}",
8109 width,
8110 final_height
8111 );
8112 }
8113 _ => {
8114 debug_info!(
8119 ctx,
8120 "[collect_inline_span_recursive] Inlinifying block-level child {:?} \
8121 (display: {:?}) inside inline span per css-display-3 §2.7",
8122 child_dom_id,
8123 child_display
8124 );
8125 let child_style = get_style_properties(ctx.styled_dom, child_dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
8126 collect_inline_span_recursive(
8127 ctx,
8128 tree,
8129 child_dom_id,
8130 &child_style,
8131 content,
8132 parent_children,
8133 constraints,
8134 )?;
8135 }
8136 }
8137 }
8138
8139 Ok(())
8140}
8141
8142fn position_floated_child(
8145 _child_index: usize,
8146 child_margin_box_size: LogicalSize,
8147 float_type: LayoutFloat,
8148 constraints: &LayoutConstraints<'_>,
8149 _bfc_content_box: LogicalRect,
8150 current_main_offset: f32,
8151 floating_context: &mut FloatingContext,
8152) -> Result<LogicalPosition> {
8153 let wm = constraints.writing_mode;
8154 let child_main_size = child_margin_box_size.main(wm);
8155 let child_cross_size = child_margin_box_size.cross(wm);
8156 let bfc_cross_size = constraints.available_size.cross(wm);
8157 let mut placement_main_offset = current_main_offset;
8158
8159 loop {
8160 let (available_cross_start, available_cross_end) = floating_context
8163 .available_line_box_space(
8164 placement_main_offset,
8165 placement_main_offset + child_main_size,
8166 bfc_cross_size,
8167 wm,
8168 );
8169
8170 let available_cross_width = available_cross_end - available_cross_start;
8171
8172 if child_cross_size <= available_cross_width {
8174 let final_cross_pos = match float_type {
8177 LayoutFloat::Left => available_cross_start,
8178 LayoutFloat::Right => available_cross_end - child_cross_size,
8180 LayoutFloat::None => {
8181 return Err(LayoutError::PositioningFailed);
8182 }
8183 };
8184 let final_pos =
8185 LogicalPosition::from_main_cross(placement_main_offset, final_cross_pos, wm);
8186
8187 let new_float_box = FloatBox {
8188 kind: float_type,
8189 rect: LogicalRect::new(final_pos, child_margin_box_size),
8190 margin: EdgeSizes::default(), };
8192 floating_context.floats.push(new_float_box);
8193 return Ok(final_pos);
8194 }
8195 {
8196 let mut next_main_offset = f32::INFINITY;
8201 for existing_float in &floating_context.floats {
8202 let float_main_start = existing_float.rect.origin.main(wm);
8203 let float_main_end = float_main_start + existing_float.rect.size.main(wm);
8204
8205 if placement_main_offset < float_main_end {
8207 next_main_offset = next_main_offset.min(float_main_end);
8208 }
8209 }
8210
8211 if next_main_offset.is_infinite() {
8212 return Err(LayoutError::PositioningFailed);
8215 }
8216 placement_main_offset = next_main_offset;
8217 }
8218 }
8219}
8220
8221fn get_float_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutFloat {
8225 let Some(id) = dom_id else {
8226 return LayoutFloat::None;
8227 };
8228 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
8229 get_float(styled_dom, id, node_state).unwrap_or(LayoutFloat::None)
8230}
8231
8232fn get_clear_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutClear {
8233 let Some(id) = dom_id else {
8234 return LayoutClear::None;
8235 };
8236 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
8237 get_clear(styled_dom, id, node_state).unwrap_or(LayoutClear::None)
8238}
8239#[must_use] pub fn check_scrollbar_necessity(
8252 content_size: LogicalSize,
8253 container_size: LogicalSize,
8254 overflow_x: OverflowBehavior,
8255 overflow_y: OverflowBehavior,
8256 scrollbar_width_px: f32,
8257) -> ScrollbarRequirements {
8258 const EPSILON: f32 = 1.0;
8262
8263 let mut needs_horizontal = match overflow_x {
8270 OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
8271 OverflowBehavior::Scroll => true,
8272 OverflowBehavior::Auto => content_size.width > container_size.width + EPSILON,
8273 };
8274
8275 let mut needs_vertical = match overflow_y {
8276 OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
8277 OverflowBehavior::Scroll => true,
8278 OverflowBehavior::Auto => content_size.height > container_size.height + EPSILON,
8279 };
8280
8281 if scrollbar_width_px > 0.0 {
8289 if needs_vertical && !needs_horizontal && overflow_x == OverflowBehavior::Auto
8290 && content_size.width > (container_size.width - scrollbar_width_px) + EPSILON {
8291 needs_horizontal = true;
8292 }
8293 if needs_horizontal && !needs_vertical && overflow_y == OverflowBehavior::Auto
8294 && content_size.height > (container_size.height - scrollbar_width_px) + EPSILON {
8295 needs_vertical = true;
8296 }
8297 }
8298
8299 ScrollbarRequirements {
8300 needs_horizontal,
8301 needs_vertical,
8302 scrollbar_width: if needs_vertical {
8303 scrollbar_width_px
8304 } else {
8305 0.0
8306 },
8307 scrollbar_height: if needs_horizontal {
8308 scrollbar_width_px
8309 } else {
8310 0.0
8311 },
8312 visual_width_px: 0.0,
8315 }
8316}
8317
8318#[must_use] pub fn collapse_margins(a: f32, b: f32) -> f32 {
8326 if a.is_sign_positive() && b.is_sign_positive() {
8327 a.max(b)
8328 } else if a.is_sign_negative() && b.is_sign_negative() {
8329 a.min(b)
8330 } else {
8331 a + b
8332 }
8333}
8334
8335fn advance_pen_with_margin_collapse(
8355 pen: &mut f32,
8356 last_margin_bottom: f32,
8357 current_margin_top: f32,
8358) -> f32 {
8359 let collapsed_margin = collapse_margins(last_margin_bottom, current_margin_top);
8361
8362 *pen += collapsed_margin;
8364
8365 collapsed_margin
8367}
8368
8369fn has_margin_collapse_blocker(
8387 box_props: &crate::solver3::geometry::BoxProps,
8388 writing_mode: LayoutWritingMode,
8389 check_start: bool, ) -> bool {
8391 if check_start {
8392 let border_start = box_props.border.main_start(writing_mode);
8394 let padding_start = box_props.padding.main_start(writing_mode);
8395 border_start > 0.0 || padding_start > 0.0
8396 } else {
8397 let border_end = box_props.border.main_end(writing_mode);
8399 let padding_end = box_props.padding.main_end(writing_mode);
8400 border_end > 0.0 || padding_end > 0.0
8401 }
8402}
8403
8404fn is_empty_block(tree: &LayoutTree, node_index: usize) -> bool {
8419 let Some(node) = tree.get(node_index) else {
8420 return true;
8421 };
8422 if !tree.children(node_index).is_empty() {
8430 return false;
8431 }
8432
8433 if tree.warm(node_index).and_then(|w| w.inline_layout_result.as_ref()).is_some() {
8435 return false;
8436 }
8437
8438 if let Some(size) = node.used_size {
8441 if size.height > 0.0 {
8442 return false;
8443 }
8444 }
8445
8446 true
8448}
8449
8450fn generate_list_marker_text(
8459 tree: &LayoutTree,
8460 styled_dom: &StyledDom,
8461 marker_index: usize,
8462 counters: &HashMap<(usize, String), i32>,
8463 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
8464) -> String {
8465 use crate::solver3::counters::format_counter;
8466
8467 let Some(marker_node) = tree.get(marker_index) else {
8469 return String::new();
8470 };
8471
8472 let marker_pseudo = tree.warm(marker_index).and_then(|w| w.pseudo_element);
8475 let marker_anonymous_type = tree.cold(marker_index).and_then(|c| c.anonymous_type);
8476 if marker_pseudo != Some(PseudoElement::Marker) {
8477 if let Some(msgs) = debug_messages {
8478 msgs.push(LayoutDebugMessage::warning(format!(
8479 "[generate_list_marker_text] WARNING: Node {marker_index} is not a ::marker pseudo-element \
8480 (pseudo={marker_pseudo:?}, anonymous_type={marker_anonymous_type:?})"
8481 )));
8482 }
8483 if marker_anonymous_type != Some(AnonymousBoxType::ListItemMarker) {
8485 return String::new();
8486 }
8487 }
8488
8489 let Some(list_item_index) = marker_node.parent else {
8491 if let Some(msgs) = debug_messages {
8492 msgs.push(LayoutDebugMessage::error(
8493 "[generate_list_marker_text] ERROR: Marker has no parent".to_string(),
8494 ));
8495 }
8496 return String::new();
8497 };
8498
8499 let Some(list_item_node) = tree.get(list_item_index) else {
8500 return String::new();
8501 };
8502
8503 let Some(list_item_dom_id) = list_item_node.dom_node_id else {
8504 if let Some(msgs) = debug_messages {
8505 msgs.push(LayoutDebugMessage::error(
8506 "[generate_list_marker_text] ERROR: List-item has no DOM ID".to_string(),
8507 ));
8508 }
8509 return String::new();
8510 };
8511
8512 if let Some(msgs) = debug_messages {
8513 msgs.push(LayoutDebugMessage::info(format!(
8514 "[generate_list_marker_text] marker_index={marker_index}, list_item_index={list_item_index}, \
8515 list_item_dom_id={list_item_dom_id:?}"
8516 )));
8517 }
8518
8519 let list_container_dom_id = list_item_node.parent.and_then(|grandparent_index| {
8521 tree.get(grandparent_index).and_then(|grandparent| grandparent.dom_node_id)
8522 });
8523
8524 let list_style_type = list_container_dom_id.map_or_else(|| get_list_style_type(styled_dom, Some(list_item_dom_id)), |container_id| {
8527 let container_type = get_list_style_type(styled_dom, Some(container_id));
8528 if container_type == StyleListStyleType::default() {
8529 get_list_style_type(styled_dom, Some(list_item_dom_id))
8530 } else {
8531 container_type
8532 }
8533 });
8534
8535 let counter_value = counters
8539 .get(&(list_item_index, "list-item".to_string()))
8540 .copied()
8541 .unwrap_or_else(|| {
8542 if let Some(msgs) = debug_messages {
8543 msgs.push(LayoutDebugMessage::warning(format!(
8544 "[generate_list_marker_text] WARNING: No counter found for list-item at index \
8545 {list_item_index}, defaulting to 1"
8546 )));
8547 }
8548 1
8549 });
8550
8551 if let Some(msgs) = debug_messages {
8552 msgs.push(LayoutDebugMessage::info(format!(
8553 "[generate_list_marker_text] counter_value={counter_value} for list_item_index={list_item_index}"
8554 )));
8555 }
8556
8557 let marker_text = format_counter(counter_value, list_style_type);
8559
8560 if matches!(
8563 list_style_type,
8564 StyleListStyleType::Decimal
8565 | StyleListStyleType::DecimalLeadingZero
8566 | StyleListStyleType::LowerAlpha
8567 | StyleListStyleType::UpperAlpha
8568 | StyleListStyleType::LowerRoman
8569 | StyleListStyleType::UpperRoman
8570 | StyleListStyleType::LowerGreek
8571 | StyleListStyleType::UpperGreek
8572 ) {
8573 format!("{marker_text}. ")
8574 } else {
8575 format!("{marker_text} ")
8576 }
8577}
8578
8579fn generate_list_marker_segments(
8585 tree: &LayoutTree,
8586 styled_dom: &StyledDom,
8587 marker_index: usize,
8588 counters: &HashMap<(usize, String), i32>,
8589 base_style: Arc<StyleProperties>,
8590 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
8591) -> Vec<StyledRun> {
8592 let marker_text =
8594 generate_list_marker_text(tree, styled_dom, marker_index, counters, debug_messages);
8595 if marker_text.is_empty() {
8596 return Vec::new();
8597 }
8598
8599 if let Some(msgs) = debug_messages {
8600 let font_families: Vec<&str> = match &base_style.font_stack {
8601 text3::cache::FontStack::Stack(selectors) => {
8602 selectors.iter().map(|f| f.family.as_str()).collect()
8603 }
8604 text3::cache::FontStack::Ref(_) => vec!["<embedded-font>"],
8605 };
8606 msgs.push(LayoutDebugMessage::info(format!(
8607 "[generate_list_marker_segments] Marker text: '{marker_text}' with font stack: {font_families:?}"
8608 )));
8609 }
8610
8611 vec![StyledRun {
8614 text: marker_text,
8615 style: base_style,
8616 logical_start_byte: 0,
8617 source_node_id: None,
8618 }]
8619}
8620
8621#[inline]
8625const fn is_bk_or_nl_class(c: char) -> bool {
8626 matches!(c, '\u{000B}' | '\u{000C}' | '\u{0085}' | '\u{2028}' | '\u{2029}')
8627}
8628
8629fn split_at_forced_breaks(text: &str) -> Vec<String> {
8633 let mut segments = Vec::new();
8634 let mut current = String::new();
8635 let mut chars = text.chars().peekable();
8636 while let Some(c) = chars.next() {
8637 if c == '\n' {
8638 segments.push(std::mem::take(&mut current));
8639 } else if c == '\r' {
8640 segments.push(std::mem::take(&mut current));
8641 if chars.peek() == Some(&'\n') {
8642 chars.next();
8643 }
8644 } else if is_bk_or_nl_class(c) {
8645 segments.push(std::mem::take(&mut current));
8646 } else {
8647 current.push(c);
8648 }
8649 }
8650 segments.push(current);
8651 segments
8652}
8653
8654fn split_at_bk_nl_chars(text: &str) -> Vec<String> {
8658 let mut segments = Vec::new();
8659 let mut current = String::new();
8660 for c in text.chars() {
8661 if is_bk_or_nl_class(c) {
8662 segments.push(std::mem::take(&mut current));
8663 } else {
8664 current.push(c);
8665 }
8666 }
8667 segments.push(current);
8668 segments
8669}
8670
8671fn is_east_asian_wide(c: char) -> bool {
8674 let cp = c as u32;
8675 (0x4E00..=0x9FFF).contains(&cp)
8677 || (0x3400..=0x4DBF).contains(&cp)
8678 || (0x20000..=0x2A6DF).contains(&cp)
8679 || (0xF900..=0xFAFF).contains(&cp)
8680 || (0x3040..=0x309F).contains(&cp)
8682 || (0x30A0..=0x30FF).contains(&cp)
8684 || (0x31F0..=0x31FF).contains(&cp)
8685 || (0x2E80..=0x2EFF).contains(&cp)
8687 || (0x2F00..=0x2FDF).contains(&cp)
8688 || (0x2FF0..=0x2FFF).contains(&cp)
8689 || (0x3000..=0x303F).contains(&cp)
8691 || (0x3200..=0x32FF).contains(&cp)
8692 || (0x3300..=0x33FF).contains(&cp)
8693 || (0x3100..=0x312F).contains(&cp)
8695 || (0xAC00..=0xD7AF).contains(&cp)
8697 || (0xFF01..=0xFF60).contains(&cp)
8699 || (0xFFE0..=0xFFE6).contains(&cp)
8700}
8701
8702fn is_east_asian_fullwidth_or_wide(ch: char) -> bool {
8704 let cp = ch as u32;
8705 if (0x1100..=0x11FF).contains(&cp)
8707 || (0x3130..=0x318F).contains(&cp)
8708 || (0xAC00..=0xD7AF).contains(&cp)
8709 || (0xA960..=0xA97F).contains(&cp)
8710 || (0xD7B0..=0xD7FF).contains(&cp)
8711 {
8712 return false;
8713 }
8714 is_east_asian_wide(ch)
8715 || (0xFF61..=0xFFDC).contains(&cp)
8716 || (0xFFE8..=0xFFEE).contains(&cp)
8717 || (0xA000..=0xA4CF).contains(&cp)
8718}
8719
8720fn apply_segment_break_transform(text: &str) -> String {
8728 let chars: Vec<char> = text.chars().collect();
8729 let len = chars.len();
8730 let mut result = String::with_capacity(text.len());
8731 let mut i = 0;
8732
8733 while i < len {
8734 let ch = chars[i];
8735 if ch == '\n' || ch == '\r' {
8736 let break_end = if ch == '\r' && i + 1 < len && chars[i + 1] == '\n' {
8737 i + 2
8738 } else {
8739 i + 1
8740 };
8741
8742 while result.ends_with(' ') || result.ends_with('\t') {
8745 result.pop();
8746 }
8747
8748 let mut after_idx = break_end;
8749 while after_idx < len && (chars[after_idx] == ' ' || chars[after_idx] == '\t') {
8750 after_idx += 1;
8751 }
8752
8753 let char_before = result.chars().last();
8754 let char_after = if after_idx < len { Some(chars[after_idx]) } else { None };
8755
8756 if char_before == Some('\u{200B}') || char_after == Some('\u{200B}') {
8758 }
8760 else if let (Some(before), Some(after)) = (char_before, char_after) {
8762 if is_east_asian_fullwidth_or_wide(before) && is_east_asian_fullwidth_or_wide(after) {
8763 } else {
8765 result.push(' ');
8766 }
8767 } else {
8768 result.push(' ');
8769 }
8770
8771 i = after_idx;
8772 } else {
8773 result.push(ch);
8774 i += 1;
8775 }
8776 }
8777
8778 result
8779}
8780
8781const fn is_bidi_control(c: char) -> bool {
8788 matches!(c,
8789 '\u{200E}' | '\u{200F}' | '\u{202A}' | '\u{202B}' | '\u{202C}' | '\u{202D}' | '\u{202E}' | '\u{2066}' | '\u{2067}' | '\u{2068}' | '\u{2069}' | '\u{061C}' )
8802}
8803
8804#[inline]
8809const fn is_css_document_whitespace(c: char) -> bool {
8810 matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')
8811}
8812
8813#[allow(clippy::too_many_lines)] pub fn split_text_for_whitespace(
8837 styled_dom: &StyledDom,
8838 dom_id: NodeId,
8839 text: &str,
8840 style: &Arc<StyleProperties>,
8841) -> Vec<InlineContent> {
8842 let text_owned;
8846 let text: &str = if text.chars().any(is_bidi_control) {
8847 text_owned = text.chars().filter(|c| !is_bidi_control(*c)).collect::<String>();
8848 &text_owned
8849 } else {
8850 text
8851 };
8852
8853 let node_hierarchy = styled_dom.node_hierarchy.as_container();
8856 let parent_id = node_hierarchy[dom_id].parent_id();
8857
8858 let white_space = parent_id.map_or(StyleWhiteSpace::Normal, |parent| {
8860 let styled_nodes = styled_dom.styled_nodes.as_container();
8861 let parent_state = styled_nodes
8862 .get(parent)
8863 .map(|n| n.styled_node_state)
8864 .unwrap_or_default();
8865
8866 match get_white_space_property(styled_dom, parent, &parent_state) {
8867 MultiValue::Exact(ws) => ws,
8868 _ => StyleWhiteSpace::Normal,
8869 }
8870 });
8871
8872 let mut result = Vec::new();
8873
8874 let text_cr;
8880 let text: &str = if text.contains('\r') {
8881 text_cr = text.replace("\r\n", "\n").replace('\r', "\n");
8882 &text_cr
8883 } else {
8884 text
8885 };
8886
8887 match white_space {
8892 StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => {
8893 let segments = split_at_forced_breaks(text);
8897 let segment_count = segments.len();
8898 let mut content_index = 0;
8899
8900 for (seg_idx, segment) in segments.into_iter().enumerate() {
8901 let mut tab_parts = segment.split('\t').peekable();
8903 while let Some(part) = tab_parts.next() {
8904 if !part.is_empty() {
8905 result.push(InlineContent::Text(StyledRun {
8906 text: part.to_string(),
8907 style: Arc::clone(style),
8908 logical_start_byte: 0,
8909 source_node_id: Some(dom_id),
8910 }));
8911 }
8912
8913 if tab_parts.peek().is_some() {
8914 result.push(InlineContent::Tab { style: Arc::clone(style) });
8915 }
8916 }
8917
8918 if seg_idx + 1 < segment_count {
8919 result.push(InlineContent::LineBreak(InlineBreak {
8920 break_type: BreakType::Hard,
8921 clear: ClearType::None,
8922 content_index,
8923 }));
8924 content_index += 1;
8925 }
8926 }
8927 }
8928 StyleWhiteSpace::PreLine => {
8929 let segments = split_at_forced_breaks(text);
8931 let segment_count = segments.len();
8932 let mut content_index = 0;
8933
8934 for (seg_idx, segment) in segments.into_iter().enumerate() {
8935 let collapsed: String = segment
8937 .split(|c: char| is_css_document_whitespace(c))
8938 .filter(|s| !s.is_empty())
8939 .collect::<Vec<_>>()
8940 .join(" ");
8941
8942 if !collapsed.is_empty() {
8943 result.push(InlineContent::Text(StyledRun {
8944 text: collapsed,
8945 style: Arc::clone(style),
8946 logical_start_byte: 0,
8947 source_node_id: Some(dom_id),
8948 }));
8949 }
8950
8951 if seg_idx + 1 < segment_count {
8952 result.push(InlineContent::LineBreak(InlineBreak {
8953 break_type: BreakType::Hard,
8954 clear: ClearType::None,
8955 content_index,
8956 }));
8957 content_index += 1;
8958 }
8959 }
8960 }
8961 StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap => {
8962 let segments = split_at_bk_nl_chars(text);
8974 let segment_count = segments.len();
8975 let mut content_index = 0;
8976
8977 for (seg_idx, segment) in segments.into_iter().enumerate() {
8978 let after_segment_breaks = apply_segment_break_transform(&segment);
8979
8980 let collapsed: String = after_segment_breaks
8982 .chars()
8983 .map(|c| if is_css_document_whitespace(c) { ' ' } else { c })
8984 .collect::<String>()
8985 .split(' ')
8986 .filter(|s| !s.is_empty())
8987 .collect::<Vec<_>>()
8988 .join(" ");
8989
8990 let final_text = if collapsed.is_empty() && !segment.is_empty() {
8991 " ".to_string()
8992 } else if !collapsed.is_empty() {
8993 let had_leading = segment.chars().next().is_some_and(is_css_document_whitespace);
8995 let had_trailing = segment.chars().last().is_some_and(is_css_document_whitespace);
8996
8997 let mut r = String::new();
8998 if had_leading { r.push(' '); }
8999 r.push_str(&collapsed);
9000 if had_trailing && !had_leading { r.push(' '); }
9001 else if had_trailing && had_leading && collapsed.is_empty() { }
9002 else if had_trailing { r.push(' '); }
9003 r
9004 } else {
9005 collapsed
9006 };
9007
9008 if !final_text.is_empty() {
9009 result.push(InlineContent::Text(StyledRun {
9010 text: final_text,
9011 style: Arc::clone(style),
9012 logical_start_byte: 0,
9013 source_node_id: Some(dom_id),
9014 }));
9015 }
9016
9017 if seg_idx + 1 < segment_count {
9019 result.push(InlineContent::LineBreak(InlineBreak {
9020 break_type: BreakType::Hard,
9021 clear: ClearType::None,
9022 content_index,
9023 }));
9024 content_index += 1;
9025 }
9026 }
9027 }
9028 }
9029
9030 let text_transform = style.text_transform;
9034 if text_transform != text3::cache::TextTransform::None {
9035 for item in &mut result {
9036 if let InlineContent::Text(run) = item {
9037 run.text = apply_text_transform(&run.text, text_transform);
9038 }
9039 }
9040 }
9041
9042 result
9043}
9044
9045fn apply_text_transform(text: &str, transform: text3::cache::TextTransform) -> String {
9046 use crate::text3::cache::TextTransform;
9047 match transform {
9048 TextTransform::None => text.to_string(),
9049 TextTransform::Uppercase => text.to_uppercase(),
9050 TextTransform::Lowercase => text.to_lowercase(),
9051 TextTransform::Capitalize => {
9052 let mut result = String::with_capacity(text.len());
9053 let mut prev_is_word_boundary = true;
9054 for c in text.chars() {
9055 if prev_is_word_boundary && c.is_alphabetic() {
9056 for uc in c.to_uppercase() {
9057 result.push(uc);
9058 }
9059 prev_is_word_boundary = false;
9060 } else {
9061 result.push(c);
9062 prev_is_word_boundary = c.is_whitespace() || c.is_ascii_punctuation();
9063 }
9064 }
9065 result
9066 }
9067 TextTransform::FullWidth => {
9068 text.chars().map(|c| match c {
9072 ' ' => '\u{3000}', '!' ..= '~' => {
9074 char::from_u32(c as u32 - 0x0021 + 0xFF01).unwrap_or(c)
9076 }
9077 _ => c,
9078 }).collect()
9079 }
9080 }
9081}
9082
9083#[allow(clippy::cast_precision_loss)] #[must_use] pub fn layout_initial_letter(
9123 initial_letter_size: f32,
9124 initial_letter_sink: u32,
9125 content_box_width: f32,
9126 line_height: f32,
9127) -> (f32, f32) {
9128 const CAP_WIDTH_RATIO: f32 = 0.7;
9132
9133 const LETTER_GAP: f32 = 4.0;
9136
9137 if initial_letter_size <= 0.0 || line_height <= 0.0 || content_box_width <= 0.0 {
9139 return (0.0, 0.0);
9140 }
9141
9142 let letter_height = initial_letter_size * line_height;
9146
9147 let letter_width_raw = letter_height * CAP_WIDTH_RATIO;
9148
9149 let letter_width = (letter_width_raw + LETTER_GAP).min(content_box_width);
9150
9151 let exclusion_height = (initial_letter_sink as f32) * line_height;
9158
9159 let effective_height = exclusion_height.max(letter_height);
9164
9165 (letter_width, effective_height)
9166}
9167
9168#[cfg(test)]
9169#[allow(clippy::float_cmp, clippy::too_many_lines)]
9170mod autotest_generated {
9171 use azul_core::dom::{AttributeType, Dom, IdOrClass};
9172
9173 use super::*;
9174 use crate::{
9175 solver3::geometry::{MarginAuto, PackedBoxProps},
9176 text3::cache::{OverflowInfo, TextTransform, UnifiedLayout},
9177 };
9178
9179 const HTB: LayoutWritingMode = LayoutWritingMode::HorizontalTb;
9184 const VRL: LayoutWritingMode = LayoutWritingMode::VerticalRl;
9185
9186 fn size(w: f32, h: f32) -> LogicalSize {
9187 LogicalSize::new(w, h)
9188 }
9189
9190 fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
9191 EdgeSizes {
9192 top,
9193 right,
9194 bottom,
9195 left,
9196 }
9197 }
9198
9199 fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
9200 LogicalRect::new(LogicalPosition::new(x, y), size(w, h))
9201 }
9202
9203 fn box_props(margin: EdgeSizes, border: EdgeSizes, padding: EdgeSizes) -> BoxProps {
9204 BoxProps {
9205 margin,
9206 padding,
9207 border,
9208 margin_auto: MarginAuto::default(),
9209 }
9210 }
9211
9212 fn hot(
9213 parent: Option<usize>,
9214 used_size: Option<LogicalSize>,
9215 bp: &BoxProps,
9216 ) -> LayoutNodeHot {
9217 LayoutNodeHot {
9218 box_props: PackedBoxProps::pack(bp),
9219 dom_node_id: None,
9220 used_size,
9221 formatting_context: FormattingContext::Block {
9222 establishes_new_context: false,
9223 },
9224 parent,
9225 }
9226 }
9227
9228 fn build_tree(
9230 nodes: Vec<LayoutNodeHot>,
9231 warm: Vec<LayoutNodeWarm>,
9232 child_lists: &[Vec<usize>],
9233 ) -> LayoutTree {
9234 let n = nodes.len();
9235 let mut children_arena: Vec<usize> = Vec::new();
9236 let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
9237 for cl in child_lists {
9238 let start = u32::try_from(children_arena.len()).unwrap();
9239 children_arena.extend_from_slice(cl);
9240 children_offsets.push((start, u32::try_from(cl.len()).unwrap()));
9241 }
9242 while children_offsets.len() < n {
9243 children_offsets.push((0, 0));
9244 }
9245 LayoutTree {
9246 nodes,
9247 warm,
9248 cold: vec![LayoutNodeCold::default(); n],
9249 root: 0,
9250 dom_to_layout: BTreeMap::new(),
9251 children_arena,
9252 children_offsets,
9253 subtree_needs_intrinsic: Vec::new(),
9254 }
9255 }
9256
9257 fn empty_inline_layout() -> CachedInlineLayout {
9260 CachedInlineLayout::new(
9261 Arc::new(UnifiedLayout {
9262 items: Vec::new(),
9263 overflow: OverflowInfo::default(),
9264 }),
9265 Text3AvailableSpace::MaxContent,
9266 false,
9267 )
9268 }
9269
9270 fn constraints(available: LogicalSize, wm: LayoutWritingMode) -> LayoutConstraints<'static> {
9271 LayoutConstraints {
9272 available_size: available,
9273 writing_mode: wm,
9274 writing_mode_ctx: super::super::geometry::WritingModeContext::default(),
9275 bfc_state: None,
9276 text_align: TextAlign::Start,
9277 containing_block_size: available,
9278 available_width_type: Text3AvailableSpace::Definite(available.width),
9279 }
9280 }
9281
9282 fn styled(dom: Dom, css_str: &str) -> StyledDom {
9283 let mut dom = dom;
9284 let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
9285 StyledDom::create(&mut dom, css)
9286 }
9287
9288 fn text_dom(text: &str, css_str: &str) -> StyledDom {
9290 styled(
9291 Dom::create_body().with_child(
9292 Dom::create_div()
9293 .with_ids_and_classes(vec![IdOrClass::Class("p".into())].into())
9294 .with_child(Dom::create_text(text)),
9295 ),
9296 css_str,
9297 )
9298 }
9299
9300 const TEXT_NODE: NodeId = NodeId::new(2);
9301 const DIV_NODE: NodeId = NodeId::new(1);
9302
9303 fn plain_style() -> Arc<StyleProperties> {
9304 Arc::new(StyleProperties::default())
9305 }
9306
9307 fn text_of(item: &InlineContent) -> Option<&str> {
9308 match item {
9309 InlineContent::Text(run) => Some(run.text.as_str()),
9310 _ => None,
9311 }
9312 }
9313
9314 const ALL_OVERFLOW: [OverflowBehavior; 5] = [
9319 OverflowBehavior::Visible,
9320 OverflowBehavior::Hidden,
9321 OverflowBehavior::Clip,
9322 OverflowBehavior::Scroll,
9323 OverflowBehavior::Auto,
9324 ];
9325
9326 #[test]
9327 fn overflow_behavior_is_clipped_is_exhaustive_and_total() {
9328 assert!(!OverflowBehavior::Visible.is_clipped());
9329 assert!(OverflowBehavior::Hidden.is_clipped());
9330 assert!(OverflowBehavior::Clip.is_clipped());
9331 assert!(OverflowBehavior::Scroll.is_clipped());
9332 assert!(OverflowBehavior::Auto.is_clipped());
9333 assert_eq!(ALL_OVERFLOW.iter().filter(|o| o.is_clipped()).count(), 4);
9335 }
9336
9337 #[test]
9338 fn overflow_behavior_is_scroll_only_for_scroll_and_auto() {
9339 assert!(!OverflowBehavior::Visible.is_scroll());
9340 assert!(!OverflowBehavior::Hidden.is_scroll());
9341 assert!(!OverflowBehavior::Clip.is_scroll());
9342 assert!(OverflowBehavior::Scroll.is_scroll());
9343 assert!(OverflowBehavior::Auto.is_scroll());
9344 }
9345
9346 #[test]
9347 fn overflow_behavior_scroll_implies_clipped_invariant() {
9348 for o in ALL_OVERFLOW {
9350 assert!(
9351 !o.is_scroll() || o.is_clipped(),
9352 "{o:?} is scroll but not clipped"
9353 );
9354 }
9355 }
9356
9357 #[test]
9362 fn bfc_layout_result_from_output_preserves_output_and_nulls_escaped_margins() {
9363 let mut positions = BTreeMap::new();
9364 positions.insert(usize::MAX, LogicalPosition::new(f32::MIN, f32::MAX));
9365 let output = LayoutOutput {
9366 positions,
9367 overflow_size: size(f32::NAN, f32::INFINITY),
9368 baseline: Some(-0.0),
9369 };
9370 let res = BfcLayoutResult::from_output(output);
9371 assert!(res.escaped_top_margin.is_none());
9372 assert!(res.escaped_bottom_margin.is_none());
9373 assert_eq!(res.output.positions.len(), 1);
9374 assert!(res.output.overflow_size.width.is_nan());
9375 assert!(res.output.overflow_size.height.is_infinite());
9376 assert_eq!(res.output.baseline, Some(-0.0));
9377 }
9378
9379 #[test]
9380 fn bfc_state_new_matches_default_and_starts_empty() {
9381 let s = BfcState::new();
9382 assert_eq!(s.pen, LogicalPosition::zero());
9383 assert!(s.floats.floats.is_empty());
9384 assert_eq!(s.margins.last_in_flow_margin_bottom, 0.0);
9385
9386 let d = BfcState::default();
9387 assert_eq!(d.pen, s.pen);
9388 assert!(d.floats.floats.is_empty());
9389 }
9390
9391 #[test]
9392 fn table_layout_context_new_starts_empty_and_separate() {
9393 let t = TableLayoutContext::new();
9394 assert!(t.columns.is_empty());
9395 assert!(t.cells.is_empty());
9396 assert_eq!(t.num_rows, 0);
9397 assert!(!t.use_fixed_layout);
9398 assert!(t.row_heights.is_empty());
9399 assert!(t.row_baselines.is_empty());
9400 assert!(matches!(t.border_collapse, StyleBorderCollapse::Separate));
9401 assert!(t.caption_index.is_none());
9402 assert!(t.collapsed_rows.is_empty());
9403 assert!(t.collapsed_columns.is_empty());
9404 assert!(t.hidden_empty_rows.is_empty());
9405 assert!(t.row_node_indices.is_empty());
9406 }
9407
9408 #[test]
9413 fn add_float_accepts_extreme_geometry_without_panicking() {
9414 let mut fc = FloatingContext::default();
9415 fc.add_float(LayoutFloat::Left, rect(0.0, 0.0, 0.0, 0.0), EdgeSizes::default());
9416 fc.add_float(
9417 LayoutFloat::Right,
9418 rect(f32::MIN, f32::MIN, f32::MAX, f32::MAX),
9419 edges(f32::MAX, f32::MAX, f32::MAX, f32::MAX),
9420 );
9421 fc.add_float(
9422 LayoutFloat::None,
9423 rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
9424 edges(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0),
9425 );
9426 assert_eq!(fc.floats.len(), 3);
9427 assert!(fc.floats[2].rect.size.width.is_nan());
9429 }
9430
9431 #[test]
9432 fn available_line_box_space_with_no_floats_returns_the_full_band() {
9433 let fc = FloatingContext::default();
9434 assert_eq!(fc.available_line_box_space(0.0, 0.0, 0.0, HTB), (0.0, 0.0));
9435 assert_eq!(
9436 fc.available_line_box_space(0.0, 20.0, 300.0, HTB),
9437 (0.0, 300.0)
9438 );
9439 assert_eq!(
9441 fc.available_line_box_space(-50.0, -10.0, -5.0, HTB),
9442 (0.0, -5.0)
9443 );
9444 let (s, e) = fc.available_line_box_space(f32::MIN, f32::MAX, f32::MAX, HTB);
9445 assert_eq!(s, 0.0);
9446 assert_eq!(e, f32::MAX);
9447 }
9448
9449 #[test]
9450 fn available_line_box_space_subtracts_left_and_right_float_margin_boxes() {
9451 let mut fc = FloatingContext::default();
9452 fc.add_float(
9454 LayoutFloat::Left,
9455 rect(10.0, 10.0, 100.0, 50.0),
9456 edges(10.0, 10.0, 10.0, 10.0),
9457 );
9458 fc.add_float(
9460 LayoutFloat::Right,
9461 rect(200.0, 10.0, 90.0, 50.0),
9462 edges(10.0, 10.0, 10.0, 10.0),
9463 );
9464
9465 let (start, end) = fc.available_line_box_space(10.0, 20.0, 300.0, HTB);
9467 assert_eq!(start, 120.0); assert_eq!(end, 190.0); assert_eq!(
9472 fc.available_line_box_space(1000.0, 1010.0, 300.0, HTB),
9473 (0.0, 300.0)
9474 );
9475 }
9476
9477 #[test]
9478 fn available_line_box_space_main_axis_overlap_is_half_open() {
9479 let mut fc = FloatingContext::default();
9480 fc.add_float(
9482 LayoutFloat::Left,
9483 rect(0.0, 0.0, 100.0, 50.0),
9484 EdgeSizes::default(),
9485 );
9486 assert_eq!(
9488 fc.available_line_box_space(50.0, 70.0, 300.0, HTB),
9489 (0.0, 300.0)
9490 );
9491 assert_eq!(
9494 fc.available_line_box_space(0.0, 0.0, 300.0, HTB),
9495 (0.0, 300.0)
9496 );
9497 assert_eq!(
9499 fc.available_line_box_space(49.0, 70.0, 300.0, HTB),
9500 (100.0, 300.0)
9501 );
9502 }
9503
9504 #[test]
9505 fn available_line_box_space_ignores_nan_geometry_instead_of_panicking() {
9506 let mut fc = FloatingContext::default();
9507 fc.add_float(
9508 LayoutFloat::Left,
9509 rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
9510 EdgeSizes::default(),
9511 );
9512 assert_eq!(
9514 fc.available_line_box_space(0.0, 20.0, 300.0, HTB),
9515 (0.0, 300.0)
9516 );
9517 let (s, e) = fc.available_line_box_space(f32::NAN, f32::NAN, 300.0, HTB);
9519 assert_eq!((s, e), (0.0, 300.0));
9520 }
9521
9522 #[test]
9523 fn available_line_box_space_is_writing_mode_aware() {
9524 let mut fc = FloatingContext::default();
9525 fc.add_float(
9527 LayoutFloat::Left,
9528 rect(0.0, 0.0, 50.0, 100.0),
9529 EdgeSizes::default(),
9530 );
9531 assert_eq!(
9533 fc.available_line_box_space(10.0, 20.0, 300.0, VRL),
9534 (100.0, 300.0)
9535 );
9536 assert_eq!(
9538 fc.available_line_box_space(10.0, 20.0, 300.0, HTB),
9539 (50.0, 300.0)
9540 );
9541 }
9542
9543 #[test]
9544 fn clearance_offset_never_moves_content_upwards() {
9545 let mut fc = FloatingContext::default();
9546 fc.add_float(
9547 LayoutFloat::Left,
9548 rect(0.0, 0.0, 100.0, 80.0),
9549 edges(0.0, 0.0, 20.0, 0.0), );
9551 fc.add_float(
9552 LayoutFloat::Right,
9553 rect(200.0, 0.0, 100.0, 40.0),
9554 EdgeSizes::default(),
9555 );
9556
9557 assert_eq!(fc.clearance_offset(LayoutClear::Left, 0.0, HTB), 100.0);
9558 assert_eq!(fc.clearance_offset(LayoutClear::Right, 0.0, HTB), 40.0);
9559 assert_eq!(fc.clearance_offset(LayoutClear::Both, 0.0, HTB), 100.0);
9560 assert_eq!(fc.clearance_offset(LayoutClear::None, 25.0, HTB), 25.0);
9562 assert_eq!(fc.clearance_offset(LayoutClear::Both, 500.0, HTB), 500.0);
9564 }
9565
9566 #[test]
9567 fn clearance_offset_clamps_negative_offsets_to_zero() {
9568 let fc = FloatingContext::default();
9569 assert_eq!(fc.clearance_offset(LayoutClear::Both, -10.0, HTB), 0.0);
9572 assert_eq!(fc.clearance_offset(LayoutClear::None, -10.0, HTB), 0.0);
9573 assert_eq!(fc.clearance_offset(LayoutClear::None, 0.0, HTB), 0.0);
9575 }
9576
9577 #[test]
9578 fn clearance_offset_handles_nan_and_infinite_floats() {
9579 let mut fc = FloatingContext::default();
9580 fc.add_float(
9581 LayoutFloat::Left,
9582 rect(0.0, 0.0, 10.0, f32::INFINITY),
9583 EdgeSizes::default(),
9584 );
9585 assert!(fc.clearance_offset(LayoutClear::Left, 0.0, HTB).is_infinite());
9586
9587 assert!(fc.clearance_offset(LayoutClear::Left, f32::NAN, HTB).is_nan());
9589
9590 let mut nan_fc = FloatingContext::default();
9591 nan_fc.add_float(
9592 LayoutFloat::Left,
9593 rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
9594 EdgeSizes::default(),
9595 );
9596 assert_eq!(nan_fc.clearance_offset(LayoutClear::Left, 5.0, HTB), 5.0);
9598 }
9599
9600 #[test]
9601 fn clearance_offset_saturates_at_float_extremes() {
9602 let mut fc = FloatingContext::default();
9603 fc.add_float(
9604 LayoutFloat::Right,
9605 rect(f32::MAX, 0.0, f32::MAX, f32::MAX),
9606 edges(0.0, 0.0, f32::MAX, 0.0),
9607 );
9608 let out = fc.clearance_offset(LayoutClear::Right, f32::MIN, HTB);
9609 assert!(out.is_infinite() && out.is_sign_positive());
9611 }
9612
9613 #[test]
9618 fn position_float_places_left_and_right_floats_at_the_band_edges() {
9619 let fc = FloatingContext::default();
9620 let m = edges(10.0, 10.0, 10.0, 10.0);
9621
9622 let left = position_float(&fc, LayoutFloat::Left, size(100.0, 50.0), &m, 0.0, 300.0, HTB);
9623 assert_eq!(left.origin.x, 10.0); assert_eq!(left.origin.y, 10.0); assert_eq!(left.size, size(100.0, 50.0)); let right = position_float(&fc, LayoutFloat::Right, size(100.0, 50.0), &m, 0.0, 300.0, HTB);
9628 assert_eq!(right.origin.x, 190.0);
9630 assert_eq!(right.origin.y, 10.0);
9631 }
9632
9633 #[test]
9634 fn position_float_at_zero_size_and_zero_band() {
9635 let fc = FloatingContext::default();
9636 let r = position_float(
9637 &fc,
9638 LayoutFloat::Left,
9639 size(0.0, 0.0),
9640 &EdgeSizes::default(),
9641 0.0,
9642 0.0,
9643 HTB,
9644 );
9645 assert_eq!(r.origin, LogicalPosition::zero());
9646 assert_eq!(r.size, size(0.0, 0.0));
9647 }
9648
9649 #[test]
9650 fn position_float_wider_than_the_bfc_does_not_hang() {
9651 let fc = FloatingContext::default();
9652 let left = position_float(
9654 &fc,
9655 LayoutFloat::Left,
9656 size(500.0, 50.0),
9657 &EdgeSizes::default(),
9658 0.0,
9659 300.0,
9660 HTB,
9661 );
9662 assert_eq!(left.origin.x, 0.0);
9663
9664 let right = position_float(
9665 &fc,
9666 LayoutFloat::Right,
9667 size(500.0, 50.0),
9668 &EdgeSizes::default(),
9669 0.0,
9670 300.0,
9671 HTB,
9672 );
9673 assert_eq!(right.origin.x, -200.0);
9675 }
9676
9677 #[test]
9678 fn position_float_stacks_then_shifts_down_past_the_lowest_float() {
9679 let mut fc = FloatingContext::default();
9680 fc.add_float(
9681 LayoutFloat::Left,
9682 rect(0.0, 0.0, 100.0, 50.0),
9683 EdgeSizes::default(),
9684 );
9685
9686 let beside = position_float(
9688 &fc,
9689 LayoutFloat::Left,
9690 size(100.0, 50.0),
9691 &EdgeSizes::default(),
9692 0.0,
9693 300.0,
9694 HTB,
9695 );
9696 assert_eq!(beside.origin.x, 100.0);
9697 assert_eq!(beside.origin.y, 0.0);
9698
9699 let below = position_float(
9701 &fc,
9702 LayoutFloat::Left,
9703 size(250.0, 50.0),
9704 &EdgeSizes::default(),
9705 0.0,
9706 300.0,
9707 HTB,
9708 );
9709 assert_eq!(below.origin.x, 0.0);
9710 assert_eq!(below.origin.y, 50.0);
9711 }
9712
9713 #[test]
9714 fn position_float_terminates_on_nan_and_infinite_sizes() {
9715 let mut fc = FloatingContext::default();
9716 fc.add_float(
9717 LayoutFloat::Left,
9718 rect(0.0, 0.0, 100.0, 50.0),
9719 EdgeSizes::default(),
9720 );
9721
9722 let nan = position_float(
9725 &fc,
9726 LayoutFloat::Left,
9727 size(f32::NAN, f32::NAN),
9728 &EdgeSizes::default(),
9729 0.0,
9730 300.0,
9731 HTB,
9732 );
9733 assert!(nan.size.width.is_nan());
9734 assert!(nan.origin.x.is_finite());
9735
9736 let inf = position_float(
9738 &fc,
9739 LayoutFloat::Left,
9740 size(f32::INFINITY, f32::INFINITY),
9741 &EdgeSizes::default(),
9742 0.0,
9743 300.0,
9744 HTB,
9745 );
9746 assert_eq!(inf.origin.x, 0.0);
9747 }
9748
9749 #[test]
9750 fn position_float_honours_vertical_writing_mode() {
9751 let fc = FloatingContext::default();
9752 let r = position_float(
9754 &fc,
9755 LayoutFloat::Left,
9756 size(50.0, 100.0),
9757 &EdgeSizes::default(),
9758 20.0,
9759 300.0,
9760 VRL,
9761 );
9762 assert_eq!(r.origin.x, 20.0); assert_eq!(r.origin.y, 0.0); }
9765
9766 #[test]
9771 fn position_floated_child_rejects_float_none() {
9772 let mut fc = FloatingContext::default();
9773 let c = constraints(size(300.0, 300.0), HTB);
9774 let out = position_floated_child(
9775 0,
9776 size(10.0, 10.0),
9777 LayoutFloat::None,
9778 &c,
9779 rect(0.0, 0.0, 300.0, 300.0),
9780 0.0,
9781 &mut fc,
9782 );
9783 assert!(matches!(out, Err(LayoutError::PositioningFailed)));
9784 assert!(fc.floats.is_empty());
9785 }
9786
9787 #[test]
9788 fn position_floated_child_places_and_records_the_float() {
9789 let mut fc = FloatingContext::default();
9790 let c = constraints(size(300.0, 300.0), HTB);
9791
9792 let left = position_floated_child(
9793 0,
9794 size(100.0, 50.0),
9795 LayoutFloat::Left,
9796 &c,
9797 rect(0.0, 0.0, 300.0, 300.0),
9798 0.0,
9799 &mut fc,
9800 )
9801 .expect("fits");
9802 assert_eq!(left, LogicalPosition::new(0.0, 0.0));
9803 assert_eq!(fc.floats.len(), 1);
9804
9805 let right = position_floated_child(
9806 1,
9807 size(100.0, 50.0),
9808 LayoutFloat::Right,
9809 &c,
9810 rect(0.0, 0.0, 300.0, 300.0),
9811 0.0,
9812 &mut fc,
9813 )
9814 .expect("fits");
9815 assert_eq!(right, LogicalPosition::new(200.0, 0.0));
9816 assert_eq!(fc.floats.len(), 2);
9817
9818 let third = position_floated_child(
9820 2,
9821 size(150.0, 50.0),
9822 LayoutFloat::Left,
9823 &c,
9824 rect(0.0, 0.0, 300.0, 300.0),
9825 0.0,
9826 &mut fc,
9827 )
9828 .expect("drops to the next band");
9829 assert_eq!(third, LogicalPosition::new(0.0, 50.0));
9830 }
9831
9832 #[test]
9833 fn position_floated_child_errors_instead_of_looping_when_nothing_can_fit() {
9834 let mut fc = FloatingContext::default();
9835 let c = constraints(size(300.0, 300.0), HTB);
9836 let out = position_floated_child(
9838 0,
9839 size(500.0, 50.0),
9840 LayoutFloat::Left,
9841 &c,
9842 rect(0.0, 0.0, 300.0, 300.0),
9843 0.0,
9844 &mut fc,
9845 );
9846 assert!(matches!(out, Err(LayoutError::PositioningFailed)));
9847 assert!(fc.floats.is_empty());
9848
9849 let nan = position_floated_child(
9851 0,
9852 size(f32::NAN, f32::NAN),
9853 LayoutFloat::Left,
9854 &c,
9855 rect(0.0, 0.0, 300.0, 300.0),
9856 0.0,
9857 &mut fc,
9858 );
9859 assert!(matches!(nan, Err(LayoutError::PositioningFailed)));
9860 }
9861
9862 #[test]
9867 fn translate_taffy_size_round_trips_through_taffy() {
9868 for s in [
9869 size(0.0, 0.0),
9870 size(-0.0, 1.5),
9871 size(f32::MIN, f32::MAX),
9872 size(-1.0, -2.0),
9873 size(f32::INFINITY, f32::NEG_INFINITY),
9874 size(f32::MIN_POSITIVE, f32::EPSILON),
9875 ] {
9876 let t = translate_taffy_size(s);
9877 let back = translate_taffy_size_back(TaffySize {
9878 width: t.width.unwrap(),
9879 height: t.height.unwrap(),
9880 });
9881 assert_eq!(back.width.to_bits(), s.width.to_bits(), "width for {s:?}");
9882 assert_eq!(back.height.to_bits(), s.height.to_bits(), "height for {s:?}");
9883 }
9884 }
9885
9886 #[test]
9887 fn translate_taffy_size_preserves_nan_without_panicking() {
9888 let t = translate_taffy_size(size(f32::NAN, f32::NAN));
9889 assert!(t.width.unwrap().is_nan());
9890 let back = translate_taffy_size_back(TaffySize {
9891 width: t.width.unwrap(),
9892 height: t.height.unwrap(),
9893 });
9894 assert!(back.width.is_nan() && back.height.is_nan());
9895 }
9896
9897 #[test]
9898 fn translate_taffy_point_back_is_a_field_for_field_copy() {
9899 for (x, y) in [
9900 (0.0_f32, 0.0_f32),
9901 (f32::MIN, f32::MAX),
9902 (-1.5, 2.5),
9903 (f32::INFINITY, f32::NEG_INFINITY),
9904 ] {
9905 let p = translate_taffy_point_back(taffy::Point { x, y });
9906 assert_eq!(p.x.to_bits(), x.to_bits());
9907 assert_eq!(p.y.to_bits(), y.to_bits());
9908 }
9909 let nan = translate_taffy_point_back(taffy::Point {
9910 x: f32::NAN,
9911 y: f32::NAN,
9912 });
9913 assert!(nan.x.is_nan() && nan.y.is_nan());
9914 }
9915
9916 fn resolve(metric: SizeMetric, value: f32) -> f32 {
9921 resolve_size_metric(metric, value, 200.0, size(1000.0, 500.0), 16.0, 10.0)
9922 }
9923
9924 fn approx(actual: f32, expected: f32) {
9927 assert!(
9928 (actual - expected).abs() < 0.001,
9929 "expected ~{expected}, got {actual}"
9930 );
9931 }
9932
9933 #[test]
9934 fn resolve_size_metric_converts_every_unit() {
9935 assert_eq!(resolve(SizeMetric::Px, 42.0), 42.0);
9936 approx(resolve(SizeMetric::Pt, 72.0), 96.0); assert_eq!(resolve(SizeMetric::Percent, 50.0), 100.0); assert_eq!(resolve(SizeMetric::Em, 2.0), 32.0); assert_eq!(resolve(SizeMetric::Rem, 2.0), 20.0); approx(resolve(SizeMetric::Vw, 10.0), 100.0); approx(resolve(SizeMetric::Vh, 10.0), 50.0); assert_eq!(resolve(SizeMetric::Vmin, 100.0), 500.0); assert_eq!(resolve(SizeMetric::Vmax, 100.0), 1000.0); assert_eq!(resolve(SizeMetric::In, 1.0), 96.0);
9945 approx(resolve(SizeMetric::Cm, 2.54), 96.0);
9946 approx(resolve(SizeMetric::Mm, 25.4), 96.0);
9947 }
9948
9949 #[test]
9950 fn resolve_size_metric_at_zero_and_negative_values() {
9951 for m in [
9952 SizeMetric::Px,
9953 SizeMetric::Pt,
9954 SizeMetric::Percent,
9955 SizeMetric::Em,
9956 SizeMetric::Rem,
9957 SizeMetric::Vw,
9958 SizeMetric::Vh,
9959 SizeMetric::Vmin,
9960 SizeMetric::Vmax,
9961 SizeMetric::In,
9962 SizeMetric::Cm,
9963 SizeMetric::Mm,
9964 ] {
9965 assert_eq!(resolve(m, 0.0), 0.0, "{m:?} at zero");
9966 assert!(resolve(m, -10.0) <= 0.0, "{m:?} keeps the sign of a negative");
9967 }
9968 assert_eq!(
9970 resolve_size_metric(SizeMetric::Percent, 50.0, -200.0, size(0.0, 0.0), 16.0, 16.0),
9971 -100.0
9972 );
9973 }
9974
9975 #[test]
9976 fn resolve_size_metric_saturates_instead_of_panicking_at_the_limits() {
9977 let huge = resolve_size_metric(
9978 SizeMetric::Em,
9979 f32::MAX,
9980 0.0,
9981 size(0.0, 0.0),
9982 f32::MAX,
9983 16.0,
9984 );
9985 assert!(huge.is_infinite(), "MAX * MAX saturates to +inf");
9986
9987 let neg = resolve_size_metric(
9988 SizeMetric::Percent,
9989 f32::MIN,
9990 f32::MAX,
9991 size(0.0, 0.0),
9992 16.0,
9993 16.0,
9994 );
9995 assert!(neg.is_infinite() && neg.is_sign_negative());
9996 }
9997
9998 #[test]
9999 fn resolve_size_metric_propagates_nan_and_inf_without_panicking() {
10000 assert!(resolve(SizeMetric::Px, f32::NAN).is_nan());
10001 assert!(resolve(SizeMetric::Px, f32::INFINITY).is_infinite());
10002 assert!(resolve(SizeMetric::Percent, f32::NAN).is_nan());
10003 assert!(resolve_size_metric(
10005 SizeMetric::Em,
10006 0.0,
10007 0.0,
10008 size(0.0, 0.0),
10009 f32::INFINITY,
10010 16.0
10011 )
10012 .is_nan());
10013 let vp = size(f32::NAN, 400.0);
10015 assert_eq!(
10016 resolve_size_metric(SizeMetric::Vmin, 100.0, 0.0, vp, 16.0, 16.0),
10017 400.0
10018 );
10019 assert_eq!(
10020 resolve_size_metric(SizeMetric::Vmax, 100.0, 0.0, vp, 16.0, 16.0),
10021 400.0
10022 );
10023 }
10024
10025 #[test]
10030 fn convert_font_style_maps_every_variant() {
10031 assert_eq!(
10032 convert_font_style(StyleFontStyle::Normal),
10033 crate::font_traits::FontStyle::Normal
10034 );
10035 assert_eq!(
10036 convert_font_style(StyleFontStyle::Italic),
10037 crate::font_traits::FontStyle::Italic
10038 );
10039 assert_eq!(
10040 convert_font_style(StyleFontStyle::Oblique),
10041 crate::font_traits::FontStyle::Oblique
10042 );
10043 }
10044
10045 #[test]
10046 fn convert_font_weight_maps_every_variant_monotonically() {
10047 assert_eq!(convert_font_weight(StyleFontWeight::W100), FcWeight::Thin);
10048 assert_eq!(
10049 convert_font_weight(StyleFontWeight::W200),
10050 FcWeight::ExtraLight
10051 );
10052 assert_eq!(convert_font_weight(StyleFontWeight::W300), FcWeight::Light);
10053 assert_eq!(
10054 convert_font_weight(StyleFontWeight::Lighter),
10055 FcWeight::Light
10056 );
10057 assert_eq!(convert_font_weight(StyleFontWeight::Normal), FcWeight::Normal);
10058 assert_eq!(convert_font_weight(StyleFontWeight::W500), FcWeight::Medium);
10059 assert_eq!(
10060 convert_font_weight(StyleFontWeight::W600),
10061 FcWeight::SemiBold
10062 );
10063 assert_eq!(convert_font_weight(StyleFontWeight::Bold), FcWeight::Bold);
10064 assert_eq!(
10065 convert_font_weight(StyleFontWeight::W800),
10066 FcWeight::ExtraBold
10067 );
10068 assert_eq!(convert_font_weight(StyleFontWeight::W900), FcWeight::Black);
10069 assert_eq!(convert_font_weight(StyleFontWeight::Bolder), FcWeight::Black);
10070 }
10071
10072 fn bi(width: f32, style: BorderStyle, source: BorderSource) -> BorderInfo {
10077 BorderInfo::new(width, style, ColorU::BLACK, source)
10078 }
10079
10080 #[test]
10081 fn border_info_new_stores_its_arguments_verbatim() {
10082 let b = BorderInfo::new(f32::NAN, BorderStyle::Dotted, ColorU::RED, BorderSource::Cell);
10083 assert!(b.width.is_nan());
10084 assert_eq!(b.style, BorderStyle::Dotted);
10085 assert_eq!(b.color, ColorU::RED);
10086 assert_eq!(b.source, BorderSource::Cell);
10087 }
10088
10089 #[test]
10090 fn border_style_priority_follows_the_css_ordering() {
10091 assert_eq!(BorderInfo::style_priority(&BorderStyle::Hidden), 255);
10092 assert_eq!(BorderInfo::style_priority(&BorderStyle::None), 0);
10093 let ordered = [
10095 BorderStyle::Double,
10096 BorderStyle::Solid,
10097 BorderStyle::Dashed,
10098 BorderStyle::Dotted,
10099 BorderStyle::Ridge,
10100 BorderStyle::Outset,
10101 BorderStyle::Groove,
10102 BorderStyle::Inset,
10103 ];
10104 for w in ordered.windows(2) {
10105 assert!(
10106 BorderInfo::style_priority(&w[0]) > BorderInfo::style_priority(&w[1]),
10107 "{:?} must outrank {:?}",
10108 w[0],
10109 w[1]
10110 );
10111 }
10112 }
10113
10114 #[test]
10115 fn resolve_conflict_hidden_suppresses_everything() {
10116 let hidden = bi(1.0, BorderStyle::Hidden, BorderSource::Table);
10117 let fat = bi(99.0, BorderStyle::Solid, BorderSource::Cell);
10118 assert!(BorderInfo::resolve_conflict(&hidden, &fat).is_none());
10119 assert!(BorderInfo::resolve_conflict(&fat, &hidden).is_none());
10120 }
10121
10122 #[test]
10123 fn resolve_conflict_none_always_loses() {
10124 let none = bi(50.0, BorderStyle::None, BorderSource::Cell);
10125 let thin = bi(1.0, BorderStyle::Solid, BorderSource::Table);
10126 assert!(BorderInfo::resolve_conflict(&none, &none).is_none());
10127 assert_eq!(
10129 BorderInfo::resolve_conflict(&none, &thin).unwrap().style,
10130 BorderStyle::Solid
10131 );
10132 assert_eq!(
10133 BorderInfo::resolve_conflict(&thin, &none).unwrap().style,
10134 BorderStyle::Solid
10135 );
10136 }
10137
10138 #[test]
10139 fn resolve_conflict_falls_through_width_then_style_then_source() {
10140 let wide = bi(5.0, BorderStyle::Dotted, BorderSource::Table);
10141 let narrow = bi(2.0, BorderStyle::Double, BorderSource::Cell);
10142 assert_eq!(BorderInfo::resolve_conflict(&wide, &narrow).unwrap().width, 5.0);
10143
10144 let a = bi(3.0, BorderStyle::Dotted, BorderSource::Cell);
10146 let b = bi(3.0, BorderStyle::Double, BorderSource::Table);
10147 assert_eq!(
10148 BorderInfo::resolve_conflict(&a, &b).unwrap().style,
10149 BorderStyle::Double
10150 );
10151
10152 let t = bi(3.0, BorderStyle::Solid, BorderSource::Table);
10154 let c = bi(3.0, BorderStyle::Solid, BorderSource::Cell);
10155 assert_eq!(
10156 BorderInfo::resolve_conflict(&t, &c).unwrap().source,
10157 BorderSource::Cell
10158 );
10159 assert_eq!(
10160 BorderInfo::resolve_conflict(&c, &t).unwrap().source,
10161 BorderSource::Cell
10162 );
10163
10164 let first = BorderInfo::new(3.0, BorderStyle::Solid, ColorU::RED, BorderSource::Row);
10166 let second = BorderInfo::new(3.0, BorderStyle::Solid, ColorU::GREEN, BorderSource::Row);
10167 assert_eq!(
10168 BorderInfo::resolve_conflict(&first, &second).unwrap().color,
10169 ColorU::RED
10170 );
10171 }
10172
10173 #[test]
10174 fn resolve_conflict_with_nan_widths_is_deterministic() {
10175 let nan = bi(f32::NAN, BorderStyle::Solid, BorderSource::Table);
10178 let ok = bi(1.0, BorderStyle::Solid, BorderSource::Cell);
10179 let win = BorderInfo::resolve_conflict(&nan, &ok).unwrap();
10180 assert_eq!(win.source, BorderSource::Cell); assert_eq!(win.width, 1.0);
10182
10183 let nan_b = bi(f32::NAN, BorderStyle::Solid, BorderSource::Table);
10185 let other = bi(2.0, BorderStyle::Solid, BorderSource::Table);
10186 assert!(BorderInfo::resolve_conflict(&nan_b, &other)
10187 .unwrap()
10188 .width
10189 .is_nan());
10190 }
10191
10192 #[test]
10193 fn resolve_conflict_infinite_width_wins() {
10194 let inf = bi(f32::INFINITY, BorderStyle::Inset, BorderSource::Table);
10195 let solid = bi(f32::MAX, BorderStyle::Solid, BorderSource::Cell);
10196 assert!(BorderInfo::resolve_conflict(&inf, &solid)
10197 .unwrap()
10198 .width
10199 .is_infinite());
10200 }
10201
10202 fn cols(n: usize, min: f32, max: f32) -> Vec<TableColumnInfo> {
10207 (0..n)
10208 .map(|_| TableColumnInfo {
10209 min_width: min,
10210 max_width: max,
10211 computed_width: None,
10212 })
10213 .collect()
10214 }
10215
10216 #[test]
10217 fn distribute_cell_width_spreads_the_deficit_evenly() {
10218 let mut c = cols(2, 10.0, 20.0);
10219 let collapsed = std::collections::HashSet::new();
10220 distribute_cell_width_across_columns(&mut c, 0, 2, 50.0, 30.0, &collapsed);
10221 assert_eq!(c[0].min_width, 25.0);
10223 assert_eq!(c[1].min_width, 25.0);
10224 assert_eq!(c[0].max_width, 20.0);
10225 assert_eq!(c[1].max_width, 20.0);
10226 }
10227
10228 #[test]
10229 fn distribute_cell_width_is_a_noop_when_the_span_overruns_the_columns() {
10230 let mut c = cols(2, 10.0, 20.0);
10231 let collapsed = std::collections::HashSet::new();
10232 distribute_cell_width_across_columns(&mut c, 1, 5, 500.0, 500.0, &collapsed);
10233 assert_eq!(c[0].min_width, 10.0);
10234 assert_eq!(c[1].min_width, 10.0);
10235
10236 distribute_cell_width_across_columns(&mut c, 99, 1, 500.0, 500.0, &collapsed);
10238 distribute_cell_width_across_columns(&mut c, usize::MAX, 0, 500.0, 500.0, &collapsed);
10239 assert_eq!(c[0].min_width, 10.0);
10240 }
10241
10242 #[test]
10243 fn distribute_cell_width_with_zero_colspan_does_not_divide_by_zero() {
10244 let mut c = cols(2, 10.0, 20.0);
10245 let collapsed = std::collections::HashSet::new();
10246 distribute_cell_width_across_columns(&mut c, 0, 0, 1000.0, 1000.0, &collapsed);
10247 assert_eq!(c[0].min_width, 10.0);
10248 assert_eq!(c[1].min_width, 10.0);
10249 assert!(c[0].min_width.is_finite());
10250 }
10251
10252 #[test]
10253 fn distribute_cell_width_skips_fully_collapsed_spans() {
10254 let mut c = cols(2, 10.0, 20.0);
10255 let collapsed: std::collections::HashSet<usize> = [0, 1].into_iter().collect();
10256 distribute_cell_width_across_columns(&mut c, 0, 2, 1000.0, 1000.0, &collapsed);
10257 assert_eq!(c[0].min_width, 10.0);
10258 assert_eq!(c[1].min_width, 10.0);
10259
10260 let collapsed_one: std::collections::HashSet<usize> = [0].into_iter().collect();
10262 distribute_cell_width_across_columns(&mut c, 0, 2, 100.0, 0.0, &collapsed_one);
10263 assert_eq!(c[0].min_width, 10.0, "collapsed column is untouched");
10264 assert_eq!(c[1].min_width, 100.0, "10 + (100 - 10) / 1");
10265 }
10266
10267 #[test]
10268 fn distribute_cell_width_ignores_nan_and_saturates_on_inf() {
10269 let mut c = cols(2, 10.0, 20.0);
10270 let collapsed = std::collections::HashSet::new();
10271 distribute_cell_width_across_columns(&mut c, 0, 2, f32::NAN, f32::NAN, &collapsed);
10273 assert_eq!(c[0].min_width, 10.0);
10274 assert_eq!(c[1].max_width, 20.0);
10275
10276 distribute_cell_width_across_columns(
10278 &mut c,
10279 0,
10280 2,
10281 f32::INFINITY,
10282 f32::INFINITY,
10283 &collapsed,
10284 );
10285 assert!(c[0].min_width.is_infinite());
10286 assert!(c[1].max_width.is_infinite());
10287 }
10288
10289 #[test]
10290 fn distribute_cell_width_does_not_shrink_columns() {
10291 let mut c = cols(2, 100.0, 200.0);
10292 let collapsed = std::collections::HashSet::new();
10293 distribute_cell_width_across_columns(&mut c, 0, 2, 1.0, 1.0, &collapsed);
10295 assert_eq!(c[0].min_width, 100.0);
10296 assert_eq!(c[0].max_width, 200.0);
10297 distribute_cell_width_across_columns(&mut c, 0, 2, -1000.0, -1000.0, &collapsed);
10299 assert_eq!(c[1].min_width, 100.0);
10300 }
10301
10302 #[test]
10307 fn is_cell_empty_treats_missing_and_childless_cells_as_empty() {
10308 let tree = build_tree(
10309 vec![hot(None, Some(size(10.0, 10.0)), &BoxProps::default())],
10310 vec![LayoutNodeWarm::default()],
10311 &[vec![]],
10312 );
10313 assert!(is_cell_empty(&tree, 0), "no children => empty");
10314 assert!(is_cell_empty(&tree, 1), "out-of-range index => empty");
10315 assert!(is_cell_empty(&tree, usize::MAX), "usize::MAX must not panic");
10316 }
10317
10318 #[test]
10319 fn is_cell_empty_uses_the_inline_layout_when_present() {
10320 let bp = BoxProps::default();
10321 let mut warm = vec![LayoutNodeWarm::default(), LayoutNodeWarm::default()];
10322 warm[0].inline_layout_result = Some(empty_inline_layout());
10324 let tree = build_tree(
10325 vec![
10326 hot(None, Some(size(10.0, 10.0)), &bp),
10327 hot(Some(0), Some(size(10.0, 10.0)), &bp),
10328 ],
10329 warm,
10330 &[vec![1], vec![]],
10331 );
10332 assert!(is_cell_empty(&tree, 0));
10333
10334 let tree2 = build_tree(
10336 vec![
10337 hot(None, Some(size(10.0, 10.0)), &bp),
10338 hot(Some(0), Some(size(10.0, 10.0)), &bp),
10339 ],
10340 vec![LayoutNodeWarm::default(), LayoutNodeWarm::default()],
10341 &[vec![1], vec![]],
10342 );
10343 assert!(!is_cell_empty(&tree2, 0));
10344 }
10345
10346 #[test]
10347 fn is_empty_block_requires_no_children_no_inline_content_and_no_height() {
10348 let bp = BoxProps::default();
10349
10350 let empty_tree = build_tree(Vec::new(), Vec::new(), &[]);
10352 assert!(is_empty_block(&empty_tree, 0));
10353 assert!(is_empty_block(&empty_tree, usize::MAX));
10354
10355 let t = build_tree(
10357 vec![hot(None, None, &bp)],
10358 vec![LayoutNodeWarm::default()],
10359 &[vec![]],
10360 );
10361 assert!(is_empty_block(&t, 0));
10362
10363 for h in [0.0, -5.0] {
10365 let t = build_tree(
10366 vec![hot(None, Some(size(100.0, h)), &bp)],
10367 vec![LayoutNodeWarm::default()],
10368 &[vec![]],
10369 );
10370 assert!(is_empty_block(&t, 0), "height {h} must count as empty");
10371 }
10372
10373 let t = build_tree(
10375 vec![hot(None, Some(size(100.0, 0.5)), &bp)],
10376 vec![LayoutNodeWarm::default()],
10377 &[vec![]],
10378 );
10379 assert!(!is_empty_block(&t, 0));
10380
10381 let t = build_tree(
10383 vec![hot(None, None, &bp), hot(Some(0), None, &bp)],
10384 vec![LayoutNodeWarm::default(), LayoutNodeWarm::default()],
10385 &[vec![1], vec![]],
10386 );
10387 assert!(!is_empty_block(&t, 0));
10388
10389 let mut warm = vec![LayoutNodeWarm::default()];
10391 warm[0].inline_layout_result = Some(empty_inline_layout());
10392 let t = build_tree(vec![hot(None, None, &bp)], warm, &[vec![]]);
10393 assert!(!is_empty_block(&t, 0));
10394 }
10395
10396 #[test]
10397 fn compute_cell_baseline_falls_back_to_the_content_edge() {
10398 let bp = box_props(
10399 EdgeSizes::default(),
10400 edges(0.0, 0.0, 2.0, 0.0), edges(0.0, 0.0, 5.0, 0.0), );
10403 let tree = build_tree(
10404 vec![hot(None, Some(size(50.0, 100.0)), &bp)],
10405 vec![LayoutNodeWarm::default()],
10406 &[vec![]],
10407 );
10408 assert_eq!(compute_cell_baseline(0, &tree), 93.0);
10410
10411 assert_eq!(compute_cell_baseline(usize::MAX, &tree), 0.0);
10413 }
10414
10415 #[test]
10416 fn compute_cell_baseline_without_used_size_can_go_negative() {
10417 let bp = box_props(
10418 EdgeSizes::default(),
10419 edges(0.0, 0.0, 2.0, 0.0),
10420 edges(0.0, 0.0, 5.0, 0.0),
10421 );
10422 let tree = build_tree(
10423 vec![hot(None, None, &bp)],
10424 vec![LayoutNodeWarm::default()],
10425 &[vec![]],
10426 );
10427 assert_eq!(compute_cell_baseline(0, &tree), -7.0);
10429 }
10430
10431 #[test]
10436 fn check_scrollbar_necessity_never_scrolls_for_visible_hidden_or_clip() {
10437 for o in [
10438 OverflowBehavior::Visible,
10439 OverflowBehavior::Hidden,
10440 OverflowBehavior::Clip,
10441 ] {
10442 let r = check_scrollbar_necessity(size(9999.0, 9999.0), size(10.0, 10.0), o, o, 16.0);
10443 assert!(!r.needs_horizontal, "{o:?}");
10444 assert!(!r.needs_vertical, "{o:?}");
10445 assert_eq!(r.scrollbar_width, 0.0);
10446 assert_eq!(r.scrollbar_height, 0.0);
10447 }
10448 }
10449
10450 #[test]
10451 fn check_scrollbar_necessity_always_scrolls_for_scroll_even_with_no_content() {
10452 let r = check_scrollbar_necessity(
10453 size(0.0, 0.0),
10454 size(500.0, 500.0),
10455 OverflowBehavior::Scroll,
10456 OverflowBehavior::Scroll,
10457 16.0,
10458 );
10459 assert!(r.needs_horizontal && r.needs_vertical);
10460 assert_eq!(r.scrollbar_width, 16.0);
10461 assert_eq!(r.scrollbar_height, 16.0);
10462 }
10463
10464 #[test]
10465 fn check_scrollbar_necessity_auto_honours_the_one_pixel_epsilon() {
10466 let auto = OverflowBehavior::Auto;
10467 let r = check_scrollbar_necessity(size(301.0, 301.0), size(300.0, 300.0), auto, auto, 0.0);
10469 assert!(!r.needs_horizontal);
10470 assert!(!r.needs_vertical);
10471
10472 let r = check_scrollbar_necessity(size(302.0, 302.0), size(300.0, 300.0), auto, auto, 0.0);
10474 assert!(r.needs_horizontal && r.needs_vertical);
10475 assert_eq!(r.scrollbar_width, 0.0);
10477 assert_eq!(r.scrollbar_height, 0.0);
10478 }
10479
10480 #[test]
10481 fn check_scrollbar_necessity_two_pass_adds_the_second_scrollbar() {
10482 let auto = OverflowBehavior::Auto;
10483 let r = check_scrollbar_necessity(size(300.0, 400.0), size(300.0, 300.0), auto, auto, 16.0);
10486 assert!(r.needs_vertical);
10487 assert!(r.needs_horizontal, "vertical scrollbar must force a horizontal one");
10488 assert_eq!(r.scrollbar_width, 16.0);
10489 assert_eq!(r.scrollbar_height, 16.0);
10490
10491 let r = check_scrollbar_necessity(size(300.0, 400.0), size(300.0, 300.0), auto, auto, 0.0);
10493 assert!(r.needs_vertical);
10494 assert!(!r.needs_horizontal);
10495 }
10496
10497 #[test]
10498 fn check_scrollbar_necessity_with_nan_and_negative_sizes_is_deterministic() {
10499 let auto = OverflowBehavior::Auto;
10500 let r = check_scrollbar_necessity(
10502 size(f32::NAN, f32::NAN),
10503 size(300.0, 300.0),
10504 auto,
10505 auto,
10506 16.0,
10507 );
10508 assert!(!r.needs_horizontal && !r.needs_vertical);
10509
10510 let r = check_scrollbar_necessity(
10512 size(500.0, 500.0),
10513 size(f32::NAN, f32::NAN),
10514 auto,
10515 auto,
10516 16.0,
10517 );
10518 assert!(!r.needs_horizontal && !r.needs_vertical);
10519
10520 let r = check_scrollbar_necessity(
10522 size(0.0, 0.0),
10523 size(-100.0, -100.0),
10524 auto,
10525 auto,
10526 16.0,
10527 );
10528 assert!(r.needs_horizontal && r.needs_vertical);
10529
10530 let r = check_scrollbar_necessity(
10532 size(f32::INFINITY, f32::INFINITY),
10533 size(300.0, 300.0),
10534 auto,
10535 auto,
10536 16.0,
10537 );
10538 assert!(r.needs_horizontal && r.needs_vertical);
10539 }
10540
10541 #[test]
10542 fn check_scrollbar_necessity_with_negative_scrollbar_width_skips_the_second_pass() {
10543 let auto = OverflowBehavior::Auto;
10544 let r =
10545 check_scrollbar_necessity(size(300.0, 400.0), size(300.0, 300.0), auto, auto, -16.0);
10546 assert!(r.needs_vertical);
10547 assert!(!r.needs_horizontal, "the `> 0.0` guard skips the two-pass check");
10548 assert_eq!(r.scrollbar_width, -16.0); }
10550
10551 #[test]
10556 fn collapse_margins_follows_css_2_1_section_8_3_1() {
10557 assert_eq!(collapse_margins(10.0, 20.0), 20.0); assert_eq!(collapse_margins(-10.0, -20.0), -20.0); assert_eq!(collapse_margins(20.0, -5.0), 15.0); assert_eq!(collapse_margins(-5.0, 20.0), 15.0);
10561 assert_eq!(collapse_margins(0.0, 0.0), 0.0);
10562 assert_eq!(collapse_margins(0.0, 10.0), 10.0);
10563 assert_eq!(collapse_margins(0.0, -5.0), -5.0);
10565 }
10566
10567 #[test]
10568 fn collapse_margins_is_commutative_for_finite_inputs() {
10569 let vals = [-100.0_f32, -1.0, -0.5, 0.0, 0.5, 1.0, 100.0, f32::MAX, f32::MIN];
10570 for a in vals {
10571 for b in vals {
10572 let ab = collapse_margins(a, b);
10573 let ba = collapse_margins(b, a);
10574 assert_eq!(ab.to_bits(), ba.to_bits(), "collapse_margins({a}, {b})");
10575 }
10576 }
10577 }
10578
10579 #[test]
10580 fn collapse_margins_saturates_and_defines_nan_inf_behaviour() {
10581 assert!(collapse_margins(f32::MAX, -f32::MAX).abs() < 1.0); assert_eq!(collapse_margins(f32::INFINITY, 5.0), f32::INFINITY);
10583 assert_eq!(collapse_margins(f32::NEG_INFINITY, -5.0), f32::NEG_INFINITY);
10584 assert!(collapse_margins(f32::INFINITY, f32::NEG_INFINITY).is_nan());
10586 assert_eq!(collapse_margins(f32::NAN, 1.0), 1.0);
10588 assert_eq!(collapse_margins(1.0, f32::NAN), 1.0);
10589 assert_eq!(collapse_margins(-f32::NAN, -1.0), -1.0);
10590 }
10591
10592 #[test]
10593 fn advance_pen_with_margin_collapse_advances_by_exactly_the_collapsed_margin() {
10594 let mut pen = 100.0_f32;
10595 let collapsed = advance_pen_with_margin_collapse(&mut pen, 10.0, 20.0);
10596 assert_eq!(collapsed, 20.0);
10597 assert_eq!(pen, 120.0);
10598
10599 let mut pen = 100.0_f32;
10601 let collapsed = advance_pen_with_margin_collapse(&mut pen, 30.0, -10.0);
10602 assert_eq!(collapsed, 20.0);
10603 assert_eq!(pen, 120.0);
10604
10605 let mut pen = 7.5_f32;
10607 assert_eq!(advance_pen_with_margin_collapse(&mut pen, 0.0, 0.0), 0.0);
10608 assert_eq!(pen, 7.5);
10609 }
10610
10611 #[test]
10612 fn advance_pen_with_margin_collapse_saturates_at_the_f32_limits() {
10613 let mut pen = f32::MAX;
10614 let collapsed = advance_pen_with_margin_collapse(&mut pen, f32::MAX, f32::MAX);
10615 assert_eq!(collapsed, f32::MAX);
10616 assert!(pen.is_infinite(), "MAX + MAX saturates to +inf, no panic");
10617
10618 let mut pen = f32::NAN;
10619 let collapsed = advance_pen_with_margin_collapse(&mut pen, 1.0, 2.0);
10620 assert_eq!(collapsed, 2.0);
10621 assert!(pen.is_nan(), "a NaN pen stays NaN");
10622 }
10623
10624 #[test]
10629 fn has_margin_collapse_blocker_basic_true_false() {
10630 let none = BoxProps::default();
10631 assert!(!has_margin_collapse_blocker(&none, HTB, true));
10632 assert!(!has_margin_collapse_blocker(&none, HTB, false));
10633
10634 let top_border = box_props(
10635 EdgeSizes::default(),
10636 edges(1.0, 0.0, 0.0, 0.0),
10637 EdgeSizes::default(),
10638 );
10639 assert!(has_margin_collapse_blocker(&top_border, HTB, true));
10640 assert!(!has_margin_collapse_blocker(&top_border, HTB, false));
10641
10642 let bottom_padding = box_props(
10643 EdgeSizes::default(),
10644 EdgeSizes::default(),
10645 edges(0.0, 0.0, 0.5, 0.0),
10646 );
10647 assert!(!has_margin_collapse_blocker(&bottom_padding, HTB, true));
10648 assert!(has_margin_collapse_blocker(&bottom_padding, HTB, false));
10649 }
10650
10651 #[test]
10652 fn has_margin_collapse_blocker_maps_edges_per_writing_mode() {
10653 let left_border = box_props(
10655 EdgeSizes::default(),
10656 edges(0.0, 0.0, 0.0, 3.0),
10657 EdgeSizes::default(),
10658 );
10659 assert!(has_margin_collapse_blocker(&left_border, VRL, true));
10660 assert!(!has_margin_collapse_blocker(&left_border, VRL, false));
10661 assert!(!has_margin_collapse_blocker(&left_border, HTB, true));
10663 assert!(!has_margin_collapse_blocker(&left_border, HTB, false));
10664 }
10665
10666 #[test]
10667 fn has_margin_collapse_blocker_ignores_negative_and_nan_edges() {
10668 let weird = box_props(
10669 EdgeSizes::default(),
10670 edges(-5.0, 0.0, f32::NAN, 0.0),
10671 edges(f32::NAN, 0.0, -1.0, 0.0),
10672 );
10673 assert!(!has_margin_collapse_blocker(&weird, HTB, true));
10675 assert!(!has_margin_collapse_blocker(&weird, HTB, false));
10676
10677 let inf = box_props(
10679 EdgeSizes::default(),
10680 edges(f32::INFINITY, 0.0, 0.0, 0.0),
10681 EdgeSizes::default(),
10682 );
10683 assert!(has_margin_collapse_blocker(&inf, HTB, true));
10684 }
10685
10686 #[test]
10691 fn is_bk_or_nl_class_covers_exactly_vt_ff_nel_ls_ps() {
10692 for c in ['\u{000B}', '\u{000C}', '\u{0085}', '\u{2028}', '\u{2029}'] {
10693 assert!(is_bk_or_nl_class(c), "{:04X} must be BK/NL", c as u32);
10694 }
10695 for c in ['\n', '\r', ' ', '\t', 'a', '\0', '\u{200B}', char::MAX] {
10697 assert!(!is_bk_or_nl_class(c), "{:04X} must not be BK/NL", c as u32);
10698 }
10699 }
10700
10701 #[test]
10702 fn is_bidi_control_covers_the_uax9_set_only() {
10703 for c in [
10704 '\u{200E}', '\u{200F}', '\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}',
10705 '\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}', '\u{061C}',
10706 ] {
10707 assert!(is_bidi_control(c), "{:04X} must be a bidi control", c as u32);
10708 }
10709 for c in ['a', ' ', '\u{200B}', '\u{2028}', '\u{202F}', '\u{2065}', char::MAX] {
10710 assert!(!is_bidi_control(c), "{:04X} must not be a bidi control", c as u32);
10711 }
10712 }
10713
10714 #[test]
10715 fn is_css_document_whitespace_excludes_other_unicode_spaces() {
10716 for c in [' ', '\t', '\n', '\r', '\x0C'] {
10717 assert!(is_css_document_whitespace(c));
10718 }
10719 for c in ['\u{00A0}', '\u{3000}', '\u{200B}', '\u{2028}', '\u{000B}', 'a'] {
10721 assert!(
10722 !is_css_document_whitespace(c),
10723 "{:04X} must not be document white space",
10724 c as u32
10725 );
10726 }
10727 }
10728
10729 #[test]
10734 fn split_at_forced_breaks_handles_every_newline_flavour() {
10735 assert_eq!(split_at_forced_breaks(""), vec![String::new()]);
10736 assert_eq!(split_at_forced_breaks("abc"), vec!["abc".to_string()]);
10737 assert_eq!(split_at_forced_breaks("a\nb"), vec!["a", "b"]);
10738 assert_eq!(split_at_forced_breaks("a\r\nb"), vec!["a", "b"]);
10739 assert_eq!(split_at_forced_breaks("a\rb"), vec!["a", "b"]);
10740 assert_eq!(split_at_forced_breaks("a\r\rb"), vec!["a", "", "b"]);
10742 assert_eq!(split_at_forced_breaks("\na\n"), vec!["", "a", ""]);
10744 assert_eq!(split_at_forced_breaks("a\u{2028}b"), vec!["a", "b"]);
10746 assert_eq!(split_at_forced_breaks("a\u{000B}b"), vec!["a", "b"]);
10747 }
10748
10749 #[test]
10750 fn split_at_forced_breaks_preserves_every_non_break_char() {
10751 let text = "héllo 🌍\u{0301}\u{200B}x";
10752 let segs = split_at_forced_breaks(text);
10753 assert_eq!(segs.len(), 1);
10754 assert_eq!(segs[0], text, "no char-boundary slicing damage");
10755
10756 let many = "a\nb\rc\r\nd\u{2029}e";
10758 assert_eq!(split_at_forced_breaks(many).len(), 5);
10759 assert_eq!(split_at_forced_breaks(many).concat(), "abcde");
10760 }
10761
10762 #[test]
10763 fn split_at_forced_breaks_survives_a_huge_all_break_input() {
10764 let text = "\n".repeat(10_000);
10765 let segs = split_at_forced_breaks(&text);
10766 assert_eq!(segs.len(), 10_001);
10767 assert!(segs.iter().all(String::is_empty));
10768 }
10769
10770 #[test]
10771 fn split_at_bk_nl_chars_ignores_lf_and_cr() {
10772 assert_eq!(split_at_bk_nl_chars(""), vec![String::new()]);
10773 assert_eq!(split_at_bk_nl_chars("a\nb\rc"), vec!["a\nb\rc".to_string()]);
10775 assert_eq!(split_at_bk_nl_chars("a\u{0085}b"), vec!["a", "b"]);
10777 assert_eq!(split_at_bk_nl_chars("\u{2029}"), vec!["", ""]);
10778 }
10779
10780 #[test]
10785 fn is_east_asian_wide_matches_cjk_kana_and_hangul() {
10786 for c in ['中', '一', 'あ', 'カ', '。', '가', 'ㄅ', 'A'] {
10787 assert!(is_east_asian_wide(c), "{:04X} must be wide", c as u32);
10788 }
10789 for c in ['a', 'Z', '0', ' ', 'é', '\u{0000}', char::MAX] {
10790 assert!(!is_east_asian_wide(c), "{:04X} must not be wide", c as u32);
10791 }
10792 }
10793
10794 #[test]
10795 fn is_east_asian_wide_range_boundaries_are_inclusive() {
10796 assert!(!is_east_asian_wide('\u{4DFF}')); assert!(is_east_asian_wide('\u{4E00}')); assert!(is_east_asian_wide('\u{9FFF}')); assert!(!is_east_asian_wide('\u{A000}')); assert!(!is_east_asian_wide('\u{FF00}')); assert!(is_east_asian_wide('\u{FF01}')); assert!(is_east_asian_wide('\u{FF60}')); assert!(!is_east_asian_wide('\u{FF61}')); }
10805
10806 #[test]
10807 fn is_east_asian_fullwidth_or_wide_excludes_hangul_but_wide_does_not() {
10808 assert!(is_east_asian_wide('가'));
10811 assert!(!is_east_asian_fullwidth_or_wide('가'));
10812 assert!(!is_east_asian_fullwidth_or_wide('\u{1100}')); assert!(!is_east_asian_fullwidth_or_wide('\u{3130}')); assert!(!is_east_asian_fullwidth_or_wide('\u{A960}')); assert!(!is_east_asian_fullwidth_or_wide('\u{D7B0}')); assert!(is_east_asian_fullwidth_or_wide('中'));
10819 assert!(is_east_asian_fullwidth_or_wide('あ'));
10820 assert!(is_east_asian_fullwidth_or_wide('\u{FF61}'));
10821 assert!(is_east_asian_fullwidth_or_wide('\u{A000}'));
10822 assert!(!is_east_asian_fullwidth_or_wide('a'));
10823 assert!(!is_east_asian_fullwidth_or_wide(char::MAX));
10824 }
10825
10826 #[test]
10831 fn apply_segment_break_transform_converts_breaks_to_a_single_space() {
10832 assert_eq!(apply_segment_break_transform(""), "");
10833 assert_eq!(apply_segment_break_transform("ab"), "ab");
10834 assert_eq!(apply_segment_break_transform("a\nb"), "a b");
10835 assert_eq!(apply_segment_break_transform("a\r\nb"), "a b");
10836 assert_eq!(apply_segment_break_transform("a\rb"), "a b");
10837 assert_eq!(apply_segment_break_transform("a \n b"), "a b");
10839 assert_eq!(apply_segment_break_transform("a\t\n\tb"), "a b");
10840 assert_eq!(apply_segment_break_transform("a\n\n\nb"), "a b");
10842 }
10843
10844 #[test]
10845 fn apply_segment_break_transform_at_the_string_edges() {
10846 assert_eq!(apply_segment_break_transform("\na"), " a");
10848 assert_eq!(apply_segment_break_transform("a\n"), "a ");
10849 assert_eq!(apply_segment_break_transform("\n"), " ");
10850 assert_eq!(apply_segment_break_transform(" \n "), " ");
10851 }
10852
10853 #[test]
10854 fn apply_segment_break_transform_removes_breaks_around_zwsp_and_cjk() {
10855 assert_eq!(apply_segment_break_transform("a\u{200B}\nb"), "a\u{200B}b");
10857 assert_eq!(apply_segment_break_transform("a\n\u{200B}b"), "a\u{200B}b");
10858 assert_eq!(apply_segment_break_transform("中\n文"), "中文");
10860 assert_eq!(apply_segment_break_transform("あ \n い"), "あい");
10861 assert_eq!(apply_segment_break_transform("中\na"), "中 a");
10863 assert_eq!(apply_segment_break_transform("a\n中"), "a 中");
10864 assert_eq!(apply_segment_break_transform("가\n나"), "가 나");
10866 }
10867
10868 #[test]
10869 fn apply_segment_break_transform_survives_pathological_input() {
10870 let text = "\n".repeat(5_000);
10871 assert_eq!(apply_segment_break_transform(&text), " ");
10872 let mixed = "🌍\n🌍\n🌍";
10874 assert_eq!(apply_segment_break_transform(mixed), "🌍 🌍 🌍");
10875 }
10876
10877 #[test]
10882 fn apply_text_transform_none_is_the_identity() {
10883 for s in ["", "abc", "ÄÖÜ", "🌍", " \t\n"] {
10884 assert_eq!(apply_text_transform(s, TextTransform::None), s);
10885 }
10886 }
10887
10888 #[test]
10889 fn apply_text_transform_case_changes_handle_growing_and_multibyte_chars() {
10890 assert_eq!(apply_text_transform("abc", TextTransform::Uppercase), "ABC");
10891 assert_eq!(apply_text_transform("straße", TextTransform::Uppercase), "STRASSE");
10893 assert_eq!(apply_text_transform("ÄÖÜ", TextTransform::Lowercase), "äöü");
10894 assert_eq!(apply_text_transform("", TextTransform::Uppercase), "");
10895 assert_eq!(apply_text_transform("🌍", TextTransform::Uppercase), "🌍");
10896 }
10897
10898 #[test]
10899 fn apply_text_transform_capitalize_uses_word_boundaries() {
10900 assert_eq!(
10901 apply_text_transform("hello world", TextTransform::Capitalize),
10902 "Hello World"
10903 );
10904 assert_eq!(
10906 apply_text_transform("1st place", TextTransform::Capitalize),
10907 "1st Place"
10908 );
10909 assert_eq!(
10911 apply_text_transform("a-b (c)", TextTransform::Capitalize),
10912 "A-B (C)"
10913 );
10914 assert_eq!(apply_text_transform("élan", TextTransform::Capitalize), "Élan");
10915 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
10916 }
10917
10918 #[test]
10919 fn apply_text_transform_fullwidth_maps_the_ascii_block() {
10920 assert_eq!(apply_text_transform("a", TextTransform::FullWidth), "\u{FF41}");
10921 assert_eq!(apply_text_transform("!", TextTransform::FullWidth), "\u{FF01}");
10922 assert_eq!(apply_text_transform("~", TextTransform::FullWidth), "\u{FF5E}");
10923 assert_eq!(apply_text_transform(" ", TextTransform::FullWidth), "\u{3000}");
10924 assert_eq!(apply_text_transform("\t\n", TextTransform::FullWidth), "\t\n");
10926 assert_eq!(apply_text_transform("\u{7F}", TextTransform::FullWidth), "\u{7F}");
10927 assert_eq!(apply_text_transform("中🌍", TextTransform::FullWidth), "中🌍");
10928 }
10929
10930 #[test]
10935 fn layout_initial_letter_rejects_degenerate_parameters() {
10936 assert_eq!(layout_initial_letter(0.0, 3, 1000.0, 20.0), (0.0, 0.0));
10938 assert_eq!(layout_initial_letter(-1.0, 3, 1000.0, 20.0), (0.0, 0.0));
10939 assert_eq!(layout_initial_letter(3.0, 3, 1000.0, 0.0), (0.0, 0.0));
10940 assert_eq!(layout_initial_letter(3.0, 3, 1000.0, -20.0), (0.0, 0.0));
10941 assert_eq!(layout_initial_letter(3.0, 3, 0.0, 20.0), (0.0, 0.0));
10942 assert_eq!(layout_initial_letter(3.0, 3, -10.0, 20.0), (0.0, 0.0));
10943 }
10944
10945 #[test]
10946 fn layout_initial_letter_computes_a_classic_drop_cap() {
10947 assert_eq!(layout_initial_letter(3.0, 3, 1000.0, 20.0), (46.0, 60.0));
10949 assert_eq!(layout_initial_letter(3.0, 3, 10.0, 20.0), (10.0, 60.0));
10951 assert_eq!(layout_initial_letter(3.0, 0, 1000.0, 20.0), (46.0, 60.0));
10953 assert_eq!(layout_initial_letter(2.0, 5, 1000.0, 20.0), (32.0, 100.0));
10955 }
10956
10957 #[test]
10958 fn layout_initial_letter_exclusion_is_never_shorter_than_the_letter() {
10959 for size_lines in [0.5_f32, 1.0, 3.0, 100.0] {
10960 for sink in [0_u32, 1, 3, 100] {
10961 let (w, h) = layout_initial_letter(size_lines, sink, 10_000.0, 20.0);
10962 let letter_height = size_lines * 20.0;
10963 assert!(h >= letter_height, "{size_lines}/{sink}: {h} < {letter_height}");
10964 assert!(w > 0.0 && w.is_finite());
10965 }
10966 }
10967 }
10968
10969 #[test]
10970 fn layout_initial_letter_saturates_at_extreme_sinks_and_sizes() {
10971 let (w, h) = layout_initial_letter(1.0, u32::MAX, 1000.0, 20.0);
10973 assert!(h.is_finite() && h > 0.0);
10974 assert_eq!(w, 18.0); let (w, h) = layout_initial_letter(f32::MAX, 1, 1000.0, f32::MAX);
10978 assert_eq!(w, 1000.0);
10979 assert!(h.is_infinite());
10980 }
10981
10982 #[test]
10983 fn layout_initial_letter_with_nan_and_inf_produces_a_defined_result() {
10984 let (w, h) = layout_initial_letter(f32::NAN, 3, 1000.0, 20.0);
10988 assert_eq!(w, 1000.0);
10989 assert_eq!(h, 60.0);
10990
10991 let (w, h) = layout_initial_letter(3.0, 3, 1000.0, f32::INFINITY);
10993 assert_eq!(w, 1000.0);
10994 assert!(h.is_infinite());
10995 }
10996
10997 fn cell_dom(attrs: Vec<AttributeType>) -> StyledDom {
11002 let mut cell = Dom::create_div();
11003 for a in attrs {
11004 cell = cell.with_attribute(a);
11005 }
11006 styled(Dom::create_body().with_child(cell), "")
11007 }
11008
11009 #[test]
11010 fn get_cell_spans_defaults_to_one_when_unset() {
11011 let dom = cell_dom(Vec::new());
11012 assert_eq!(get_cell_spans(&dom, DIV_NODE), (1, 1));
11013 }
11014
11015 #[test]
11016 fn get_cell_spans_clamps_hostile_html_values() {
11017 for n in [0, -1, i32::MIN] {
11019 let dom = cell_dom(vec![AttributeType::ColSpan(n), AttributeType::RowSpan(n)]);
11020 assert_eq!(get_cell_spans(&dom, DIV_NODE), (1, 1), "span {n}");
11021 }
11022 let dom = cell_dom(vec![
11025 AttributeType::ColSpan(i32::MAX),
11026 AttributeType::RowSpan(i32::MAX),
11027 ]);
11028 assert_eq!(get_cell_spans(&dom, DIV_NODE), (1000, 65534));
11029
11030 let dom = cell_dom(vec![
11032 AttributeType::ColSpan(3),
11033 AttributeType::RowSpan(7),
11034 ]);
11035 assert_eq!(get_cell_spans(&dom, DIV_NODE), (3, 7));
11036 }
11037
11038 #[test]
11043 fn get_float_and_clear_properties_default_to_none_for_a_missing_node() {
11044 let dom = text_dom("x", "");
11045 assert_eq!(get_float_property(&dom, None), LayoutFloat::None);
11046 assert_eq!(get_clear_property(&dom, None), LayoutClear::None);
11047 assert_eq!(get_float_property(&dom, Some(DIV_NODE)), LayoutFloat::None);
11049 assert_eq!(get_clear_property(&dom, Some(DIV_NODE)), LayoutClear::None);
11050 }
11051
11052 #[test]
11053 fn get_float_and_clear_properties_read_the_cascade() {
11054 let dom = text_dom("x", ".p { float: right; clear: both; }");
11055 assert_eq!(get_float_property(&dom, Some(DIV_NODE)), LayoutFloat::Right);
11056 assert_eq!(get_clear_property(&dom, Some(DIV_NODE)), LayoutClear::Both);
11057
11058 let dom = text_dom("x", ".p { float: left; clear: left; }");
11059 assert_eq!(get_float_property(&dom, Some(DIV_NODE)), LayoutFloat::Left);
11060 assert_eq!(get_clear_property(&dom, Some(DIV_NODE)), LayoutClear::Left);
11061 }
11062
11063 #[test]
11068 fn split_text_for_whitespace_collapses_runs_in_normal_mode() {
11069 let dom = text_dom(" a b ", "");
11070 let out = split_text_for_whitespace(&dom, TEXT_NODE, " a b ", &plain_style());
11071 assert_eq!(out.len(), 1);
11072 assert_eq!(text_of(&out[0]), Some(" a b "));
11075 }
11076
11077 #[test]
11078 fn split_text_for_whitespace_on_empty_and_whitespace_only_text() {
11079 let dom = text_dom("", "");
11080 assert!(split_text_for_whitespace(&dom, TEXT_NODE, "", &plain_style()).is_empty());
11081
11082 let out = split_text_for_whitespace(&dom, TEXT_NODE, " \t ", &plain_style());
11084 assert_eq!(out.len(), 1);
11085 assert_eq!(text_of(&out[0]), Some(" "));
11086 }
11087
11088 #[test]
11089 fn split_text_for_whitespace_honours_newlines_in_pre() {
11090 let dom = text_dom("a\nb", ".p { white-space: pre; }");
11091 let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\nb", &plain_style());
11092 assert_eq!(out.len(), 3);
11093 assert_eq!(text_of(&out[0]), Some("a"));
11094 assert!(matches!(out[1], InlineContent::LineBreak(_)));
11095 assert_eq!(text_of(&out[2]), Some("b"));
11096
11097 let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\tb", &plain_style());
11099 assert_eq!(out.len(), 3);
11100 assert_eq!(text_of(&out[0]), Some("a"));
11101 assert!(matches!(out[1], InlineContent::Tab { .. }));
11102 assert_eq!(text_of(&out[2]), Some("b"));
11103
11104 let out = split_text_for_whitespace(&dom, TEXT_NODE, " a ", &plain_style());
11106 assert_eq!(text_of(&out[0]), Some(" a "));
11107 }
11108
11109 #[test]
11110 fn split_text_for_whitespace_normalises_cr_and_crlf() {
11111 let dom = text_dom("x", ".p { white-space: pre; }");
11112 for text in ["a\r\nb", "a\rb", "a\nb"] {
11114 let out = split_text_for_whitespace(&dom, TEXT_NODE, text, &plain_style());
11115 assert_eq!(out.len(), 3, "{text:?}");
11116 assert!(matches!(out[1], InlineContent::LineBreak(_)), "{text:?}");
11117 }
11118 }
11119
11120 #[test]
11121 fn split_text_for_whitespace_forces_breaks_on_bk_nl_chars_in_every_mode() {
11122 let dom = text_dom("x", "");
11124 let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\u{2028}b", &plain_style());
11125 assert_eq!(out.len(), 3);
11126 assert!(matches!(out[1], InlineContent::LineBreak(_)));
11127
11128 let dom = text_dom("x", ".p { white-space: pre-line; }");
11129 let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\u{000C}b", &plain_style());
11130 assert_eq!(out.len(), 3);
11131 assert!(matches!(out[1], InlineContent::LineBreak(_)));
11132 }
11133
11134 #[test]
11135 fn split_text_for_whitespace_strips_bidi_controls_before_collapsing() {
11136 let dom = text_dom("x", "");
11137 let out = split_text_for_whitespace(&dom, TEXT_NODE, "a \u{200F} b", &plain_style());
11139 assert_eq!(out.len(), 1);
11140 assert_eq!(text_of(&out[0]), Some("a b"));
11141 }
11142
11143 #[test]
11144 fn split_text_for_whitespace_pre_line_collapses_spaces_but_keeps_breaks() {
11145 let dom = text_dom("x", ".p { white-space: pre-line; }");
11146 let out = split_text_for_whitespace(&dom, TEXT_NODE, "a b\n c ", &plain_style());
11147 assert_eq!(out.len(), 3);
11148 assert_eq!(text_of(&out[0]), Some("a b"));
11149 assert!(matches!(out[1], InlineContent::LineBreak(_)));
11150 assert_eq!(text_of(&out[2]), Some("c"));
11151 }
11152
11153 #[test]
11154 fn split_text_for_whitespace_survives_a_huge_multibyte_input() {
11155 let dom = text_dom("x", ".p { white-space: pre; }");
11156 let text = "🌍\n".repeat(2_000);
11157 let out = split_text_for_whitespace(&dom, TEXT_NODE, &text, &plain_style());
11158 assert_eq!(out.len(), 4_000);
11160 assert_eq!(text_of(&out[0]), Some("🌍"));
11161 }
11162}