we (web engine): Experimental web browser project to understand the limits of Claude
at main 5474 lines 190 kB view raw
1//! Block layout engine: box generation, block/inline layout, and text wrapping. 2//! 3//! Builds a layout tree from a styled tree (DOM + computed styles) and positions 4//! block-level elements vertically with proper inline formatting context. 5 6use std::collections::HashMap; 7 8use we_css::values::Color; 9use we_dom::{Document, NodeData, NodeId}; 10use we_style::computed::{ 11 AlignContent, AlignItems, AlignSelf, BorderStyle, BoxSizing, Clear, ComputedStyle, Display, 12 FlexDirection, FlexWrap, Float, JustifyContent, LengthOrAuto, Overflow, Position, StyledNode, 13 TextAlign, TextDecoration, Visibility, 14}; 15use we_text::font::Font; 16 17/// Width of scroll bars in pixels. 18pub const SCROLLBAR_WIDTH: f32 = 15.0; 19 20/// Edge sizes for box model (margin, padding, border). 21#[derive(Debug, Clone, Copy, Default, PartialEq)] 22pub struct EdgeSizes { 23 pub top: f32, 24 pub right: f32, 25 pub bottom: f32, 26 pub left: f32, 27} 28 29/// A positioned rectangle with content area dimensions. 30#[derive(Debug, Clone, Copy, Default, PartialEq)] 31pub struct Rect { 32 pub x: f32, 33 pub y: f32, 34 pub width: f32, 35 pub height: f32, 36} 37 38/// The type of layout box. 39#[derive(Debug)] 40pub enum BoxType { 41 /// Block-level box from an element. 42 Block(NodeId), 43 /// Inline-level box from an element. 44 Inline(NodeId), 45 /// A run of text from a text node. 46 TextRun { node: NodeId, text: String }, 47 /// Anonymous block wrapping inline content within a block container. 48 Anonymous, 49} 50 51/// A single positioned text fragment with its own styling. 52/// 53/// Multiple fragments can share the same y-coordinate when they are 54/// on the same visual line (e.g. `<p>Hello <em>world</em></p>` produces 55/// two fragments at the same y). 56#[derive(Debug, Clone, PartialEq)] 57pub struct TextLine { 58 pub text: String, 59 pub x: f32, 60 pub y: f32, 61 pub width: f32, 62 pub font_size: f32, 63 pub color: Color, 64 pub text_decoration: TextDecoration, 65 pub background_color: Color, 66} 67 68/// A box in the layout tree with dimensions and child boxes. 69#[derive(Debug)] 70pub struct LayoutBox { 71 pub box_type: BoxType, 72 pub display: Display, 73 pub rect: Rect, 74 pub margin: EdgeSizes, 75 pub padding: EdgeSizes, 76 pub border: EdgeSizes, 77 pub children: Vec<LayoutBox>, 78 pub font_size: f32, 79 /// Positioned text fragments (populated for boxes with inline content). 80 pub lines: Vec<TextLine>, 81 /// Text color. 82 pub color: Color, 83 /// Background color. 84 pub background_color: Color, 85 /// Text decoration (underline, etc.). 86 pub text_decoration: TextDecoration, 87 /// Border styles (top, right, bottom, left). 88 pub border_styles: [BorderStyle; 4], 89 /// Border colors (top, right, bottom, left). 90 pub border_colors: [Color; 4], 91 /// Text alignment for this box's inline content. 92 pub text_align: TextAlign, 93 /// Computed line height in px. 94 pub line_height: f32, 95 /// For replaced elements (e.g., `<img>`): content dimensions (width, height). 96 pub replaced_size: Option<(f32, f32)>, 97 /// CSS `position` property. 98 pub position: Position, 99 /// CSS `z-index` property (None = auto). 100 pub z_index: Option<i32>, 101 /// Relative position offset (dx, dy) applied after normal flow layout. 102 pub relative_offset: (f32, f32), 103 /// CSS `overflow` property. 104 pub overflow: Overflow, 105 /// CSS `box-sizing` property. 106 pub box_sizing: BoxSizing, 107 /// CSS `width` property (explicit or auto, may contain percentage). 108 pub css_width: LengthOrAuto, 109 /// CSS `height` property (explicit or auto, may contain percentage). 110 pub css_height: LengthOrAuto, 111 /// CSS margin values (may contain percentages for layout resolution). 112 pub css_margin: [LengthOrAuto; 4], 113 /// CSS padding values (may contain percentages for layout resolution). 114 pub css_padding: [LengthOrAuto; 4], 115 /// CSS position offset values (top, right, bottom, left) for relative/sticky positioning. 116 pub css_offsets: [LengthOrAuto; 4], 117 /// For `position: sticky`: the containing block's content rect in document 118 /// coordinates. Paint-time logic clamps the element within this rectangle. 119 pub sticky_constraint: Option<Rect>, 120 /// CSS `visibility` property. 121 pub visibility: Visibility, 122 /// CSS `float` property. 123 pub float: Float, 124 /// CSS `clear` property. 125 pub clear: Clear, 126 /// Natural content height before CSS height override. 127 /// Used to determine overflow for scroll containers. 128 pub content_height: f32, 129 // Flex container properties 130 pub flex_direction: FlexDirection, 131 pub flex_wrap: FlexWrap, 132 pub justify_content: JustifyContent, 133 pub align_items: AlignItems, 134 pub align_content: AlignContent, 135 pub row_gap: f32, 136 pub column_gap: f32, 137 // Flex item properties 138 pub flex_grow: f32, 139 pub flex_shrink: f32, 140 pub flex_basis: LengthOrAuto, 141 pub align_self: AlignSelf, 142 pub order: i32, 143} 144 145impl LayoutBox { 146 fn new(box_type: BoxType, style: &ComputedStyle) -> Self { 147 LayoutBox { 148 box_type, 149 display: style.display, 150 rect: Rect::default(), 151 margin: EdgeSizes::default(), 152 padding: EdgeSizes::default(), 153 border: EdgeSizes::default(), 154 children: Vec::new(), 155 font_size: style.font_size, 156 lines: Vec::new(), 157 color: style.color, 158 background_color: style.background_color, 159 text_decoration: style.text_decoration, 160 border_styles: [ 161 style.border_top_style, 162 style.border_right_style, 163 style.border_bottom_style, 164 style.border_left_style, 165 ], 166 border_colors: [ 167 style.border_top_color, 168 style.border_right_color, 169 style.border_bottom_color, 170 style.border_left_color, 171 ], 172 text_align: style.text_align, 173 line_height: style.line_height, 174 replaced_size: None, 175 position: style.position, 176 z_index: style.z_index, 177 relative_offset: (0.0, 0.0), 178 overflow: style.overflow, 179 box_sizing: style.box_sizing, 180 css_width: style.width, 181 css_height: style.height, 182 css_margin: [ 183 style.margin_top, 184 style.margin_right, 185 style.margin_bottom, 186 style.margin_left, 187 ], 188 css_padding: [ 189 style.padding_top, 190 style.padding_right, 191 style.padding_bottom, 192 style.padding_left, 193 ], 194 css_offsets: [style.top, style.right, style.bottom, style.left], 195 sticky_constraint: None, 196 visibility: style.visibility, 197 float: style.float, 198 clear: style.clear, 199 content_height: 0.0, 200 flex_direction: style.flex_direction, 201 flex_wrap: style.flex_wrap, 202 justify_content: style.justify_content, 203 align_items: style.align_items, 204 align_content: style.align_content, 205 row_gap: style.row_gap, 206 column_gap: style.column_gap, 207 flex_grow: style.flex_grow, 208 flex_shrink: style.flex_shrink, 209 flex_basis: style.flex_basis, 210 align_self: style.align_self, 211 order: style.order, 212 } 213 } 214 215 /// Total height including margin, border, and padding. 216 pub fn margin_box_height(&self) -> f32 { 217 self.margin.top 218 + self.border.top 219 + self.padding.top 220 + self.rect.height 221 + self.padding.bottom 222 + self.border.bottom 223 + self.margin.bottom 224 } 225 226 /// Iterate over all boxes in depth-first pre-order. 227 pub fn iter(&self) -> LayoutBoxIter<'_> { 228 LayoutBoxIter { stack: vec![self] } 229 } 230} 231 232/// Depth-first pre-order iterator over layout boxes. 233pub struct LayoutBoxIter<'a> { 234 stack: Vec<&'a LayoutBox>, 235} 236 237impl<'a> Iterator for LayoutBoxIter<'a> { 238 type Item = &'a LayoutBox; 239 240 fn next(&mut self) -> Option<&'a LayoutBox> { 241 let node = self.stack.pop()?; 242 for child in node.children.iter().rev() { 243 self.stack.push(child); 244 } 245 Some(node) 246 } 247} 248 249/// The result of laying out a document. 250#[derive(Debug)] 251pub struct LayoutTree { 252 pub root: LayoutBox, 253 pub width: f32, 254 pub height: f32, 255} 256 257impl LayoutTree { 258 /// Iterate over all layout boxes in depth-first pre-order. 259 pub fn iter(&self) -> LayoutBoxIter<'_> { 260 self.root.iter() 261 } 262} 263 264// --------------------------------------------------------------------------- 265// Resolve LengthOrAuto to f32 266// --------------------------------------------------------------------------- 267 268/// Resolve a `LengthOrAuto` to px. Percentages are resolved against 269/// `reference` (typically the containing block width). Auto resolves to 0. 270fn resolve_length_against(value: LengthOrAuto, reference: f32) -> f32 { 271 match value { 272 LengthOrAuto::Length(px) => px, 273 LengthOrAuto::Percentage(p) => p / 100.0 * reference, 274 LengthOrAuto::Auto => 0.0, 275 } 276} 277 278/// Resolve horizontal offset for `position: relative`. 279/// If both `left` and `right` are specified, `left` wins (CSS2 §9.4.3, ltr). 280fn resolve_relative_horizontal(left: LengthOrAuto, right: LengthOrAuto, cb_width: f32) -> f32 { 281 match left { 282 LengthOrAuto::Length(px) => px, 283 LengthOrAuto::Percentage(p) => p / 100.0 * cb_width, 284 LengthOrAuto::Auto => match right { 285 LengthOrAuto::Length(px) => -px, 286 LengthOrAuto::Percentage(p) => -(p / 100.0 * cb_width), 287 LengthOrAuto::Auto => 0.0, 288 }, 289 } 290} 291 292/// Resolve vertical offset for `position: relative`. 293/// If both `top` and `bottom` are specified, `top` wins (CSS2 §9.4.3). 294fn resolve_relative_vertical(top: LengthOrAuto, bottom: LengthOrAuto, cb_height: f32) -> f32 { 295 match top { 296 LengthOrAuto::Length(px) => px, 297 LengthOrAuto::Percentage(p) => p / 100.0 * cb_height, 298 LengthOrAuto::Auto => match bottom { 299 LengthOrAuto::Length(px) => -px, 300 LengthOrAuto::Percentage(p) => -(p / 100.0 * cb_height), 301 LengthOrAuto::Auto => 0.0, 302 }, 303 } 304} 305 306// --------------------------------------------------------------------------- 307// Build layout tree from styled tree 308// --------------------------------------------------------------------------- 309 310fn build_box( 311 styled: &StyledNode, 312 doc: &Document, 313 image_sizes: &HashMap<NodeId, (f32, f32)>, 314) -> Option<LayoutBox> { 315 let node = styled.node; 316 let style = &styled.style; 317 318 match doc.node_data(node) { 319 NodeData::Document => { 320 let mut children = Vec::new(); 321 for child in &styled.children { 322 if let Some(child_box) = build_box(child, doc, image_sizes) { 323 children.push(child_box); 324 } 325 } 326 if children.len() == 1 { 327 children.into_iter().next() 328 } else if children.is_empty() { 329 None 330 } else { 331 let mut b = LayoutBox::new(BoxType::Anonymous, style); 332 b.children = children; 333 Some(b) 334 } 335 } 336 NodeData::Element { .. } => { 337 if style.display == Display::None { 338 return None; 339 } 340 341 // Margin and padding: resolve absolute lengths now; percentages 342 // will be re-resolved in compute_layout against containing block. 343 // Use 0.0 as a placeholder reference for percentages — they'll be 344 // resolved properly in compute_layout. 345 let margin = EdgeSizes { 346 top: resolve_length_against(style.margin_top, 0.0), 347 right: resolve_length_against(style.margin_right, 0.0), 348 bottom: resolve_length_against(style.margin_bottom, 0.0), 349 left: resolve_length_against(style.margin_left, 0.0), 350 }; 351 let padding = EdgeSizes { 352 top: resolve_length_against(style.padding_top, 0.0), 353 right: resolve_length_against(style.padding_right, 0.0), 354 bottom: resolve_length_against(style.padding_bottom, 0.0), 355 left: resolve_length_against(style.padding_left, 0.0), 356 }; 357 let border = EdgeSizes { 358 top: if style.border_top_style != BorderStyle::None { 359 style.border_top_width 360 } else { 361 0.0 362 }, 363 right: if style.border_right_style != BorderStyle::None { 364 style.border_right_width 365 } else { 366 0.0 367 }, 368 bottom: if style.border_bottom_style != BorderStyle::None { 369 style.border_bottom_width 370 } else { 371 0.0 372 }, 373 left: if style.border_left_style != BorderStyle::None { 374 style.border_left_width 375 } else { 376 0.0 377 }, 378 }; 379 380 let mut children = Vec::new(); 381 for child in &styled.children { 382 if let Some(child_box) = build_box(child, doc, image_sizes) { 383 children.push(child_box); 384 } 385 } 386 387 // Per CSS2 §9.7: float forces display to block. 388 let effective_display = 389 if style.float != Float::None && style.display == Display::Inline { 390 Display::Block 391 } else { 392 style.display 393 }; 394 395 let box_type = match effective_display { 396 Display::Block | Display::Flex | Display::InlineFlex => BoxType::Block(node), 397 Display::Inline => BoxType::Inline(node), 398 Display::None => unreachable!(), 399 }; 400 401 if effective_display == Display::Block { 402 children = normalize_children(children, style); 403 } 404 405 let mut b = LayoutBox::new(box_type, style); 406 b.margin = margin; 407 b.padding = padding; 408 b.border = border; 409 b.children = children; 410 411 // Check for replaced element (e.g., <img>). 412 if let Some(&(w, h)) = image_sizes.get(&node) { 413 b.replaced_size = Some((w, h)); 414 } 415 416 // Relative position offsets are resolved in compute_layout 417 // where the containing block dimensions are known. 418 419 Some(b) 420 } 421 NodeData::Text { data } => { 422 let collapsed = collapse_whitespace(data); 423 if collapsed.is_empty() { 424 return None; 425 } 426 Some(LayoutBox::new( 427 BoxType::TextRun { 428 node, 429 text: collapsed, 430 }, 431 style, 432 )) 433 } 434 NodeData::Comment { .. } => None, 435 } 436} 437 438/// Collapse runs of whitespace to a single space. 439fn collapse_whitespace(s: &str) -> String { 440 let mut result = String::new(); 441 let mut in_ws = false; 442 for ch in s.chars() { 443 if ch.is_whitespace() { 444 if !in_ws { 445 result.push(' '); 446 } 447 in_ws = true; 448 } else { 449 in_ws = false; 450 result.push(ch); 451 } 452 } 453 result 454} 455 456/// If a block container has a mix of block-level and inline-level children, 457/// wrap consecutive inline runs in anonymous block boxes. 458/// Out-of-flow elements (absolute/fixed) are excluded from the determination 459/// and placed at the top level without affecting normalization. 460fn normalize_children(children: Vec<LayoutBox>, parent_style: &ComputedStyle) -> Vec<LayoutBox> { 461 if children.is_empty() { 462 return children; 463 } 464 465 // Consider in-flow block children and floated children as "block-level" 466 // for normalization purposes. When floats are present alongside inline 467 // content, the inline content must be wrapped in anonymous blocks so that 468 // `layout_block_children` can lay it out via its inline formatting context. 469 let has_block = children 470 .iter() 471 .any(|c| (is_in_flow(c) && is_block_level(c)) || is_floated(c)); 472 if !has_block { 473 return children; 474 } 475 476 let has_inline = children.iter().any(|c| is_in_flow(c) && !is_block_level(c)); 477 if !has_inline { 478 return children; 479 } 480 481 let mut result = Vec::new(); 482 let mut inline_group: Vec<LayoutBox> = Vec::new(); 483 484 for child in children { 485 if !is_in_flow(&child) || is_block_level(&child) { 486 // Out-of-flow or block-level: flush any pending inline group first. 487 if !inline_group.is_empty() { 488 let mut anon = LayoutBox::new(BoxType::Anonymous, parent_style); 489 anon.children = std::mem::take(&mut inline_group); 490 result.push(anon); 491 } 492 result.push(child); 493 } else { 494 inline_group.push(child); 495 } 496 } 497 498 if !inline_group.is_empty() { 499 let mut anon = LayoutBox::new(BoxType::Anonymous, parent_style); 500 anon.children = inline_group; 501 result.push(anon); 502 } 503 504 result 505} 506 507fn is_block_level(b: &LayoutBox) -> bool { 508 matches!(b.box_type, BoxType::Block(_) | BoxType::Anonymous) 509} 510 511/// Returns `true` if this box is in normal flow (not absolutely/fixed positioned, not floated). 512fn is_in_flow(b: &LayoutBox) -> bool { 513 b.position != Position::Absolute && b.position != Position::Fixed && b.float == Float::None 514} 515 516/// Returns `true` if this box is floated. 517fn is_floated(b: &LayoutBox) -> bool { 518 b.float != Float::None 519} 520 521// --------------------------------------------------------------------------- 522// Float tracking 523// --------------------------------------------------------------------------- 524 525/// A positioned float rectangle used for tracking placed floats. 526#[derive(Debug, Clone, Copy)] 527struct PlacedFloat { 528 /// Left edge of the float's margin box. 529 x: f32, 530 /// Top edge of the float's margin box. 531 y: f32, 532 /// Width of the float's margin box. 533 width: f32, 534 /// Height of the float's margin box. 535 height: f32, 536 /// Which side this float is on. 537 side: Float, 538} 539 540/// Tracks active floats within a block formatting context. 541#[derive(Debug, Default)] 542struct FloatContext { 543 floats: Vec<PlacedFloat>, 544} 545 546impl FloatContext { 547 /// Get the available x-range at a given y position, narrowed by active floats. 548 /// Returns (left_edge, right_edge) within the containing block. 549 fn available_range( 550 &self, 551 y: f32, 552 line_height: f32, 553 container_x: f32, 554 container_width: f32, 555 ) -> (f32, f32) { 556 let mut left = container_x; 557 let mut right = container_x + container_width; 558 559 for f in &self.floats { 560 let float_top = f.y; 561 let float_bottom = f.y + f.height; 562 563 // Check if this float overlaps vertically with the line. 564 if y + line_height > float_top && y < float_bottom { 565 match f.side { 566 Float::Left => { 567 let float_right_edge = f.x + f.width; 568 if float_right_edge > left { 569 left = float_right_edge; 570 } 571 } 572 Float::Right => { 573 let float_left_edge = f.x; 574 if float_left_edge < right { 575 right = float_left_edge; 576 } 577 } 578 Float::None => {} 579 } 580 } 581 } 582 583 (left, right) 584 } 585 586 /// Find the y position below all floats that match the given clear side. 587 fn clear_y(&self, clear: Clear) -> f32 { 588 let mut y = 0.0f32; 589 for f in &self.floats { 590 let dominated = match clear { 591 Clear::Left => f.side == Float::Left, 592 Clear::Right => f.side == Float::Right, 593 Clear::Both => true, 594 Clear::None => false, 595 }; 596 if dominated { 597 let bottom = f.y + f.height; 598 if bottom > y { 599 y = bottom; 600 } 601 } 602 } 603 y 604 } 605 606 /// Find the bottom edge of all placed floats. 607 fn max_float_bottom(&self) -> f32 { 608 let mut bottom = 0.0f32; 609 for f in &self.floats { 610 let fb = f.y + f.height; 611 if fb > bottom { 612 bottom = fb; 613 } 614 } 615 bottom 616 } 617 618 /// Place a float and return its position. 619 fn place_float( 620 &mut self, 621 float_side: Float, 622 float_width: f32, 623 float_height: f32, 624 cursor_y: f32, 625 container_x: f32, 626 container_width: f32, 627 ) -> (f32, f32) { 628 // Start at cursor_y and find a position where the float fits. 629 let mut y = cursor_y; 630 631 loop { 632 let (left, right) = self.available_range(y, float_height, container_x, container_width); 633 let available = right - left; 634 635 if available >= float_width || available >= container_width { 636 let x = match float_side { 637 Float::Left => left, 638 Float::Right => right - float_width, 639 Float::None => unreachable!(), 640 }; 641 self.floats.push(PlacedFloat { 642 x, 643 y, 644 width: float_width, 645 height: float_height, 646 side: float_side, 647 }); 648 return (x, y); 649 } 650 651 // Move down below the topmost interfering float and try again. 652 let mut next_y = f32::MAX; 653 for f in &self.floats { 654 let fb = f.y + f.height; 655 if fb > y && fb < next_y { 656 next_y = fb; 657 } 658 } 659 if next_y == f32::MAX { 660 // No more floats to clear, place at current position. 661 let x = match float_side { 662 Float::Left => container_x, 663 Float::Right => container_x + container_width - float_width, 664 Float::None => unreachable!(), 665 }; 666 self.floats.push(PlacedFloat { 667 x, 668 y, 669 width: float_width, 670 height: float_height, 671 side: float_side, 672 }); 673 return (x, y); 674 } 675 y = next_y; 676 } 677 } 678} 679 680// --------------------------------------------------------------------------- 681// Layout algorithm 682// --------------------------------------------------------------------------- 683 684/// Position and size a layout box within `available_width` at position (`x`, `y`). 685/// 686/// `available_width` is the containing block width — used as the reference for 687/// percentage widths, margins, and paddings (per CSS spec, even vertical margins/ 688/// padding resolve against the containing block width). 689/// 690/// `abs_cb` is the padding box of the nearest positioned ancestor, used as the 691/// containing block for absolutely positioned descendants. 692#[allow(clippy::too_many_arguments)] 693fn compute_layout( 694 b: &mut LayoutBox, 695 x: f32, 696 y: f32, 697 available_width: f32, 698 viewport_width: f32, 699 viewport_height: f32, 700 font: &Font, 701 doc: &Document, 702 abs_cb: Rect, 703 float_ctx: Option<&FloatContext>, 704) { 705 // Resolve percentage margins against containing block width. 706 // Only re-resolve percentages — absolute margins may have been modified 707 // by margin collapsing and must not be overwritten. 708 if matches!(b.css_margin[0], LengthOrAuto::Percentage(_)) { 709 b.margin.top = resolve_length_against(b.css_margin[0], available_width); 710 } 711 if matches!(b.css_margin[1], LengthOrAuto::Percentage(_)) { 712 b.margin.right = resolve_length_against(b.css_margin[1], available_width); 713 } 714 if matches!(b.css_margin[2], LengthOrAuto::Percentage(_)) { 715 b.margin.bottom = resolve_length_against(b.css_margin[2], available_width); 716 } 717 if matches!(b.css_margin[3], LengthOrAuto::Percentage(_)) { 718 b.margin.left = resolve_length_against(b.css_margin[3], available_width); 719 } 720 721 // Resolve percentage padding against containing block width. 722 if matches!(b.css_padding[0], LengthOrAuto::Percentage(_)) { 723 b.padding.top = resolve_length_against(b.css_padding[0], available_width); 724 } 725 if matches!(b.css_padding[1], LengthOrAuto::Percentage(_)) { 726 b.padding.right = resolve_length_against(b.css_padding[1], available_width); 727 } 728 if matches!(b.css_padding[2], LengthOrAuto::Percentage(_)) { 729 b.padding.bottom = resolve_length_against(b.css_padding[2], available_width); 730 } 731 if matches!(b.css_padding[3], LengthOrAuto::Percentage(_)) { 732 b.padding.left = resolve_length_against(b.css_padding[3], available_width); 733 } 734 735 let content_x = x + b.margin.left + b.border.left + b.padding.left; 736 let content_y = y + b.margin.top + b.border.top + b.padding.top; 737 738 let horizontal_extra = b.border.left + b.border.right + b.padding.left + b.padding.right; 739 740 // Resolve content width: explicit CSS width (adjusted for box-sizing) or auto. 741 let content_width = match b.css_width { 742 LengthOrAuto::Length(w) => match b.box_sizing { 743 BoxSizing::ContentBox => w.max(0.0), 744 BoxSizing::BorderBox => (w - horizontal_extra).max(0.0), 745 }, 746 LengthOrAuto::Percentage(p) => { 747 let resolved = p / 100.0 * available_width; 748 match b.box_sizing { 749 BoxSizing::ContentBox => resolved.max(0.0), 750 BoxSizing::BorderBox => (resolved - horizontal_extra).max(0.0), 751 } 752 } 753 LengthOrAuto::Auto => { 754 (available_width - b.margin.left - b.margin.right - horizontal_extra).max(0.0) 755 } 756 }; 757 758 b.rect.x = content_x; 759 b.rect.y = content_y; 760 b.rect.width = content_width; 761 762 // For overflow:scroll, reserve space for vertical scrollbar. 763 if b.overflow == Overflow::Scroll { 764 b.rect.width = (b.rect.width - SCROLLBAR_WIDTH).max(0.0); 765 } 766 767 // Replaced elements (e.g., <img>) have intrinsic dimensions. 768 if let Some((rw, rh)) = b.replaced_size { 769 b.rect.width = rw.min(b.rect.width); 770 b.rect.height = rh; 771 set_sticky_constraints(b); 772 layout_abspos_children(b, abs_cb, viewport_width, viewport_height, font, doc); 773 apply_relative_offset(b, available_width, viewport_height); 774 return; 775 } 776 777 match &b.box_type { 778 BoxType::Block(_) | BoxType::Anonymous => { 779 if matches!(b.display, Display::Flex | Display::InlineFlex) { 780 layout_flex_children(b, viewport_width, viewport_height, font, doc, abs_cb); 781 } else if has_block_children(b) || has_float_children(b) { 782 layout_block_children(b, viewport_width, viewport_height, font, doc, abs_cb); 783 } else { 784 layout_inline_children(b, font, doc, float_ctx); 785 } 786 } 787 BoxType::TextRun { .. } | BoxType::Inline(_) => { 788 // Handled by the parent's inline layout. 789 } 790 } 791 792 // Save the natural content height before CSS height override. 793 b.content_height = b.rect.height; 794 795 // Apply explicit CSS height (adjusted for box-sizing), overriding auto height. 796 match b.css_height { 797 LengthOrAuto::Length(h) => { 798 let vertical_extra = b.border.top + b.border.bottom + b.padding.top + b.padding.bottom; 799 b.rect.height = match b.box_sizing { 800 BoxSizing::ContentBox => h.max(0.0), 801 BoxSizing::BorderBox => (h - vertical_extra).max(0.0), 802 }; 803 } 804 LengthOrAuto::Percentage(p) => { 805 // Height percentage resolves against containing block height. 806 // For the root element, use viewport height. 807 let cb_height = viewport_height; 808 let resolved = p / 100.0 * cb_height; 809 let vertical_extra = b.border.top + b.border.bottom + b.padding.top + b.padding.bottom; 810 b.rect.height = match b.box_sizing { 811 BoxSizing::ContentBox => resolved.max(0.0), 812 BoxSizing::BorderBox => (resolved - vertical_extra).max(0.0), 813 }; 814 } 815 LengthOrAuto::Auto => {} 816 } 817 818 // Set sticky constraint rects now that this box's dimensions are final. 819 set_sticky_constraints(b); 820 821 // Layout absolutely and fixed positioned children after this box's 822 // dimensions are fully resolved. 823 layout_abspos_children(b, abs_cb, viewport_width, viewport_height, font, doc); 824 825 apply_relative_offset(b, available_width, viewport_height); 826} 827 828/// For each direct child with `position: sticky`, record the parent's content 829/// rect as the sticky constraint rectangle. This is called after the parent's 830/// dimensions are fully resolved so that the rect is accurate. 831fn set_sticky_constraints(parent: &mut LayoutBox) { 832 let content_rect = parent.rect; 833 for child in &mut parent.children { 834 if child.position == Position::Sticky { 835 child.sticky_constraint = Some(content_rect); 836 } 837 } 838} 839 840/// Apply `position: relative` offset to a box and all its descendants. 841/// 842/// Resolves the CSS position offsets (which may contain percentages) and 843/// shifts the visual position without affecting the normal-flow layout. 844fn apply_relative_offset(b: &mut LayoutBox, cb_width: f32, cb_height: f32) { 845 if b.position != Position::Relative { 846 return; 847 } 848 let [top, right, bottom, left] = b.css_offsets; 849 let dx = resolve_relative_horizontal(left, right, cb_width); 850 let dy = resolve_relative_vertical(top, bottom, cb_height); 851 b.relative_offset = (dx, dy); 852 if dx == 0.0 && dy == 0.0 { 853 return; 854 } 855 shift_box(b, dx, dy); 856} 857 858/// Recursively shift a box and all its descendants by (dx, dy). 859fn shift_box(b: &mut LayoutBox, dx: f32, dy: f32) { 860 b.rect.x += dx; 861 b.rect.y += dy; 862 for line in &mut b.lines { 863 line.x += dx; 864 line.y += dy; 865 } 866 for child in &mut b.children { 867 shift_box(child, dx, dy); 868 } 869} 870 871// --------------------------------------------------------------------------- 872// Absolute / fixed positioning 873// --------------------------------------------------------------------------- 874 875/// Resolve a `LengthOrAuto` offset to an optional pixel value. 876/// Returns `None` for `Auto` (offset not specified). 877fn resolve_offset(value: LengthOrAuto, reference: f32) -> Option<f32> { 878 match value { 879 LengthOrAuto::Length(px) => Some(px), 880 LengthOrAuto::Percentage(p) => Some(p / 100.0 * reference), 881 LengthOrAuto::Auto => None, 882 } 883} 884 885/// Compute the padding box rectangle for a layout box. 886fn padding_box_rect(b: &LayoutBox) -> Rect { 887 Rect { 888 x: b.rect.x - b.padding.left, 889 y: b.rect.y - b.padding.top, 890 width: b.rect.width + b.padding.left + b.padding.right, 891 height: b.rect.height + b.padding.top + b.padding.bottom, 892 } 893} 894 895/// Lay out all absolutely and fixed positioned children of `parent`. 896/// 897/// `abs_cb` is the padding box of the nearest positioned ancestor passed from 898/// further up the tree. If `parent` itself is positioned (not `static`), its 899/// padding box becomes the new containing block for absolute descendants. 900fn layout_abspos_children( 901 parent: &mut LayoutBox, 902 abs_cb: Rect, 903 viewport_width: f32, 904 viewport_height: f32, 905 font: &Font, 906 doc: &Document, 907) { 908 let viewport_cb = Rect { 909 x: 0.0, 910 y: 0.0, 911 width: viewport_width, 912 height: viewport_height, 913 }; 914 915 // If this box is positioned, it becomes the containing block for absolute 916 // descendants. 917 let new_abs_cb = if parent.position != Position::Static { 918 padding_box_rect(parent) 919 } else { 920 abs_cb 921 }; 922 923 for i in 0..parent.children.len() { 924 if parent.children[i].position == Position::Absolute { 925 layout_absolute_child( 926 &mut parent.children[i], 927 new_abs_cb, 928 viewport_width, 929 viewport_height, 930 font, 931 doc, 932 ); 933 } else if parent.children[i].position == Position::Fixed { 934 layout_absolute_child( 935 &mut parent.children[i], 936 viewport_cb, 937 viewport_width, 938 viewport_height, 939 font, 940 doc, 941 ); 942 } 943 } 944} 945 946/// Lay out a single absolutely or fixed positioned element. 947/// 948/// `cb` is the containing block (padding box of nearest positioned ancestor 949/// for absolute, or the viewport for fixed). 950fn layout_absolute_child( 951 child: &mut LayoutBox, 952 cb: Rect, 953 viewport_width: f32, 954 viewport_height: f32, 955 font: &Font, 956 doc: &Document, 957) { 958 let [css_top, css_right, css_bottom, css_left] = child.css_offsets; 959 960 // Resolve margins against containing block width. 961 child.margin = EdgeSizes { 962 top: resolve_length_against(child.css_margin[0], cb.width), 963 right: resolve_length_against(child.css_margin[1], cb.width), 964 bottom: resolve_length_against(child.css_margin[2], cb.width), 965 left: resolve_length_against(child.css_margin[3], cb.width), 966 }; 967 968 // Resolve padding against containing block width. 969 child.padding = EdgeSizes { 970 top: resolve_length_against(child.css_padding[0], cb.width), 971 right: resolve_length_against(child.css_padding[1], cb.width), 972 bottom: resolve_length_against(child.css_padding[2], cb.width), 973 left: resolve_length_against(child.css_padding[3], cb.width), 974 }; 975 976 let horiz_extra = 977 child.border.left + child.border.right + child.padding.left + child.padding.right; 978 let vert_extra = 979 child.border.top + child.border.bottom + child.padding.top + child.padding.bottom; 980 981 // Resolve offsets. 982 let left_offset = resolve_offset(css_left, cb.width); 983 let right_offset = resolve_offset(css_right, cb.width); 984 let top_offset = resolve_offset(css_top, cb.height); 985 let bottom_offset = resolve_offset(css_bottom, cb.height); 986 987 // --- Resolve content width --- 988 let content_width = match child.css_width { 989 LengthOrAuto::Length(w) => match child.box_sizing { 990 BoxSizing::ContentBox => w.max(0.0), 991 BoxSizing::BorderBox => (w - horiz_extra).max(0.0), 992 }, 993 LengthOrAuto::Percentage(p) => { 994 let resolved = p / 100.0 * cb.width; 995 match child.box_sizing { 996 BoxSizing::ContentBox => resolved.max(0.0), 997 BoxSizing::BorderBox => (resolved - horiz_extra).max(0.0), 998 } 999 } 1000 LengthOrAuto::Auto => { 1001 // If both left and right are specified, stretch to fill. 1002 if let (Some(l), Some(r)) = (left_offset, right_offset) { 1003 (cb.width - l - r - child.margin.left - child.margin.right - horiz_extra).max(0.0) 1004 } else { 1005 // Use available width minus margins. 1006 (cb.width - child.margin.left - child.margin.right - horiz_extra).max(0.0) 1007 } 1008 } 1009 }; 1010 1011 child.rect.width = content_width; 1012 1013 // --- Resolve horizontal position --- 1014 child.rect.x = if let Some(l) = left_offset { 1015 cb.x + l + child.margin.left + child.border.left + child.padding.left 1016 } else if let Some(r) = right_offset { 1017 cb.x + cb.width 1018 - r 1019 - child.margin.right 1020 - child.border.right 1021 - child.padding.right 1022 - content_width 1023 } else { 1024 // Static position: containing block origin. 1025 cb.x + child.margin.left + child.border.left + child.padding.left 1026 }; 1027 1028 // Set a temporary y for child content layout. 1029 child.rect.y = cb.y; 1030 1031 // For overflow:scroll, reserve scrollbar width. 1032 if child.overflow == Overflow::Scroll { 1033 child.rect.width = (child.rect.width - SCROLLBAR_WIDTH).max(0.0); 1034 } 1035 1036 // --- Layout child's own content --- 1037 if let Some((rw, rh)) = child.replaced_size { 1038 child.rect.width = rw.min(child.rect.width); 1039 child.rect.height = rh; 1040 } else { 1041 let child_abs_cb = if child.position != Position::Static { 1042 padding_box_rect(child) 1043 } else { 1044 cb 1045 }; 1046 match &child.box_type { 1047 BoxType::Block(_) | BoxType::Anonymous => { 1048 if matches!(child.display, Display::Flex | Display::InlineFlex) { 1049 layout_flex_children( 1050 child, 1051 viewport_width, 1052 viewport_height, 1053 font, 1054 doc, 1055 child_abs_cb, 1056 ); 1057 } else if has_block_children(child) || has_float_children(child) { 1058 layout_block_children( 1059 child, 1060 viewport_width, 1061 viewport_height, 1062 font, 1063 doc, 1064 child_abs_cb, 1065 ); 1066 } else { 1067 layout_inline_children(child, font, doc, None); 1068 } 1069 } 1070 _ => {} 1071 } 1072 } 1073 1074 // Save natural content height. 1075 child.content_height = child.rect.height; 1076 1077 // --- Resolve content height --- 1078 match child.css_height { 1079 LengthOrAuto::Length(h) => { 1080 child.rect.height = match child.box_sizing { 1081 BoxSizing::ContentBox => h.max(0.0), 1082 BoxSizing::BorderBox => (h - vert_extra).max(0.0), 1083 }; 1084 } 1085 LengthOrAuto::Percentage(p) => { 1086 let resolved = p / 100.0 * cb.height; 1087 child.rect.height = match child.box_sizing { 1088 BoxSizing::ContentBox => resolved.max(0.0), 1089 BoxSizing::BorderBox => (resolved - vert_extra).max(0.0), 1090 }; 1091 } 1092 LengthOrAuto::Auto => { 1093 // If both top and bottom are specified, stretch. 1094 if let (Some(t), Some(b_val)) = (top_offset, bottom_offset) { 1095 child.rect.height = 1096 (cb.height - t - b_val - child.margin.top - child.margin.bottom - vert_extra) 1097 .max(0.0); 1098 } 1099 // Otherwise keep content height. 1100 } 1101 } 1102 1103 // --- Resolve vertical position --- 1104 let final_y = if let Some(t) = top_offset { 1105 cb.y + t + child.margin.top + child.border.top + child.padding.top 1106 } else if let Some(b_val) = bottom_offset { 1107 cb.y + cb.height 1108 - b_val 1109 - child.margin.bottom 1110 - child.border.bottom 1111 - child.padding.bottom 1112 - child.rect.height 1113 } else { 1114 // Static position: containing block origin. 1115 cb.y + child.margin.top + child.border.top + child.padding.top 1116 }; 1117 1118 // Shift from temporary y to final y. 1119 let dy = final_y - child.rect.y; 1120 if dy != 0.0 { 1121 shift_box(child, 0.0, dy); 1122 } 1123 1124 // Set sticky constraints now that this child's dimensions are final. 1125 set_sticky_constraints(child); 1126 1127 // Recursively lay out any absolutely positioned grandchildren. 1128 let new_abs_cb = if child.position != Position::Static { 1129 padding_box_rect(child) 1130 } else { 1131 cb 1132 }; 1133 layout_abspos_children( 1134 child, 1135 new_abs_cb, 1136 viewport_width, 1137 viewport_height, 1138 font, 1139 doc, 1140 ); 1141} 1142 1143fn has_block_children(b: &LayoutBox) -> bool { 1144 b.children 1145 .iter() 1146 .any(|c| is_in_flow(c) && is_block_level(c)) 1147} 1148 1149fn has_float_children(b: &LayoutBox) -> bool { 1150 b.children.iter().any(is_floated) 1151} 1152 1153/// Collapse two adjoining margins per CSS2 §8.3.1. 1154/// 1155/// Both non-negative → use the larger. 1156/// Both negative → use the more negative. 1157/// Mixed → sum the largest positive and most negative. 1158fn collapse_margins(a: f32, b: f32) -> f32 { 1159 if a >= 0.0 && b >= 0.0 { 1160 a.max(b) 1161 } else if a < 0.0 && b < 0.0 { 1162 a.min(b) 1163 } else { 1164 a + b 1165 } 1166} 1167 1168/// Returns `true` if this box establishes a new block formatting context, 1169/// which prevents its margins from collapsing with children. 1170fn establishes_bfc(b: &LayoutBox) -> bool { 1171 b.overflow != Overflow::Visible 1172 || matches!(b.display, Display::Flex | Display::InlineFlex) 1173 || b.float != Float::None 1174} 1175 1176/// Returns `true` if a block box has no in-flow content (empty block). 1177fn is_empty_block(b: &LayoutBox) -> bool { 1178 b.children.is_empty() 1179 && b.lines.is_empty() 1180 && b.replaced_size.is_none() 1181 && matches!(b.css_height, LengthOrAuto::Auto) 1182} 1183 1184/// Pre-collapse parent-child margins (CSS2 §8.3.1). 1185/// 1186/// When a parent has no border/padding/BFC separating it from its first/last 1187/// child, the child's margin collapses into the parent's margin. This must 1188/// happen *before* positioning so the parent is placed using the collapsed 1189/// value. The function walks bottom-up: children are pre-collapsed first, then 1190/// their (possibly enlarged) margins are folded into the parent. 1191fn pre_collapse_margins(b: &mut LayoutBox) { 1192 // Recurse into in-flow block children first (bottom-up). 1193 for child in &mut b.children { 1194 if is_in_flow(child) && is_block_level(child) { 1195 pre_collapse_margins(child); 1196 } 1197 } 1198 1199 if !matches!(b.box_type, BoxType::Block(_) | BoxType::Anonymous) { 1200 return; 1201 } 1202 if establishes_bfc(b) { 1203 return; 1204 } 1205 if !has_block_children(b) { 1206 return; 1207 } 1208 1209 // --- Top: collapse with first non-empty child --- 1210 if b.border.top == 0.0 && b.padding.top == 0.0 { 1211 if let Some(child_top) = first_block_top_margin(&b.children) { 1212 b.margin.top = collapse_margins(b.margin.top, child_top); 1213 } 1214 } 1215 1216 // --- Bottom: collapse with last non-empty child --- 1217 if b.border.bottom == 0.0 && b.padding.bottom == 0.0 { 1218 if let Some(child_bottom) = last_block_bottom_margin(&b.children) { 1219 b.margin.bottom = collapse_margins(b.margin.bottom, child_bottom); 1220 } 1221 } 1222} 1223 1224/// Top margin of the first non-empty in-flow block child (already pre-collapsed). 1225fn first_block_top_margin(children: &[LayoutBox]) -> Option<f32> { 1226 for child in children { 1227 if is_in_flow(child) && is_block_level(child) { 1228 if is_empty_block(child) { 1229 continue; 1230 } 1231 return Some(child.margin.top); 1232 } 1233 } 1234 // All in-flow block children empty — fold all their collapsed margins. 1235 let mut m = 0.0f32; 1236 for child in children 1237 .iter() 1238 .filter(|c| is_in_flow(c) && is_block_level(c)) 1239 { 1240 m = collapse_margins(m, collapse_margins(child.margin.top, child.margin.bottom)); 1241 } 1242 if m != 0.0 { 1243 Some(m) 1244 } else { 1245 None 1246 } 1247} 1248 1249/// Bottom margin of the last non-empty in-flow block child (already pre-collapsed). 1250fn last_block_bottom_margin(children: &[LayoutBox]) -> Option<f32> { 1251 for child in children.iter().rev() { 1252 if is_in_flow(child) && is_block_level(child) { 1253 if is_empty_block(child) { 1254 continue; 1255 } 1256 return Some(child.margin.bottom); 1257 } 1258 } 1259 let mut m = 0.0f32; 1260 for child in children 1261 .iter() 1262 .filter(|c| is_in_flow(c) && is_block_level(c)) 1263 { 1264 m = collapse_margins(m, collapse_margins(child.margin.top, child.margin.bottom)); 1265 } 1266 if m != 0.0 { 1267 Some(m) 1268 } else { 1269 None 1270 } 1271} 1272 1273/// Lay out block-level children with vertical margin collapsing (CSS2 §8.3.1) 1274/// and float support. 1275/// 1276/// Handles adjacent-sibling collapsing, empty-block collapsing, 1277/// parent-child internal spacing (the parent's external margins were already 1278/// updated by `pre_collapse_margins`), and float placement. 1279fn layout_block_children( 1280 parent: &mut LayoutBox, 1281 viewport_width: f32, 1282 viewport_height: f32, 1283 font: &Font, 1284 doc: &Document, 1285 abs_cb: Rect, 1286) { 1287 let content_x = parent.rect.x; 1288 let content_width = parent.rect.width; 1289 let mut cursor_y = parent.rect.y; 1290 1291 let parent_top_open = 1292 parent.border.top == 0.0 && parent.padding.top == 0.0 && !establishes_bfc(parent); 1293 let parent_bottom_open = 1294 parent.border.bottom == 0.0 && parent.padding.bottom == 0.0 && !establishes_bfc(parent); 1295 1296 // Pending bottom margin from the previous sibling. 1297 let mut pending_margin: Option<f32> = None; 1298 let child_count = parent.children.len(); 1299 // Track whether we've seen any in-flow children (for parent_top_open). 1300 let mut first_in_flow = true; 1301 let mut float_ctx = FloatContext::default(); 1302 1303 for i in 0..child_count { 1304 // Skip out-of-flow children (absolute/fixed) — they are laid out 1305 // separately in layout_abspos_children. 1306 if parent.children[i].position == Position::Absolute 1307 || parent.children[i].position == Position::Fixed 1308 { 1309 continue; 1310 } 1311 1312 // --- Handle floated children --- 1313 if is_floated(&parent.children[i]) { 1314 layout_float_child( 1315 &mut parent.children[i], 1316 &mut float_ctx, 1317 cursor_y, 1318 content_x, 1319 content_width, 1320 viewport_width, 1321 viewport_height, 1322 font, 1323 doc, 1324 abs_cb, 1325 ); 1326 continue; 1327 } 1328 1329 let child_top_margin = parent.children[i].margin.top; 1330 let child_bottom_margin = parent.children[i].margin.bottom; 1331 1332 // --- Handle clear property --- 1333 if parent.children[i].clear != Clear::None { 1334 let clear_y = float_ctx.clear_y(parent.children[i].clear); 1335 if clear_y > cursor_y { 1336 cursor_y = clear_y; 1337 // Clear resets pending margin. 1338 pending_margin = None; 1339 } 1340 } 1341 1342 // --- Empty block: top+bottom margins self-collapse --- 1343 if is_empty_block(&parent.children[i]) { 1344 let self_collapsed = collapse_margins(child_top_margin, child_bottom_margin); 1345 pending_margin = Some(match pending_margin { 1346 Some(prev) => collapse_margins(prev, self_collapsed), 1347 None => self_collapsed, 1348 }); 1349 // Position at cursor_y with zero height. 1350 let child = &mut parent.children[i]; 1351 child.rect.x = content_x + child.border.left + child.padding.left; 1352 child.rect.y = cursor_y + child.border.top + child.padding.top; 1353 child.rect.width = (content_width 1354 - child.border.left 1355 - child.border.right 1356 - child.padding.left 1357 - child.padding.right) 1358 .max(0.0); 1359 child.rect.height = 0.0; 1360 first_in_flow = false; 1361 continue; 1362 } 1363 1364 // --- Compute effective top spacing --- 1365 let collapsed_top = if let Some(prev_bottom) = pending_margin.take() { 1366 // Sibling collapsing: previous bottom vs this top. 1367 collapse_margins(prev_bottom, child_top_margin) 1368 } else if first_in_flow && parent_top_open { 1369 // First child, parent top open: margin was already pulled into 1370 // parent by pre_collapse_margins — no internal spacing. 1371 0.0 1372 } else { 1373 child_top_margin 1374 }; 1375 1376 // `compute_layout` adds `child.margin.top` internally, so compensate. 1377 let y_for_child = cursor_y + collapsed_top - child_top_margin; 1378 compute_layout( 1379 &mut parent.children[i], 1380 content_x, 1381 y_for_child, 1382 content_width, 1383 viewport_width, 1384 viewport_height, 1385 font, 1386 doc, 1387 abs_cb, 1388 Some(&float_ctx), 1389 ); 1390 1391 let child = &parent.children[i]; 1392 // Use the normal-flow position (before relative offset) so that 1393 // `position: relative` does not affect sibling placement. 1394 let (_, rel_dy) = child.relative_offset; 1395 cursor_y = (child.rect.y - rel_dy) 1396 + child.rect.height 1397 + child.padding.bottom 1398 + child.border.bottom; 1399 pending_margin = Some(child_bottom_margin); 1400 first_in_flow = false; 1401 } 1402 1403 // Trailing margin. 1404 if let Some(trailing) = pending_margin { 1405 if !parent_bottom_open { 1406 // Parent has border/padding at bottom — margin stays inside. 1407 cursor_y += trailing; 1408 } 1409 // If parent_bottom_open, the margin was already pulled into the 1410 // parent by pre_collapse_margins. 1411 } 1412 1413 parent.rect.height = cursor_y - parent.rect.y; 1414 1415 // BFC containment: if this box establishes a BFC, it must expand to 1416 // contain all of its floated children. 1417 if establishes_bfc(parent) { 1418 let float_bottom = float_ctx.max_float_bottom(); 1419 let needed = float_bottom - parent.rect.y; 1420 if needed > parent.rect.height { 1421 parent.rect.height = needed; 1422 } 1423 } 1424} 1425 1426/// Lay out a single floated child element. 1427#[allow(clippy::too_many_arguments)] 1428fn layout_float_child( 1429 child: &mut LayoutBox, 1430 float_ctx: &mut FloatContext, 1431 cursor_y: f32, 1432 container_x: f32, 1433 container_width: f32, 1434 viewport_width: f32, 1435 viewport_height: f32, 1436 font: &Font, 1437 doc: &Document, 1438 abs_cb: Rect, 1439) { 1440 let float_side = child.float; 1441 1442 // Resolve margins against containing block width. 1443 child.margin = EdgeSizes { 1444 top: resolve_length_against(child.css_margin[0], container_width), 1445 right: resolve_length_against(child.css_margin[1], container_width), 1446 bottom: resolve_length_against(child.css_margin[2], container_width), 1447 left: resolve_length_against(child.css_margin[3], container_width), 1448 }; 1449 1450 // Resolve padding against containing block width. 1451 child.padding = EdgeSizes { 1452 top: resolve_length_against(child.css_padding[0], container_width), 1453 right: resolve_length_against(child.css_padding[1], container_width), 1454 bottom: resolve_length_against(child.css_padding[2], container_width), 1455 left: resolve_length_against(child.css_padding[3], container_width), 1456 }; 1457 1458 let horiz_extra = 1459 child.border.left + child.border.right + child.padding.left + child.padding.right; 1460 1461 // Resolve content width. 1462 let content_width = match child.css_width { 1463 LengthOrAuto::Length(w) => match child.box_sizing { 1464 BoxSizing::ContentBox => w.max(0.0), 1465 BoxSizing::BorderBox => (w - horiz_extra).max(0.0), 1466 }, 1467 LengthOrAuto::Percentage(p) => { 1468 let resolved = p / 100.0 * container_width; 1469 match child.box_sizing { 1470 BoxSizing::ContentBox => resolved.max(0.0), 1471 BoxSizing::BorderBox => (resolved - horiz_extra).max(0.0), 1472 } 1473 } 1474 LengthOrAuto::Auto => { 1475 // Shrink-to-fit: measure content. 1476 let max_content = measure_float_content_width(child, font); 1477 let available = 1478 (container_width - child.margin.left - child.margin.right - horiz_extra).max(0.0); 1479 max_content.min(available) 1480 } 1481 }; 1482 1483 child.rect.width = content_width; 1484 1485 // Temporary position for layout. 1486 child.rect.x = container_x + child.margin.left + child.border.left + child.padding.left; 1487 child.rect.y = cursor_y + child.margin.top + child.border.top + child.padding.top; 1488 1489 // Layout child content. 1490 if let Some((rw, rh)) = child.replaced_size { 1491 child.rect.width = rw.min(child.rect.width); 1492 child.rect.height = rh; 1493 } else { 1494 match &child.box_type { 1495 BoxType::Block(_) | BoxType::Anonymous => { 1496 if matches!(child.display, Display::Flex | Display::InlineFlex) { 1497 layout_flex_children(child, viewport_width, viewport_height, font, doc, abs_cb); 1498 } else if has_block_children(child) || has_float_children(child) { 1499 layout_block_children( 1500 child, 1501 viewport_width, 1502 viewport_height, 1503 font, 1504 doc, 1505 abs_cb, 1506 ); 1507 } else { 1508 layout_inline_children(child, font, doc, None); 1509 } 1510 } 1511 _ => {} 1512 } 1513 } 1514 1515 // Resolve explicit CSS height. 1516 child.content_height = child.rect.height; 1517 match child.css_height { 1518 LengthOrAuto::Length(h) => { 1519 let vert_extra = 1520 child.border.top + child.border.bottom + child.padding.top + child.padding.bottom; 1521 child.rect.height = match child.box_sizing { 1522 BoxSizing::ContentBox => h.max(0.0), 1523 BoxSizing::BorderBox => (h - vert_extra).max(0.0), 1524 }; 1525 } 1526 LengthOrAuto::Percentage(p) => { 1527 let resolved = p / 100.0 * viewport_height; 1528 let vert_extra = 1529 child.border.top + child.border.bottom + child.padding.top + child.padding.bottom; 1530 child.rect.height = match child.box_sizing { 1531 BoxSizing::ContentBox => resolved.max(0.0), 1532 BoxSizing::BorderBox => (resolved - vert_extra).max(0.0), 1533 }; 1534 } 1535 LengthOrAuto::Auto => {} 1536 } 1537 1538 // Compute the float's margin box dimensions. 1539 let margin_box_width = child.margin.left 1540 + child.border.left 1541 + child.padding.left 1542 + child.rect.width 1543 + child.padding.right 1544 + child.border.right 1545 + child.margin.right; 1546 let margin_box_height = child.margin.top 1547 + child.border.top 1548 + child.padding.top 1549 + child.rect.height 1550 + child.padding.bottom 1551 + child.border.bottom 1552 + child.margin.bottom; 1553 1554 // Place the float. 1555 let (fx, fy) = float_ctx.place_float( 1556 float_side, 1557 margin_box_width, 1558 margin_box_height, 1559 cursor_y, 1560 container_x, 1561 container_width, 1562 ); 1563 1564 // Position the child's content box relative to the placed margin box. 1565 let final_x = fx + child.margin.left + child.border.left + child.padding.left; 1566 let final_y = fy + child.margin.top + child.border.top + child.padding.top; 1567 1568 // Shift the entire box tree from its temporary position to the final one. 1569 let dx = final_x - child.rect.x; 1570 let dy = final_y - child.rect.y; 1571 if dx != 0.0 || dy != 0.0 { 1572 shift_box(child, dx, dy); 1573 } 1574 1575 // Set sticky constraints and handle abspos children. 1576 set_sticky_constraints(child); 1577 layout_abspos_children(child, abs_cb, viewport_width, viewport_height, font, doc); 1578 apply_relative_offset(child, container_width, viewport_height); 1579} 1580 1581/// Measure the max-content width of a float's content (shrink-to-fit). 1582fn measure_float_content_width(b: &LayoutBox, font: &Font) -> f32 { 1583 let mut max_width = 0.0f32; 1584 measure_box_content_width(b, font, &mut max_width); 1585 max_width 1586} 1587 1588// --------------------------------------------------------------------------- 1589// Flex layout 1590// --------------------------------------------------------------------------- 1591 1592/// Measure the intrinsic max-content width of a flex item's content. 1593/// Returns the width needed to lay out content without wrapping. 1594fn measure_flex_item_max_content_width(child: &LayoutBox, font: &Font, _doc: &Document) -> f32 { 1595 // Measure the widest text line from all inline content. 1596 let mut max_width = 0.0f32; 1597 measure_box_content_width(child, font, &mut max_width); 1598 max_width 1599} 1600 1601/// Recursively measure max content width of a layout box tree. 1602fn measure_box_content_width(b: &LayoutBox, font: &Font, max_w: &mut f32) { 1603 match &b.box_type { 1604 BoxType::TextRun { text, .. } => { 1605 let w = measure_text_width(font, text, b.font_size); 1606 if w > *max_w { 1607 *max_w = w; 1608 } 1609 } 1610 _ => { 1611 let horiz = b.border.left + b.border.right + b.padding.left + b.padding.right; 1612 let mut child_max = 0.0f32; 1613 for child in &b.children { 1614 measure_box_content_width(child, font, &mut child_max); 1615 } 1616 let total = child_max + horiz; 1617 if total > *max_w { 1618 *max_w = total; 1619 } 1620 } 1621 } 1622} 1623 1624/// Lay out children according to the CSS Flexbox algorithm (Level 1 §9). 1625fn layout_flex_children( 1626 parent: &mut LayoutBox, 1627 viewport_width: f32, 1628 viewport_height: f32, 1629 font: &Font, 1630 doc: &Document, 1631 abs_cb: Rect, 1632) { 1633 let flex_direction = parent.flex_direction; 1634 let flex_wrap = parent.flex_wrap; 1635 let justify_content = parent.justify_content; 1636 let align_items = parent.align_items; 1637 let align_content = parent.align_content; 1638 let container_main_size = match flex_direction { 1639 FlexDirection::Row | FlexDirection::RowReverse => parent.rect.width, 1640 FlexDirection::Column | FlexDirection::ColumnReverse => { 1641 match parent.css_height { 1642 LengthOrAuto::Length(h) => h, 1643 LengthOrAuto::Percentage(p) => p / 100.0 * viewport_height, 1644 LengthOrAuto::Auto => viewport_height, // fallback 1645 } 1646 } 1647 }; 1648 let container_cross_size = match flex_direction { 1649 FlexDirection::Row | FlexDirection::RowReverse => match parent.css_height { 1650 LengthOrAuto::Length(h) => Some(h), 1651 LengthOrAuto::Percentage(p) => Some(p / 100.0 * viewport_height), 1652 LengthOrAuto::Auto => None, 1653 }, 1654 FlexDirection::Column | FlexDirection::ColumnReverse => Some(parent.rect.width), 1655 }; 1656 1657 let is_row = matches!( 1658 flex_direction, 1659 FlexDirection::Row | FlexDirection::RowReverse 1660 ); 1661 let is_reverse = matches!( 1662 flex_direction, 1663 FlexDirection::RowReverse | FlexDirection::ColumnReverse 1664 ); 1665 1666 // Per CSS Box Alignment §8, row-gap applies between rows and column-gap 1667 // between columns. In a row flex container the main axis is horizontal 1668 // (column-gap between items, row-gap between lines). In a column flex 1669 // container the axes are swapped. 1670 let (main_gap, cross_gap) = if is_row { 1671 (parent.column_gap, parent.row_gap) 1672 } else { 1673 (parent.row_gap, parent.column_gap) 1674 }; 1675 1676 if parent.children.is_empty() { 1677 parent.rect.height = 0.0; 1678 return; 1679 } 1680 1681 // Sort children by `order` (stable sort preserves DOM order for equal values). 1682 let child_count = parent.children.len(); 1683 let mut order_indices: Vec<usize> = (0..child_count).collect(); 1684 order_indices.sort_by_key(|&i| parent.children[i].order); 1685 1686 // Step 1: Determine flex base sizes and hypothetical main sizes. 1687 struct FlexItemInfo { 1688 index: usize, 1689 base_size: f32, 1690 hypothetical_main: f32, 1691 flex_grow: f32, 1692 flex_shrink: f32, 1693 frozen: bool, 1694 target_main: f32, 1695 outer_main: f32, // margin+border+padding on main axis 1696 outer_cross: f32, // margin+border+padding on cross axis 1697 } 1698 1699 let mut items: Vec<FlexItemInfo> = Vec::with_capacity(child_count); 1700 1701 for &idx in &order_indices { 1702 // Skip out-of-flow children (absolute/fixed). 1703 if !is_in_flow(&parent.children[idx]) { 1704 continue; 1705 } 1706 let child = &mut parent.children[idx]; 1707 1708 // Resolve margin/padding percentages for the child. 1709 let cb_width = if is_row { 1710 container_main_size 1711 } else { 1712 container_cross_size.unwrap_or(parent.rect.width) 1713 }; 1714 1715 // Resolve percentage margins and padding against containing block width. 1716 for i in 0..4 { 1717 if matches!(child.css_margin[i], LengthOrAuto::Percentage(_)) { 1718 let resolved = resolve_length_against(child.css_margin[i], cb_width); 1719 match i { 1720 0 => child.margin.top = resolved, 1721 1 => child.margin.right = resolved, 1722 2 => child.margin.bottom = resolved, 1723 3 => child.margin.left = resolved, 1724 _ => {} 1725 } 1726 } 1727 if matches!(child.css_padding[i], LengthOrAuto::Percentage(_)) { 1728 let resolved = resolve_length_against(child.css_padding[i], cb_width); 1729 match i { 1730 0 => child.padding.top = resolved, 1731 1 => child.padding.right = resolved, 1732 2 => child.padding.bottom = resolved, 1733 3 => child.padding.left = resolved, 1734 _ => {} 1735 } 1736 } 1737 } 1738 1739 let margin_main = if is_row { 1740 child.margin.left + child.margin.right 1741 } else { 1742 child.margin.top + child.margin.bottom 1743 }; 1744 let border_padding_main = if is_row { 1745 child.border.left + child.border.right + child.padding.left + child.padding.right 1746 } else { 1747 child.border.top + child.border.bottom + child.padding.top + child.padding.bottom 1748 }; 1749 let margin_cross = if is_row { 1750 child.margin.top + child.margin.bottom 1751 } else { 1752 child.margin.left + child.margin.right 1753 }; 1754 let border_padding_cross = if is_row { 1755 child.border.top + child.border.bottom + child.padding.top + child.padding.bottom 1756 } else { 1757 child.border.left + child.border.right + child.padding.left + child.padding.right 1758 }; 1759 1760 // Determine flex-basis. 1761 let flex_basis = child.flex_basis; 1762 let specified_main = if is_row { 1763 child.css_width 1764 } else { 1765 child.css_height 1766 }; 1767 1768 let base_size = match flex_basis { 1769 LengthOrAuto::Length(px) => px, 1770 LengthOrAuto::Percentage(p) => p / 100.0 * container_main_size, 1771 LengthOrAuto::Auto => { 1772 // Use specified main size if set, otherwise content size. 1773 match specified_main { 1774 LengthOrAuto::Length(px) => px, 1775 LengthOrAuto::Percentage(p) => p / 100.0 * container_main_size, 1776 LengthOrAuto::Auto => { 1777 // Content-based sizing: use max-content size. 1778 if is_row { 1779 measure_flex_item_max_content_width(child, font, doc) 1780 } else { 1781 // For column direction, measure content height. 1782 let avail = container_cross_size.unwrap_or(parent.rect.width); 1783 compute_layout( 1784 child, 1785 0.0, 1786 0.0, 1787 avail, 1788 viewport_width, 1789 viewport_height, 1790 font, 1791 doc, 1792 abs_cb, 1793 None, 1794 ); 1795 child.rect.height 1796 } 1797 } 1798 } 1799 } 1800 }; 1801 1802 let hypothetical_main = base_size.max(0.0); 1803 let outer = margin_main + border_padding_main; 1804 1805 items.push(FlexItemInfo { 1806 index: idx, 1807 base_size, 1808 hypothetical_main, 1809 flex_grow: child.flex_grow, 1810 flex_shrink: child.flex_shrink, 1811 frozen: false, 1812 target_main: hypothetical_main, 1813 outer_main: outer, 1814 outer_cross: margin_cross + border_padding_cross, 1815 }); 1816 } 1817 1818 // Step 2: Collect items into flex lines. 1819 let mut lines: Vec<Vec<usize>> = Vec::new(); // indices into `items` 1820 1821 if flex_wrap == FlexWrap::Nowrap { 1822 // All items on one line. 1823 lines.push((0..items.len()).collect()); 1824 } else { 1825 let mut current_line: Vec<usize> = Vec::new(); 1826 let mut line_main_size = 0.0f32; 1827 1828 for (i, item) in items.iter().enumerate() { 1829 let item_outer = item.hypothetical_main + item.outer_main; 1830 let gap = if current_line.is_empty() { 1831 0.0 1832 } else { 1833 main_gap 1834 }; 1835 1836 if !current_line.is_empty() && line_main_size + gap + item_outer > container_main_size { 1837 lines.push(std::mem::take(&mut current_line)); 1838 line_main_size = 0.0; 1839 } 1840 1841 if !current_line.is_empty() { 1842 line_main_size += main_gap; 1843 } 1844 line_main_size += item_outer; 1845 current_line.push(i); 1846 } 1847 1848 if !current_line.is_empty() { 1849 lines.push(current_line); 1850 } 1851 } 1852 1853 if flex_wrap == FlexWrap::WrapReverse { 1854 lines.reverse(); 1855 } 1856 1857 // Step 3: Resolve flexible lengths for each line. 1858 for line in &lines { 1859 // Total hypothetical main sizes + gaps. 1860 let total_gaps = if line.len() > 1 { 1861 (line.len() - 1) as f32 * main_gap 1862 } else { 1863 0.0 1864 }; 1865 let total_outer_hypo: f32 = line 1866 .iter() 1867 .map(|&i| items[i].hypothetical_main + items[i].outer_main) 1868 .sum(); 1869 let used_space = total_outer_hypo + total_gaps; 1870 let free_space = container_main_size - used_space; 1871 1872 // Reset frozen state. 1873 for &i in line { 1874 items[i].frozen = false; 1875 items[i].target_main = items[i].hypothetical_main; 1876 } 1877 1878 if free_space > 0.0 { 1879 // Grow items. 1880 let total_grow: f32 = line.iter().map(|&i| items[i].flex_grow).sum(); 1881 if total_grow > 0.0 { 1882 for &i in line { 1883 if items[i].flex_grow > 0.0 { 1884 items[i].target_main += free_space * (items[i].flex_grow / total_grow); 1885 } 1886 } 1887 } 1888 } else if free_space < 0.0 { 1889 // Shrink items. 1890 let total_shrink_scaled: f32 = line 1891 .iter() 1892 .map(|&i| items[i].flex_shrink * items[i].base_size) 1893 .sum(); 1894 if total_shrink_scaled > 0.0 { 1895 for &i in line { 1896 let scaled = items[i].flex_shrink * items[i].base_size; 1897 let ratio = scaled / total_shrink_scaled; 1898 items[i].target_main = 1899 (items[i].hypothetical_main + free_space * ratio).max(0.0); 1900 } 1901 } 1902 } 1903 } 1904 1905 // Step 4: Determine cross sizes of items by laying them out at their target main size. 1906 struct LineCrossInfo { 1907 max_cross: f32, 1908 } 1909 1910 let mut line_infos: Vec<LineCrossInfo> = Vec::new(); 1911 1912 for line in &lines { 1913 let mut max_cross = 0.0f32; 1914 1915 for &i in line { 1916 let idx = items[i].index; 1917 let child = &mut parent.children[idx]; 1918 let target_main = items[i].target_main; 1919 1920 // Set up the child for layout at the resolved main size. 1921 if is_row { 1922 child.css_width = LengthOrAuto::Length(target_main); 1923 compute_layout( 1924 child, 1925 0.0, 1926 0.0, 1927 target_main, 1928 viewport_width, 1929 viewport_height, 1930 font, 1931 doc, 1932 abs_cb, 1933 None, 1934 ); 1935 let cross = child.rect.height + items[i].outer_cross; 1936 if cross > max_cross { 1937 max_cross = cross; 1938 } 1939 } else { 1940 let avail = container_cross_size.unwrap_or(parent.rect.width); 1941 compute_layout( 1942 child, 1943 0.0, 1944 0.0, 1945 avail, 1946 viewport_width, 1947 viewport_height, 1948 font, 1949 doc, 1950 abs_cb, 1951 None, 1952 ); 1953 child.rect.height = target_main; 1954 let cross = child.rect.width 1955 + child.border.left 1956 + child.border.right 1957 + child.padding.left 1958 + child.padding.right 1959 + child.margin.left 1960 + child.margin.right; 1961 if cross > max_cross { 1962 max_cross = cross; 1963 } 1964 } 1965 } 1966 1967 line_infos.push(LineCrossInfo { max_cross }); 1968 } 1969 1970 // Per CSS Flexbox §9.4: If the flex container is single-line and has a 1971 // definite cross size, the cross size of the flex line is the container's 1972 // cross size (clamped to min/max). This ensures alignment works relative 1973 // to the full container. 1974 if lines.len() == 1 { 1975 if let Some(cs) = container_cross_size { 1976 if line_infos[0].max_cross < cs { 1977 line_infos[0].max_cross = cs; 1978 } 1979 } 1980 } 1981 1982 // Step 5: Handle align-items: stretch — stretch items to fill the line cross size. 1983 for (line_idx, line) in lines.iter().enumerate() { 1984 let line_cross = line_infos[line_idx].max_cross; 1985 1986 for &i in line { 1987 let idx = items[i].index; 1988 let child = &mut parent.children[idx]; 1989 1990 let effective_align = match child.align_self { 1991 AlignSelf::Auto => align_items, 1992 AlignSelf::FlexStart => AlignItems::FlexStart, 1993 AlignSelf::FlexEnd => AlignItems::FlexEnd, 1994 AlignSelf::Center => AlignItems::Center, 1995 AlignSelf::Baseline => AlignItems::Baseline, 1996 AlignSelf::Stretch => AlignItems::Stretch, 1997 }; 1998 1999 if effective_align == AlignItems::Stretch { 2000 let item_cross_space = line_cross - items[i].outer_cross; 2001 if is_row { 2002 if matches!(child.css_height, LengthOrAuto::Auto) { 2003 child.rect.height = item_cross_space.max(0.0); 2004 } 2005 } else if matches!(child.css_width, LengthOrAuto::Auto) { 2006 child.rect.width = item_cross_space.max(0.0); 2007 } 2008 } 2009 } 2010 } 2011 2012 // Step 6: Position items on main and cross axes. 2013 let total_cross_gaps = if lines.len() > 1 { 2014 (lines.len() - 1) as f32 * cross_gap 2015 } else { 2016 0.0 2017 }; 2018 let total_line_cross: f32 = line_infos.iter().map(|li| li.max_cross).sum(); 2019 let total_cross_used = total_line_cross + total_cross_gaps; 2020 2021 // For definite cross size containers, compute align-content offsets. 2022 let cross_free_space = container_cross_size 2023 .map(|cs| (cs - total_cross_used).max(0.0)) 2024 .unwrap_or(0.0); 2025 2026 let (ac_initial_offset, ac_between_offset) = compute_content_distribution( 2027 align_content_to_justify(align_content), 2028 cross_free_space, 2029 lines.len(), 2030 ); 2031 2032 let mut cross_cursor = if is_row { parent.rect.y } else { parent.rect.x } + ac_initial_offset; 2033 2034 for (line_idx, line) in lines.iter().enumerate() { 2035 let line_cross = line_infos[line_idx].max_cross; 2036 2037 // Main-axis justification. 2038 let total_main_gaps = if line.len() > 1 { 2039 (line.len() - 1) as f32 * main_gap 2040 } else { 2041 0.0 2042 }; 2043 let items_main: f32 = line 2044 .iter() 2045 .map(|&i| items[i].target_main + items[i].outer_main) 2046 .sum(); 2047 let main_free = (container_main_size - items_main - total_main_gaps).max(0.0); 2048 2049 let (jc_initial, jc_between) = 2050 compute_content_distribution(justify_content, main_free, line.len()); 2051 2052 // Determine the starting main position. 2053 let mut main_cursor = if is_row { parent.rect.x } else { parent.rect.y }; 2054 2055 if is_reverse { 2056 // For reverse directions, start from the end. 2057 main_cursor += container_main_size; 2058 } 2059 2060 main_cursor += if is_reverse { -jc_initial } else { jc_initial }; 2061 2062 let line_items: Vec<usize> = if is_reverse { 2063 line.iter().rev().copied().collect() 2064 } else { 2065 line.to_vec() 2066 }; 2067 2068 for (item_pos, &i) in line_items.iter().enumerate() { 2069 let idx = items[i].index; 2070 let child = &mut parent.children[idx]; 2071 let target_main = items[i].target_main; 2072 2073 // Main-axis position. 2074 let child_main_margin_start = if is_row { 2075 child.margin.left 2076 } else { 2077 child.margin.top 2078 }; 2079 let child_main_margin_end = if is_row { 2080 child.margin.right 2081 } else { 2082 child.margin.bottom 2083 }; 2084 let child_main_bp_start = if is_row { 2085 child.border.left + child.padding.left 2086 } else { 2087 child.border.top + child.padding.top 2088 }; 2089 let child_main_bp_end = if is_row { 2090 child.border.right + child.padding.right 2091 } else { 2092 child.border.bottom + child.padding.bottom 2093 }; 2094 2095 let outer_size = child_main_margin_start 2096 + child_main_bp_start 2097 + target_main 2098 + child_main_bp_end 2099 + child_main_margin_end; 2100 2101 if is_reverse { 2102 main_cursor -= outer_size; 2103 } 2104 2105 let content_main = main_cursor + child_main_margin_start + child_main_bp_start; 2106 2107 // Cross-axis position. 2108 let child_cross_margin_start = if is_row { 2109 child.margin.top 2110 } else { 2111 child.margin.left 2112 }; 2113 let child_cross_bp_start = if is_row { 2114 child.border.top + child.padding.top 2115 } else { 2116 child.border.left + child.padding.left 2117 }; 2118 let child_cross_total = if is_row { 2119 child.rect.height + items[i].outer_cross 2120 } else { 2121 child.rect.width + items[i].outer_cross 2122 }; 2123 2124 let effective_align = match child.align_self { 2125 AlignSelf::Auto => align_items, 2126 AlignSelf::FlexStart => AlignItems::FlexStart, 2127 AlignSelf::FlexEnd => AlignItems::FlexEnd, 2128 AlignSelf::Center => AlignItems::Center, 2129 AlignSelf::Baseline => AlignItems::Baseline, 2130 AlignSelf::Stretch => AlignItems::Stretch, 2131 }; 2132 2133 let cross_offset = match effective_align { 2134 AlignItems::FlexStart | AlignItems::Stretch | AlignItems::Baseline => 0.0, 2135 AlignItems::FlexEnd => line_cross - child_cross_total, 2136 AlignItems::Center => (line_cross - child_cross_total) / 2.0, 2137 }; 2138 2139 let content_cross = 2140 cross_cursor + cross_offset + child_cross_margin_start + child_cross_bp_start; 2141 2142 // Set child position. 2143 if is_row { 2144 child.rect.x = content_main; 2145 child.rect.y = content_cross; 2146 child.rect.width = target_main; 2147 } else { 2148 child.rect.x = content_cross; 2149 child.rect.y = content_main; 2150 child.rect.height = target_main; 2151 } 2152 2153 // Shift any text lines to match new position. 2154 reposition_lines(child, font, doc); 2155 2156 if !is_reverse { 2157 main_cursor += outer_size; 2158 } 2159 2160 // Add gap between items (not after last). 2161 if item_pos < line_items.len() - 1 { 2162 if is_reverse { 2163 main_cursor -= main_gap; 2164 } else { 2165 main_cursor += main_gap; 2166 } 2167 } 2168 2169 // Also add justify-content between spacing. 2170 if item_pos < line_items.len() - 1 { 2171 if is_reverse { 2172 main_cursor -= jc_between; 2173 } else { 2174 main_cursor += jc_between; 2175 } 2176 } 2177 } 2178 2179 cross_cursor += line_cross + cross_gap + ac_between_offset; 2180 } 2181 2182 // Set parent height based on children. 2183 if is_row { 2184 if matches!(parent.css_height, LengthOrAuto::Auto) { 2185 parent.rect.height = total_cross_used; 2186 } 2187 } else if matches!(parent.css_height, LengthOrAuto::Auto) { 2188 // For column flex, height is the main axis. 2189 let total_main: f32 = lines 2190 .iter() 2191 .map(|line| { 2192 let items_main: f32 = line 2193 .iter() 2194 .map(|&i| items[i].target_main + items[i].outer_main) 2195 .sum(); 2196 let gaps = if line.len() > 1 { 2197 (line.len() - 1) as f32 * main_gap 2198 } else { 2199 0.0 2200 }; 2201 items_main + gaps 2202 }) 2203 .fold(0.0f32, f32::max); 2204 parent.rect.height = total_main; 2205 } 2206} 2207 2208/// Reposition inline text lines inside a layout box after moving it. 2209/// Re-runs inline layout if the box has inline children. 2210fn reposition_lines(b: &mut LayoutBox, font: &Font, doc: &Document) { 2211 if !b.lines.is_empty() { 2212 // Re-run inline layout at the new position. 2213 b.lines.clear(); 2214 layout_inline_children(b, font, doc, None); 2215 } 2216 // Recursively reposition children that have their own inline content. 2217 for child in &mut b.children { 2218 if !child.lines.is_empty() { 2219 child.lines.clear(); 2220 layout_inline_children(child, font, doc, None); 2221 } 2222 } 2223} 2224 2225/// Convert AlignContent to JustifyContent for reuse of distribution logic. 2226fn align_content_to_justify(ac: AlignContent) -> JustifyContent { 2227 match ac { 2228 AlignContent::FlexStart | AlignContent::Stretch => JustifyContent::FlexStart, 2229 AlignContent::FlexEnd => JustifyContent::FlexEnd, 2230 AlignContent::Center => JustifyContent::Center, 2231 AlignContent::SpaceBetween => JustifyContent::SpaceBetween, 2232 AlignContent::SpaceAround => JustifyContent::SpaceAround, 2233 } 2234} 2235 2236/// Compute the initial offset and between-item spacing for content distribution. 2237/// Returns (initial_offset, between_spacing). 2238fn compute_content_distribution( 2239 mode: JustifyContent, 2240 free_space: f32, 2241 item_count: usize, 2242) -> (f32, f32) { 2243 if item_count == 0 { 2244 return (0.0, 0.0); 2245 } 2246 match mode { 2247 JustifyContent::FlexStart => (0.0, 0.0), 2248 JustifyContent::FlexEnd => (free_space, 0.0), 2249 JustifyContent::Center => (free_space / 2.0, 0.0), 2250 JustifyContent::SpaceBetween => { 2251 if item_count <= 1 { 2252 (0.0, 0.0) 2253 } else { 2254 (0.0, free_space / (item_count - 1) as f32) 2255 } 2256 } 2257 JustifyContent::SpaceAround => { 2258 let per_item = free_space / item_count as f32; 2259 (per_item / 2.0, per_item) 2260 } 2261 JustifyContent::SpaceEvenly => { 2262 let spacing = free_space / (item_count + 1) as f32; 2263 (spacing, spacing) 2264 } 2265 } 2266} 2267 2268// --------------------------------------------------------------------------- 2269// Inline formatting context 2270// --------------------------------------------------------------------------- 2271 2272/// An inline item produced by flattening the inline tree. 2273enum InlineItemKind { 2274 /// A word of text with associated styling. 2275 Word { 2276 text: String, 2277 font_size: f32, 2278 color: Color, 2279 text_decoration: TextDecoration, 2280 background_color: Color, 2281 }, 2282 /// Whitespace between words. 2283 Space { font_size: f32 }, 2284 /// Forced line break (`<br>`). 2285 ForcedBreak, 2286 /// Start of an inline box (for margin/padding/border tracking). 2287 InlineStart { 2288 margin_left: f32, 2289 padding_left: f32, 2290 border_left: f32, 2291 }, 2292 /// End of an inline box. 2293 InlineEnd { 2294 margin_right: f32, 2295 padding_right: f32, 2296 border_right: f32, 2297 }, 2298} 2299 2300/// A pending fragment on the current line. 2301struct PendingFragment { 2302 text: String, 2303 x: f32, 2304 width: f32, 2305 font_size: f32, 2306 color: Color, 2307 text_decoration: TextDecoration, 2308 background_color: Color, 2309} 2310 2311/// Flatten the inline children tree into a sequence of items. 2312fn flatten_inline_tree(children: &[LayoutBox], doc: &Document, items: &mut Vec<InlineItemKind>) { 2313 for child in children { 2314 // Skip out-of-flow children (absolute/fixed positioned). 2315 if !is_in_flow(child) { 2316 continue; 2317 } 2318 match &child.box_type { 2319 BoxType::TextRun { text, .. } => { 2320 let words = split_into_words(text); 2321 for segment in words { 2322 match segment { 2323 WordSegment::Word(w) => { 2324 items.push(InlineItemKind::Word { 2325 text: w, 2326 font_size: child.font_size, 2327 color: child.color, 2328 text_decoration: child.text_decoration, 2329 background_color: child.background_color, 2330 }); 2331 } 2332 WordSegment::Space => { 2333 items.push(InlineItemKind::Space { 2334 font_size: child.font_size, 2335 }); 2336 } 2337 } 2338 } 2339 } 2340 BoxType::Inline(node_id) => { 2341 if let NodeData::Element { tag_name, .. } = doc.node_data(*node_id) { 2342 if tag_name == "br" { 2343 items.push(InlineItemKind::ForcedBreak); 2344 continue; 2345 } 2346 } 2347 2348 items.push(InlineItemKind::InlineStart { 2349 margin_left: child.margin.left, 2350 padding_left: child.padding.left, 2351 border_left: child.border.left, 2352 }); 2353 2354 flatten_inline_tree(&child.children, doc, items); 2355 2356 items.push(InlineItemKind::InlineEnd { 2357 margin_right: child.margin.right, 2358 padding_right: child.padding.right, 2359 border_right: child.border.right, 2360 }); 2361 } 2362 _ => {} 2363 } 2364 } 2365} 2366 2367enum WordSegment { 2368 Word(String), 2369 Space, 2370} 2371 2372/// Split text into alternating words and spaces. 2373fn split_into_words(text: &str) -> Vec<WordSegment> { 2374 let mut segments = Vec::new(); 2375 let mut current_word = String::new(); 2376 2377 for ch in text.chars() { 2378 if ch == ' ' { 2379 if !current_word.is_empty() { 2380 segments.push(WordSegment::Word(std::mem::take(&mut current_word))); 2381 } 2382 segments.push(WordSegment::Space); 2383 } else { 2384 current_word.push(ch); 2385 } 2386 } 2387 2388 if !current_word.is_empty() { 2389 segments.push(WordSegment::Word(current_word)); 2390 } 2391 2392 segments 2393} 2394 2395/// Lay out inline children using a proper inline formatting context. 2396/// 2397/// If `float_ctx` is provided, line boxes are shortened to avoid overlapping 2398/// with active floats from the parent block formatting context. 2399fn layout_inline_children( 2400 parent: &mut LayoutBox, 2401 font: &Font, 2402 doc: &Document, 2403 float_ctx: Option<&FloatContext>, 2404) { 2405 let available_width = parent.rect.width; 2406 let text_align = parent.text_align; 2407 let line_height = parent.line_height; 2408 2409 let mut items = Vec::new(); 2410 flatten_inline_tree(&parent.children, doc, &mut items); 2411 2412 if items.is_empty() { 2413 parent.rect.height = 0.0; 2414 return; 2415 } 2416 2417 // Build line boxes, respecting float-narrowed available widths. 2418 let mut all_lines: Vec<(Vec<PendingFragment>, f32, f32)> = Vec::new(); // (fragments, line_left_offset, line_available_width) 2419 let mut current_line: Vec<PendingFragment> = Vec::new(); 2420 let mut cursor_x: f32 = 0.0; 2421 let mut line_y = parent.rect.y; 2422 2423 // Compute the available width for the current line, narrowed by floats. 2424 let line_avail = |y: f32| -> (f32, f32) { 2425 if let Some(fctx) = float_ctx { 2426 let (left, right) = 2427 fctx.available_range(y, line_height, parent.rect.x, available_width); 2428 let offset = (left - parent.rect.x).max(0.0); 2429 let width = (right - left).max(0.0); 2430 (offset, width) 2431 } else { 2432 (0.0, available_width) 2433 } 2434 }; 2435 2436 let (mut current_line_offset, mut current_line_width) = line_avail(line_y); 2437 2438 for item in &items { 2439 match item { 2440 InlineItemKind::Word { 2441 text, 2442 font_size, 2443 color, 2444 text_decoration, 2445 background_color, 2446 } => { 2447 let word_width = measure_text_width(font, text, *font_size); 2448 2449 // If this word doesn't fit and the line isn't empty, break. 2450 if cursor_x > 0.0 && cursor_x + word_width > current_line_width { 2451 all_lines.push(( 2452 std::mem::take(&mut current_line), 2453 current_line_offset, 2454 current_line_width, 2455 )); 2456 line_y += line_height; 2457 let (off, w) = line_avail(line_y); 2458 current_line_offset = off; 2459 current_line_width = w; 2460 cursor_x = 0.0; 2461 } 2462 2463 current_line.push(PendingFragment { 2464 text: text.clone(), 2465 x: cursor_x, 2466 width: word_width, 2467 font_size: *font_size, 2468 color: *color, 2469 text_decoration: *text_decoration, 2470 background_color: *background_color, 2471 }); 2472 cursor_x += word_width; 2473 } 2474 InlineItemKind::Space { font_size } => { 2475 // Only add space if we have content on the line. 2476 if !current_line.is_empty() { 2477 let space_width = measure_text_width(font, " ", *font_size); 2478 if cursor_x + space_width <= current_line_width { 2479 cursor_x += space_width; 2480 } 2481 } 2482 } 2483 InlineItemKind::ForcedBreak => { 2484 all_lines.push(( 2485 std::mem::take(&mut current_line), 2486 current_line_offset, 2487 current_line_width, 2488 )); 2489 line_y += line_height; 2490 let (off, w) = line_avail(line_y); 2491 current_line_offset = off; 2492 current_line_width = w; 2493 cursor_x = 0.0; 2494 } 2495 InlineItemKind::InlineStart { 2496 margin_left, 2497 padding_left, 2498 border_left, 2499 } => { 2500 cursor_x += margin_left + padding_left + border_left; 2501 } 2502 InlineItemKind::InlineEnd { 2503 margin_right, 2504 padding_right, 2505 border_right, 2506 } => { 2507 cursor_x += margin_right + padding_right + border_right; 2508 } 2509 } 2510 } 2511 2512 // Flush the last line. 2513 if !current_line.is_empty() { 2514 all_lines.push((current_line, current_line_offset, current_line_width)); 2515 } 2516 2517 if all_lines.is_empty() { 2518 parent.rect.height = 0.0; 2519 return; 2520 } 2521 2522 // Position lines vertically and apply text-align. 2523 let mut text_lines = Vec::new(); 2524 let mut y = parent.rect.y; 2525 let num_lines = all_lines.len(); 2526 2527 for (line_idx, (line_fragments, line_offset, line_avail_w)) in all_lines.iter().enumerate() { 2528 if line_fragments.is_empty() { 2529 y += line_height; 2530 continue; 2531 } 2532 2533 // Compute line width from last fragment. 2534 let line_width = match line_fragments.last() { 2535 Some(last) => last.x + last.width, 2536 None => 0.0, 2537 }; 2538 2539 // Compute text-align offset. 2540 let is_last_line = line_idx == num_lines - 1; 2541 let align_offset = 2542 compute_align_offset(text_align, *line_avail_w, line_width, is_last_line); 2543 2544 for frag in line_fragments { 2545 text_lines.push(TextLine { 2546 text: frag.text.clone(), 2547 x: parent.rect.x + line_offset + frag.x + align_offset, 2548 y, 2549 width: frag.width, 2550 font_size: frag.font_size, 2551 color: frag.color, 2552 text_decoration: frag.text_decoration, 2553 background_color: frag.background_color, 2554 }); 2555 } 2556 2557 y += line_height; 2558 } 2559 2560 parent.rect.height = num_lines as f32 * line_height; 2561 parent.lines = text_lines; 2562} 2563 2564/// Compute the horizontal offset for text alignment. 2565fn compute_align_offset( 2566 align: TextAlign, 2567 available_width: f32, 2568 line_width: f32, 2569 is_last_line: bool, 2570) -> f32 { 2571 let extra_space = (available_width - line_width).max(0.0); 2572 match align { 2573 TextAlign::Left => 0.0, 2574 TextAlign::Center => extra_space / 2.0, 2575 TextAlign::Right => extra_space, 2576 TextAlign::Justify => { 2577 // Don't justify the last line (CSS spec behavior). 2578 if is_last_line { 2579 0.0 2580 } else { 2581 // For justify, we shift the whole line by 0 — the actual distribution 2582 // of space between words would need per-word spacing. For now, treat 2583 // as left-aligned; full justify support is a future enhancement. 2584 0.0 2585 } 2586 } 2587 } 2588} 2589 2590// --------------------------------------------------------------------------- 2591// Text measurement 2592// --------------------------------------------------------------------------- 2593 2594/// Measure the total advance width of a text string at the given font size. 2595fn measure_text_width(font: &Font, text: &str, font_size: f32) -> f32 { 2596 let shaped = font.shape_text(text, font_size); 2597 match shaped.last() { 2598 Some(last) => last.x_offset + last.x_advance, 2599 None => 0.0, 2600 } 2601} 2602 2603// --------------------------------------------------------------------------- 2604// Public API 2605// --------------------------------------------------------------------------- 2606 2607/// Build and lay out from a styled tree (produced by `resolve_styles`). 2608/// 2609/// Returns a `LayoutTree` with positioned boxes ready for rendering. 2610pub fn layout( 2611 styled_root: &StyledNode, 2612 doc: &Document, 2613 viewport_width: f32, 2614 viewport_height: f32, 2615 font: &Font, 2616 image_sizes: &HashMap<NodeId, (f32, f32)>, 2617) -> LayoutTree { 2618 let mut root = match build_box(styled_root, doc, image_sizes) { 2619 Some(b) => b, 2620 None => { 2621 return LayoutTree { 2622 root: LayoutBox::new(BoxType::Anonymous, &ComputedStyle::default()), 2623 width: viewport_width, 2624 height: 0.0, 2625 }; 2626 } 2627 }; 2628 2629 // Pre-collapse parent-child margins before positioning. 2630 pre_collapse_margins(&mut root); 2631 2632 // The initial containing block for absolutely positioned elements is the viewport. 2633 let viewport_cb = Rect { 2634 x: 0.0, 2635 y: 0.0, 2636 width: viewport_width, 2637 height: viewport_height, 2638 }; 2639 compute_layout( 2640 &mut root, 2641 0.0, 2642 0.0, 2643 viewport_width, 2644 viewport_width, 2645 viewport_height, 2646 font, 2647 doc, 2648 viewport_cb, 2649 None, 2650 ); 2651 2652 let height = root.margin_box_height(); 2653 LayoutTree { 2654 root, 2655 width: viewport_width, 2656 height, 2657 } 2658} 2659 2660#[cfg(test)] 2661mod tests { 2662 use super::*; 2663 use we_dom::Document; 2664 use we_style::computed::{extract_stylesheets, resolve_styles}; 2665 2666 fn test_font() -> Font { 2667 let paths = [ 2668 "/System/Library/Fonts/Geneva.ttf", 2669 "/System/Library/Fonts/Monaco.ttf", 2670 ]; 2671 for path in &paths { 2672 let p = std::path::Path::new(path); 2673 if p.exists() { 2674 return Font::from_file(p).expect("failed to parse font"); 2675 } 2676 } 2677 panic!("no test font found"); 2678 } 2679 2680 fn layout_doc(doc: &Document) -> LayoutTree { 2681 let font = test_font(); 2682 let sheets = extract_stylesheets(doc); 2683 let styled = resolve_styles(doc, &sheets, (800.0, 600.0)).unwrap(); 2684 layout(&styled, doc, 800.0, 600.0, &font, &HashMap::new()) 2685 } 2686 2687 #[test] 2688 fn empty_document() { 2689 let doc = Document::new(); 2690 let font = test_font(); 2691 let sheets = extract_stylesheets(&doc); 2692 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)); 2693 if let Some(styled) = styled { 2694 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2695 assert_eq!(tree.width, 800.0); 2696 } 2697 } 2698 2699 #[test] 2700 fn single_paragraph() { 2701 let mut doc = Document::new(); 2702 let root = doc.root(); 2703 let html = doc.create_element("html"); 2704 let body = doc.create_element("body"); 2705 let p = doc.create_element("p"); 2706 let text = doc.create_text("Hello world"); 2707 doc.append_child(root, html); 2708 doc.append_child(html, body); 2709 doc.append_child(body, p); 2710 doc.append_child(p, text); 2711 2712 let tree = layout_doc(&doc); 2713 2714 assert!(matches!(tree.root.box_type, BoxType::Block(_))); 2715 2716 let body_box = &tree.root.children[0]; 2717 assert!(matches!(body_box.box_type, BoxType::Block(_))); 2718 2719 let p_box = &body_box.children[0]; 2720 assert!(matches!(p_box.box_type, BoxType::Block(_))); 2721 2722 assert!(!p_box.lines.is_empty(), "p should have text fragments"); 2723 2724 // Collect all text on the first visual line. 2725 let first_y = p_box.lines[0].y; 2726 let line_text: String = p_box 2727 .lines 2728 .iter() 2729 .filter(|l| (l.y - first_y).abs() < 0.01) 2730 .map(|l| l.text.as_str()) 2731 .collect::<Vec<_>>() 2732 .join(" "); 2733 assert!( 2734 line_text.contains("Hello") && line_text.contains("world"), 2735 "line should contain Hello and world, got: {line_text}" 2736 ); 2737 2738 assert_eq!(p_box.margin.top, 16.0); 2739 assert_eq!(p_box.margin.bottom, 16.0); 2740 } 2741 2742 #[test] 2743 fn blocks_stack_vertically() { 2744 let mut doc = Document::new(); 2745 let root = doc.root(); 2746 let html = doc.create_element("html"); 2747 let body = doc.create_element("body"); 2748 let p1 = doc.create_element("p"); 2749 let t1 = doc.create_text("First"); 2750 let p2 = doc.create_element("p"); 2751 let t2 = doc.create_text("Second"); 2752 doc.append_child(root, html); 2753 doc.append_child(html, body); 2754 doc.append_child(body, p1); 2755 doc.append_child(p1, t1); 2756 doc.append_child(body, p2); 2757 doc.append_child(p2, t2); 2758 2759 let tree = layout_doc(&doc); 2760 let body_box = &tree.root.children[0]; 2761 let first = &body_box.children[0]; 2762 let second = &body_box.children[1]; 2763 2764 assert!( 2765 second.rect.y > first.rect.y, 2766 "second p (y={}) should be below first p (y={})", 2767 second.rect.y, 2768 first.rect.y 2769 ); 2770 } 2771 2772 #[test] 2773 fn heading_larger_than_body() { 2774 let mut doc = Document::new(); 2775 let root = doc.root(); 2776 let html = doc.create_element("html"); 2777 let body = doc.create_element("body"); 2778 let h1 = doc.create_element("h1"); 2779 let h1_text = doc.create_text("Title"); 2780 let p = doc.create_element("p"); 2781 let p_text = doc.create_text("Text"); 2782 doc.append_child(root, html); 2783 doc.append_child(html, body); 2784 doc.append_child(body, h1); 2785 doc.append_child(h1, h1_text); 2786 doc.append_child(body, p); 2787 doc.append_child(p, p_text); 2788 2789 let tree = layout_doc(&doc); 2790 let body_box = &tree.root.children[0]; 2791 let h1_box = &body_box.children[0]; 2792 let p_box = &body_box.children[1]; 2793 2794 assert!( 2795 h1_box.font_size > p_box.font_size, 2796 "h1 font_size ({}) should be > p font_size ({})", 2797 h1_box.font_size, 2798 p_box.font_size 2799 ); 2800 assert_eq!(h1_box.font_size, 32.0); 2801 2802 assert!( 2803 h1_box.rect.height > p_box.rect.height, 2804 "h1 height ({}) should be > p height ({})", 2805 h1_box.rect.height, 2806 p_box.rect.height 2807 ); 2808 } 2809 2810 #[test] 2811 fn body_has_default_margin() { 2812 let mut doc = Document::new(); 2813 let root = doc.root(); 2814 let html = doc.create_element("html"); 2815 let body = doc.create_element("body"); 2816 let p = doc.create_element("p"); 2817 let text = doc.create_text("Test"); 2818 doc.append_child(root, html); 2819 doc.append_child(html, body); 2820 doc.append_child(body, p); 2821 doc.append_child(p, text); 2822 2823 let tree = layout_doc(&doc); 2824 let body_box = &tree.root.children[0]; 2825 2826 // body default margin is 8px, but it collapses with p's 16px margin 2827 // (parent-child collapsing: no border/padding on body). 2828 assert_eq!(body_box.margin.top, 16.0); 2829 assert_eq!(body_box.margin.right, 8.0); 2830 assert_eq!(body_box.margin.bottom, 16.0); 2831 assert_eq!(body_box.margin.left, 8.0); 2832 2833 assert_eq!(body_box.rect.x, 8.0); 2834 // body.rect.y = collapsed margin (16) from viewport top. 2835 assert_eq!(body_box.rect.y, 16.0); 2836 } 2837 2838 #[test] 2839 fn text_wraps_at_container_width() { 2840 let mut doc = Document::new(); 2841 let root = doc.root(); 2842 let html = doc.create_element("html"); 2843 let body = doc.create_element("body"); 2844 let p = doc.create_element("p"); 2845 let text = 2846 doc.create_text("The quick brown fox jumps over the lazy dog and more words to wrap"); 2847 doc.append_child(root, html); 2848 doc.append_child(html, body); 2849 doc.append_child(body, p); 2850 doc.append_child(p, text); 2851 2852 let font = test_font(); 2853 let sheets = extract_stylesheets(&doc); 2854 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 2855 let tree = layout(&styled, &doc, 100.0, 600.0, &font, &HashMap::new()); 2856 let body_box = &tree.root.children[0]; 2857 let p_box = &body_box.children[0]; 2858 2859 // Count distinct y-positions to count visual lines. 2860 let mut ys: Vec<f32> = p_box.lines.iter().map(|l| l.y).collect(); 2861 ys.sort_by(|a, b| a.partial_cmp(b).unwrap()); 2862 ys.dedup_by(|a, b| (*a - *b).abs() < 0.01); 2863 2864 assert!( 2865 ys.len() > 1, 2866 "text should wrap to multiple lines, got {} visual lines", 2867 ys.len() 2868 ); 2869 } 2870 2871 #[test] 2872 fn layout_produces_positive_dimensions() { 2873 let mut doc = Document::new(); 2874 let root = doc.root(); 2875 let html = doc.create_element("html"); 2876 let body = doc.create_element("body"); 2877 let div = doc.create_element("div"); 2878 let text = doc.create_text("Content"); 2879 doc.append_child(root, html); 2880 doc.append_child(html, body); 2881 doc.append_child(body, div); 2882 doc.append_child(div, text); 2883 2884 let tree = layout_doc(&doc); 2885 2886 for b in tree.iter() { 2887 assert!(b.rect.width >= 0.0, "width should be >= 0"); 2888 assert!(b.rect.height >= 0.0, "height should be >= 0"); 2889 } 2890 2891 assert!(tree.height > 0.0, "layout height should be > 0"); 2892 } 2893 2894 #[test] 2895 fn head_is_hidden() { 2896 let mut doc = Document::new(); 2897 let root = doc.root(); 2898 let html = doc.create_element("html"); 2899 let head = doc.create_element("head"); 2900 let title = doc.create_element("title"); 2901 let title_text = doc.create_text("Page Title"); 2902 let body = doc.create_element("body"); 2903 let p = doc.create_element("p"); 2904 let p_text = doc.create_text("Visible"); 2905 doc.append_child(root, html); 2906 doc.append_child(html, head); 2907 doc.append_child(head, title); 2908 doc.append_child(title, title_text); 2909 doc.append_child(html, body); 2910 doc.append_child(body, p); 2911 doc.append_child(p, p_text); 2912 2913 let tree = layout_doc(&doc); 2914 2915 assert_eq!( 2916 tree.root.children.len(), 2917 1, 2918 "html should have 1 child (body), head should be hidden" 2919 ); 2920 } 2921 2922 #[test] 2923 fn mixed_block_and_inline() { 2924 let mut doc = Document::new(); 2925 let root = doc.root(); 2926 let html = doc.create_element("html"); 2927 let body = doc.create_element("body"); 2928 let div = doc.create_element("div"); 2929 let text1 = doc.create_text("Text"); 2930 let p = doc.create_element("p"); 2931 let p_text = doc.create_text("Block"); 2932 let text2 = doc.create_text("More"); 2933 doc.append_child(root, html); 2934 doc.append_child(html, body); 2935 doc.append_child(body, div); 2936 doc.append_child(div, text1); 2937 doc.append_child(div, p); 2938 doc.append_child(p, p_text); 2939 doc.append_child(div, text2); 2940 2941 let tree = layout_doc(&doc); 2942 let body_box = &tree.root.children[0]; 2943 let div_box = &body_box.children[0]; 2944 2945 assert_eq!( 2946 div_box.children.len(), 2947 3, 2948 "div should have 3 children (anon, block, anon), got {}", 2949 div_box.children.len() 2950 ); 2951 2952 assert!(matches!(div_box.children[0].box_type, BoxType::Anonymous)); 2953 assert!(matches!(div_box.children[1].box_type, BoxType::Block(_))); 2954 assert!(matches!(div_box.children[2].box_type, BoxType::Anonymous)); 2955 } 2956 2957 #[test] 2958 fn inline_elements_contribute_text() { 2959 let mut doc = Document::new(); 2960 let root = doc.root(); 2961 let html = doc.create_element("html"); 2962 let body = doc.create_element("body"); 2963 let p = doc.create_element("p"); 2964 let t1 = doc.create_text("Hello "); 2965 let em = doc.create_element("em"); 2966 let t2 = doc.create_text("world"); 2967 let t3 = doc.create_text("!"); 2968 doc.append_child(root, html); 2969 doc.append_child(html, body); 2970 doc.append_child(body, p); 2971 doc.append_child(p, t1); 2972 doc.append_child(p, em); 2973 doc.append_child(em, t2); 2974 doc.append_child(p, t3); 2975 2976 let tree = layout_doc(&doc); 2977 let body_box = &tree.root.children[0]; 2978 let p_box = &body_box.children[0]; 2979 2980 assert!(!p_box.lines.is_empty()); 2981 2982 let first_y = p_box.lines[0].y; 2983 let line_texts: Vec<&str> = p_box 2984 .lines 2985 .iter() 2986 .filter(|l| (l.y - first_y).abs() < 0.01) 2987 .map(|l| l.text.as_str()) 2988 .collect(); 2989 let combined = line_texts.join(""); 2990 assert!( 2991 combined.contains("Hello") && combined.contains("world") && combined.contains("!"), 2992 "line should contain all text, got: {combined}" 2993 ); 2994 } 2995 2996 #[test] 2997 fn collapse_whitespace_works() { 2998 assert_eq!(collapse_whitespace("hello world"), "hello world"); 2999 assert_eq!(collapse_whitespace(" spaces "), " spaces "); 3000 assert_eq!(collapse_whitespace("\n\ttabs\n"), " tabs "); 3001 assert_eq!(collapse_whitespace("no-extra"), "no-extra"); 3002 assert_eq!(collapse_whitespace(" "), " "); 3003 } 3004 3005 #[test] 3006 fn content_width_respects_body_margin() { 3007 let mut doc = Document::new(); 3008 let root = doc.root(); 3009 let html = doc.create_element("html"); 3010 let body = doc.create_element("body"); 3011 let div = doc.create_element("div"); 3012 let text = doc.create_text("Content"); 3013 doc.append_child(root, html); 3014 doc.append_child(html, body); 3015 doc.append_child(body, div); 3016 doc.append_child(div, text); 3017 3018 let font = test_font(); 3019 let sheets = extract_stylesheets(&doc); 3020 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3021 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3022 let body_box = &tree.root.children[0]; 3023 3024 assert_eq!(body_box.rect.width, 784.0); 3025 3026 let div_box = &body_box.children[0]; 3027 assert_eq!(div_box.rect.width, 784.0); 3028 } 3029 3030 #[test] 3031 fn multiple_heading_levels() { 3032 let mut doc = Document::new(); 3033 let root = doc.root(); 3034 let html = doc.create_element("html"); 3035 let body = doc.create_element("body"); 3036 doc.append_child(root, html); 3037 doc.append_child(html, body); 3038 3039 let tags = ["h1", "h2", "h3"]; 3040 for tag in &tags { 3041 let h = doc.create_element(tag); 3042 let t = doc.create_text(tag); 3043 doc.append_child(body, h); 3044 doc.append_child(h, t); 3045 } 3046 3047 let tree = layout_doc(&doc); 3048 let body_box = &tree.root.children[0]; 3049 3050 let h1 = &body_box.children[0]; 3051 let h2 = &body_box.children[1]; 3052 let h3 = &body_box.children[2]; 3053 assert!(h1.font_size > h2.font_size); 3054 assert!(h2.font_size > h3.font_size); 3055 3056 assert!(h2.rect.y > h1.rect.y); 3057 assert!(h3.rect.y > h2.rect.y); 3058 } 3059 3060 #[test] 3061 fn layout_tree_iteration() { 3062 let mut doc = Document::new(); 3063 let root = doc.root(); 3064 let html = doc.create_element("html"); 3065 let body = doc.create_element("body"); 3066 let p = doc.create_element("p"); 3067 let text = doc.create_text("Test"); 3068 doc.append_child(root, html); 3069 doc.append_child(html, body); 3070 doc.append_child(body, p); 3071 doc.append_child(p, text); 3072 3073 let tree = layout_doc(&doc); 3074 let count = tree.iter().count(); 3075 assert!(count >= 3, "should have at least html, body, p boxes"); 3076 } 3077 3078 #[test] 3079 fn css_style_affects_layout() { 3080 let html_str = r#"<!DOCTYPE html> 3081<html> 3082<head> 3083<style> 3084p { margin-top: 50px; margin-bottom: 50px; } 3085</style> 3086</head> 3087<body> 3088<p>First</p> 3089<p>Second</p> 3090</body> 3091</html>"#; 3092 let doc = we_html::parse_html(html_str); 3093 let font = test_font(); 3094 let sheets = extract_stylesheets(&doc); 3095 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3096 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3097 3098 let body_box = &tree.root.children[0]; 3099 let first = &body_box.children[0]; 3100 let second = &body_box.children[1]; 3101 3102 assert_eq!(first.margin.top, 50.0); 3103 assert_eq!(first.margin.bottom, 50.0); 3104 3105 // Adjacent sibling margins collapse: gap = max(50, 50) = 50, not 100. 3106 let gap = second.rect.y - (first.rect.y + first.rect.height); 3107 assert!( 3108 (gap - 50.0).abs() < 1.0, 3109 "collapsed margin gap should be ~50px, got {gap}" 3110 ); 3111 } 3112 3113 #[test] 3114 fn inline_style_affects_layout() { 3115 let html_str = r#"<!DOCTYPE html> 3116<html> 3117<body> 3118<div style="padding-top: 20px; padding-bottom: 20px;"> 3119<p>Content</p> 3120</div> 3121</body> 3122</html>"#; 3123 let doc = we_html::parse_html(html_str); 3124 let font = test_font(); 3125 let sheets = extract_stylesheets(&doc); 3126 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3127 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3128 3129 let body_box = &tree.root.children[0]; 3130 let div_box = &body_box.children[0]; 3131 3132 assert_eq!(div_box.padding.top, 20.0); 3133 assert_eq!(div_box.padding.bottom, 20.0); 3134 } 3135 3136 #[test] 3137 fn css_color_propagates_to_layout() { 3138 let html_str = r#"<!DOCTYPE html> 3139<html> 3140<head><style>p { color: red; background-color: blue; }</style></head> 3141<body><p>Colored</p></body> 3142</html>"#; 3143 let doc = we_html::parse_html(html_str); 3144 let font = test_font(); 3145 let sheets = extract_stylesheets(&doc); 3146 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3147 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3148 3149 let body_box = &tree.root.children[0]; 3150 let p_box = &body_box.children[0]; 3151 3152 assert_eq!(p_box.color, Color::rgb(255, 0, 0)); 3153 assert_eq!(p_box.background_color, Color::rgb(0, 0, 255)); 3154 } 3155 3156 // --- New inline layout tests --- 3157 3158 #[test] 3159 fn inline_elements_have_per_fragment_styling() { 3160 let html_str = r#"<!DOCTYPE html> 3161<html> 3162<head><style>em { color: red; }</style></head> 3163<body><p>Hello <em>world</em></p></body> 3164</html>"#; 3165 let doc = we_html::parse_html(html_str); 3166 let font = test_font(); 3167 let sheets = extract_stylesheets(&doc); 3168 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3169 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3170 3171 let body_box = &tree.root.children[0]; 3172 let p_box = &body_box.children[0]; 3173 3174 let colors: Vec<Color> = p_box.lines.iter().map(|l| l.color).collect(); 3175 assert!( 3176 colors.iter().any(|c| *c == Color::rgb(0, 0, 0)), 3177 "should have black text" 3178 ); 3179 assert!( 3180 colors.iter().any(|c| *c == Color::rgb(255, 0, 0)), 3181 "should have red text from <em>" 3182 ); 3183 } 3184 3185 #[test] 3186 fn br_element_forces_line_break() { 3187 let html_str = r#"<!DOCTYPE html> 3188<html> 3189<body><p>Line one<br>Line two</p></body> 3190</html>"#; 3191 let doc = we_html::parse_html(html_str); 3192 let font = test_font(); 3193 let sheets = extract_stylesheets(&doc); 3194 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3195 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3196 3197 let body_box = &tree.root.children[0]; 3198 let p_box = &body_box.children[0]; 3199 3200 let mut ys: Vec<f32> = p_box.lines.iter().map(|l| l.y).collect(); 3201 ys.sort_by(|a, b| a.partial_cmp(b).unwrap()); 3202 ys.dedup_by(|a, b| (*a - *b).abs() < 0.01); 3203 3204 assert!( 3205 ys.len() >= 2, 3206 "<br> should produce 2 visual lines, got {}", 3207 ys.len() 3208 ); 3209 } 3210 3211 #[test] 3212 fn text_align_center() { 3213 let html_str = r#"<!DOCTYPE html> 3214<html> 3215<head><style>p { text-align: center; }</style></head> 3216<body><p>Hi</p></body> 3217</html>"#; 3218 let doc = we_html::parse_html(html_str); 3219 let font = test_font(); 3220 let sheets = extract_stylesheets(&doc); 3221 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3222 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3223 3224 let body_box = &tree.root.children[0]; 3225 let p_box = &body_box.children[0]; 3226 3227 assert!(!p_box.lines.is_empty()); 3228 let first = &p_box.lines[0]; 3229 // Center-aligned: text should be noticeably offset from content x. 3230 assert!( 3231 first.x > p_box.rect.x + 10.0, 3232 "center-aligned text x ({}) should be offset from content x ({})", 3233 first.x, 3234 p_box.rect.x 3235 ); 3236 } 3237 3238 #[test] 3239 fn text_align_right() { 3240 let html_str = r#"<!DOCTYPE html> 3241<html> 3242<head><style>p { text-align: right; }</style></head> 3243<body><p>Hi</p></body> 3244</html>"#; 3245 let doc = we_html::parse_html(html_str); 3246 let font = test_font(); 3247 let sheets = extract_stylesheets(&doc); 3248 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3249 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3250 3251 let body_box = &tree.root.children[0]; 3252 let p_box = &body_box.children[0]; 3253 3254 assert!(!p_box.lines.is_empty()); 3255 let first = &p_box.lines[0]; 3256 let right_edge = p_box.rect.x + p_box.rect.width; 3257 assert!( 3258 (first.x + first.width - right_edge).abs() < 1.0, 3259 "right-aligned text end ({}) should be near right edge ({})", 3260 first.x + first.width, 3261 right_edge 3262 ); 3263 } 3264 3265 #[test] 3266 fn inline_padding_offsets_text() { 3267 let html_str = r#"<!DOCTYPE html> 3268<html> 3269<head><style>span { padding-left: 20px; padding-right: 20px; }</style></head> 3270<body><p>A<span>B</span>C</p></body> 3271</html>"#; 3272 let doc = we_html::parse_html(html_str); 3273 let font = test_font(); 3274 let sheets = extract_stylesheets(&doc); 3275 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3276 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3277 3278 let body_box = &tree.root.children[0]; 3279 let p_box = &body_box.children[0]; 3280 3281 // Should have at least 3 fragments: A, B, C 3282 assert!( 3283 p_box.lines.len() >= 3, 3284 "should have fragments for A, B, C, got {}", 3285 p_box.lines.len() 3286 ); 3287 3288 // B should be offset by the span's padding. 3289 let a_frag = &p_box.lines[0]; 3290 let b_frag = &p_box.lines[1]; 3291 let gap = b_frag.x - (a_frag.x + a_frag.width); 3292 // Gap should include the 20px padding-left from the span. 3293 assert!( 3294 gap >= 19.0, 3295 "gap between A and B ({gap}) should include span padding-left (20px)" 3296 ); 3297 } 3298 3299 #[test] 3300 fn text_fragments_have_correct_font_size() { 3301 let html_str = r#"<!DOCTYPE html> 3302<html> 3303<body><h1>Big</h1><p>Small</p></body> 3304</html>"#; 3305 let doc = we_html::parse_html(html_str); 3306 let font = test_font(); 3307 let sheets = extract_stylesheets(&doc); 3308 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3309 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3310 3311 let body_box = &tree.root.children[0]; 3312 let h1_box = &body_box.children[0]; 3313 let p_box = &body_box.children[1]; 3314 3315 assert!(!h1_box.lines.is_empty()); 3316 assert!(!p_box.lines.is_empty()); 3317 assert_eq!(h1_box.lines[0].font_size, 32.0); 3318 assert_eq!(p_box.lines[0].font_size, 16.0); 3319 } 3320 3321 #[test] 3322 fn line_height_from_computed_style() { 3323 let html_str = r#"<!DOCTYPE html> 3324<html> 3325<head><style>p { line-height: 30px; }</style></head> 3326<body><p>Line one Line two Line three</p></body> 3327</html>"#; 3328 let doc = we_html::parse_html(html_str); 3329 let font = test_font(); 3330 let sheets = extract_stylesheets(&doc); 3331 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3332 // Narrow viewport to force wrapping. 3333 let tree = layout(&styled, &doc, 100.0, 600.0, &font, &HashMap::new()); 3334 3335 let body_box = &tree.root.children[0]; 3336 let p_box = &body_box.children[0]; 3337 3338 let mut ys: Vec<f32> = p_box.lines.iter().map(|l| l.y).collect(); 3339 ys.sort_by(|a, b| a.partial_cmp(b).unwrap()); 3340 ys.dedup_by(|a, b| (*a - *b).abs() < 0.01); 3341 3342 if ys.len() >= 2 { 3343 let gap = ys[1] - ys[0]; 3344 assert!( 3345 (gap - 30.0).abs() < 1.0, 3346 "line spacing ({gap}) should be ~30px from line-height" 3347 ); 3348 } 3349 } 3350 3351 // --- Relative positioning tests --- 3352 3353 #[test] 3354 fn relative_position_top_left() { 3355 let html_str = r#"<!DOCTYPE html> 3356<html> 3357<body> 3358<div style="position: relative; top: 10px; left: 20px;">Content</div> 3359</body> 3360</html>"#; 3361 let doc = we_html::parse_html(html_str); 3362 let font = test_font(); 3363 let sheets = extract_stylesheets(&doc); 3364 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3365 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3366 3367 let body_box = &tree.root.children[0]; 3368 let div_box = &body_box.children[0]; 3369 3370 assert_eq!(div_box.position, Position::Relative); 3371 assert_eq!(div_box.relative_offset, (20.0, 10.0)); 3372 3373 // The div should be shifted from where it would be in normal flow. 3374 // Normal flow position: body.rect.x + margin, body.rect.y + margin. 3375 // With relative offset: shifted by (20, 10). 3376 // Body has 8px margin by default, so content starts at x=8, y=8. 3377 assert!( 3378 (div_box.rect.x - (8.0 + 20.0)).abs() < 0.01, 3379 "div x ({}) should be 28.0 (8 + 20)", 3380 div_box.rect.x 3381 ); 3382 assert!( 3383 (div_box.rect.y - (8.0 + 10.0)).abs() < 0.01, 3384 "div y ({}) should be 18.0 (8 + 10)", 3385 div_box.rect.y 3386 ); 3387 } 3388 3389 #[test] 3390 fn relative_position_does_not_affect_siblings() { 3391 let html_str = r#"<!DOCTYPE html> 3392<html> 3393<head><style> 3394p { margin: 0; } 3395</style></head> 3396<body> 3397<p id="first" style="position: relative; top: 50px;">First</p> 3398<p id="second">Second</p> 3399</body> 3400</html>"#; 3401 let doc = we_html::parse_html(html_str); 3402 let font = test_font(); 3403 let sheets = extract_stylesheets(&doc); 3404 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3405 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3406 3407 let body_box = &tree.root.children[0]; 3408 let first = &body_box.children[0]; 3409 let second = &body_box.children[1]; 3410 3411 // The first paragraph is shifted down by 50px visually. 3412 assert_eq!(first.relative_offset, (0.0, 50.0)); 3413 3414 // But the second paragraph should be at its normal-flow position, 3415 // as if the first paragraph were NOT shifted. The second paragraph 3416 // should come right after the first's normal-flow height. 3417 // Body content starts at y=8 (default body margin). First p has 0 margin. 3418 // Second p should start right after first p's height (without offset). 3419 let first_normal_y = 8.0; // body margin 3420 let first_height = first.rect.height; 3421 let expected_second_y = first_normal_y + first_height; 3422 assert!( 3423 (second.rect.y - expected_second_y).abs() < 1.0, 3424 "second y ({}) should be at normal-flow position ({expected_second_y}), not affected by first's relative offset", 3425 second.rect.y 3426 ); 3427 } 3428 3429 #[test] 3430 fn relative_position_conflicting_offsets() { 3431 // When both top and bottom are specified, top wins. 3432 // When both left and right are specified, left wins. 3433 let html_str = r#"<!DOCTYPE html> 3434<html> 3435<body> 3436<div style="position: relative; top: 10px; bottom: 20px; left: 30px; right: 40px;">Content</div> 3437</body> 3438</html>"#; 3439 let doc = we_html::parse_html(html_str); 3440 let font = test_font(); 3441 let sheets = extract_stylesheets(&doc); 3442 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3443 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3444 3445 let body_box = &tree.root.children[0]; 3446 let div_box = &body_box.children[0]; 3447 3448 // top wins over bottom: dy = 10 (not -20) 3449 // left wins over right: dx = 30 (not -40) 3450 assert_eq!(div_box.relative_offset, (30.0, 10.0)); 3451 } 3452 3453 #[test] 3454 fn relative_position_auto_offsets() { 3455 // auto offsets should resolve to 0 (no movement). 3456 let html_str = r#"<!DOCTYPE html> 3457<html> 3458<body> 3459<div style="position: relative;">Content</div> 3460</body> 3461</html>"#; 3462 let doc = we_html::parse_html(html_str); 3463 let font = test_font(); 3464 let sheets = extract_stylesheets(&doc); 3465 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3466 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3467 3468 let body_box = &tree.root.children[0]; 3469 let div_box = &body_box.children[0]; 3470 3471 assert_eq!(div_box.position, Position::Relative); 3472 assert_eq!(div_box.relative_offset, (0.0, 0.0)); 3473 } 3474 3475 #[test] 3476 fn relative_position_bottom_right() { 3477 // bottom: 15px should shift up by 15px (negative direction). 3478 // right: 25px should shift left by 25px (negative direction). 3479 let html_str = r#"<!DOCTYPE html> 3480<html> 3481<body> 3482<div style="position: relative; bottom: 15px; right: 25px;">Content</div> 3483</body> 3484</html>"#; 3485 let doc = we_html::parse_html(html_str); 3486 let font = test_font(); 3487 let sheets = extract_stylesheets(&doc); 3488 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3489 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3490 3491 let body_box = &tree.root.children[0]; 3492 let div_box = &body_box.children[0]; 3493 3494 assert_eq!(div_box.relative_offset, (-25.0, -15.0)); 3495 } 3496 3497 #[test] 3498 fn relative_position_shifts_text_lines() { 3499 let html_str = r#"<!DOCTYPE html> 3500<html> 3501<head><style>p { margin: 0; }</style></head> 3502<body> 3503<p style="position: relative; top: 30px; left: 40px;">Hello</p> 3504</body> 3505</html>"#; 3506 let doc = we_html::parse_html(html_str); 3507 let font = test_font(); 3508 let sheets = extract_stylesheets(&doc); 3509 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3510 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3511 3512 let body_box = &tree.root.children[0]; 3513 let p_box = &body_box.children[0]; 3514 3515 assert!(!p_box.lines.is_empty(), "p should have text lines"); 3516 let first_line = &p_box.lines[0]; 3517 3518 // Text should be shifted by the relative offset. 3519 // Body content starts at x=8, y=8. With offset: x=48, y=38. 3520 assert!( 3521 first_line.x >= 8.0 + 40.0 - 1.0, 3522 "text x ({}) should be shifted by left offset", 3523 first_line.x 3524 ); 3525 assert!( 3526 first_line.y >= 8.0 + 30.0 - 1.0, 3527 "text y ({}) should be shifted by top offset", 3528 first_line.y 3529 ); 3530 } 3531 3532 #[test] 3533 fn static_position_has_no_offset() { 3534 let html_str = r#"<!DOCTYPE html> 3535<html> 3536<body> 3537<div>Normal flow</div> 3538</body> 3539</html>"#; 3540 let doc = we_html::parse_html(html_str); 3541 let font = test_font(); 3542 let sheets = extract_stylesheets(&doc); 3543 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3544 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3545 3546 let body_box = &tree.root.children[0]; 3547 let div_box = &body_box.children[0]; 3548 3549 assert_eq!(div_box.position, Position::Static); 3550 assert_eq!(div_box.relative_offset, (0.0, 0.0)); 3551 } 3552 3553 // --- Margin collapsing tests --- 3554 3555 #[test] 3556 fn adjacent_sibling_margins_collapse() { 3557 // Two <p> elements each with margin 16px: gap should be 16px (max), not 32px (sum). 3558 let html_str = r#"<!DOCTYPE html> 3559<html> 3560<head><style> 3561body { margin: 0; border-top: 1px solid black; } 3562p { margin-top: 16px; margin-bottom: 16px; } 3563</style></head> 3564<body> 3565<p>First</p> 3566<p>Second</p> 3567</body> 3568</html>"#; 3569 let doc = we_html::parse_html(html_str); 3570 let font = test_font(); 3571 let sheets = extract_stylesheets(&doc); 3572 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3573 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3574 3575 let body_box = &tree.root.children[0]; 3576 let first = &body_box.children[0]; 3577 let second = &body_box.children[1]; 3578 3579 // Gap between first's bottom border-box and second's top border-box 3580 // should be the collapsed margin: max(16, 16) = 16. 3581 let first_bottom = 3582 first.rect.y + first.rect.height + first.padding.bottom + first.border.bottom; 3583 let gap = second.rect.y - second.border.top - second.padding.top - first_bottom; 3584 assert!( 3585 (gap - 16.0).abs() < 1.0, 3586 "collapsed sibling margin should be ~16px, got {gap}" 3587 ); 3588 } 3589 3590 #[test] 3591 fn sibling_margins_collapse_unequal() { 3592 // p1 bottom-margin 20, p2 top-margin 30: gap should be 30 (max). 3593 let html_str = r#"<!DOCTYPE html> 3594<html> 3595<head><style> 3596body { margin: 0; border-top: 1px solid black; } 3597.first { margin-top: 0; margin-bottom: 20px; } 3598.second { margin-top: 30px; margin-bottom: 0; } 3599</style></head> 3600<body> 3601<p class="first">First</p> 3602<p class="second">Second</p> 3603</body> 3604</html>"#; 3605 let doc = we_html::parse_html(html_str); 3606 let font = test_font(); 3607 let sheets = extract_stylesheets(&doc); 3608 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3609 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3610 3611 let body_box = &tree.root.children[0]; 3612 let first = &body_box.children[0]; 3613 let second = &body_box.children[1]; 3614 3615 let first_bottom = 3616 first.rect.y + first.rect.height + first.padding.bottom + first.border.bottom; 3617 let gap = second.rect.y - second.border.top - second.padding.top - first_bottom; 3618 assert!( 3619 (gap - 30.0).abs() < 1.0, 3620 "collapsed margin should be max(20, 30) = 30, got {gap}" 3621 ); 3622 } 3623 3624 #[test] 3625 fn parent_first_child_margin_collapsing() { 3626 // Parent with no padding/border: first child's top margin collapses. 3627 let html_str = r#"<!DOCTYPE html> 3628<html> 3629<head><style> 3630body { margin: 0; border-top: 1px solid black; } 3631.parent { margin-top: 10px; } 3632.child { margin-top: 20px; } 3633</style></head> 3634<body> 3635<div class="parent"><p class="child">Child</p></div> 3636</body> 3637</html>"#; 3638 let doc = we_html::parse_html(html_str); 3639 let font = test_font(); 3640 let sheets = extract_stylesheets(&doc); 3641 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3642 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3643 3644 let body_box = &tree.root.children[0]; 3645 let parent_box = &body_box.children[0]; 3646 3647 // Parent margin collapses with child's: max(10, 20) = 20. 3648 assert_eq!(parent_box.margin.top, 20.0); 3649 } 3650 3651 #[test] 3652 fn negative_margin_collapsing() { 3653 // One positive (20) and one negative (-10): collapsed = 20 + (-10) = 10. 3654 let html_str = r#"<!DOCTYPE html> 3655<html> 3656<head><style> 3657body { margin: 0; border-top: 1px solid black; } 3658.first { margin-top: 0; margin-bottom: 20px; } 3659.second { margin-top: -10px; margin-bottom: 0; } 3660</style></head> 3661<body> 3662<p class="first">First</p> 3663<p class="second">Second</p> 3664</body> 3665</html>"#; 3666 let doc = we_html::parse_html(html_str); 3667 let font = test_font(); 3668 let sheets = extract_stylesheets(&doc); 3669 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3670 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3671 3672 let body_box = &tree.root.children[0]; 3673 let first = &body_box.children[0]; 3674 let second = &body_box.children[1]; 3675 3676 let first_bottom = 3677 first.rect.y + first.rect.height + first.padding.bottom + first.border.bottom; 3678 let gap = second.rect.y - second.border.top - second.padding.top - first_bottom; 3679 // 20 + (-10) = 10 3680 assert!( 3681 (gap - 10.0).abs() < 1.0, 3682 "positive + negative margin collapse should be 10, got {gap}" 3683 ); 3684 } 3685 3686 #[test] 3687 fn both_negative_margins_collapse() { 3688 // Both negative: use the more negative value. 3689 let html_str = r#"<!DOCTYPE html> 3690<html> 3691<head><style> 3692body { margin: 0; border-top: 1px solid black; } 3693.first { margin-top: 0; margin-bottom: -10px; } 3694.second { margin-top: -20px; margin-bottom: 0; } 3695</style></head> 3696<body> 3697<p class="first">First</p> 3698<p class="second">Second</p> 3699</body> 3700</html>"#; 3701 let doc = we_html::parse_html(html_str); 3702 let font = test_font(); 3703 let sheets = extract_stylesheets(&doc); 3704 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3705 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3706 3707 let body_box = &tree.root.children[0]; 3708 let first = &body_box.children[0]; 3709 let second = &body_box.children[1]; 3710 3711 let first_bottom = 3712 first.rect.y + first.rect.height + first.padding.bottom + first.border.bottom; 3713 let gap = second.rect.y - second.border.top - second.padding.top - first_bottom; 3714 // Both negative: min(-10, -20) = -20 3715 assert!( 3716 (gap - (-20.0)).abs() < 1.0, 3717 "both-negative margin collapse should be -20, got {gap}" 3718 ); 3719 } 3720 3721 #[test] 3722 fn border_blocks_margin_collapsing() { 3723 // When border separates margins, they don't collapse. 3724 let html_str = r#"<!DOCTYPE html> 3725<html> 3726<head><style> 3727body { margin: 0; border-top: 1px solid black; } 3728.first { margin-top: 0; margin-bottom: 20px; border-bottom: 1px solid black; } 3729.second { margin-top: 20px; border-top: 1px solid black; } 3730</style></head> 3731<body> 3732<p class="first">First</p> 3733<p class="second">Second</p> 3734</body> 3735</html>"#; 3736 let doc = we_html::parse_html(html_str); 3737 let font = test_font(); 3738 let sheets = extract_stylesheets(&doc); 3739 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3740 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3741 3742 let body_box = &tree.root.children[0]; 3743 let first = &body_box.children[0]; 3744 let second = &body_box.children[1]; 3745 3746 // Borders are on the elements themselves, but the MARGINS are still 3747 // between the border boxes — sibling margins still collapse regardless 3748 // of borders on the elements. The margin gap = max(20, 20) = 20. 3749 let first_bottom = 3750 first.rect.y + first.rect.height + first.padding.bottom + first.border.bottom; 3751 let gap = second.rect.y - second.border.top - second.padding.top - first_bottom; 3752 assert!( 3753 (gap - 20.0).abs() < 1.0, 3754 "sibling margins collapse even with borders on elements, gap should be 20, got {gap}" 3755 ); 3756 } 3757 3758 #[test] 3759 fn padding_blocks_parent_child_collapsing() { 3760 // Parent with padding-top prevents margin collapsing with first child. 3761 let html_str = r#"<!DOCTYPE html> 3762<html> 3763<head><style> 3764body { margin: 0; border-top: 1px solid black; } 3765.parent { margin-top: 10px; padding-top: 5px; } 3766.child { margin-top: 20px; } 3767</style></head> 3768<body> 3769<div class="parent"><p class="child">Child</p></div> 3770</body> 3771</html>"#; 3772 let doc = we_html::parse_html(html_str); 3773 let font = test_font(); 3774 let sheets = extract_stylesheets(&doc); 3775 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3776 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3777 3778 let body_box = &tree.root.children[0]; 3779 let parent_box = &body_box.children[0]; 3780 3781 // Parent has padding-top, so no collapsing: margin stays at 10. 3782 assert_eq!(parent_box.margin.top, 10.0); 3783 } 3784 3785 #[test] 3786 fn empty_block_margins_collapse() { 3787 // An empty div's top and bottom margins collapse with adjacent margins. 3788 let html_str = r#"<!DOCTYPE html> 3789<html> 3790<head><style> 3791body { margin: 0; border-top: 1px solid black; } 3792.spacer { margin-top: 10px; margin-bottom: 10px; } 3793p { margin-top: 5px; margin-bottom: 5px; } 3794</style></head> 3795<body> 3796<p>Before</p> 3797<div class="spacer"></div> 3798<p>After</p> 3799</body> 3800</html>"#; 3801 let doc = we_html::parse_html(html_str); 3802 let font = test_font(); 3803 let sheets = extract_stylesheets(&doc); 3804 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3805 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3806 3807 let body_box = &tree.root.children[0]; 3808 let before = &body_box.children[0]; 3809 let after = &body_box.children[2]; // [0]=p, [1]=empty div, [2]=p 3810 3811 // Empty div's margins (10+10) self-collapse to max(10,10)=10. 3812 // Then collapse with before's bottom (5) and after's top (5): 3813 // collapse(5, collapse(10, 10)) = collapse(5, 10) = 10 3814 // Then collapse(10, 5) = 10. 3815 // So total gap between before and after = 10. 3816 let before_bottom = 3817 before.rect.y + before.rect.height + before.padding.bottom + before.border.bottom; 3818 let gap = after.rect.y - after.border.top - after.padding.top - before_bottom; 3819 assert!( 3820 (gap - 10.0).abs() < 1.0, 3821 "empty block margin collapse gap should be ~10px, got {gap}" 3822 ); 3823 } 3824 3825 #[test] 3826 fn collapse_margins_unit() { 3827 // Unit tests for the collapse_margins helper. 3828 assert_eq!(collapse_margins(10.0, 20.0), 20.0); 3829 assert_eq!(collapse_margins(20.0, 10.0), 20.0); 3830 assert_eq!(collapse_margins(0.0, 15.0), 15.0); 3831 assert_eq!(collapse_margins(-5.0, -10.0), -10.0); 3832 assert_eq!(collapse_margins(20.0, -5.0), 15.0); 3833 assert_eq!(collapse_margins(-5.0, 20.0), 15.0); 3834 assert_eq!(collapse_margins(0.0, 0.0), 0.0); 3835 } 3836 3837 // --- Box-sizing tests --- 3838 3839 #[test] 3840 fn content_box_default_width_applies_to_content() { 3841 // Default box-sizing (content-box): width = content width only. 3842 let html_str = r#"<!DOCTYPE html> 3843<html> 3844<head><style> 3845body { margin: 0; } 3846div { width: 200px; padding: 10px; border: 5px solid black; } 3847</style></head> 3848<body> 3849<div>Content</div> 3850</body> 3851</html>"#; 3852 let doc = we_html::parse_html(html_str); 3853 let font = test_font(); 3854 let sheets = extract_stylesheets(&doc); 3855 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3856 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3857 3858 let body_box = &tree.root.children[0]; 3859 let div_box = &body_box.children[0]; 3860 3861 // content-box: rect.width = 200 (the specified width IS the content) 3862 assert_eq!(div_box.rect.width, 200.0); 3863 assert_eq!(div_box.padding.left, 10.0); 3864 assert_eq!(div_box.padding.right, 10.0); 3865 assert_eq!(div_box.border.left, 5.0); 3866 assert_eq!(div_box.border.right, 5.0); 3867 } 3868 3869 #[test] 3870 fn border_box_width_includes_padding_and_border() { 3871 // box-sizing: border-box: width includes padding and border. 3872 let html_str = r#"<!DOCTYPE html> 3873<html> 3874<head><style> 3875body { margin: 0; } 3876div { box-sizing: border-box; width: 200px; padding: 10px; border: 5px solid black; } 3877</style></head> 3878<body> 3879<div>Content</div> 3880</body> 3881</html>"#; 3882 let doc = we_html::parse_html(html_str); 3883 let font = test_font(); 3884 let sheets = extract_stylesheets(&doc); 3885 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3886 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3887 3888 let body_box = &tree.root.children[0]; 3889 let div_box = &body_box.children[0]; 3890 3891 // border-box: content width = 200 - 10*2 (padding) - 5*2 (border) = 170 3892 assert_eq!(div_box.rect.width, 170.0); 3893 assert_eq!(div_box.padding.left, 10.0); 3894 assert_eq!(div_box.padding.right, 10.0); 3895 assert_eq!(div_box.border.left, 5.0); 3896 assert_eq!(div_box.border.right, 5.0); 3897 } 3898 3899 #[test] 3900 fn border_box_padding_exceeds_width_clamps_to_zero() { 3901 // border-box with padding+border > specified width: content clamps to 0. 3902 let html_str = r#"<!DOCTYPE html> 3903<html> 3904<head><style> 3905body { margin: 0; } 3906div { box-sizing: border-box; width: 20px; padding: 15px; border: 5px solid black; } 3907</style></head> 3908<body> 3909<div>X</div> 3910</body> 3911</html>"#; 3912 let doc = we_html::parse_html(html_str); 3913 let font = test_font(); 3914 let sheets = extract_stylesheets(&doc); 3915 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3916 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3917 3918 let body_box = &tree.root.children[0]; 3919 let div_box = &body_box.children[0]; 3920 3921 // border-box: content = 20 - 15*2 - 5*2 = 20 - 40 = -20 → clamped to 0 3922 assert_eq!(div_box.rect.width, 0.0); 3923 } 3924 3925 #[test] 3926 fn box_sizing_is_not_inherited() { 3927 // box-sizing is not inherited: child should use default content-box. 3928 let html_str = r#"<!DOCTYPE html> 3929<html> 3930<head><style> 3931body { margin: 0; } 3932.parent { box-sizing: border-box; width: 300px; padding: 10px; border: 5px solid black; } 3933.child { width: 100px; padding: 10px; border: 5px solid black; } 3934</style></head> 3935<body> 3936<div class="parent"><div class="child">Inner</div></div> 3937</body> 3938</html>"#; 3939 let doc = we_html::parse_html(html_str); 3940 let font = test_font(); 3941 let sheets = extract_stylesheets(&doc); 3942 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3943 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3944 3945 let body_box = &tree.root.children[0]; 3946 let parent_box = &body_box.children[0]; 3947 let child_box = &parent_box.children[0]; 3948 3949 // Parent: border-box → content = 300 - 20 - 10 = 270 3950 assert_eq!(parent_box.rect.width, 270.0); 3951 // Child: default content-box → content = 100 (not reduced by padding/border) 3952 assert_eq!(child_box.rect.width, 100.0); 3953 } 3954 3955 #[test] 3956 fn border_box_height() { 3957 // box-sizing: border-box also applies to height. 3958 let html_str = r#"<!DOCTYPE html> 3959<html> 3960<head><style> 3961body { margin: 0; } 3962div { box-sizing: border-box; height: 100px; padding: 10px; border: 5px solid black; } 3963</style></head> 3964<body> 3965<div>Content</div> 3966</body> 3967</html>"#; 3968 let doc = we_html::parse_html(html_str); 3969 let font = test_font(); 3970 let sheets = extract_stylesheets(&doc); 3971 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3972 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3973 3974 let body_box = &tree.root.children[0]; 3975 let div_box = &body_box.children[0]; 3976 3977 // border-box: content height = 100 - 10*2 (padding) - 5*2 (border) = 70 3978 assert_eq!(div_box.rect.height, 70.0); 3979 } 3980 3981 #[test] 3982 fn content_box_explicit_height() { 3983 // content-box: height applies to content only. 3984 let html_str = r#"<!DOCTYPE html> 3985<html> 3986<head><style> 3987body { margin: 0; } 3988div { height: 100px; padding: 10px; border: 5px solid black; } 3989</style></head> 3990<body> 3991<div>Content</div> 3992</body> 3993</html>"#; 3994 let doc = we_html::parse_html(html_str); 3995 let font = test_font(); 3996 let sheets = extract_stylesheets(&doc); 3997 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 3998 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 3999 4000 let body_box = &tree.root.children[0]; 4001 let div_box = &body_box.children[0]; 4002 4003 // content-box: rect.height = 100 (specified height IS content height) 4004 assert_eq!(div_box.rect.height, 100.0); 4005 } 4006 4007 // --- Visibility / display:none tests --- 4008 4009 #[test] 4010 fn display_none_excludes_from_layout_tree() { 4011 let html_str = r#"<!DOCTYPE html> 4012<html> 4013<head><style> 4014body { margin: 0; } 4015.hidden { display: none; } 4016p { margin: 0; } 4017</style></head> 4018<body> 4019<p>First</p> 4020<div class="hidden">Hidden content</div> 4021<p>Second</p> 4022</body> 4023</html>"#; 4024 let doc = we_html::parse_html(html_str); 4025 let font = test_font(); 4026 let sheets = extract_stylesheets(&doc); 4027 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4028 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4029 4030 let body_box = &tree.root.children[0]; 4031 // display:none element is excluded — body should have only 2 children. 4032 assert_eq!(body_box.children.len(), 2); 4033 4034 let first = &body_box.children[0]; 4035 let second = &body_box.children[1]; 4036 // Second paragraph should be directly below first (no gap for hidden). 4037 assert!( 4038 second.rect.y == first.rect.y + first.rect.height, 4039 "display:none should not occupy space" 4040 ); 4041 } 4042 4043 #[test] 4044 fn visibility_hidden_preserves_layout_space() { 4045 let html_str = r#"<!DOCTYPE html> 4046<html> 4047<head><style> 4048body { margin: 0; } 4049.hidden { visibility: hidden; height: 50px; margin: 0; } 4050p { margin: 0; } 4051</style></head> 4052<body> 4053<p>First</p> 4054<div class="hidden">Hidden</div> 4055<p>Second</p> 4056</body> 4057</html>"#; 4058 let doc = we_html::parse_html(html_str); 4059 let font = test_font(); 4060 let sheets = extract_stylesheets(&doc); 4061 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4062 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4063 4064 let body_box = &tree.root.children[0]; 4065 // visibility:hidden still in layout tree — body has 3 children. 4066 assert_eq!(body_box.children.len(), 3); 4067 4068 let hidden_box = &body_box.children[1]; 4069 assert_eq!(hidden_box.visibility, Visibility::Hidden); 4070 assert_eq!(hidden_box.rect.height, 50.0); 4071 4072 let second = &body_box.children[2]; 4073 // Second paragraph should be below hidden div (it occupies 50px). 4074 assert!( 4075 second.rect.y >= hidden_box.rect.y + 50.0, 4076 "visibility:hidden should preserve layout space" 4077 ); 4078 } 4079 4080 #[test] 4081 fn visibility_inherited_by_children() { 4082 let html_str = r#"<!DOCTYPE html> 4083<html> 4084<head><style> 4085body { margin: 0; } 4086.parent { visibility: hidden; } 4087</style></head> 4088<body> 4089<div class="parent"><p>Child</p></div> 4090</body> 4091</html>"#; 4092 let doc = we_html::parse_html(html_str); 4093 let font = test_font(); 4094 let sheets = extract_stylesheets(&doc); 4095 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4096 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4097 4098 let body_box = &tree.root.children[0]; 4099 let parent_box = &body_box.children[0]; 4100 let child_box = &parent_box.children[0]; 4101 assert_eq!(parent_box.visibility, Visibility::Hidden); 4102 assert_eq!(child_box.visibility, Visibility::Hidden); 4103 } 4104 4105 #[test] 4106 fn visibility_visible_overrides_hidden_parent() { 4107 let html_str = r#"<!DOCTYPE html> 4108<html> 4109<head><style> 4110body { margin: 0; } 4111.parent { visibility: hidden; } 4112.child { visibility: visible; } 4113</style></head> 4114<body> 4115<div class="parent"><p class="child">Visible child</p></div> 4116</body> 4117</html>"#; 4118 let doc = we_html::parse_html(html_str); 4119 let font = test_font(); 4120 let sheets = extract_stylesheets(&doc); 4121 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4122 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4123 4124 let body_box = &tree.root.children[0]; 4125 let parent_box = &body_box.children[0]; 4126 let child_box = &parent_box.children[0]; 4127 assert_eq!(parent_box.visibility, Visibility::Hidden); 4128 assert_eq!(child_box.visibility, Visibility::Visible); 4129 } 4130 4131 #[test] 4132 fn visibility_collapse_on_non_table_treated_as_hidden() { 4133 let html_str = r#"<!DOCTYPE html> 4134<html> 4135<head><style> 4136body { margin: 0; } 4137div { visibility: collapse; height: 50px; } 4138</style></head> 4139<body> 4140<div>Collapsed</div> 4141</body> 4142</html>"#; 4143 let doc = we_html::parse_html(html_str); 4144 let font = test_font(); 4145 let sheets = extract_stylesheets(&doc); 4146 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4147 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4148 4149 let body_box = &tree.root.children[0]; 4150 let div_box = &body_box.children[0]; 4151 assert_eq!(div_box.visibility, Visibility::Collapse); 4152 // Still occupies space (non-table collapse = hidden behavior). 4153 assert_eq!(div_box.rect.height, 50.0); 4154 } 4155 4156 // --- Viewport units and percentage resolution tests --- 4157 4158 #[test] 4159 fn width_50_percent_resolves_to_half_containing_block() { 4160 let html_str = r#"<!DOCTYPE html> 4161<html> 4162<head><style> 4163body { margin: 0; } 4164div { width: 50%; } 4165</style></head> 4166<body><div>Half width</div></body> 4167</html>"#; 4168 let doc = we_html::parse_html(html_str); 4169 let font = test_font(); 4170 let sheets = extract_stylesheets(&doc); 4171 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4172 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4173 4174 let body_box = &tree.root.children[0]; 4175 let div_box = &body_box.children[0]; 4176 assert!( 4177 (div_box.rect.width - 400.0).abs() < 0.01, 4178 "width: 50% should be 400px on 800px viewport, got {}", 4179 div_box.rect.width 4180 ); 4181 } 4182 4183 #[test] 4184 fn margin_10_percent_resolves_against_containing_block_width() { 4185 let html_str = r#"<!DOCTYPE html> 4186<html> 4187<head><style> 4188body { margin: 0; } 4189div { margin: 10%; width: 100px; height: 50px; } 4190</style></head> 4191<body><div>Box</div></body> 4192</html>"#; 4193 let doc = we_html::parse_html(html_str); 4194 let font = test_font(); 4195 let sheets = extract_stylesheets(&doc); 4196 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4197 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4198 4199 let body_box = &tree.root.children[0]; 4200 let div_box = &body_box.children[0]; 4201 // All margins (including top/bottom) resolve against containing block WIDTH. 4202 assert!( 4203 (div_box.margin.left - 80.0).abs() < 0.01, 4204 "margin-left: 10% should be 80px on 800px viewport, got {}", 4205 div_box.margin.left 4206 ); 4207 assert!( 4208 (div_box.margin.top - 80.0).abs() < 0.01, 4209 "margin-top: 10% should be 80px (against width, not height), got {}", 4210 div_box.margin.top 4211 ); 4212 } 4213 4214 #[test] 4215 fn width_50vw_resolves_to_half_viewport() { 4216 let html_str = r#"<!DOCTYPE html> 4217<html> 4218<head><style> 4219body { margin: 0; } 4220div { width: 50vw; } 4221</style></head> 4222<body><div>Half viewport</div></body> 4223</html>"#; 4224 let doc = we_html::parse_html(html_str); 4225 let font = test_font(); 4226 let sheets = extract_stylesheets(&doc); 4227 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4228 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4229 4230 let body_box = &tree.root.children[0]; 4231 let div_box = &body_box.children[0]; 4232 assert!( 4233 (div_box.rect.width - 400.0).abs() < 0.01, 4234 "width: 50vw should be 400px on 800px viewport, got {}", 4235 div_box.rect.width 4236 ); 4237 } 4238 4239 #[test] 4240 fn height_100vh_resolves_to_full_viewport() { 4241 let html_str = r#"<!DOCTYPE html> 4242<html> 4243<head><style> 4244body { margin: 0; } 4245div { height: 100vh; } 4246</style></head> 4247<body><div>Full height</div></body> 4248</html>"#; 4249 let doc = we_html::parse_html(html_str); 4250 let font = test_font(); 4251 let sheets = extract_stylesheets(&doc); 4252 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4253 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4254 4255 let body_box = &tree.root.children[0]; 4256 let div_box = &body_box.children[0]; 4257 assert!( 4258 (div_box.rect.height - 600.0).abs() < 0.01, 4259 "height: 100vh should be 600px on 600px viewport, got {}", 4260 div_box.rect.height 4261 ); 4262 } 4263 4264 #[test] 4265 fn font_size_5vmin_resolves_to_smaller_dimension() { 4266 let html_str = r#"<!DOCTYPE html> 4267<html> 4268<head><style> 4269body { margin: 0; } 4270p { font-size: 5vmin; } 4271</style></head> 4272<body><p>Text</p></body> 4273</html>"#; 4274 let doc = we_html::parse_html(html_str); 4275 let font = test_font(); 4276 let sheets = extract_stylesheets(&doc); 4277 // viewport 800x600 → vmin = 600 4278 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4279 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4280 4281 let body_box = &tree.root.children[0]; 4282 let p_box = &body_box.children[0]; 4283 // 5vmin = 5% of min(800, 600) = 5% of 600 = 30px 4284 assert!( 4285 (p_box.font_size - 30.0).abs() < 0.01, 4286 "font-size: 5vmin should be 30px, got {}", 4287 p_box.font_size 4288 ); 4289 } 4290 4291 #[test] 4292 fn nested_percentage_widths_compound() { 4293 let html_str = r#"<!DOCTYPE html> 4294<html> 4295<head><style> 4296body { margin: 0; } 4297.outer { width: 50%; } 4298.inner { width: 50%; } 4299</style></head> 4300<body> 4301<div class="outer"><div class="inner">Nested</div></div> 4302</body> 4303</html>"#; 4304 let doc = we_html::parse_html(html_str); 4305 let font = test_font(); 4306 let sheets = extract_stylesheets(&doc); 4307 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4308 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4309 4310 let body_box = &tree.root.children[0]; 4311 let outer_box = &body_box.children[0]; 4312 let inner_box = &outer_box.children[0]; 4313 // outer = 50% of 800 = 400 4314 assert!( 4315 (outer_box.rect.width - 400.0).abs() < 0.01, 4316 "outer width should be 400px, got {}", 4317 outer_box.rect.width 4318 ); 4319 // inner = 50% of 400 = 200 4320 assert!( 4321 (inner_box.rect.width - 200.0).abs() < 0.01, 4322 "inner width should be 200px (50% of 400), got {}", 4323 inner_box.rect.width 4324 ); 4325 } 4326 4327 #[test] 4328 fn padding_percent_resolves_against_width() { 4329 let html_str = r#"<!DOCTYPE html> 4330<html> 4331<head><style> 4332body { margin: 0; } 4333div { padding: 5%; width: 400px; } 4334</style></head> 4335<body><div>Content</div></body> 4336</html>"#; 4337 let doc = we_html::parse_html(html_str); 4338 let font = test_font(); 4339 let sheets = extract_stylesheets(&doc); 4340 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4341 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4342 4343 let body_box = &tree.root.children[0]; 4344 let div_box = &body_box.children[0]; 4345 // padding: 5% resolves against containing block width (800px) 4346 assert!( 4347 (div_box.padding.top - 40.0).abs() < 0.01, 4348 "padding-top: 5% should be 40px (5% of 800), got {}", 4349 div_box.padding.top 4350 ); 4351 assert!( 4352 (div_box.padding.left - 40.0).abs() < 0.01, 4353 "padding-left: 5% should be 40px (5% of 800), got {}", 4354 div_box.padding.left 4355 ); 4356 } 4357 4358 #[test] 4359 fn vmax_uses_larger_dimension() { 4360 let html_str = r#"<!DOCTYPE html> 4361<html> 4362<head><style> 4363body { margin: 0; } 4364div { width: 10vmax; } 4365</style></head> 4366<body><div>Content</div></body> 4367</html>"#; 4368 let doc = we_html::parse_html(html_str); 4369 let font = test_font(); 4370 let sheets = extract_stylesheets(&doc); 4371 // viewport 800x600 → vmax = 800 4372 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4373 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4374 4375 let body_box = &tree.root.children[0]; 4376 let div_box = &body_box.children[0]; 4377 // 10vmax = 10% of max(800, 600) = 10% of 800 = 80px 4378 assert!( 4379 (div_box.rect.width - 80.0).abs() < 0.01, 4380 "width: 10vmax should be 80px, got {}", 4381 div_box.rect.width 4382 ); 4383 } 4384 4385 #[test] 4386 fn height_50_percent_resolves_against_viewport() { 4387 let html_str = r#"<!DOCTYPE html> 4388<html> 4389<head><style> 4390body { margin: 0; } 4391div { height: 50%; } 4392</style></head> 4393<body><div>Half height</div></body> 4394</html>"#; 4395 let doc = we_html::parse_html(html_str); 4396 let font = test_font(); 4397 let sheets = extract_stylesheets(&doc); 4398 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 4399 let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 4400 4401 let body_box = &tree.root.children[0]; 4402 let div_box = &body_box.children[0]; 4403 // height: 50% resolves against viewport height (600) 4404 assert!( 4405 (div_box.rect.height - 300.0).abs() < 0.01, 4406 "height: 50% should be 300px on 600px viewport, got {}", 4407 div_box.rect.height 4408 ); 4409 } 4410 4411 // ----------------------------------------------------------------------- 4412 // Flexbox tests 4413 // ----------------------------------------------------------------------- 4414 4415 #[test] 4416 fn flex_row_default_items_laid_out_horizontally() { 4417 let html_str = r#"<!DOCTYPE html> 4418<html><head><style> 4419body { margin: 0; } 4420.container { display: flex; width: 600px; } 4421.item { width: 100px; height: 50px; } 4422</style></head> 4423<body> 4424<div class="container"> 4425 <div class="item">A</div> 4426 <div class="item">B</div> 4427 <div class="item">C</div> 4428</div> 4429</body></html>"#; 4430 let doc = we_html::parse_html(html_str); 4431 let tree = layout_doc(&doc); 4432 4433 let body = &tree.root.children[0]; 4434 let container = &body.children[0]; 4435 assert_eq!(container.children.len(), 3); 4436 4437 // Items should be at x=0, 100, 200 4438 assert!( 4439 (container.children[0].rect.x - 0.0).abs() < 1.0, 4440 "first item x should be 0, got {}", 4441 container.children[0].rect.x 4442 ); 4443 assert!( 4444 (container.children[1].rect.x - 100.0).abs() < 1.0, 4445 "second item x should be 100, got {}", 4446 container.children[1].rect.x 4447 ); 4448 assert!( 4449 (container.children[2].rect.x - 200.0).abs() < 1.0, 4450 "third item x should be 200, got {}", 4451 container.children[2].rect.x 4452 ); 4453 4454 // All items should have the same y. 4455 let y0 = container.children[0].rect.y; 4456 assert!((container.children[1].rect.y - y0).abs() < 1.0); 4457 assert!((container.children[2].rect.y - y0).abs() < 1.0); 4458 } 4459 4460 #[test] 4461 fn flex_direction_column() { 4462 let html_str = r#"<!DOCTYPE html> 4463<html><head><style> 4464body { margin: 0; } 4465.container { display: flex; flex-direction: column; width: 600px; } 4466.item { height: 50px; } 4467</style></head> 4468<body> 4469<div class="container"> 4470 <div class="item">A</div> 4471 <div class="item">B</div> 4472 <div class="item">C</div> 4473</div> 4474</body></html>"#; 4475 let doc = we_html::parse_html(html_str); 4476 let tree = layout_doc(&doc); 4477 4478 let body = &tree.root.children[0]; 4479 let container = &body.children[0]; 4480 assert_eq!(container.children.len(), 3); 4481 4482 // Items should be stacked vertically at y offsets: 0, 50, 100 (relative to container). 4483 let cy = container.rect.y; 4484 assert!((container.children[0].rect.y - cy).abs() < 1.0); 4485 assert!( 4486 (container.children[1].rect.y - cy - 50.0).abs() < 1.0, 4487 "second item y should be {}, got {}", 4488 cy + 50.0, 4489 container.children[1].rect.y 4490 ); 4491 assert!( 4492 (container.children[2].rect.y - cy - 100.0).abs() < 1.0, 4493 "third item y should be {}, got {}", 4494 cy + 100.0, 4495 container.children[2].rect.y 4496 ); 4497 } 4498 4499 #[test] 4500 fn flex_grow_distributes_space() { 4501 let html_str = r#"<!DOCTYPE html> 4502<html><head><style> 4503body { margin: 0; } 4504.container { display: flex; width: 600px; } 4505.a { flex-grow: 1; flex-basis: 0; height: 50px; } 4506.b { flex-grow: 2; flex-basis: 0; height: 50px; } 4507.c { flex-grow: 1; flex-basis: 0; height: 50px; } 4508</style></head> 4509<body> 4510<div class="container"> 4511 <div class="a">A</div> 4512 <div class="b">B</div> 4513 <div class="c">C</div> 4514</div> 4515</body></html>"#; 4516 let doc = we_html::parse_html(html_str); 4517 let tree = layout_doc(&doc); 4518 4519 let body = &tree.root.children[0]; 4520 let container = &body.children[0]; 4521 4522 // flex-grow 1:2:1 should split 600px as 150:300:150 4523 let a = &container.children[0]; 4524 let b = &container.children[1]; 4525 let c = &container.children[2]; 4526 4527 assert!( 4528 (a.rect.width - 150.0).abs() < 1.0, 4529 "item A width should be 150, got {}", 4530 a.rect.width 4531 ); 4532 assert!( 4533 (b.rect.width - 300.0).abs() < 1.0, 4534 "item B width should be 300, got {}", 4535 b.rect.width 4536 ); 4537 assert!( 4538 (c.rect.width - 150.0).abs() < 1.0, 4539 "item C width should be 150, got {}", 4540 c.rect.width 4541 ); 4542 } 4543 4544 #[test] 4545 fn flex_justify_content_center() { 4546 let html_str = r#"<!DOCTYPE html> 4547<html><head><style> 4548body { margin: 0; } 4549.container { display: flex; width: 600px; justify-content: center; } 4550.item { width: 100px; height: 50px; } 4551</style></head> 4552<body> 4553<div class="container"> 4554 <div class="item">A</div> 4555 <div class="item">B</div> 4556</div> 4557</body></html>"#; 4558 let doc = we_html::parse_html(html_str); 4559 let tree = layout_doc(&doc); 4560 4561 let body = &tree.root.children[0]; 4562 let container = &body.children[0]; 4563 4564 // 600px container, 200px of items, 400px free space, offset = 200. 4565 let a = &container.children[0]; 4566 let b = &container.children[1]; 4567 4568 assert!( 4569 (a.rect.x - 200.0).abs() < 1.0, 4570 "first item x should be 200, got {}", 4571 a.rect.x 4572 ); 4573 assert!( 4574 (b.rect.x - 300.0).abs() < 1.0, 4575 "second item x should be 300, got {}", 4576 b.rect.x 4577 ); 4578 } 4579 4580 #[test] 4581 fn flex_justify_content_space_between() { 4582 let html_str = r#"<!DOCTYPE html> 4583<html><head><style> 4584body { margin: 0; } 4585.container { display: flex; width: 600px; justify-content: space-between; } 4586.item { width: 100px; height: 50px; } 4587</style></head> 4588<body> 4589<div class="container"> 4590 <div class="item">A</div> 4591 <div class="item">B</div> 4592 <div class="item">C</div> 4593</div> 4594</body></html>"#; 4595 let doc = we_html::parse_html(html_str); 4596 let tree = layout_doc(&doc); 4597 4598 let body = &tree.root.children[0]; 4599 let container = &body.children[0]; 4600 4601 // 3 items of 100px each = 300px, 300px free, between = 150 4602 // Positions: 0, 250, 500 4603 let a = &container.children[0]; 4604 let b = &container.children[1]; 4605 let c = &container.children[2]; 4606 4607 assert!( 4608 (a.rect.x - 0.0).abs() < 1.0, 4609 "first item x should be 0, got {}", 4610 a.rect.x 4611 ); 4612 assert!( 4613 (b.rect.x - 250.0).abs() < 1.0, 4614 "second item x should be 250, got {}", 4615 b.rect.x 4616 ); 4617 assert!( 4618 (c.rect.x - 500.0).abs() < 1.0, 4619 "third item x should be 500, got {}", 4620 c.rect.x 4621 ); 4622 } 4623 4624 #[test] 4625 fn flex_align_items_center() { 4626 let html_str = r#"<!DOCTYPE html> 4627<html><head><style> 4628body { margin: 0; } 4629.container { display: flex; width: 600px; height: 200px; align-items: center; } 4630.item { width: 100px; height: 50px; } 4631</style></head> 4632<body> 4633<div class="container"> 4634 <div class="item">A</div> 4635</div> 4636</body></html>"#; 4637 let doc = we_html::parse_html(html_str); 4638 let tree = layout_doc(&doc); 4639 4640 let body = &tree.root.children[0]; 4641 let container = &body.children[0]; 4642 let a = &container.children[0]; 4643 4644 // Container height=200, item height=50, centered at y = container.y + 75 4645 let expected_y = container.rect.y + 75.0; 4646 assert!( 4647 (a.rect.y - expected_y).abs() < 1.0, 4648 "item y should be {}, got {}", 4649 expected_y, 4650 a.rect.y 4651 ); 4652 } 4653 4654 #[test] 4655 fn flex_wrap_wraps_to_new_line() { 4656 let html_str = r#"<!DOCTYPE html> 4657<html><head><style> 4658body { margin: 0; } 4659.container { display: flex; flex-wrap: wrap; width: 250px; } 4660.item { width: 100px; height: 50px; } 4661</style></head> 4662<body> 4663<div class="container"> 4664 <div class="item">A</div> 4665 <div class="item">B</div> 4666 <div class="item">C</div> 4667</div> 4668</body></html>"#; 4669 let doc = we_html::parse_html(html_str); 4670 let tree = layout_doc(&doc); 4671 4672 let body = &tree.root.children[0]; 4673 let container = &body.children[0]; 4674 4675 // 250px container, 100px items: A and B fit on line 1, C wraps to line 2 4676 let a = &container.children[0]; 4677 let b = &container.children[1]; 4678 let c = &container.children[2]; 4679 4680 // A and B should be on the same row 4681 assert!((a.rect.y - b.rect.y).abs() < 1.0); 4682 // C should be on a different row (50px below) 4683 assert!( 4684 (c.rect.y - a.rect.y - 50.0).abs() < 1.0, 4685 "C should be 50px below A, got y diff {}", 4686 c.rect.y - a.rect.y 4687 ); 4688 } 4689 4690 #[test] 4691 fn flex_gap_adds_spacing() { 4692 let html_str = r#"<!DOCTYPE html> 4693<html><head><style> 4694body { margin: 0; } 4695.container { display: flex; width: 600px; gap: 20px; } 4696.item { width: 100px; height: 50px; } 4697</style></head> 4698<body> 4699<div class="container"> 4700 <div class="item">A</div> 4701 <div class="item">B</div> 4702 <div class="item">C</div> 4703</div> 4704</body></html>"#; 4705 let doc = we_html::parse_html(html_str); 4706 let tree = layout_doc(&doc); 4707 4708 let body = &tree.root.children[0]; 4709 let container = &body.children[0]; 4710 4711 let a = &container.children[0]; 4712 let b = &container.children[1]; 4713 let c = &container.children[2]; 4714 4715 // With gap: 20px, positions should be: 0, 120, 240 4716 assert!((a.rect.x - 0.0).abs() < 1.0); 4717 assert!( 4718 (b.rect.x - 120.0).abs() < 1.0, 4719 "B x should be 120, got {}", 4720 b.rect.x 4721 ); 4722 assert!( 4723 (c.rect.x - 240.0).abs() < 1.0, 4724 "C x should be 240, got {}", 4725 c.rect.x 4726 ); 4727 } 4728 4729 #[test] 4730 fn flex_order_changes_visual_order() { 4731 let html_str = r#"<!DOCTYPE html> 4732<html><head><style> 4733body { margin: 0; } 4734.container { display: flex; width: 600px; } 4735.a { width: 100px; height: 50px; order: 2; } 4736.b { width: 100px; height: 50px; order: 1; } 4737.c { width: 100px; height: 50px; order: 3; } 4738</style></head> 4739<body> 4740<div class="container"> 4741 <div class="a">A</div> 4742 <div class="b">B</div> 4743 <div class="c">C</div> 4744</div> 4745</body></html>"#; 4746 let doc = we_html::parse_html(html_str); 4747 let tree = layout_doc(&doc); 4748 4749 let body = &tree.root.children[0]; 4750 let container = &body.children[0]; 4751 4752 // DOM order: A(order:2), B(order:1), C(order:3) 4753 // Visual order: B(1), A(2), C(3) 4754 // B is at index 1 in DOM, A at 0, C at 2. 4755 // B (index 1) should be first visually (x ≈ 0) 4756 // A (index 0) should be second visually (x ≈ 100) 4757 // C (index 2) should be third visually (x ≈ 200) 4758 let a = &container.children[0]; // DOM index 0, order 2 4759 let b = &container.children[1]; // DOM index 1, order 1 4760 let c = &container.children[2]; // DOM index 2, order 3 4761 4762 assert!( 4763 b.rect.x < a.rect.x, 4764 "B (order:1) should be before A (order:2), B.x={} A.x={}", 4765 b.rect.x, 4766 a.rect.x 4767 ); 4768 assert!( 4769 a.rect.x < c.rect.x, 4770 "A (order:2) should be before C (order:3), A.x={} C.x={}", 4771 a.rect.x, 4772 c.rect.x 4773 ); 4774 } 4775 4776 #[test] 4777 fn flex_shrink_items() { 4778 let html_str = r#"<!DOCTYPE html> 4779<html><head><style> 4780body { margin: 0; } 4781.container { display: flex; width: 200px; } 4782.item { width: 100px; height: 50px; flex-shrink: 1; } 4783</style></head> 4784<body> 4785<div class="container"> 4786 <div class="item">A</div> 4787 <div class="item">B</div> 4788 <div class="item">C</div> 4789</div> 4790</body></html>"#; 4791 let doc = we_html::parse_html(html_str); 4792 let tree = layout_doc(&doc); 4793 4794 let body = &tree.root.children[0]; 4795 let container = &body.children[0]; 4796 4797 // 3 items * 100px = 300px in 200px container, shrink evenly 4798 // Each should be ~66.67px 4799 for (i, child) in container.children.iter().enumerate() { 4800 assert!( 4801 (child.rect.width - 200.0 / 3.0).abs() < 1.0, 4802 "item {} width should be ~66.67, got {}", 4803 i, 4804 child.rect.width 4805 ); 4806 } 4807 } 4808 4809 // ----------------------------------------------------------------------- 4810 // Absolute positioning tests 4811 // ----------------------------------------------------------------------- 4812 4813 /// Helper to create: <html><body>...content...</body></html> and return 4814 /// (doc, html_id, body_id). 4815 fn make_html_body(doc: &mut Document) -> (NodeId, NodeId, NodeId) { 4816 let root = doc.root(); 4817 let html = doc.create_element("html"); 4818 let body = doc.create_element("body"); 4819 doc.append_child(root, html); 4820 doc.append_child(html, body); 4821 (root, html, body) 4822 } 4823 4824 #[test] 4825 fn absolute_positioned_with_top_left() { 4826 let mut doc = Document::new(); 4827 let (_, _, body) = make_html_body(&mut doc); 4828 // <div style="position: relative; width: 400px; height: 300px;"> 4829 // <div style="position: absolute; top: 10px; left: 20px; width: 100px; height: 50px;"></div> 4830 // </div> 4831 let container = doc.create_element("div"); 4832 let abs_child = doc.create_element("div"); 4833 doc.append_child(body, container); 4834 doc.append_child(container, abs_child); 4835 doc.set_attribute( 4836 container, 4837 "style", 4838 "position: relative; width: 400px; height: 300px;", 4839 ); 4840 doc.set_attribute( 4841 abs_child, 4842 "style", 4843 "position: absolute; top: 10px; left: 20px; width: 100px; height: 50px;", 4844 ); 4845 4846 let tree = layout_doc(&doc); 4847 let body_box = &tree.root.children[0]; 4848 let container_box = &body_box.children[0]; 4849 let abs_box = &container_box.children[0]; 4850 4851 assert_eq!(abs_box.position, Position::Absolute); 4852 assert_eq!(abs_box.rect.width, 100.0); 4853 assert_eq!(abs_box.rect.height, 50.0); 4854 4855 // The containing block is the container's padding box. 4856 // Container content starts at container_box.rect.x, container_box.rect.y. 4857 // Padding box x = container_box.rect.x - container_box.padding.left. 4858 let cb_x = container_box.rect.x - container_box.padding.left; 4859 let cb_y = container_box.rect.y - container_box.padding.top; 4860 4861 // abs_box content area x = cb_x + 20 (left) + 0 (margin) + 0 (border) + 0 (padding) 4862 assert!( 4863 (abs_box.rect.x - (cb_x + 20.0)).abs() < 0.01, 4864 "abs x should be cb_x + 20, got {} (cb_x={})", 4865 abs_box.rect.x, 4866 cb_x, 4867 ); 4868 assert!( 4869 (abs_box.rect.y - (cb_y + 10.0)).abs() < 0.01, 4870 "abs y should be cb_y + 10, got {} (cb_y={})", 4871 abs_box.rect.y, 4872 cb_y, 4873 ); 4874 } 4875 4876 #[test] 4877 fn absolute_does_not_affect_sibling_layout() { 4878 let mut doc = Document::new(); 4879 let (_, _, body) = make_html_body(&mut doc); 4880 // <div style="position: relative;"> 4881 // <div style="position: absolute; top: 0; left: 0; width: 100px; height: 100px;"></div> 4882 // <p>Normal text</p> 4883 // </div> 4884 let container = doc.create_element("div"); 4885 let abs_child = doc.create_element("div"); 4886 let p = doc.create_element("p"); 4887 let text = doc.create_text("Normal text"); 4888 doc.append_child(body, container); 4889 doc.append_child(container, abs_child); 4890 doc.append_child(container, p); 4891 doc.append_child(p, text); 4892 doc.set_attribute(container, "style", "position: relative;"); 4893 doc.set_attribute( 4894 abs_child, 4895 "style", 4896 "position: absolute; top: 0; left: 0; width: 100px; height: 100px;", 4897 ); 4898 4899 let tree = layout_doc(&doc); 4900 let body_box = &tree.root.children[0]; 4901 let container_box = &body_box.children[0]; 4902 4903 // The <p> should start at the top of the container (abs child doesn't 4904 // push it down). Find the first in-flow child. 4905 let p_box = container_box 4906 .children 4907 .iter() 4908 .find(|c| c.position != Position::Absolute && c.position != Position::Fixed) 4909 .expect("should have an in-flow child"); 4910 4911 // p's y should be at (or very near) the container's content y. 4912 let expected_y = container_box.rect.y; 4913 assert!( 4914 (p_box.rect.y - expected_y).abs() < 1.0, 4915 "p should start near container top: p.y={}, expected={}", 4916 p_box.rect.y, 4917 expected_y, 4918 ); 4919 } 4920 4921 #[test] 4922 fn fixed_positioned_relative_to_viewport() { 4923 let mut doc = Document::new(); 4924 let (_, _, body) = make_html_body(&mut doc); 4925 // <div style="position: fixed; top: 5px; left: 10px; width: 50px; height: 30px;"></div> 4926 let fixed = doc.create_element("div"); 4927 doc.append_child(body, fixed); 4928 doc.set_attribute( 4929 fixed, 4930 "style", 4931 "position: fixed; top: 5px; left: 10px; width: 50px; height: 30px;", 4932 ); 4933 4934 let tree = layout_doc(&doc); 4935 let body_box = &tree.root.children[0]; 4936 let fixed_box = &body_box.children[0]; 4937 4938 assert_eq!(fixed_box.position, Position::Fixed); 4939 assert_eq!(fixed_box.rect.width, 50.0); 4940 assert_eq!(fixed_box.rect.height, 30.0); 4941 4942 // Fixed should be relative to viewport (0,0). 4943 assert!( 4944 (fixed_box.rect.x - 10.0).abs() < 0.01, 4945 "fixed x should be 10, got {}", 4946 fixed_box.rect.x, 4947 ); 4948 assert!( 4949 (fixed_box.rect.y - 5.0).abs() < 0.01, 4950 "fixed y should be 5, got {}", 4951 fixed_box.rect.y, 4952 ); 4953 } 4954 4955 #[test] 4956 fn z_index_ordering() { 4957 let mut doc = Document::new(); 4958 let (_, _, body) = make_html_body(&mut doc); 4959 // Create a positioned container with two abs children at different z-index. 4960 let container = doc.create_element("div"); 4961 let low = doc.create_element("div"); 4962 let high = doc.create_element("div"); 4963 doc.append_child(body, container); 4964 doc.append_child(container, low); 4965 doc.append_child(container, high); 4966 doc.set_attribute( 4967 container, 4968 "style", 4969 "position: relative; width: 200px; height: 200px;", 4970 ); 4971 doc.set_attribute( 4972 low, 4973 "style", 4974 "position: absolute; z-index: 1; top: 0; left: 0; width: 50px; height: 50px;", 4975 ); 4976 doc.set_attribute( 4977 high, 4978 "style", 4979 "position: absolute; z-index: 5; top: 0; left: 0; width: 50px; height: 50px;", 4980 ); 4981 4982 let tree = layout_doc(&doc); 4983 let body_box = &tree.root.children[0]; 4984 let container_box = &body_box.children[0]; 4985 4986 // Both should have correct z-index stored. 4987 let low_box = &container_box.children[0]; 4988 let high_box = &container_box.children[1]; 4989 assert_eq!(low_box.z_index, Some(1)); 4990 assert_eq!(high_box.z_index, Some(5)); 4991 } 4992 4993 #[test] 4994 fn absolute_stretching_left_right() { 4995 let mut doc = Document::new(); 4996 let (_, _, body) = make_html_body(&mut doc); 4997 // <div style="position: relative; width: 400px; height: 300px;"> 4998 // <div style="position: absolute; left: 10px; right: 20px; height: 50px;"></div> 4999 // </div> 5000 let container = doc.create_element("div"); 5001 let abs_child = doc.create_element("div"); 5002 doc.append_child(body, container); 5003 doc.append_child(container, abs_child); 5004 doc.set_attribute( 5005 container, 5006 "style", 5007 "position: relative; width: 400px; height: 300px;", 5008 ); 5009 doc.set_attribute( 5010 abs_child, 5011 "style", 5012 "position: absolute; left: 10px; right: 20px; height: 50px;", 5013 ); 5014 5015 let tree = layout_doc(&doc); 5016 let body_box = &tree.root.children[0]; 5017 let container_box = &body_box.children[0]; 5018 let abs_box = &container_box.children[0]; 5019 5020 // Width should be: container_width - left - right = 400 - 10 - 20 = 370 5021 assert!( 5022 (abs_box.rect.width - 370.0).abs() < 0.01, 5023 "stretched width should be 370, got {}", 5024 abs_box.rect.width, 5025 ); 5026 } 5027 5028 #[test] 5029 fn absolute_bottom_right_positioning() { 5030 let mut doc = Document::new(); 5031 let (_, _, body) = make_html_body(&mut doc); 5032 // <div style="position: relative; width: 400px; height: 300px;"> 5033 // <div style="position: absolute; bottom: 10px; right: 20px; width: 50px; height: 30px;"></div> 5034 // </div> 5035 let container = doc.create_element("div"); 5036 let abs_child = doc.create_element("div"); 5037 doc.append_child(body, container); 5038 doc.append_child(container, abs_child); 5039 doc.set_attribute( 5040 container, 5041 "style", 5042 "position: relative; width: 400px; height: 300px;", 5043 ); 5044 doc.set_attribute( 5045 abs_child, 5046 "style", 5047 "position: absolute; bottom: 10px; right: 20px; width: 50px; height: 30px;", 5048 ); 5049 5050 let tree = layout_doc(&doc); 5051 let body_box = &tree.root.children[0]; 5052 let container_box = &body_box.children[0]; 5053 let abs_box = &container_box.children[0]; 5054 5055 let cb_x = container_box.rect.x - container_box.padding.left; 5056 let cb_y = container_box.rect.y - container_box.padding.top; 5057 let cb_w = 5058 container_box.rect.width + container_box.padding.left + container_box.padding.right; 5059 let cb_h = 5060 container_box.rect.height + container_box.padding.top + container_box.padding.bottom; 5061 5062 // x = cb_x + cb_w - right - width = cb_x + cb_w - 20 - 50 5063 let expected_x = cb_x + cb_w - 20.0 - 50.0; 5064 // y = cb_y + cb_h - bottom - height = cb_y + cb_h - 10 - 30 5065 let expected_y = cb_y + cb_h - 10.0 - 30.0; 5066 5067 assert!( 5068 (abs_box.rect.x - expected_x).abs() < 0.01, 5069 "abs x with right should be {}, got {}", 5070 expected_x, 5071 abs_box.rect.x, 5072 ); 5073 assert!( 5074 (abs_box.rect.y - expected_y).abs() < 0.01, 5075 "abs y with bottom should be {}, got {}", 5076 expected_y, 5077 abs_box.rect.y, 5078 ); 5079 } 5080 5081 // ----------------------------------------------------------------------- 5082 // Sticky positioning tests 5083 // ----------------------------------------------------------------------- 5084 5085 #[test] 5086 fn sticky_element_laid_out_in_normal_flow() { 5087 // A sticky element should participate in normal flow just like a 5088 // static or relative element — its siblings should be positioned 5089 // as if sticky doesn't exist. 5090 let mut doc = Document::new(); 5091 let (_, _, body) = make_html_body(&mut doc); 5092 let container = doc.create_element("div"); 5093 let before = doc.create_element("div"); 5094 let sticky = doc.create_element("div"); 5095 let after = doc.create_element("div"); 5096 doc.append_child(body, container); 5097 doc.append_child(container, before); 5098 doc.append_child(container, sticky); 5099 doc.append_child(container, after); 5100 5101 doc.set_attribute(container, "style", "width: 400px;"); 5102 doc.set_attribute(before, "style", "height: 50px;"); 5103 doc.set_attribute(sticky, "style", "position: sticky; top: 0; height: 30px;"); 5104 doc.set_attribute(after, "style", "height: 60px;"); 5105 5106 let tree = layout_doc(&doc); 5107 let body_box = &tree.root.children[0]; 5108 let container_box = &body_box.children[0]; 5109 5110 // All three children should be present (sticky is in-flow). 5111 let in_flow: Vec<&LayoutBox> = container_box.children.iter().collect(); 5112 assert!( 5113 in_flow.len() >= 3, 5114 "expected 3 children, got {}", 5115 in_flow.len() 5116 ); 5117 5118 let before_box = &in_flow[0]; 5119 let sticky_box = &in_flow[1]; 5120 let after_box = &in_flow[2]; 5121 5122 assert_eq!(sticky_box.position, Position::Sticky); 5123 5124 // Sticky element should be right after 'before' (at y = before.y + 50). 5125 let expected_sticky_y = before_box.rect.y + 50.0; 5126 assert!( 5127 (sticky_box.rect.y - expected_sticky_y).abs() < 1.0, 5128 "sticky y should be ~{}, got {}", 5129 expected_sticky_y, 5130 sticky_box.rect.y, 5131 ); 5132 5133 // 'after' should follow the sticky element (at y = sticky.y + 30). 5134 let expected_after_y = sticky_box.rect.y + 30.0; 5135 assert!( 5136 (after_box.rect.y - expected_after_y).abs() < 1.0, 5137 "after y should be ~{}, got {}", 5138 expected_after_y, 5139 after_box.rect.y, 5140 ); 5141 } 5142 5143 #[test] 5144 fn sticky_constraint_rect_is_set() { 5145 // The layout engine should set `sticky_constraint` to the parent's 5146 // content rect for sticky children. 5147 let mut doc = Document::new(); 5148 let (_, _, body) = make_html_body(&mut doc); 5149 let container = doc.create_element("div"); 5150 let sticky = doc.create_element("div"); 5151 doc.append_child(body, container); 5152 doc.append_child(container, sticky); 5153 5154 doc.set_attribute(container, "style", "width: 400px; height: 300px;"); 5155 doc.set_attribute(sticky, "style", "position: sticky; top: 0; height: 50px;"); 5156 5157 let tree = layout_doc(&doc); 5158 let body_box = &tree.root.children[0]; 5159 let container_box = &body_box.children[0]; 5160 let sticky_box = &container_box.children[0]; 5161 5162 assert_eq!(sticky_box.position, Position::Sticky); 5163 let constraint = sticky_box 5164 .sticky_constraint 5165 .expect("sticky element should have a constraint rect"); 5166 5167 // Constraint should match the container's content rect. 5168 assert!( 5169 (constraint.width - container_box.rect.width).abs() < 0.01, 5170 "constraint width should match container: {} vs {}", 5171 constraint.width, 5172 container_box.rect.width, 5173 ); 5174 assert!( 5175 (constraint.height - container_box.rect.height).abs() < 0.01, 5176 "constraint height should match container: {} vs {}", 5177 constraint.height, 5178 container_box.rect.height, 5179 ); 5180 } 5181 5182 // ----------------------------------------------------------------------- 5183 // Float layout tests 5184 // ----------------------------------------------------------------------- 5185 5186 #[test] 5187 fn float_left_positioned_at_left_edge() { 5188 // A float:left element should be placed at the left edge of the container. 5189 let mut doc = Document::new(); 5190 let root = doc.root(); 5191 let html = doc.create_element("html"); 5192 let body = doc.create_element("body"); 5193 let container = doc.create_element("div"); 5194 let float_elem = doc.create_element("div"); 5195 doc.append_child(root, html); 5196 doc.append_child(html, body); 5197 doc.append_child(body, container); 5198 doc.append_child(container, float_elem); 5199 doc.set_attribute(container, "style", "width: 400px;"); 5200 doc.set_attribute( 5201 float_elem, 5202 "style", 5203 "float: left; width: 100px; height: 50px;", 5204 ); 5205 5206 let tree = layout_doc(&doc); 5207 let body_box = &tree.root.children[0]; 5208 let container_box = &body_box.children[0]; 5209 let float_box = &container_box.children[0]; 5210 5211 assert_eq!(float_box.float, Float::Left); 5212 assert!( 5213 (float_box.rect.width - 100.0).abs() < 0.01, 5214 "float width: {}", 5215 float_box.rect.width 5216 ); 5217 assert!( 5218 (float_box.rect.height - 50.0).abs() < 0.01, 5219 "float height: {}", 5220 float_box.rect.height 5221 ); 5222 // Float should be at the left edge of the container (same x as container content). 5223 assert!( 5224 (float_box.rect.x - container_box.rect.x).abs() < 0.01, 5225 "float x should be at container left edge: {} vs {}", 5226 float_box.rect.x, 5227 container_box.rect.x, 5228 ); 5229 } 5230 5231 #[test] 5232 fn float_right_positioned_at_right_edge() { 5233 let mut doc = Document::new(); 5234 let root = doc.root(); 5235 let html = doc.create_element("html"); 5236 let body = doc.create_element("body"); 5237 let container = doc.create_element("div"); 5238 let float_elem = doc.create_element("div"); 5239 doc.append_child(root, html); 5240 doc.append_child(html, body); 5241 doc.append_child(body, container); 5242 doc.append_child(container, float_elem); 5243 doc.set_attribute(container, "style", "width: 400px;"); 5244 doc.set_attribute( 5245 float_elem, 5246 "style", 5247 "float: right; width: 100px; height: 50px;", 5248 ); 5249 5250 let tree = layout_doc(&doc); 5251 let body_box = &tree.root.children[0]; 5252 let container_box = &body_box.children[0]; 5253 let float_box = &container_box.children[0]; 5254 5255 assert_eq!(float_box.float, Float::Right); 5256 // Float's right edge (content + padding + border + margin) should align 5257 // with the container's right content edge. 5258 let float_right = float_box.rect.x + float_box.rect.width; 5259 let container_right = container_box.rect.x + container_box.rect.width; 5260 assert!( 5261 (float_right - container_right).abs() < 0.01, 5262 "float right edge {} should match container right edge {}", 5263 float_right, 5264 container_right, 5265 ); 5266 } 5267 5268 #[test] 5269 fn two_left_floats_stack_horizontally() { 5270 let mut doc = Document::new(); 5271 let root = doc.root(); 5272 let html = doc.create_element("html"); 5273 let body = doc.create_element("body"); 5274 let container = doc.create_element("div"); 5275 let float1 = doc.create_element("div"); 5276 let float2 = doc.create_element("div"); 5277 doc.append_child(root, html); 5278 doc.append_child(html, body); 5279 doc.append_child(body, container); 5280 doc.append_child(container, float1); 5281 doc.append_child(container, float2); 5282 doc.set_attribute(container, "style", "width: 400px;"); 5283 doc.set_attribute(float1, "style", "float: left; width: 100px; height: 50px;"); 5284 doc.set_attribute(float2, "style", "float: left; width: 120px; height: 50px;"); 5285 5286 let tree = layout_doc(&doc); 5287 let body_box = &tree.root.children[0]; 5288 let container_box = &body_box.children[0]; 5289 let f1 = &container_box.children[0]; 5290 let f2 = &container_box.children[1]; 5291 5292 // Second float should be placed to the right of the first. 5293 let f1_right = 5294 f1.rect.x + f1.rect.width + f1.padding.right + f1.border.right + f1.margin.right; 5295 assert!( 5296 f2.rect.x >= f1_right - 0.01, 5297 "second float x ({}) should be >= first float right edge ({})", 5298 f2.rect.x, 5299 f1_right, 5300 ); 5301 // Both should be on the same row (same y). 5302 assert!( 5303 (f1.rect.y - f2.rect.y).abs() < 0.01, 5304 "floats should be on the same row: {} vs {}", 5305 f1.rect.y, 5306 f2.rect.y, 5307 ); 5308 } 5309 5310 #[test] 5311 fn clear_both_moves_below_floats() { 5312 let mut doc = Document::new(); 5313 let root = doc.root(); 5314 let html = doc.create_element("html"); 5315 let body = doc.create_element("body"); 5316 let container = doc.create_element("div"); 5317 let float_elem = doc.create_element("div"); 5318 let cleared = doc.create_element("div"); 5319 let text = doc.create_text("After clear"); 5320 doc.append_child(root, html); 5321 doc.append_child(html, body); 5322 doc.append_child(body, container); 5323 doc.append_child(container, float_elem); 5324 doc.append_child(container, cleared); 5325 doc.append_child(cleared, text); 5326 doc.set_attribute(container, "style", "width: 400px;"); 5327 doc.set_attribute( 5328 float_elem, 5329 "style", 5330 "float: left; width: 100px; height: 80px;", 5331 ); 5332 doc.set_attribute(cleared, "style", "clear: both;"); 5333 5334 let tree = layout_doc(&doc); 5335 let body_box = &tree.root.children[0]; 5336 let container_box = &body_box.children[0]; 5337 5338 // Find the cleared element (skip the float). 5339 let float_box = &container_box.children[0]; 5340 let cleared_box = &container_box.children[1]; 5341 5342 let float_bottom = float_box.rect.y 5343 + float_box.rect.height 5344 + float_box.padding.bottom 5345 + float_box.border.bottom 5346 + float_box.margin.bottom; 5347 assert!( 5348 cleared_box.rect.y >= float_bottom - 0.01, 5349 "cleared element y ({}) should be >= float bottom ({})", 5350 cleared_box.rect.y, 5351 float_bottom, 5352 ); 5353 } 5354 5355 #[test] 5356 fn bfc_contains_floats() { 5357 // A container with overflow:hidden (establishes BFC) should expand 5358 // to contain its floated children. 5359 let mut doc = Document::new(); 5360 let root = doc.root(); 5361 let html = doc.create_element("html"); 5362 let body = doc.create_element("body"); 5363 let container = doc.create_element("div"); 5364 let float_elem = doc.create_element("div"); 5365 doc.append_child(root, html); 5366 doc.append_child(html, body); 5367 doc.append_child(body, container); 5368 doc.append_child(container, float_elem); 5369 doc.set_attribute(container, "style", "width: 400px; overflow: hidden;"); 5370 doc.set_attribute( 5371 float_elem, 5372 "style", 5373 "float: left; width: 100px; height: 150px;", 5374 ); 5375 5376 let tree = layout_doc(&doc); 5377 let body_box = &tree.root.children[0]; 5378 let container_box = &body_box.children[0]; 5379 5380 // Container should be at least 150px tall to contain the float. 5381 assert!( 5382 container_box.rect.height >= 150.0 - 0.01, 5383 "BFC container height ({}) should be >= float height (150)", 5384 container_box.rect.height, 5385 ); 5386 } 5387 5388 #[test] 5389 fn inline_text_wraps_around_float() { 5390 // Text in a block should flow around a float. 5391 let mut doc = Document::new(); 5392 let root = doc.root(); 5393 let html = doc.create_element("html"); 5394 let body = doc.create_element("body"); 5395 let container = doc.create_element("div"); 5396 let float_elem = doc.create_element("div"); 5397 let text_p = doc.create_element("p"); 5398 let text = doc.create_text("Some text content that wraps"); 5399 doc.append_child(root, html); 5400 doc.append_child(html, body); 5401 doc.append_child(body, container); 5402 doc.append_child(container, float_elem); 5403 doc.append_child(container, text_p); 5404 doc.append_child(text_p, text); 5405 doc.set_attribute(container, "style", "width: 400px;"); 5406 doc.set_attribute( 5407 float_elem, 5408 "style", 5409 "float: left; width: 100px; height: 80px;", 5410 ); 5411 5412 let tree = layout_doc(&doc); 5413 let body_box = &tree.root.children[0]; 5414 let container_box = &body_box.children[0]; 5415 5416 // Find the text paragraph (it should be the second child, after the float). 5417 let text_box = &container_box.children[1]; 5418 5419 // The text lines that overlap with the float should be offset to 5420 // the right of the float. Check that the first line's x is shifted. 5421 if !text_box.lines.is_empty() { 5422 let first_line = &text_box.lines[0]; 5423 let float_box = &container_box.children[0]; 5424 let float_right = float_box.rect.x 5425 + float_box.rect.width 5426 + float_box.padding.right 5427 + float_box.border.right 5428 + float_box.margin.right; 5429 assert!( 5430 first_line.x >= float_right - 0.01, 5431 "first line x ({}) should be >= float right edge ({})", 5432 first_line.x, 5433 float_right, 5434 ); 5435 } 5436 } 5437 5438 #[test] 5439 fn bare_text_alongside_float_is_laid_out() { 5440 // When a container has only float children + bare text (no block 5441 // siblings), the text should still be wrapped in an anonymous block 5442 // and laid out correctly. 5443 let mut doc = Document::new(); 5444 let root = doc.root(); 5445 let html = doc.create_element("html"); 5446 let body = doc.create_element("body"); 5447 let container = doc.create_element("div"); 5448 let float_elem = doc.create_element("div"); 5449 let text = doc.create_text("Hello world"); 5450 doc.append_child(root, html); 5451 doc.append_child(html, body); 5452 doc.append_child(body, container); 5453 doc.append_child(container, float_elem); 5454 doc.append_child(container, text); 5455 doc.set_attribute(container, "style", "width: 400px;"); 5456 doc.set_attribute( 5457 float_elem, 5458 "style", 5459 "float: left; width: 100px; height: 50px;", 5460 ); 5461 5462 let tree = layout_doc(&doc); 5463 let body_box = &tree.root.children[0]; 5464 let container_box = &body_box.children[0]; 5465 5466 // The text should be wrapped in an anonymous block and produce text lines. 5467 let has_text = container_box.children.iter().any(|c| !c.lines.is_empty()); 5468 assert!( 5469 has_text, 5470 "bare text alongside a float should be laid out (found {} children, none with text lines)", 5471 container_box.children.len(), 5472 ); 5473 } 5474}