we (web engine): Experimental web browser project to understand the limits of Claude

Implement absolute and fixed positioning with z-index stacking

- Add z_index field to ComputedStyle and parse CSS z-index property
- Remove absolute/fixed elements from normal flow (block, inline, flex layout)
- Position absolute elements relative to nearest positioned ancestor's padding box
- Position fixed elements relative to viewport
- Support left/right/top/bottom offsets with percentage resolution
- Support width/height stretching when both sides are specified
- Implement stacking context paint order: negative z-index, in-flow, non-negative z-index
- Update normalize_children and margin collapsing to exclude out-of-flow elements
- Add tests for absolute positioning, fixed positioning, z-index, stretching, and
bottom/right positioning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+763 -24
+660 -20
crates/layout/src/lib.rs
··· 96 96 pub replaced_size: Option<(f32, f32)>, 97 97 /// CSS `position` property. 98 98 pub position: Position, 99 + /// CSS `z-index` property (None = auto). 100 + pub z_index: Option<i32>, 99 101 /// Relative position offset (dx, dy) applied after normal flow layout. 100 102 pub relative_offset: (f32, f32), 101 103 /// CSS `overflow` property. ··· 164 166 line_height: style.line_height, 165 167 replaced_size: None, 166 168 position: style.position, 169 + z_index: style.z_index, 167 170 relative_offset: (0.0, 0.0), 168 171 overflow: style.overflow, 169 172 box_sizing: style.box_sizing, ··· 434 437 435 438 /// If a block container has a mix of block-level and inline-level children, 436 439 /// wrap consecutive inline runs in anonymous block boxes. 440 + /// Out-of-flow elements (absolute/fixed) are excluded from the determination 441 + /// and placed at the top level without affecting normalization. 437 442 fn normalize_children(children: Vec<LayoutBox>, parent_style: &ComputedStyle) -> Vec<LayoutBox> { 438 443 if children.is_empty() { 439 444 return children; 440 445 } 441 446 442 - let has_block = children.iter().any(is_block_level); 447 + // Only consider in-flow children for block/inline normalization. 448 + let has_block = children.iter().any(|c| is_in_flow(c) && is_block_level(c)); 443 449 if !has_block { 444 450 return children; 445 451 } 446 452 447 - let has_inline = children.iter().any(|c| !is_block_level(c)); 453 + let has_inline = children.iter().any(|c| is_in_flow(c) && !is_block_level(c)); 448 454 if !has_inline { 449 455 return children; 450 456 } ··· 453 459 let mut inline_group: Vec<LayoutBox> = Vec::new(); 454 460 455 461 for child in children { 456 - if is_block_level(&child) { 462 + if !is_in_flow(&child) || is_block_level(&child) { 463 + // Out-of-flow or block-level: flush any pending inline group first. 457 464 if !inline_group.is_empty() { 458 465 let mut anon = LayoutBox::new(BoxType::Anonymous, parent_style); 459 466 anon.children = std::mem::take(&mut inline_group); ··· 478 485 matches!(b.box_type, BoxType::Block(_) | BoxType::Anonymous) 479 486 } 480 487 488 + /// Returns `true` if this box is in normal flow (not absolutely or fixed positioned). 489 + fn is_in_flow(b: &LayoutBox) -> bool { 490 + b.position != Position::Absolute && b.position != Position::Fixed 491 + } 492 + 481 493 // --------------------------------------------------------------------------- 482 494 // Layout algorithm 483 495 // --------------------------------------------------------------------------- ··· 487 499 /// `available_width` is the containing block width — used as the reference for 488 500 /// percentage widths, margins, and paddings (per CSS spec, even vertical margins/ 489 501 /// padding resolve against the containing block width). 502 + /// 503 + /// `abs_cb` is the padding box of the nearest positioned ancestor, used as the 504 + /// containing block for absolutely positioned descendants. 505 + #[allow(clippy::too_many_arguments)] 490 506 fn compute_layout( 491 507 b: &mut LayoutBox, 492 508 x: f32, 493 509 y: f32, 494 510 available_width: f32, 511 + viewport_width: f32, 495 512 viewport_height: f32, 496 513 font: &Font, 497 514 doc: &Document, 515 + abs_cb: Rect, 498 516 ) { 499 517 // Resolve percentage margins against containing block width. 500 518 // Only re-resolve percentages — absolute margins may have been modified ··· 562 580 if let Some((rw, rh)) = b.replaced_size { 563 581 b.rect.width = rw.min(b.rect.width); 564 582 b.rect.height = rh; 583 + layout_abspos_children(b, abs_cb, viewport_width, viewport_height, font, doc); 565 584 apply_relative_offset(b, available_width, viewport_height); 566 585 return; 567 586 } ··· 569 588 match &b.box_type { 570 589 BoxType::Block(_) | BoxType::Anonymous => { 571 590 if matches!(b.display, Display::Flex | Display::InlineFlex) { 572 - layout_flex_children(b, viewport_height, font, doc); 591 + layout_flex_children(b, viewport_width, viewport_height, font, doc, abs_cb); 573 592 } else if has_block_children(b) { 574 - layout_block_children(b, viewport_height, font, doc); 593 + layout_block_children(b, viewport_width, viewport_height, font, doc, abs_cb); 575 594 } else { 576 595 layout_inline_children(b, font, doc); 577 596 } ··· 606 625 } 607 626 LengthOrAuto::Auto => {} 608 627 } 628 + 629 + // Layout absolutely and fixed positioned children after this box's 630 + // dimensions are fully resolved. 631 + layout_abspos_children(b, abs_cb, viewport_width, viewport_height, font, doc); 609 632 610 633 apply_relative_offset(b, available_width, viewport_height); 611 634 } ··· 641 664 } 642 665 } 643 666 667 + // --------------------------------------------------------------------------- 668 + // Absolute / fixed positioning 669 + // --------------------------------------------------------------------------- 670 + 671 + /// Resolve a `LengthOrAuto` offset to an optional pixel value. 672 + /// Returns `None` for `Auto` (offset not specified). 673 + fn resolve_offset(value: LengthOrAuto, reference: f32) -> Option<f32> { 674 + match value { 675 + LengthOrAuto::Length(px) => Some(px), 676 + LengthOrAuto::Percentage(p) => Some(p / 100.0 * reference), 677 + LengthOrAuto::Auto => None, 678 + } 679 + } 680 + 681 + /// Compute the padding box rectangle for a layout box. 682 + fn padding_box_rect(b: &LayoutBox) -> Rect { 683 + Rect { 684 + x: b.rect.x - b.padding.left, 685 + y: b.rect.y - b.padding.top, 686 + width: b.rect.width + b.padding.left + b.padding.right, 687 + height: b.rect.height + b.padding.top + b.padding.bottom, 688 + } 689 + } 690 + 691 + /// Lay out all absolutely and fixed positioned children of `parent`. 692 + /// 693 + /// `abs_cb` is the padding box of the nearest positioned ancestor passed from 694 + /// further up the tree. If `parent` itself is positioned (not `static`), its 695 + /// padding box becomes the new containing block for absolute descendants. 696 + fn layout_abspos_children( 697 + parent: &mut LayoutBox, 698 + abs_cb: Rect, 699 + viewport_width: f32, 700 + viewport_height: f32, 701 + font: &Font, 702 + doc: &Document, 703 + ) { 704 + let viewport_cb = Rect { 705 + x: 0.0, 706 + y: 0.0, 707 + width: viewport_width, 708 + height: viewport_height, 709 + }; 710 + 711 + // If this box is positioned, it becomes the containing block for absolute 712 + // descendants. 713 + let new_abs_cb = if parent.position != Position::Static { 714 + padding_box_rect(parent) 715 + } else { 716 + abs_cb 717 + }; 718 + 719 + for i in 0..parent.children.len() { 720 + if parent.children[i].position == Position::Absolute { 721 + layout_absolute_child( 722 + &mut parent.children[i], 723 + new_abs_cb, 724 + viewport_width, 725 + viewport_height, 726 + font, 727 + doc, 728 + ); 729 + } else if parent.children[i].position == Position::Fixed { 730 + layout_absolute_child( 731 + &mut parent.children[i], 732 + viewport_cb, 733 + viewport_width, 734 + viewport_height, 735 + font, 736 + doc, 737 + ); 738 + } 739 + } 740 + } 741 + 742 + /// Lay out a single absolutely or fixed positioned element. 743 + /// 744 + /// `cb` is the containing block (padding box of nearest positioned ancestor 745 + /// for absolute, or the viewport for fixed). 746 + fn layout_absolute_child( 747 + child: &mut LayoutBox, 748 + cb: Rect, 749 + viewport_width: f32, 750 + viewport_height: f32, 751 + font: &Font, 752 + doc: &Document, 753 + ) { 754 + let [css_top, css_right, css_bottom, css_left] = child.css_offsets; 755 + 756 + // Resolve margins against containing block width. 757 + child.margin = EdgeSizes { 758 + top: resolve_length_against(child.css_margin[0], cb.width), 759 + right: resolve_length_against(child.css_margin[1], cb.width), 760 + bottom: resolve_length_against(child.css_margin[2], cb.width), 761 + left: resolve_length_against(child.css_margin[3], cb.width), 762 + }; 763 + 764 + // Resolve padding against containing block width. 765 + child.padding = EdgeSizes { 766 + top: resolve_length_against(child.css_padding[0], cb.width), 767 + right: resolve_length_against(child.css_padding[1], cb.width), 768 + bottom: resolve_length_against(child.css_padding[2], cb.width), 769 + left: resolve_length_against(child.css_padding[3], cb.width), 770 + }; 771 + 772 + let horiz_extra = 773 + child.border.left + child.border.right + child.padding.left + child.padding.right; 774 + let vert_extra = 775 + child.border.top + child.border.bottom + child.padding.top + child.padding.bottom; 776 + 777 + // Resolve offsets. 778 + let left_offset = resolve_offset(css_left, cb.width); 779 + let right_offset = resolve_offset(css_right, cb.width); 780 + let top_offset = resolve_offset(css_top, cb.height); 781 + let bottom_offset = resolve_offset(css_bottom, cb.height); 782 + 783 + // --- Resolve content width --- 784 + let content_width = match child.css_width { 785 + LengthOrAuto::Length(w) => match child.box_sizing { 786 + BoxSizing::ContentBox => w.max(0.0), 787 + BoxSizing::BorderBox => (w - horiz_extra).max(0.0), 788 + }, 789 + LengthOrAuto::Percentage(p) => { 790 + let resolved = p / 100.0 * cb.width; 791 + match child.box_sizing { 792 + BoxSizing::ContentBox => resolved.max(0.0), 793 + BoxSizing::BorderBox => (resolved - horiz_extra).max(0.0), 794 + } 795 + } 796 + LengthOrAuto::Auto => { 797 + // If both left and right are specified, stretch to fill. 798 + if let (Some(l), Some(r)) = (left_offset, right_offset) { 799 + (cb.width - l - r - child.margin.left - child.margin.right - horiz_extra).max(0.0) 800 + } else { 801 + // Use available width minus margins. 802 + (cb.width - child.margin.left - child.margin.right - horiz_extra).max(0.0) 803 + } 804 + } 805 + }; 806 + 807 + child.rect.width = content_width; 808 + 809 + // --- Resolve horizontal position --- 810 + child.rect.x = if let Some(l) = left_offset { 811 + cb.x + l + child.margin.left + child.border.left + child.padding.left 812 + } else if let Some(r) = right_offset { 813 + cb.x + cb.width 814 + - r 815 + - child.margin.right 816 + - child.border.right 817 + - child.padding.right 818 + - content_width 819 + } else { 820 + // Static position: containing block origin. 821 + cb.x + child.margin.left + child.border.left + child.padding.left 822 + }; 823 + 824 + // Set a temporary y for child content layout. 825 + child.rect.y = cb.y; 826 + 827 + // For overflow:scroll, reserve scrollbar width. 828 + if child.overflow == Overflow::Scroll { 829 + child.rect.width = (child.rect.width - SCROLLBAR_WIDTH).max(0.0); 830 + } 831 + 832 + // --- Layout child's own content --- 833 + if let Some((rw, rh)) = child.replaced_size { 834 + child.rect.width = rw.min(child.rect.width); 835 + child.rect.height = rh; 836 + } else { 837 + let child_abs_cb = if child.position != Position::Static { 838 + padding_box_rect(child) 839 + } else { 840 + cb 841 + }; 842 + match &child.box_type { 843 + BoxType::Block(_) | BoxType::Anonymous => { 844 + if matches!(child.display, Display::Flex | Display::InlineFlex) { 845 + layout_flex_children( 846 + child, 847 + viewport_width, 848 + viewport_height, 849 + font, 850 + doc, 851 + child_abs_cb, 852 + ); 853 + } else if has_block_children(child) { 854 + layout_block_children( 855 + child, 856 + viewport_width, 857 + viewport_height, 858 + font, 859 + doc, 860 + child_abs_cb, 861 + ); 862 + } else { 863 + layout_inline_children(child, font, doc); 864 + } 865 + } 866 + _ => {} 867 + } 868 + } 869 + 870 + // Save natural content height. 871 + child.content_height = child.rect.height; 872 + 873 + // --- Resolve content height --- 874 + match child.css_height { 875 + LengthOrAuto::Length(h) => { 876 + child.rect.height = match child.box_sizing { 877 + BoxSizing::ContentBox => h.max(0.0), 878 + BoxSizing::BorderBox => (h - vert_extra).max(0.0), 879 + }; 880 + } 881 + LengthOrAuto::Percentage(p) => { 882 + let resolved = p / 100.0 * cb.height; 883 + child.rect.height = match child.box_sizing { 884 + BoxSizing::ContentBox => resolved.max(0.0), 885 + BoxSizing::BorderBox => (resolved - vert_extra).max(0.0), 886 + }; 887 + } 888 + LengthOrAuto::Auto => { 889 + // If both top and bottom are specified, stretch. 890 + if let (Some(t), Some(b_val)) = (top_offset, bottom_offset) { 891 + child.rect.height = 892 + (cb.height - t - b_val - child.margin.top - child.margin.bottom - vert_extra) 893 + .max(0.0); 894 + } 895 + // Otherwise keep content height. 896 + } 897 + } 898 + 899 + // --- Resolve vertical position --- 900 + let final_y = if let Some(t) = top_offset { 901 + cb.y + t + child.margin.top + child.border.top + child.padding.top 902 + } else if let Some(b_val) = bottom_offset { 903 + cb.y + cb.height 904 + - b_val 905 + - child.margin.bottom 906 + - child.border.bottom 907 + - child.padding.bottom 908 + - child.rect.height 909 + } else { 910 + // Static position: containing block origin. 911 + cb.y + child.margin.top + child.border.top + child.padding.top 912 + }; 913 + 914 + // Shift from temporary y to final y. 915 + let dy = final_y - child.rect.y; 916 + if dy != 0.0 { 917 + shift_box(child, 0.0, dy); 918 + } 919 + 920 + // Recursively lay out any absolutely positioned grandchildren. 921 + let new_abs_cb = if child.position != Position::Static { 922 + padding_box_rect(child) 923 + } else { 924 + cb 925 + }; 926 + layout_abspos_children( 927 + child, 928 + new_abs_cb, 929 + viewport_width, 930 + viewport_height, 931 + font, 932 + doc, 933 + ); 934 + } 935 + 644 936 fn has_block_children(b: &LayoutBox) -> bool { 645 - b.children.iter().any(is_block_level) 937 + b.children 938 + .iter() 939 + .any(|c| is_in_flow(c) && is_block_level(c)) 646 940 } 647 941 648 942 /// Collapse two adjoining margins per CSS2 §8.3.1. ··· 682 976 /// value. The function walks bottom-up: children are pre-collapsed first, then 683 977 /// their (possibly enlarged) margins are folded into the parent. 684 978 fn pre_collapse_margins(b: &mut LayoutBox) { 685 - // Recurse into block children first (bottom-up). 979 + // Recurse into in-flow block children first (bottom-up). 686 980 for child in &mut b.children { 687 - if is_block_level(child) { 981 + if is_in_flow(child) && is_block_level(child) { 688 982 pre_collapse_margins(child); 689 983 } 690 984 } ··· 714 1008 } 715 1009 } 716 1010 717 - /// Top margin of the first non-empty block child (already pre-collapsed). 1011 + /// Top margin of the first non-empty in-flow block child (already pre-collapsed). 718 1012 fn first_block_top_margin(children: &[LayoutBox]) -> Option<f32> { 719 1013 for child in children { 720 - if is_block_level(child) { 1014 + if is_in_flow(child) && is_block_level(child) { 721 1015 if is_empty_block(child) { 722 1016 continue; 723 1017 } 724 1018 return Some(child.margin.top); 725 1019 } 726 1020 } 727 - // All block children empty — fold all their collapsed margins. 1021 + // All in-flow block children empty — fold all their collapsed margins. 728 1022 let mut m = 0.0f32; 729 - for child in children.iter().filter(|c| is_block_level(c)) { 1023 + for child in children 1024 + .iter() 1025 + .filter(|c| is_in_flow(c) && is_block_level(c)) 1026 + { 730 1027 m = collapse_margins(m, collapse_margins(child.margin.top, child.margin.bottom)); 731 1028 } 732 1029 if m != 0.0 { ··· 736 1033 } 737 1034 } 738 1035 739 - /// Bottom margin of the last non-empty block child (already pre-collapsed). 1036 + /// Bottom margin of the last non-empty in-flow block child (already pre-collapsed). 740 1037 fn last_block_bottom_margin(children: &[LayoutBox]) -> Option<f32> { 741 1038 for child in children.iter().rev() { 742 - if is_block_level(child) { 1039 + if is_in_flow(child) && is_block_level(child) { 743 1040 if is_empty_block(child) { 744 1041 continue; 745 1042 } ··· 747 1044 } 748 1045 } 749 1046 let mut m = 0.0f32; 750 - for child in children.iter().filter(|c| is_block_level(c)) { 1047 + for child in children 1048 + .iter() 1049 + .filter(|c| is_in_flow(c) && is_block_level(c)) 1050 + { 751 1051 m = collapse_margins(m, collapse_margins(child.margin.top, child.margin.bottom)); 752 1052 } 753 1053 if m != 0.0 { ··· 764 1064 /// updated by `pre_collapse_margins`). 765 1065 fn layout_block_children( 766 1066 parent: &mut LayoutBox, 1067 + viewport_width: f32, 767 1068 viewport_height: f32, 768 1069 font: &Font, 769 1070 doc: &Document, 1071 + abs_cb: Rect, 770 1072 ) { 771 1073 let content_x = parent.rect.x; 772 1074 let content_width = parent.rect.width; ··· 780 1082 // Pending bottom margin from the previous sibling. 781 1083 let mut pending_margin: Option<f32> = None; 782 1084 let child_count = parent.children.len(); 1085 + // Track whether we've seen any in-flow children (for parent_top_open). 1086 + let mut first_in_flow = true; 783 1087 784 1088 for i in 0..child_count { 1089 + // Skip out-of-flow children (absolute/fixed) — they are laid out 1090 + // separately in layout_abspos_children. 1091 + if !is_in_flow(&parent.children[i]) { 1092 + continue; 1093 + } 1094 + 785 1095 let child_top_margin = parent.children[i].margin.top; 786 1096 let child_bottom_margin = parent.children[i].margin.bottom; 787 1097 ··· 803 1113 - child.padding.right) 804 1114 .max(0.0); 805 1115 child.rect.height = 0.0; 1116 + first_in_flow = false; 806 1117 continue; 807 1118 } 808 1119 ··· 810 1121 let collapsed_top = if let Some(prev_bottom) = pending_margin.take() { 811 1122 // Sibling collapsing: previous bottom vs this top. 812 1123 collapse_margins(prev_bottom, child_top_margin) 813 - } else if i == 0 && parent_top_open { 1124 + } else if first_in_flow && parent_top_open { 814 1125 // First child, parent top open: margin was already pulled into 815 1126 // parent by pre_collapse_margins — no internal spacing. 816 1127 0.0 ··· 825 1136 content_x, 826 1137 y_for_child, 827 1138 content_width, 1139 + viewport_width, 828 1140 viewport_height, 829 1141 font, 830 1142 doc, 1143 + abs_cb, 831 1144 ); 832 1145 833 1146 let child = &parent.children[i]; ··· 839 1152 + child.padding.bottom 840 1153 + child.border.bottom; 841 1154 pending_margin = Some(child_bottom_margin); 1155 + first_in_flow = false; 842 1156 } 843 1157 844 1158 // Trailing margin. ··· 891 1205 } 892 1206 893 1207 /// Lay out children according to the CSS Flexbox algorithm (Level 1 §9). 894 - fn layout_flex_children(parent: &mut LayoutBox, viewport_height: f32, font: &Font, doc: &Document) { 1208 + fn layout_flex_children( 1209 + parent: &mut LayoutBox, 1210 + viewport_width: f32, 1211 + viewport_height: f32, 1212 + font: &Font, 1213 + doc: &Document, 1214 + abs_cb: Rect, 1215 + ) { 895 1216 let flex_direction = parent.flex_direction; 896 1217 let flex_wrap = parent.flex_wrap; 897 1218 let justify_content = parent.justify_content; ··· 961 1282 let mut items: Vec<FlexItemInfo> = Vec::with_capacity(child_count); 962 1283 963 1284 for &idx in &order_indices { 1285 + // Skip out-of-flow children (absolute/fixed). 1286 + if !is_in_flow(&parent.children[idx]) { 1287 + continue; 1288 + } 964 1289 let child = &mut parent.children[idx]; 965 1290 966 1291 // Resolve margin/padding percentages for the child. ··· 1038 1363 } else { 1039 1364 // For column direction, measure content height. 1040 1365 let avail = container_cross_size.unwrap_or(parent.rect.width); 1041 - compute_layout(child, 0.0, 0.0, avail, viewport_height, font, doc); 1366 + compute_layout( 1367 + child, 1368 + 0.0, 1369 + 0.0, 1370 + avail, 1371 + viewport_width, 1372 + viewport_height, 1373 + font, 1374 + doc, 1375 + abs_cb, 1376 + ); 1042 1377 child.rect.height 1043 1378 } 1044 1379 } ··· 1167 1502 // Set up the child for layout at the resolved main size. 1168 1503 if is_row { 1169 1504 child.css_width = LengthOrAuto::Length(target_main); 1170 - compute_layout(child, 0.0, 0.0, target_main, viewport_height, font, doc); 1505 + compute_layout( 1506 + child, 1507 + 0.0, 1508 + 0.0, 1509 + target_main, 1510 + viewport_width, 1511 + viewport_height, 1512 + font, 1513 + doc, 1514 + abs_cb, 1515 + ); 1171 1516 let cross = child.rect.height + items[i].outer_cross; 1172 1517 if cross > max_cross { 1173 1518 max_cross = cross; 1174 1519 } 1175 1520 } else { 1176 1521 let avail = container_cross_size.unwrap_or(parent.rect.width); 1177 - compute_layout(child, 0.0, 0.0, avail, viewport_height, font, doc); 1522 + compute_layout( 1523 + child, 1524 + 0.0, 1525 + 0.0, 1526 + avail, 1527 + viewport_width, 1528 + viewport_height, 1529 + font, 1530 + doc, 1531 + abs_cb, 1532 + ); 1178 1533 child.rect.height = target_main; 1179 1534 let cross = child.rect.width 1180 1535 + child.border.left ··· 1536 1891 /// Flatten the inline children tree into a sequence of items. 1537 1892 fn flatten_inline_tree(children: &[LayoutBox], doc: &Document, items: &mut Vec<InlineItemKind>) { 1538 1893 for child in children { 1894 + // Skip out-of-flow children (absolute/fixed positioned). 1895 + if !is_in_flow(child) { 1896 + continue; 1897 + } 1539 1898 match &child.box_type { 1540 1899 BoxType::TextRun { text, .. } => { 1541 1900 let words = split_into_words(text); ··· 1810 2169 // Pre-collapse parent-child margins before positioning. 1811 2170 pre_collapse_margins(&mut root); 1812 2171 2172 + // The initial containing block for absolutely positioned elements is the viewport. 2173 + let viewport_cb = Rect { 2174 + x: 0.0, 2175 + y: 0.0, 2176 + width: viewport_width, 2177 + height: viewport_height, 2178 + }; 1813 2179 compute_layout( 1814 2180 &mut root, 1815 2181 0.0, 1816 2182 0.0, 1817 2183 viewport_width, 2184 + viewport_width, 1818 2185 viewport_height, 1819 2186 font, 1820 2187 doc, 2188 + viewport_cb, 1821 2189 ); 1822 2190 1823 2191 let height = root.margin_box_height(); ··· 3975 4343 child.rect.width 3976 4344 ); 3977 4345 } 4346 + } 4347 + 4348 + // ----------------------------------------------------------------------- 4349 + // Absolute positioning tests 4350 + // ----------------------------------------------------------------------- 4351 + 4352 + /// Helper to create: <html><body>...content...</body></html> and return 4353 + /// (doc, html_id, body_id). 4354 + fn make_html_body(doc: &mut Document) -> (NodeId, NodeId, NodeId) { 4355 + let root = doc.root(); 4356 + let html = doc.create_element("html"); 4357 + let body = doc.create_element("body"); 4358 + doc.append_child(root, html); 4359 + doc.append_child(html, body); 4360 + (root, html, body) 4361 + } 4362 + 4363 + #[test] 4364 + fn absolute_positioned_with_top_left() { 4365 + let mut doc = Document::new(); 4366 + let (_, _, body) = make_html_body(&mut doc); 4367 + // <div style="position: relative; width: 400px; height: 300px;"> 4368 + // <div style="position: absolute; top: 10px; left: 20px; width: 100px; height: 50px;"></div> 4369 + // </div> 4370 + let container = doc.create_element("div"); 4371 + let abs_child = doc.create_element("div"); 4372 + doc.append_child(body, container); 4373 + doc.append_child(container, abs_child); 4374 + doc.set_attribute( 4375 + container, 4376 + "style", 4377 + "position: relative; width: 400px; height: 300px;", 4378 + ); 4379 + doc.set_attribute( 4380 + abs_child, 4381 + "style", 4382 + "position: absolute; top: 10px; left: 20px; width: 100px; height: 50px;", 4383 + ); 4384 + 4385 + let tree = layout_doc(&doc); 4386 + let body_box = &tree.root.children[0]; 4387 + let container_box = &body_box.children[0]; 4388 + let abs_box = &container_box.children[0]; 4389 + 4390 + assert_eq!(abs_box.position, Position::Absolute); 4391 + assert_eq!(abs_box.rect.width, 100.0); 4392 + assert_eq!(abs_box.rect.height, 50.0); 4393 + 4394 + // The containing block is the container's padding box. 4395 + // Container content starts at container_box.rect.x, container_box.rect.y. 4396 + // Padding box x = container_box.rect.x - container_box.padding.left. 4397 + let cb_x = container_box.rect.x - container_box.padding.left; 4398 + let cb_y = container_box.rect.y - container_box.padding.top; 4399 + 4400 + // abs_box content area x = cb_x + 20 (left) + 0 (margin) + 0 (border) + 0 (padding) 4401 + assert!( 4402 + (abs_box.rect.x - (cb_x + 20.0)).abs() < 0.01, 4403 + "abs x should be cb_x + 20, got {} (cb_x={})", 4404 + abs_box.rect.x, 4405 + cb_x, 4406 + ); 4407 + assert!( 4408 + (abs_box.rect.y - (cb_y + 10.0)).abs() < 0.01, 4409 + "abs y should be cb_y + 10, got {} (cb_y={})", 4410 + abs_box.rect.y, 4411 + cb_y, 4412 + ); 4413 + } 4414 + 4415 + #[test] 4416 + fn absolute_does_not_affect_sibling_layout() { 4417 + let mut doc = Document::new(); 4418 + let (_, _, body) = make_html_body(&mut doc); 4419 + // <div style="position: relative;"> 4420 + // <div style="position: absolute; top: 0; left: 0; width: 100px; height: 100px;"></div> 4421 + // <p>Normal text</p> 4422 + // </div> 4423 + let container = doc.create_element("div"); 4424 + let abs_child = doc.create_element("div"); 4425 + let p = doc.create_element("p"); 4426 + let text = doc.create_text("Normal text"); 4427 + doc.append_child(body, container); 4428 + doc.append_child(container, abs_child); 4429 + doc.append_child(container, p); 4430 + doc.append_child(p, text); 4431 + doc.set_attribute(container, "style", "position: relative;"); 4432 + doc.set_attribute( 4433 + abs_child, 4434 + "style", 4435 + "position: absolute; top: 0; left: 0; width: 100px; height: 100px;", 4436 + ); 4437 + 4438 + let tree = layout_doc(&doc); 4439 + let body_box = &tree.root.children[0]; 4440 + let container_box = &body_box.children[0]; 4441 + 4442 + // The <p> should start at the top of the container (abs child doesn't 4443 + // push it down). Find the first in-flow child. 4444 + let p_box = container_box 4445 + .children 4446 + .iter() 4447 + .find(|c| c.position != Position::Absolute && c.position != Position::Fixed) 4448 + .expect("should have an in-flow child"); 4449 + 4450 + // p's y should be at (or very near) the container's content y. 4451 + let expected_y = container_box.rect.y; 4452 + assert!( 4453 + (p_box.rect.y - expected_y).abs() < 1.0, 4454 + "p should start near container top: p.y={}, expected={}", 4455 + p_box.rect.y, 4456 + expected_y, 4457 + ); 4458 + } 4459 + 4460 + #[test] 4461 + fn fixed_positioned_relative_to_viewport() { 4462 + let mut doc = Document::new(); 4463 + let (_, _, body) = make_html_body(&mut doc); 4464 + // <div style="position: fixed; top: 5px; left: 10px; width: 50px; height: 30px;"></div> 4465 + let fixed = doc.create_element("div"); 4466 + doc.append_child(body, fixed); 4467 + doc.set_attribute( 4468 + fixed, 4469 + "style", 4470 + "position: fixed; top: 5px; left: 10px; width: 50px; height: 30px;", 4471 + ); 4472 + 4473 + let tree = layout_doc(&doc); 4474 + let body_box = &tree.root.children[0]; 4475 + let fixed_box = &body_box.children[0]; 4476 + 4477 + assert_eq!(fixed_box.position, Position::Fixed); 4478 + assert_eq!(fixed_box.rect.width, 50.0); 4479 + assert_eq!(fixed_box.rect.height, 30.0); 4480 + 4481 + // Fixed should be relative to viewport (0,0). 4482 + assert!( 4483 + (fixed_box.rect.x - 10.0).abs() < 0.01, 4484 + "fixed x should be 10, got {}", 4485 + fixed_box.rect.x, 4486 + ); 4487 + assert!( 4488 + (fixed_box.rect.y - 5.0).abs() < 0.01, 4489 + "fixed y should be 5, got {}", 4490 + fixed_box.rect.y, 4491 + ); 4492 + } 4493 + 4494 + #[test] 4495 + fn z_index_ordering() { 4496 + let mut doc = Document::new(); 4497 + let (_, _, body) = make_html_body(&mut doc); 4498 + // Create a positioned container with two abs children at different z-index. 4499 + let container = doc.create_element("div"); 4500 + let low = doc.create_element("div"); 4501 + let high = doc.create_element("div"); 4502 + doc.append_child(body, container); 4503 + doc.append_child(container, low); 4504 + doc.append_child(container, high); 4505 + doc.set_attribute( 4506 + container, 4507 + "style", 4508 + "position: relative; width: 200px; height: 200px;", 4509 + ); 4510 + doc.set_attribute( 4511 + low, 4512 + "style", 4513 + "position: absolute; z-index: 1; top: 0; left: 0; width: 50px; height: 50px;", 4514 + ); 4515 + doc.set_attribute( 4516 + high, 4517 + "style", 4518 + "position: absolute; z-index: 5; top: 0; left: 0; width: 50px; height: 50px;", 4519 + ); 4520 + 4521 + let tree = layout_doc(&doc); 4522 + let body_box = &tree.root.children[0]; 4523 + let container_box = &body_box.children[0]; 4524 + 4525 + // Both should have correct z-index stored. 4526 + let low_box = &container_box.children[0]; 4527 + let high_box = &container_box.children[1]; 4528 + assert_eq!(low_box.z_index, Some(1)); 4529 + assert_eq!(high_box.z_index, Some(5)); 4530 + } 4531 + 4532 + #[test] 4533 + fn absolute_stretching_left_right() { 4534 + let mut doc = Document::new(); 4535 + let (_, _, body) = make_html_body(&mut doc); 4536 + // <div style="position: relative; width: 400px; height: 300px;"> 4537 + // <div style="position: absolute; left: 10px; right: 20px; height: 50px;"></div> 4538 + // </div> 4539 + let container = doc.create_element("div"); 4540 + let abs_child = doc.create_element("div"); 4541 + doc.append_child(body, container); 4542 + doc.append_child(container, abs_child); 4543 + doc.set_attribute( 4544 + container, 4545 + "style", 4546 + "position: relative; width: 400px; height: 300px;", 4547 + ); 4548 + doc.set_attribute( 4549 + abs_child, 4550 + "style", 4551 + "position: absolute; left: 10px; right: 20px; height: 50px;", 4552 + ); 4553 + 4554 + let tree = layout_doc(&doc); 4555 + let body_box = &tree.root.children[0]; 4556 + let container_box = &body_box.children[0]; 4557 + let abs_box = &container_box.children[0]; 4558 + 4559 + // Width should be: container_width - left - right = 400 - 10 - 20 = 370 4560 + assert!( 4561 + (abs_box.rect.width - 370.0).abs() < 0.01, 4562 + "stretched width should be 370, got {}", 4563 + abs_box.rect.width, 4564 + ); 4565 + } 4566 + 4567 + #[test] 4568 + fn absolute_bottom_right_positioning() { 4569 + let mut doc = Document::new(); 4570 + let (_, _, body) = make_html_body(&mut doc); 4571 + // <div style="position: relative; width: 400px; height: 300px;"> 4572 + // <div style="position: absolute; bottom: 10px; right: 20px; width: 50px; height: 30px;"></div> 4573 + // </div> 4574 + let container = doc.create_element("div"); 4575 + let abs_child = doc.create_element("div"); 4576 + doc.append_child(body, container); 4577 + doc.append_child(container, abs_child); 4578 + doc.set_attribute( 4579 + container, 4580 + "style", 4581 + "position: relative; width: 400px; height: 300px;", 4582 + ); 4583 + doc.set_attribute( 4584 + abs_child, 4585 + "style", 4586 + "position: absolute; bottom: 10px; right: 20px; width: 50px; height: 30px;", 4587 + ); 4588 + 4589 + let tree = layout_doc(&doc); 4590 + let body_box = &tree.root.children[0]; 4591 + let container_box = &body_box.children[0]; 4592 + let abs_box = &container_box.children[0]; 4593 + 4594 + let cb_x = container_box.rect.x - container_box.padding.left; 4595 + let cb_y = container_box.rect.y - container_box.padding.top; 4596 + let cb_w = 4597 + container_box.rect.width + container_box.padding.left + container_box.padding.right; 4598 + let cb_h = 4599 + container_box.rect.height + container_box.padding.top + container_box.padding.bottom; 4600 + 4601 + // x = cb_x + cb_w - right - width = cb_x + cb_w - 20 - 50 4602 + let expected_x = cb_x + cb_w - 20.0 - 50.0; 4603 + // y = cb_y + cb_h - bottom - height = cb_y + cb_h - 10 - 30 4604 + let expected_y = cb_y + cb_h - 10.0 - 30.0; 4605 + 4606 + assert!( 4607 + (abs_box.rect.x - expected_x).abs() < 0.01, 4608 + "abs x with right should be {}, got {}", 4609 + expected_x, 4610 + abs_box.rect.x, 4611 + ); 4612 + assert!( 4613 + (abs_box.rect.y - expected_y).abs() < 0.01, 4614 + "abs y with bottom should be {}, got {}", 4615 + expected_y, 4616 + abs_box.rect.y, 4617 + ); 3978 4618 } 3979 4619 }
+55 -4
crates/render/src/lib.rs
··· 9 9 use we_dom::NodeId; 10 10 use we_image::pixel::Image; 11 11 use we_layout::{BoxType, LayoutBox, LayoutTree, Rect, TextLine, SCROLLBAR_WIDTH}; 12 - use we_style::computed::{BorderStyle, Overflow, TextDecoration, Visibility}; 12 + use we_style::computed::{BorderStyle, Overflow, Position, TextDecoration, Visibility}; 13 13 use we_text::font::Font; 14 14 15 15 /// Scroll state: maps NodeId of scrollable boxes to their (scroll_x, scroll_y) offsets. ··· 105 105 list 106 106 } 107 107 108 + /// Returns `true` if a box is positioned (absolute or fixed). 109 + fn is_positioned(b: &LayoutBox) -> bool { 110 + b.position == Position::Absolute || b.position == Position::Fixed 111 + } 112 + 108 113 fn paint_box( 109 114 layout_box: &LayoutBox, 110 115 list: &mut DisplayList, ··· 162 167 } 163 168 } 164 169 165 - // Always recurse into children — they may override visibility. 166 - for child in &layout_box.children { 167 - paint_box(child, list, child_translate, scroll_state); 170 + // Check if any children are positioned (absolute/fixed). 171 + let has_positioned = layout_box.children.iter().any(is_positioned); 172 + 173 + if has_positioned { 174 + // CSS stacking context ordering: 175 + // 1. Positioned children with negative z-index 176 + // 2. In-flow (non-positioned) children in tree order 177 + // 3. Positioned children with z-index >= 0 (or auto) in z-index order 178 + 179 + // Collect positioned children indices, partitioned by z-index sign. 180 + let mut negative_z: Vec<usize> = Vec::new(); 181 + let mut non_negative_z: Vec<usize> = Vec::new(); 182 + 183 + for (i, child) in layout_box.children.iter().enumerate() { 184 + if is_positioned(child) { 185 + let z = child.z_index.unwrap_or(0); 186 + if z < 0 { 187 + negative_z.push(i); 188 + } else { 189 + non_negative_z.push(i); 190 + } 191 + } 192 + } 193 + 194 + // Sort by z-index (stable sort preserves tree order for equal z-index). 195 + negative_z.sort_by_key(|&i| layout_box.children[i].z_index.unwrap_or(0)); 196 + non_negative_z.sort_by_key(|&i| layout_box.children[i].z_index.unwrap_or(0)); 197 + 198 + // Paint negative z-index positioned children. 199 + for &i in &negative_z { 200 + paint_box(&layout_box.children[i], list, child_translate, scroll_state); 201 + } 202 + 203 + // Paint in-flow children in tree order. 204 + for child in &layout_box.children { 205 + if !is_positioned(child) { 206 + paint_box(child, list, child_translate, scroll_state); 207 + } 208 + } 209 + 210 + // Paint non-negative z-index positioned children. 211 + for &i in &non_negative_z { 212 + paint_box(&layout_box.children[i], list, child_translate, scroll_state); 213 + } 214 + } else { 215 + // No positioned children — paint all in tree order. 216 + for child in &layout_box.children { 217 + paint_box(child, list, child_translate, scroll_state); 218 + } 168 219 } 169 220 170 221 if clips {
+48
crates/style/src/computed.rs
··· 291 291 pub right: LengthOrAuto, 292 292 pub bottom: LengthOrAuto, 293 293 pub left: LengthOrAuto, 294 + pub z_index: Option<i32>, 294 295 295 296 // Overflow 296 297 pub overflow: Overflow, ··· 366 367 right: LengthOrAuto::Auto, 367 368 bottom: LengthOrAuto::Auto, 368 369 left: LengthOrAuto::Auto, 370 + z_index: None, 369 371 370 372 overflow: Overflow::Visible, 371 373 visibility: Visibility::Visible, ··· 860 862 "right" => style.right = resolve_layout_length_or_auto(value, current_fs, viewport), 861 863 "bottom" => style.bottom = resolve_layout_length_or_auto(value, current_fs, viewport), 862 864 "left" => style.left = resolve_layout_length_or_auto(value, current_fs, viewport), 865 + 866 + // z-index 867 + "z-index" => { 868 + style.z_index = match value { 869 + CssValue::Number(n) => Some(*n as i32), 870 + CssValue::Keyword(k) if k == "auto" => None, 871 + _ => style.z_index, 872 + }; 873 + } 863 874 864 875 // Overflow 865 876 "overflow" => { ··· 2291 2302 let child = &parent.children[0]; 2292 2303 assert_eq!(parent.style.box_sizing, BoxSizing::BorderBox); 2293 2304 assert_eq!(child.style.box_sizing, BoxSizing::ContentBox); 2305 + } 2306 + 2307 + #[test] 2308 + fn z_index_parsing() { 2309 + let html_str = r#"<!DOCTYPE html> 2310 + <html><head><style> 2311 + .a { z-index: 5; } 2312 + .b { z-index: -3; } 2313 + .c { z-index: auto; } 2314 + </style></head> 2315 + <body> 2316 + <div class="a">A</div> 2317 + <div class="b">B</div> 2318 + <div class="c">C</div> 2319 + </body></html>"#; 2320 + let doc = we_html::parse_html(html_str); 2321 + let sheets = extract_stylesheets(&doc); 2322 + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 2323 + let body = &styled.children[0]; 2324 + let a = &body.children[0]; 2325 + let b = &body.children[1]; 2326 + let c = &body.children[2]; 2327 + assert_eq!(a.style.z_index, Some(5)); 2328 + assert_eq!(b.style.z_index, Some(-3)); 2329 + assert_eq!(c.style.z_index, None); 2330 + } 2331 + 2332 + #[test] 2333 + fn z_index_default_is_auto() { 2334 + let html_str = r#"<!DOCTYPE html> 2335 + <html><body><div>test</div></body></html>"#; 2336 + let doc = we_html::parse_html(html_str); 2337 + let sheets = extract_stylesheets(&doc); 2338 + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); 2339 + let body = &styled.children[0]; 2340 + let div = &body.children[0]; 2341 + assert_eq!(div.style.z_index, None); 2294 2342 } 2295 2343 }