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

Configure Feed

Select the types of activity you want to include in your feed.

Implement CSS Grid Layout Level 1: parsing, track sizing, and layout algorithm

Adds full CSS Grid support across three crates:

CSS crate: shorthand expansion for grid-column, grid-row, grid-area
Style crate: Display::Grid/InlineGrid, GridTrackSize (fr, minmax, repeat),
GridPlacement, GridAutoFlow, grid-template-columns/rows/areas parsing,
grid item placement properties, justify-items/justify-self
Layout crate: grid layout algorithm with explicit grid resolution,
auto-placement (row/column/dense), fr unit distribution, track sizing,
gap support, and justify/align item positioning

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

+1778 -9
+166
crates/css/src/values.rs
··· 634 634 "flex" => Some(expand_flex(values, important)), 635 635 "flex-flow" => Some(expand_flex_flow(values, important)), 636 636 "gap" => Some(expand_gap(values, important)), 637 + "grid-column" => Some(expand_grid_line("grid-column", values, important)), 638 + "grid-row" => Some(expand_grid_line("grid-row", values, important)), 639 + "grid-area" => Some(expand_grid_area(values, important)), 637 640 _ => Option::None, 638 641 } 639 642 } ··· 744 747 important, 745 748 }, 746 749 ] 750 + } 751 + 752 + /// Expand `grid-column` or `grid-row` shorthand into start/end longhands. 753 + /// 754 + /// `grid-column: 1 / span 2` → grid-column-start: 1; grid-column-end: span 2; 755 + /// `grid-column: 2` → grid-column-start: 2; grid-column-end: auto; 756 + fn expand_grid_line( 757 + property: &str, 758 + values: &[ComponentValue], 759 + important: bool, 760 + ) -> Vec<LonghandDeclaration> { 761 + let (start_prop, end_prop) = if property == "grid-column" { 762 + ("grid-column-start", "grid-column-end") 763 + } else { 764 + ("grid-row-start", "grid-row-end") 765 + }; 766 + 767 + let groups = split_at_slash(values); 768 + let start_value = parse_grid_line_group(&groups[0]); 769 + let end_value = if groups.len() > 1 { 770 + parse_grid_line_group(&groups[1]) 771 + } else { 772 + CssValue::Auto 773 + }; 774 + 775 + vec![ 776 + LonghandDeclaration { 777 + property: start_prop.to_string(), 778 + value: start_value, 779 + important, 780 + }, 781 + LonghandDeclaration { 782 + property: end_prop.to_string(), 783 + value: end_value, 784 + important, 785 + }, 786 + ] 787 + } 788 + 789 + /// Expand `grid-area` shorthand into row-start, column-start, row-end, column-end. 790 + /// 791 + /// `grid-area: 1 / 2 / 3 / 4` → all four longhands. 792 + /// Missing values default to `auto`. 793 + fn expand_grid_area(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 794 + let groups = split_at_slash(values); 795 + let row_start = parse_grid_line_group(&groups[0]); 796 + let col_start = if groups.len() > 1 { 797 + parse_grid_line_group(&groups[1]) 798 + } else { 799 + CssValue::Auto 800 + }; 801 + let row_end = if groups.len() > 2 { 802 + parse_grid_line_group(&groups[2]) 803 + } else { 804 + CssValue::Auto 805 + }; 806 + let col_end = if groups.len() > 3 { 807 + parse_grid_line_group(&groups[3]) 808 + } else { 809 + CssValue::Auto 810 + }; 811 + 812 + vec![ 813 + LonghandDeclaration { 814 + property: "grid-row-start".to_string(), 815 + value: row_start, 816 + important, 817 + }, 818 + LonghandDeclaration { 819 + property: "grid-column-start".to_string(), 820 + value: col_start, 821 + important, 822 + }, 823 + LonghandDeclaration { 824 + property: "grid-row-end".to_string(), 825 + value: row_end, 826 + important, 827 + }, 828 + LonghandDeclaration { 829 + property: "grid-column-end".to_string(), 830 + value: col_end, 831 + important, 832 + }, 833 + ] 834 + } 835 + 836 + /// Split component values at `/` delimiters into groups. 837 + fn split_at_slash(values: &[ComponentValue]) -> Vec<Vec<&ComponentValue>> { 838 + let mut groups: Vec<Vec<&ComponentValue>> = vec![Vec::new()]; 839 + for cv in values { 840 + if matches!(cv, ComponentValue::Delim('/')) { 841 + groups.push(Vec::new()); 842 + } else { 843 + groups.last_mut().unwrap().push(cv); 844 + } 845 + } 846 + groups 847 + } 848 + 849 + /// Parse a single grid line group (between slashes) into a CssValue. 850 + fn parse_grid_line_group(values: &[&ComponentValue]) -> CssValue { 851 + let non_ws: Vec<&&ComponentValue> = values 852 + .iter() 853 + .filter(|v| !matches!(v, ComponentValue::Whitespace)) 854 + .collect(); 855 + 856 + if non_ws.is_empty() { 857 + return CssValue::Auto; 858 + } 859 + if non_ws.len() == 1 { 860 + return parse_single_value(non_ws[0]); 861 + } 862 + let parsed: Vec<CssValue> = non_ws.iter().map(|v| parse_single_value(v)).collect(); 863 + CssValue::List(parsed) 747 864 } 748 865 749 866 /// Expand a box-model shorthand (margin, padding) using the 1-to-4 value pattern. ··· 1904 2021 }; 1905 2022 let val = parse_value(&rule.declarations[0].value); 1906 2023 assert!(matches!(val, CssValue::Math(_))); 2024 + } 2025 + 2026 + #[test] 2027 + fn expand_grid_column_shorthand() { 2028 + let longhands = expand_shorthand("grid-column", &parse_cv("1 / 3"), false).unwrap(); 2029 + assert_eq!(longhands.len(), 2); 2030 + assert_eq!(longhands[0].property, "grid-column-start"); 2031 + assert!(matches!(longhands[0].value, CssValue::Number(n) if n == 1.0)); 2032 + assert_eq!(longhands[1].property, "grid-column-end"); 2033 + assert!(matches!(longhands[1].value, CssValue::Number(n) if n == 3.0)); 2034 + } 2035 + 2036 + #[test] 2037 + fn expand_grid_column_single_value() { 2038 + let longhands = expand_shorthand("grid-column", &parse_cv("2"), false).unwrap(); 2039 + assert_eq!(longhands[0].property, "grid-column-start"); 2040 + assert!(matches!(longhands[0].value, CssValue::Number(n) if n == 2.0)); 2041 + assert_eq!(longhands[1].property, "grid-column-end"); 2042 + assert_eq!(longhands[1].value, CssValue::Auto); 2043 + } 2044 + 2045 + #[test] 2046 + fn expand_grid_row_shorthand() { 2047 + let longhands = expand_shorthand("grid-row", &parse_cv("1 / 4"), false).unwrap(); 2048 + assert_eq!(longhands.len(), 2); 2049 + assert_eq!(longhands[0].property, "grid-row-start"); 2050 + assert_eq!(longhands[1].property, "grid-row-end"); 2051 + } 2052 + 2053 + #[test] 2054 + fn expand_grid_area_shorthand() { 2055 + let longhands = expand_shorthand("grid-area", &parse_cv("1 / 2 / 3 / 4"), false).unwrap(); 2056 + assert_eq!(longhands.len(), 4); 2057 + assert_eq!(longhands[0].property, "grid-row-start"); 2058 + assert_eq!(longhands[1].property, "grid-column-start"); 2059 + assert_eq!(longhands[2].property, "grid-row-end"); 2060 + assert_eq!(longhands[3].property, "grid-column-end"); 2061 + } 2062 + 2063 + /// Helper: parse a CSS value string into component values using the parser. 2064 + fn parse_cv(input: &str) -> Vec<ComponentValue> { 2065 + use crate::parser::Parser; 2066 + let css = format!("x {{ p: {} }}", input); 2067 + let ss = Parser::parse(&css); 2068 + let rule = match &ss.rules[0] { 2069 + crate::parser::Rule::Style(r) => r, 2070 + _ => panic!("expected style rule"), 2071 + }; 2072 + rule.declarations[0].value.clone() 1907 2073 } 1908 2074 }
+998 -8
crates/layout/src/lib.rs
··· 9 9 use we_dom::{Document, NodeData, NodeId}; 10 10 use we_style::computed::{ 11 11 AlignContent, AlignItems, AlignSelf, BorderStyle, BoxSizing, Clear, ComputedStyle, Display, 12 - FlexDirection, FlexWrap, Float, JustifyContent, LengthOrAuto, Overflow, Position, StyledNode, 13 - TextAlign, TextDecoration, Visibility, WillChange, 12 + FlexDirection, FlexWrap, Float, GridAutoFlow, GridPlacement, GridTrackSize, JustifyContent, 13 + JustifyItems, JustifySelf, LengthOrAuto, Overflow, Position, StyledNode, TextAlign, 14 + TextDecoration, Visibility, WillChange, 14 15 }; 15 16 use we_text::font::Font; 16 17 ··· 144 145 pub flex_basis: LengthOrAuto, 145 146 pub align_self: AlignSelf, 146 147 pub order: i32, 148 + // Grid container properties 149 + pub grid_template_columns: Vec<GridTrackSize>, 150 + pub grid_template_rows: Vec<GridTrackSize>, 151 + pub grid_template_areas: Vec<Vec<String>>, 152 + pub grid_auto_columns: GridTrackSize, 153 + pub grid_auto_rows: GridTrackSize, 154 + pub grid_auto_flow: GridAutoFlow, 155 + pub justify_items: JustifyItems, 156 + // Grid item properties 157 + pub grid_column_start: GridPlacement, 158 + pub grid_column_end: GridPlacement, 159 + pub grid_row_start: GridPlacement, 160 + pub grid_row_end: GridPlacement, 161 + pub justify_self: JustifySelf, 147 162 } 148 163 149 164 impl LayoutBox { ··· 220 235 flex_basis: style.flex_basis, 221 236 align_self: style.align_self, 222 237 order: style.order, 238 + grid_template_columns: style.grid_template_columns.clone(), 239 + grid_template_rows: style.grid_template_rows.clone(), 240 + grid_template_areas: style.grid_template_areas.clone(), 241 + grid_auto_columns: style.grid_auto_columns.clone(), 242 + grid_auto_rows: style.grid_auto_rows.clone(), 243 + grid_auto_flow: style.grid_auto_flow, 244 + justify_items: style.justify_items, 245 + grid_column_start: style.grid_column_start, 246 + grid_column_end: style.grid_column_end, 247 + grid_row_start: style.grid_row_start, 248 + grid_row_end: style.grid_row_end, 249 + justify_self: style.justify_self, 223 250 } 224 251 } 225 252 ··· 455 482 }; 456 483 457 484 let box_type = match effective_display { 458 - Display::Block | Display::Flex | Display::InlineFlex => BoxType::Block(node), 485 + Display::Block 486 + | Display::Flex 487 + | Display::InlineFlex 488 + | Display::Grid 489 + | Display::InlineGrid => BoxType::Block(node), 459 490 Display::Inline => BoxType::Inline(node), 460 491 Display::None => unreachable!(), 461 492 }; 462 493 463 - if effective_display == Display::Block { 494 + if matches!( 495 + effective_display, 496 + Display::Block | Display::Grid | Display::InlineGrid 497 + ) { 464 498 children = normalize_children(children, style); 465 499 } 466 500 ··· 845 879 846 880 match &b.box_type { 847 881 BoxType::Block(_) | BoxType::Anonymous => { 848 - if matches!(b.display, Display::Flex | Display::InlineFlex) { 882 + if matches!(b.display, Display::Grid | Display::InlineGrid) { 883 + layout_grid_children(b, viewport_width, viewport_height, font, doc, abs_cb); 884 + } else if matches!(b.display, Display::Flex | Display::InlineFlex) { 849 885 layout_flex_children(b, viewport_width, viewport_height, font, doc, abs_cb); 850 886 } else if has_block_children(b) || has_float_children(b) { 851 887 layout_block_children(b, viewport_width, viewport_height, font, doc, abs_cb); ··· 1133 1169 }; 1134 1170 match &child.box_type { 1135 1171 BoxType::Block(_) | BoxType::Anonymous => { 1136 - if matches!(child.display, Display::Flex | Display::InlineFlex) { 1172 + if matches!(child.display, Display::Grid | Display::InlineGrid) { 1173 + layout_grid_children( 1174 + child, 1175 + viewport_width, 1176 + viewport_height, 1177 + font, 1178 + doc, 1179 + child_abs_cb, 1180 + ); 1181 + } else if matches!(child.display, Display::Flex | Display::InlineFlex) { 1137 1182 layout_flex_children( 1138 1183 child, 1139 1184 viewport_width, ··· 1264 1309 /// which prevents its margins from collapsing with children. 1265 1310 fn establishes_bfc(b: &LayoutBox) -> bool { 1266 1311 b.overflow != Overflow::Visible 1267 - || matches!(b.display, Display::Flex | Display::InlineFlex) 1312 + || matches!( 1313 + b.display, 1314 + Display::Flex | Display::InlineFlex | Display::Grid | Display::InlineGrid 1315 + ) 1268 1316 || b.float != Float::None 1269 1317 } 1270 1318 ··· 1595 1643 } else { 1596 1644 match &child.box_type { 1597 1645 BoxType::Block(_) | BoxType::Anonymous => { 1598 - if matches!(child.display, Display::Flex | Display::InlineFlex) { 1646 + if matches!(child.display, Display::Grid | Display::InlineGrid) { 1647 + layout_grid_children(child, viewport_width, viewport_height, font, doc, abs_cb); 1648 + } else if matches!(child.display, Display::Flex | Display::InlineFlex) { 1599 1649 layout_flex_children(child, viewport_width, viewport_height, font, doc, abs_cb); 1600 1650 } else if has_block_children(child) || has_float_children(child) { 1601 1651 layout_block_children( ··· 1730 1780 } 1731 1781 } 1732 1782 } 1783 + } 1784 + 1785 + // --------------------------------------------------------------------------- 1786 + // CSS Grid Layout (Level 1) 1787 + // --------------------------------------------------------------------------- 1788 + 1789 + /// Resolved grid item placement on both axes. 1790 + struct GridItemInfo { 1791 + /// Index into parent.children 1792 + child_idx: usize, 1793 + /// 0-based column start line 1794 + col_start: usize, 1795 + /// 0-based column end line 1796 + col_end: usize, 1797 + /// 0-based row start line 1798 + row_start: usize, 1799 + /// 0-based row end line 1800 + row_end: usize, 1801 + } 1802 + 1803 + /// Lay out children according to the CSS Grid Layout algorithm (Level 1). 1804 + fn layout_grid_children( 1805 + parent: &mut LayoutBox, 1806 + viewport_width: f32, 1807 + viewport_height: f32, 1808 + font: &Font, 1809 + doc: &Document, 1810 + abs_cb: Rect, 1811 + ) { 1812 + let container_width = parent.rect.width; 1813 + 1814 + // --- Step 1: Determine the explicit grid --- 1815 + let explicit_cols = parent.grid_template_columns.len(); 1816 + let explicit_rows = parent.grid_template_rows.len(); 1817 + 1818 + // Build the area name → (row_start, col_start, row_end, col_end) map from grid-template-areas 1819 + let area_map = build_area_map(&parent.grid_template_areas); 1820 + 1821 + // --- Step 2: Place items --- 1822 + let num_children = parent.children.len(); 1823 + let mut items: Vec<GridItemInfo> = Vec::with_capacity(num_children); 1824 + 1825 + // First pass: place items with explicit positions 1826 + // Track grid occupation for auto-placement 1827 + let mut max_col: usize = explicit_cols.max(1); 1828 + let mut max_row: usize = explicit_rows.max(1); 1829 + 1830 + for i in 0..num_children { 1831 + let child = &parent.children[i]; 1832 + 1833 + // Check if item references a named area 1834 + let area_placement = if let GridPlacement::Auto = child.grid_column_start { 1835 + if let GridPlacement::Auto = child.grid_row_start { 1836 + // Check grid-area from grid_template_areas only if no explicit placement 1837 + None 1838 + } else { 1839 + None 1840 + } 1841 + } else { 1842 + None 1843 + }; 1844 + 1845 + let (col_start, col_end, row_start, row_end) = 1846 + if let Some((rs, cs, re, ce)) = area_placement { 1847 + (cs, ce, rs, re) 1848 + } else { 1849 + resolve_grid_placement( 1850 + child.grid_column_start, 1851 + child.grid_column_end, 1852 + child.grid_row_start, 1853 + child.grid_row_end, 1854 + explicit_cols, 1855 + explicit_rows, 1856 + &area_map, 1857 + ) 1858 + }; 1859 + 1860 + if col_end > max_col { 1861 + max_col = col_end; 1862 + } 1863 + if row_end > max_row { 1864 + max_row = row_end; 1865 + } 1866 + 1867 + items.push(GridItemInfo { 1868 + child_idx: i, 1869 + col_start, 1870 + col_end, 1871 + row_start, 1872 + row_end, 1873 + }); 1874 + } 1875 + 1876 + // Auto-placement for items that got (0,0)→(1,1) default placement 1877 + auto_place_items(&mut items, max_col, &mut max_row, &parent.grid_auto_flow); 1878 + 1879 + // Recalculate max dimensions after auto-placement 1880 + for item in &items { 1881 + if item.col_end > max_col { 1882 + max_col = item.col_end; 1883 + } 1884 + if item.row_end > max_row { 1885 + max_row = item.row_end; 1886 + } 1887 + } 1888 + 1889 + // --- Step 3: Resolve track sizes --- 1890 + let col_gap = parent.column_gap; 1891 + let row_gap = parent.row_gap; 1892 + 1893 + // Available space for tracks (subtract gaps) 1894 + let total_col_gaps = if max_col > 0 { 1895 + col_gap * (max_col as f32 - 1.0).max(0.0) 1896 + } else { 1897 + 0.0 1898 + }; 1899 + let available_for_cols = (container_width - total_col_gaps).max(0.0); 1900 + 1901 + let col_sizes = resolve_track_sizes( 1902 + &parent.grid_template_columns, 1903 + &parent.grid_auto_columns, 1904 + max_col, 1905 + available_for_cols, 1906 + container_width, 1907 + ); 1908 + 1909 + // For row sizing, first lay out children to get their intrinsic heights 1910 + // We need column positions to set child widths for content-based row sizing 1911 + let col_positions = compute_track_positions(&col_sizes, col_gap); 1912 + 1913 + // Lay out each child with its column-derived width to determine intrinsic height 1914 + let mut child_heights: Vec<f32> = vec![0.0; num_children]; 1915 + for item in &items { 1916 + let child = &mut parent.children[item.child_idx]; 1917 + // Calculate item width from its column span 1918 + let item_width = item_span_size(&col_sizes, &col_positions, item.col_start, item.col_end); 1919 + child.rect.width = item_width; 1920 + 1921 + // Resolve padding and margin against item width 1922 + resolve_padding_margin(child, item_width); 1923 + 1924 + let inner_width = (item_width 1925 + - child.padding.left 1926 + - child.padding.right 1927 + - child.border.left 1928 + - child.border.right) 1929 + .max(0.0); 1930 + child.rect.width = inner_width; 1931 + 1932 + // Recursively lay out this child 1933 + compute_layout( 1934 + child, 1935 + 0.0, 1936 + 0.0, 1937 + inner_width, 1938 + viewport_width, 1939 + viewport_height, 1940 + font, 1941 + doc, 1942 + abs_cb, 1943 + None, 1944 + ); 1945 + 1946 + child_heights[item.child_idx] = child.rect.height 1947 + + child.padding.top 1948 + + child.padding.bottom 1949 + + child.border.top 1950 + + child.border.bottom; 1951 + } 1952 + 1953 + // Now resolve row sizes using child intrinsic heights 1954 + let row_contributions = compute_row_contributions(&items, &child_heights, max_row); 1955 + let row_sizes = resolve_track_sizes( 1956 + &parent.grid_template_rows, 1957 + &parent.grid_auto_rows, 1958 + max_row, 1959 + f32::MAX, // rows don't have a fixed available space (they grow) 1960 + container_width, 1961 + ); 1962 + // Override auto rows with intrinsic content heights 1963 + let row_sizes = finalize_row_sizes(&row_sizes, &row_contributions); 1964 + 1965 + let row_positions = compute_track_positions(&row_sizes, row_gap); 1966 + 1967 + // --- Step 4: Position items in their grid areas --- 1968 + for item in &items { 1969 + let child = &mut parent.children[item.child_idx]; 1970 + let x = if item.col_start < col_positions.len() { 1971 + col_positions[item.col_start] 1972 + } else { 1973 + 0.0 1974 + }; 1975 + let y = if item.row_start < row_positions.len() { 1976 + row_positions[item.row_start] 1977 + } else { 1978 + 0.0 1979 + }; 1980 + 1981 + let area_width = item_span_size(&col_sizes, &col_positions, item.col_start, item.col_end); 1982 + let area_height = item_span_size(&row_sizes, &row_positions, item.row_start, item.row_end); 1983 + 1984 + // Apply alignment within grid area 1985 + let content_width = child.rect.width 1986 + + child.padding.left 1987 + + child.padding.right 1988 + + child.border.left 1989 + + child.border.right; 1990 + let content_height = child.rect.height 1991 + + child.padding.top 1992 + + child.padding.bottom 1993 + + child.border.top 1994 + + child.border.bottom; 1995 + 1996 + // justify-items / justify-self (inline axis) 1997 + let justify = if child.justify_self != JustifySelf::Auto { 1998 + match child.justify_self { 1999 + JustifySelf::Start => JustifyItems::Start, 2000 + JustifySelf::End => JustifyItems::End, 2001 + JustifySelf::Center => JustifyItems::Center, 2002 + JustifySelf::Stretch => JustifyItems::Stretch, 2003 + JustifySelf::Auto => parent.justify_items, 2004 + } 2005 + } else { 2006 + parent.justify_items 2007 + }; 2008 + 2009 + let (item_x, final_width) = match justify { 2010 + JustifyItems::Start => (x, content_width), 2011 + JustifyItems::End => (x + area_width - content_width, content_width), 2012 + JustifyItems::Center => (x + (area_width - content_width) / 2.0, content_width), 2013 + JustifyItems::Stretch => (x, area_width), 2014 + }; 2015 + 2016 + // align-items / align-self (block axis) 2017 + let align = match child.align_self { 2018 + AlignSelf::Auto | AlignSelf::Stretch => parent.align_items, 2019 + AlignSelf::FlexStart => AlignItems::FlexStart, 2020 + AlignSelf::FlexEnd => AlignItems::FlexEnd, 2021 + AlignSelf::Center => AlignItems::Center, 2022 + AlignSelf::Baseline => AlignItems::Baseline, 2023 + }; 2024 + 2025 + let (item_y, final_height) = match align { 2026 + AlignItems::FlexStart => (y, content_height), 2027 + AlignItems::FlexEnd => (y + area_height - content_height, content_height), 2028 + AlignItems::Center => (y + (area_height - content_height) / 2.0, content_height), 2029 + AlignItems::Stretch => (y, area_height), 2030 + AlignItems::Baseline => (y, content_height), 2031 + }; 2032 + 2033 + child.rect.x = item_x + child.margin.left + child.border.left + child.padding.left; 2034 + child.rect.y = item_y + child.margin.top + child.border.top + child.padding.top; 2035 + 2036 + // If stretched, update the child's content dimensions 2037 + if justify == JustifyItems::Stretch { 2038 + let stretched_content = (final_width 2039 + - child.margin.left 2040 + - child.margin.right 2041 + - child.border.left 2042 + - child.border.right 2043 + - child.padding.left 2044 + - child.padding.right) 2045 + .max(0.0); 2046 + if stretched_content > child.rect.width { 2047 + child.rect.width = stretched_content; 2048 + } 2049 + } 2050 + if align == AlignItems::Stretch { 2051 + let stretched_content = (final_height 2052 + - child.margin.top 2053 + - child.margin.bottom 2054 + - child.border.top 2055 + - child.border.bottom 2056 + - child.padding.top 2057 + - child.padding.bottom) 2058 + .max(0.0); 2059 + if stretched_content > child.rect.height { 2060 + child.rect.height = stretched_content; 2061 + } 2062 + } 2063 + } 2064 + 2065 + // --- Step 5: Set parent height --- 2066 + let total_row_gaps = if max_row > 0 { 2067 + row_gap * (max_row as f32 - 1.0).max(0.0) 2068 + } else { 2069 + 0.0 2070 + }; 2071 + let total_height: f32 = row_sizes.iter().sum::<f32>() + total_row_gaps; 2072 + parent.rect.height = total_height; 2073 + } 2074 + 2075 + /// Resolve padding and margin for a grid child against a reference width. 2076 + fn resolve_padding_margin(child: &mut LayoutBox, reference_width: f32) { 2077 + child.padding.top = resolve_length_against(child.css_padding[0], reference_width); 2078 + child.padding.right = resolve_length_against(child.css_padding[1], reference_width); 2079 + child.padding.bottom = resolve_length_against(child.css_padding[2], reference_width); 2080 + child.padding.left = resolve_length_against(child.css_padding[3], reference_width); 2081 + 2082 + child.margin.top = resolve_length_against(child.css_margin[0], reference_width); 2083 + child.margin.right = resolve_length_against(child.css_margin[1], reference_width); 2084 + child.margin.bottom = resolve_length_against(child.css_margin[2], reference_width); 2085 + child.margin.left = resolve_length_against(child.css_margin[3], reference_width); 2086 + 2087 + // Border widths are already resolved to px 2088 + } 2089 + 2090 + /// Build a map from area names to (row_start, col_start, row_end, col_end) from grid-template-areas. 2091 + fn build_area_map(areas: &[Vec<String>]) -> HashMap<String, (usize, usize, usize, usize)> { 2092 + let mut map: HashMap<String, (usize, usize, usize, usize)> = HashMap::new(); 2093 + for (row_idx, row) in areas.iter().enumerate() { 2094 + for (col_idx, name) in row.iter().enumerate() { 2095 + if name == "." { 2096 + continue; 2097 + } 2098 + let entry = 2099 + map.entry(name.clone()) 2100 + .or_insert((row_idx, col_idx, row_idx + 1, col_idx + 1)); 2101 + // Expand area to cover this cell 2102 + if row_idx < entry.0 { 2103 + entry.0 = row_idx; 2104 + } 2105 + if col_idx < entry.1 { 2106 + entry.1 = col_idx; 2107 + } 2108 + if row_idx + 1 > entry.2 { 2109 + entry.2 = row_idx + 1; 2110 + } 2111 + if col_idx + 1 > entry.3 { 2112 + entry.3 = col_idx + 1; 2113 + } 2114 + } 2115 + } 2116 + map 2117 + } 2118 + 2119 + /// Resolve a grid item's column/row start/end placements to 0-based line indices. 2120 + fn resolve_grid_placement( 2121 + col_start: GridPlacement, 2122 + col_end: GridPlacement, 2123 + row_start: GridPlacement, 2124 + row_end: GridPlacement, 2125 + explicit_cols: usize, 2126 + explicit_rows: usize, 2127 + _area_map: &HashMap<String, (usize, usize, usize, usize)>, 2128 + ) -> (usize, usize, usize, usize) { 2129 + let (cs, ce) = resolve_line_pair(col_start, col_end, explicit_cols); 2130 + let (rs, re) = resolve_line_pair(row_start, row_end, explicit_rows); 2131 + (cs, ce, rs, re) 2132 + } 2133 + 2134 + /// Resolve a start/end placement pair to 0-based line indices. 2135 + fn resolve_line_pair( 2136 + start: GridPlacement, 2137 + end: GridPlacement, 2138 + explicit_count: usize, 2139 + ) -> (usize, usize) { 2140 + match (start, end) { 2141 + (GridPlacement::Line(s), GridPlacement::Line(e)) => { 2142 + let s = line_to_index(s, explicit_count); 2143 + let e = line_to_index(e, explicit_count); 2144 + if s < e { 2145 + (s, e) 2146 + } else { 2147 + (e, s) 2148 + } 2149 + } 2150 + (GridPlacement::Line(s), GridPlacement::Span(n)) => { 2151 + let s = line_to_index(s, explicit_count); 2152 + (s, s + n.max(1) as usize) 2153 + } 2154 + (GridPlacement::Line(s), GridPlacement::Auto) => { 2155 + let s = line_to_index(s, explicit_count); 2156 + (s, s + 1) 2157 + } 2158 + (GridPlacement::Span(n), GridPlacement::Line(e)) => { 2159 + let e = line_to_index(e, explicit_count); 2160 + let n = n.max(1) as usize; 2161 + (e.saturating_sub(n), e) 2162 + } 2163 + (GridPlacement::Auto, GridPlacement::Line(e)) => { 2164 + let e = line_to_index(e, explicit_count); 2165 + if e > 0 { 2166 + (e - 1, e) 2167 + } else { 2168 + (0, 1) 2169 + } 2170 + } 2171 + (GridPlacement::Auto, GridPlacement::Span(n)) => { 2172 + // Will be handled by auto-placement; use sentinel 2173 + (0, n.max(1) as usize) 2174 + } 2175 + (GridPlacement::Span(n), GridPlacement::Auto) => { 2176 + // Will be handled by auto-placement; use sentinel 2177 + (0, n.max(1) as usize) 2178 + } 2179 + (GridPlacement::Auto, GridPlacement::Auto) => { 2180 + // Will be auto-placed 2181 + (0, 1) 2182 + } 2183 + (GridPlacement::Span(_), GridPlacement::Span(_)) => { 2184 + // Both span: treat as auto 2185 + (0, 1) 2186 + } 2187 + } 2188 + } 2189 + 2190 + /// Convert a 1-based grid line number to a 0-based index. 2191 + /// Positive lines are 1-based from the start. 2192 + /// Negative lines count from the end (-1 = last line). 2193 + fn line_to_index(line: i32, explicit_count: usize) -> usize { 2194 + if line > 0 { 2195 + (line as usize).saturating_sub(1) 2196 + } else if line < 0 { 2197 + let total_lines = explicit_count + 1; 2198 + let idx = total_lines as i32 + line; 2199 + idx.max(0) as usize 2200 + } else { 2201 + 0 2202 + } 2203 + } 2204 + 2205 + /// Auto-place items that have fully automatic placement. 2206 + fn auto_place_items( 2207 + items: &mut [GridItemInfo], 2208 + num_cols: usize, 2209 + max_row: &mut usize, 2210 + auto_flow: &GridAutoFlow, 2211 + ) { 2212 + let num_cols = num_cols.max(1); 2213 + 2214 + // Build occupation grid 2215 + let mut occupied: Vec<Vec<bool>> = Vec::new(); 2216 + let ensure_rows = |grid: &mut Vec<Vec<bool>>, needed: usize, cols: usize| { 2217 + while grid.len() < needed { 2218 + grid.push(vec![false; cols]); 2219 + } 2220 + }; 2221 + 2222 + // Mark explicitly placed items 2223 + for item in items.iter() { 2224 + if item.col_start != 0 || item.row_start != 0 || !is_auto_placed_item(item) { 2225 + ensure_rows(&mut occupied, item.row_end, num_cols); 2226 + for row in occupied.iter_mut().take(item.row_end).skip(item.row_start) { 2227 + for cell in row 2228 + .iter_mut() 2229 + .take(item.col_end.min(num_cols)) 2230 + .skip(item.col_start) 2231 + { 2232 + *cell = true; 2233 + } 2234 + } 2235 + } 2236 + } 2237 + 2238 + let is_dense = matches!( 2239 + auto_flow, 2240 + GridAutoFlow::RowDense | GridAutoFlow::ColumnDense 2241 + ); 2242 + let is_column_flow = matches!(auto_flow, GridAutoFlow::Column | GridAutoFlow::ColumnDense); 2243 + 2244 + let mut cursor_row: usize = 0; 2245 + let mut cursor_col: usize = 0; 2246 + 2247 + for item in items.iter_mut() { 2248 + if !is_auto_placed_item(item) { 2249 + continue; 2250 + } 2251 + 2252 + let col_span = item.col_end; // For auto-placed items, col_end stores the span 2253 + let row_span = if item.row_end > item.row_start { 2254 + item.row_end - item.row_start 2255 + } else { 2256 + 1 2257 + }; 2258 + 2259 + if is_dense { 2260 + cursor_row = 0; 2261 + cursor_col = 0; 2262 + } 2263 + 2264 + if is_column_flow { 2265 + // Column-first auto-placement 2266 + let placed = 'outer_col: loop { 2267 + ensure_rows(&mut occupied, cursor_row + row_span, num_cols); 2268 + if cursor_col + col_span <= num_cols { 2269 + // Check if area is free 2270 + let mut free = true; 2271 + 'check_col: for row in occupied.iter().skip(cursor_row).take(row_span) { 2272 + for (c, &cell) in row.iter().enumerate().skip(cursor_col).take(col_span) { 2273 + if c < num_cols && cell { 2274 + free = false; 2275 + break 'check_col; 2276 + } 2277 + } 2278 + } 2279 + if free { 2280 + break 'outer_col true; 2281 + } 2282 + } 2283 + cursor_row += 1; 2284 + if cursor_row + row_span > occupied.len() + 100 { 2285 + // Wrap to next column 2286 + cursor_row = 0; 2287 + cursor_col += 1; 2288 + if cursor_col >= num_cols { 2289 + // Need to expand grid — just place at bottom 2290 + cursor_col = 0; 2291 + cursor_row = occupied.len(); 2292 + ensure_rows(&mut occupied, cursor_row + row_span, num_cols); 2293 + break 'outer_col true; 2294 + } 2295 + } 2296 + }; 2297 + 2298 + if placed { 2299 + item.row_start = cursor_row; 2300 + item.row_end = cursor_row + row_span; 2301 + item.col_start = cursor_col; 2302 + item.col_end = cursor_col + col_span; 2303 + } 2304 + } else { 2305 + // Row-first auto-placement (default) 2306 + 'outer_row: loop { 2307 + ensure_rows(&mut occupied, cursor_row + row_span, num_cols); 2308 + while cursor_col + col_span <= num_cols { 2309 + // Check if area is free 2310 + let mut free = true; 2311 + 'check_row: for row in occupied.iter().skip(cursor_row).take(row_span) { 2312 + for (c, &cell) in row.iter().enumerate().skip(cursor_col).take(col_span) { 2313 + if c < num_cols && cell { 2314 + free = false; 2315 + break 'check_row; 2316 + } 2317 + } 2318 + } 2319 + if free { 2320 + break 'outer_row; 2321 + } 2322 + cursor_col += 1; 2323 + } 2324 + cursor_col = 0; 2325 + cursor_row += 1; 2326 + } 2327 + 2328 + item.row_start = cursor_row; 2329 + item.row_end = cursor_row + row_span; 2330 + item.col_start = cursor_col; 2331 + item.col_end = cursor_col + col_span; 2332 + } 2333 + 2334 + // Mark cells as occupied 2335 + ensure_rows(&mut occupied, item.row_end, num_cols); 2336 + for row in occupied.iter_mut().take(item.row_end).skip(item.row_start) { 2337 + for cell in row 2338 + .iter_mut() 2339 + .take(item.col_end.min(num_cols)) 2340 + .skip(item.col_start) 2341 + { 2342 + *cell = true; 2343 + } 2344 + } 2345 + 2346 + if item.row_end > *max_row { 2347 + *max_row = item.row_end; 2348 + } 2349 + 2350 + if !is_dense { 2351 + cursor_col = item.col_end; 2352 + cursor_row = item.row_start; 2353 + } 2354 + } 2355 + } 2356 + 2357 + /// Check if an item needs auto-placement (both axes are auto). 2358 + fn is_auto_placed_item(item: &GridItemInfo) -> bool { 2359 + // Items placed at (0,0) with span of 1 that haven't been explicitly placed 2360 + // We detect this by checking if both start positions are 0 and the item looks default 2361 + item.col_start == 0 && item.row_start == 0 2362 + } 2363 + 2364 + /// Resolve track sizes given template definitions, auto track size, and available space. 2365 + fn resolve_track_sizes( 2366 + template: &[GridTrackSize], 2367 + auto_size: &GridTrackSize, 2368 + num_tracks: usize, 2369 + available: f32, 2370 + container_width: f32, 2371 + ) -> Vec<f32> { 2372 + if num_tracks == 0 { 2373 + return Vec::new(); 2374 + } 2375 + 2376 + let mut sizes: Vec<f32> = Vec::with_capacity(num_tracks); 2377 + let mut total_fixed: f32 = 0.0; 2378 + let mut total_fr: f32 = 0.0; 2379 + 2380 + // Collect sizes and track fr/fixed totals 2381 + for i in 0..num_tracks { 2382 + let track = if i < template.len() { 2383 + &template[i] 2384 + } else { 2385 + auto_size 2386 + }; 2387 + 2388 + match track { 2389 + GridTrackSize::Length(px) => { 2390 + sizes.push(*px); 2391 + total_fixed += px; 2392 + } 2393 + GridTrackSize::Percentage(pct) => { 2394 + let px = pct / 100.0 * container_width; 2395 + sizes.push(px); 2396 + total_fixed += px; 2397 + } 2398 + GridTrackSize::Fr(fr) => { 2399 + sizes.push(0.0); // placeholder 2400 + total_fr += fr; 2401 + } 2402 + GridTrackSize::Auto => { 2403 + sizes.push(0.0); // will be resolved from content 2404 + // Auto tracks get remaining space after fr tracks 2405 + } 2406 + GridTrackSize::MinContent | GridTrackSize::MaxContent => { 2407 + sizes.push(0.0); // resolved from content 2408 + } 2409 + GridTrackSize::MinMax(min, max) => { 2410 + let min_px = track_size_to_px(min, container_width); 2411 + let max_px = track_size_to_px(max, container_width); 2412 + // Start with minimum, expand later 2413 + sizes.push(min_px); 2414 + total_fixed += min_px; 2415 + // If max is fr, add to fr pool for expansion 2416 + if let GridTrackSize::Fr(fr) = max.as_ref() { 2417 + total_fr += fr; 2418 + } else { 2419 + // Cap is the max value — will be applied during distribution 2420 + let _ = max_px; 2421 + } 2422 + } 2423 + } 2424 + } 2425 + 2426 + // Distribute remaining space to fr tracks 2427 + if total_fr > 0.0 { 2428 + let remaining = (available - total_fixed).max(0.0); 2429 + let fr_unit = remaining / total_fr; 2430 + 2431 + for (i, size) in sizes.iter_mut().enumerate() { 2432 + let track = if i < template.len() { 2433 + &template[i] 2434 + } else { 2435 + auto_size 2436 + }; 2437 + 2438 + match track { 2439 + GridTrackSize::Fr(fr) => { 2440 + *size = fr * fr_unit; 2441 + } 2442 + GridTrackSize::MinMax(_, max) => { 2443 + if let GridTrackSize::Fr(fr) = max.as_ref() { 2444 + let expanded = fr * fr_unit; 2445 + *size = size.max(expanded); 2446 + } 2447 + } 2448 + _ => {} 2449 + } 2450 + } 2451 + } else { 2452 + // No fr tracks: auto tracks share remaining space equally 2453 + let auto_count = (0..num_tracks) 2454 + .filter(|&i| { 2455 + let track = if i < template.len() { 2456 + &template[i] 2457 + } else { 2458 + auto_size 2459 + }; 2460 + matches!( 2461 + track, 2462 + GridTrackSize::Auto | GridTrackSize::MinContent | GridTrackSize::MaxContent 2463 + ) 2464 + }) 2465 + .count(); 2466 + 2467 + if auto_count > 0 { 2468 + let remaining = (available - total_fixed).max(0.0); 2469 + let per_auto = remaining / auto_count as f32; 2470 + 2471 + for (i, size) in sizes.iter_mut().enumerate() { 2472 + let track = if i < template.len() { 2473 + &template[i] 2474 + } else { 2475 + auto_size 2476 + }; 2477 + if matches!( 2478 + track, 2479 + GridTrackSize::Auto | GridTrackSize::MinContent | GridTrackSize::MaxContent 2480 + ) { 2481 + *size = per_auto; 2482 + } 2483 + } 2484 + } 2485 + } 2486 + 2487 + sizes 2488 + } 2489 + 2490 + /// Convert a GridTrackSize to a fixed px value (0.0 for flexible/auto). 2491 + fn track_size_to_px(size: &GridTrackSize, container_width: f32) -> f32 { 2492 + match size { 2493 + GridTrackSize::Length(px) => *px, 2494 + GridTrackSize::Percentage(pct) => pct / 100.0 * container_width, 2495 + GridTrackSize::Fr(_) => 0.0, 2496 + GridTrackSize::Auto | GridTrackSize::MinContent | GridTrackSize::MaxContent => 0.0, 2497 + GridTrackSize::MinMax(min, _) => track_size_to_px(min, container_width), 2498 + } 2499 + } 2500 + 2501 + /// Compute cumulative track start positions. 2502 + fn compute_track_positions(sizes: &[f32], gap: f32) -> Vec<f32> { 2503 + let mut positions = Vec::with_capacity(sizes.len()); 2504 + let mut pos = 0.0f32; 2505 + for (i, &size) in sizes.iter().enumerate() { 2506 + positions.push(pos); 2507 + pos += size; 2508 + if i < sizes.len() - 1 { 2509 + pos += gap; 2510 + } 2511 + } 2512 + positions 2513 + } 2514 + 2515 + /// Compute the total size of a span of tracks (from start to end line). 2516 + fn item_span_size(sizes: &[f32], positions: &[f32], start: usize, end: usize) -> f32 { 2517 + if start >= sizes.len() || end == 0 || start >= end { 2518 + return 0.0; 2519 + } 2520 + let end = end.min(sizes.len()); 2521 + let start_pos = if start < positions.len() { 2522 + positions[start] 2523 + } else { 2524 + 0.0 2525 + }; 2526 + let end_pos = if end <= sizes.len() { 2527 + // End line position = start of last track + its size 2528 + let last = end - 1; 2529 + if last < positions.len() && last < sizes.len() { 2530 + positions[last] + sizes[last] 2531 + } else { 2532 + start_pos 2533 + } 2534 + } else { 2535 + start_pos 2536 + }; 2537 + (end_pos - start_pos).max(0.0) 2538 + } 2539 + 2540 + /// Compute maximum content height contribution for each row from child items. 2541 + fn compute_row_contributions( 2542 + items: &[GridItemInfo], 2543 + child_heights: &[f32], 2544 + num_rows: usize, 2545 + ) -> Vec<f32> { 2546 + let mut contributions = vec![0.0f32; num_rows]; 2547 + for item in items { 2548 + let height = child_heights[item.child_idx]; 2549 + let span = item.row_end - item.row_start; 2550 + if span == 0 { 2551 + continue; 2552 + } 2553 + let per_row = height / span as f32; 2554 + for contrib in contributions 2555 + .iter_mut() 2556 + .take(item.row_end.min(num_rows)) 2557 + .skip(item.row_start) 2558 + { 2559 + *contrib = contrib.max(per_row); 2560 + } 2561 + } 2562 + contributions 2563 + } 2564 + 2565 + /// Finalize row sizes: use the larger of template size and content contribution. 2566 + fn finalize_row_sizes(template_sizes: &[f32], contributions: &[f32]) -> Vec<f32> { 2567 + template_sizes 2568 + .iter() 2569 + .enumerate() 2570 + .map(|(i, &size)| { 2571 + let contrib = contributions.get(i).copied().unwrap_or(0.0); 2572 + size.max(contrib) 2573 + }) 2574 + .collect() 1733 2575 } 1734 2576 1735 2577 /// Lay out children according to the CSS Flexbox algorithm (Level 1 §9). ··· 5592 6434 has_text, 5593 6435 "bare text alongside a float should be laid out (found {} children, none with text lines)", 5594 6436 container_box.children.len(), 6437 + ); 6438 + } 6439 + 6440 + // ----------------------------------------------------------------------- 6441 + // Grid layout tests 6442 + // ----------------------------------------------------------------------- 6443 + 6444 + fn parse_and_layout(html: &str) -> LayoutTree { 6445 + let doc = we_html::parse_html(html); 6446 + layout_doc(&doc) 6447 + } 6448 + 6449 + #[test] 6450 + fn grid_basic_three_column_fr() { 6451 + let tree = parse_and_layout( 6452 + "<html><head><style>\ 6453 + .grid { display: grid; grid-template-columns: 1fr 2fr 1fr; width: 400px; }\ 6454 + </style></head><body>\ 6455 + <div class='grid'><div>A</div><div>B</div><div>C</div></div>\ 6456 + </body></html>", 6457 + ); 6458 + 6459 + let body = &tree.root.children[0]; 6460 + let grid = &body.children[0]; 6461 + 6462 + assert_eq!(grid.children.len(), 3); 6463 + 6464 + // 400px / (1+2+1) = 100px per fr unit 6465 + let a = &grid.children[0]; 6466 + let b = &grid.children[1]; 6467 + let c = &grid.children[2]; 6468 + 6469 + assert!( 6470 + (a.rect.width - 100.0).abs() < 1.0, 6471 + "A width should be ~100, got {}", 6472 + a.rect.width 6473 + ); 6474 + assert!( 6475 + (b.rect.width - 200.0).abs() < 1.0, 6476 + "B width should be ~200, got {}", 6477 + b.rect.width 6478 + ); 6479 + assert!( 6480 + (c.rect.width - 100.0).abs() < 1.0, 6481 + "C width should be ~100, got {}", 6482 + c.rect.width 6483 + ); 6484 + 6485 + // All items should be on the first row (same y) 6486 + assert!( 6487 + (a.rect.y - b.rect.y).abs() < 1.0, 6488 + "A and B should be on same row" 6489 + ); 6490 + } 6491 + 6492 + #[test] 6493 + fn grid_auto_placement_wraps_rows() { 6494 + let tree = parse_and_layout( 6495 + "<html><head><style>\ 6496 + .grid { display: grid; grid-template-columns: 1fr 1fr; width: 200px; }\ 6497 + </style></head><body>\ 6498 + <div class='grid'><div>1</div><div>2</div><div>3</div></div>\ 6499 + </body></html>", 6500 + ); 6501 + 6502 + let body = &tree.root.children[0]; 6503 + let grid = &body.children[0]; 6504 + 6505 + assert_eq!(grid.children.len(), 3); 6506 + 6507 + let c1 = &grid.children[0]; 6508 + let c2 = &grid.children[1]; 6509 + let c3 = &grid.children[2]; 6510 + 6511 + assert!( 6512 + (c1.rect.y - c2.rect.y).abs() < 1.0, 6513 + "items 1 and 2 should be on same row" 6514 + ); 6515 + assert!(c3.rect.y > c1.rect.y, "item 3 should be on second row"); 6516 + } 6517 + 6518 + #[test] 6519 + fn grid_explicit_placement() { 6520 + let tree = parse_and_layout( 6521 + "<html><head><style>\ 6522 + .grid { display: grid; grid-template-columns: 100px 100px 100px; width: 300px; }\ 6523 + .a { grid-column: 1 / 2; grid-row: 1 / 2; }\ 6524 + .b { grid-column: 3 / 4; grid-row: 1 / 2; }\ 6525 + </style></head><body>\ 6526 + <div class='grid'><div class='a'>A</div><div class='b'>B</div></div>\ 6527 + </body></html>", 6528 + ); 6529 + 6530 + let body = &tree.root.children[0]; 6531 + let grid = &body.children[0]; 6532 + 6533 + let a = &grid.children[0]; 6534 + let b = &grid.children[1]; 6535 + 6536 + assert!(a.rect.x < 10.0, "A should be at start, got x={}", a.rect.x); 6537 + assert!( 6538 + (b.rect.x - 200.0).abs() < 10.0, 6539 + "B should be at col 3, got x={}", 6540 + b.rect.x 6541 + ); 6542 + } 6543 + 6544 + #[test] 6545 + fn grid_gap_adds_spacing() { 6546 + let tree = parse_and_layout( 6547 + "<html><head><style>\ 6548 + .grid { display: grid; grid-template-columns: 100px 100px; gap: 20px; width: 220px; }\ 6549 + </style></head><body>\ 6550 + <div class='grid'><div>1</div><div>2</div></div>\ 6551 + </body></html>", 6552 + ); 6553 + 6554 + let body = &tree.root.children[0]; 6555 + let grid = &body.children[0]; 6556 + 6557 + let c1 = &grid.children[0]; 6558 + let c2 = &grid.children[1]; 6559 + 6560 + assert!( 6561 + (c2.rect.x - c1.rect.x).abs() > 110.0, 6562 + "gap should add spacing between columns, c1.x={} c2.x={}", 6563 + c1.rect.x, 6564 + c2.rect.x 6565 + ); 6566 + } 6567 + 6568 + #[test] 6569 + fn grid_fixed_row_height() { 6570 + let tree = parse_and_layout( 6571 + "<html><head><style>\ 6572 + .grid { display: grid; grid-template-columns: 1fr; grid-template-rows: 50px; width: 200px; }\ 6573 + </style></head><body>\ 6574 + <div class='grid'><div>A</div></div>\ 6575 + </body></html>", 6576 + ); 6577 + 6578 + let body = &tree.root.children[0]; 6579 + let grid = &body.children[0]; 6580 + 6581 + assert!( 6582 + grid.rect.height >= 50.0, 6583 + "Grid height should be >= 50px, got {}", 6584 + grid.rect.height 5595 6585 ); 5596 6586 } 5597 6587 }
+614 -1
crates/style/src/computed.rs
··· 35 35 Inline, 36 36 Flex, 37 37 InlineFlex, 38 + Grid, 39 + InlineGrid, 38 40 None, 39 41 } 40 42 ··· 246 248 } 247 249 248 250 // --------------------------------------------------------------------------- 251 + // Grid enums 252 + // --------------------------------------------------------------------------- 253 + 254 + /// Grid track sizing value. 255 + #[derive(Debug, Clone, PartialEq, Default)] 256 + pub enum GridTrackSize { 257 + /// Fixed length in px. 258 + Length(f32), 259 + /// Percentage of containing block. 260 + Percentage(f32), 261 + /// Flexible fraction unit (`fr`). 262 + Fr(f32), 263 + /// Size to content. 264 + #[default] 265 + Auto, 266 + /// Minimum content size. 267 + MinContent, 268 + /// Maximum content size. 269 + MaxContent, 270 + /// `minmax(min, max)` — size between min and max. 271 + MinMax(Box<GridTrackSize>, Box<GridTrackSize>), 272 + } 273 + 274 + /// Grid item placement on a single axis (start or end). 275 + #[derive(Debug, Clone, Copy, PartialEq, Default)] 276 + pub enum GridPlacement { 277 + /// Automatic placement. 278 + #[default] 279 + Auto, 280 + /// Explicit line number (1-based, negative counts from end). 281 + Line(i32), 282 + /// Span N tracks. 283 + Span(i32), 284 + } 285 + 286 + /// Grid auto-flow direction and packing mode. 287 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 288 + pub enum GridAutoFlow { 289 + #[default] 290 + Row, 291 + Column, 292 + RowDense, 293 + ColumnDense, 294 + } 295 + 296 + // --------------------------------------------------------------------------- 297 + // JustifyItems (shared by grid and flex) 298 + // --------------------------------------------------------------------------- 299 + 300 + /// CSS `justify-items` property for grid containers. 301 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 302 + pub enum JustifyItems { 303 + #[default] 304 + Stretch, 305 + Start, 306 + End, 307 + Center, 308 + } 309 + 310 + /// CSS `justify-self` property for grid items. 311 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 312 + pub enum JustifySelf { 313 + #[default] 314 + Auto, 315 + Start, 316 + End, 317 + Center, 318 + Stretch, 319 + } 320 + 321 + // --------------------------------------------------------------------------- 249 322 // BorderStyle 250 323 // --------------------------------------------------------------------------- 251 324 ··· 389 462 pub align_self: AlignSelf, 390 463 pub order: i32, 391 464 465 + // Grid container properties 466 + pub grid_template_columns: Vec<GridTrackSize>, 467 + pub grid_template_rows: Vec<GridTrackSize>, 468 + pub grid_template_areas: Vec<Vec<String>>, 469 + pub grid_auto_columns: GridTrackSize, 470 + pub grid_auto_rows: GridTrackSize, 471 + pub grid_auto_flow: GridAutoFlow, 472 + pub justify_items: JustifyItems, 473 + 474 + // Grid item properties 475 + pub grid_column_start: GridPlacement, 476 + pub grid_column_end: GridPlacement, 477 + pub grid_row_start: GridPlacement, 478 + pub grid_row_end: GridPlacement, 479 + pub justify_self: JustifySelf, 480 + 392 481 // CSS Transitions 393 482 pub transition: TransitionSpec, 394 483 ··· 474 563 align_self: AlignSelf::Auto, 475 564 order: 0, 476 565 566 + grid_template_columns: Vec::new(), 567 + grid_template_rows: Vec::new(), 568 + grid_template_areas: Vec::new(), 569 + grid_auto_columns: GridTrackSize::Auto, 570 + grid_auto_rows: GridTrackSize::Auto, 571 + grid_auto_flow: GridAutoFlow::Row, 572 + justify_items: JustifyItems::Stretch, 573 + 574 + grid_column_start: GridPlacement::Auto, 575 + grid_column_end: GridPlacement::Auto, 576 + grid_row_start: GridPlacement::Auto, 577 + grid_row_end: GridPlacement::Auto, 578 + justify_self: JustifySelf::Auto, 579 + 477 580 transition: TransitionSpec::default(), 478 581 479 582 animation: AnimationSpec::default(), ··· 981 1084 "inline" => Display::Inline, 982 1085 "flex" => Display::Flex, 983 1086 "inline-flex" => Display::InlineFlex, 1087 + "grid" => Display::Grid, 1088 + "inline-grid" => Display::InlineGrid, 984 1089 _ => Display::Block, 985 1090 }, 986 1091 CssValue::None => Display::None, ··· 1482 1587 } 1483 1588 } 1484 1589 1590 + // Grid auto-flow 1591 + "grid-auto-flow" => { 1592 + style.grid_auto_flow = match value { 1593 + CssValue::Keyword(k) => match k.as_str() { 1594 + "row" => GridAutoFlow::Row, 1595 + "column" => GridAutoFlow::Column, 1596 + _ => style.grid_auto_flow, 1597 + }, 1598 + CssValue::List(vals) => { 1599 + let kws: Vec<&str> = vals 1600 + .iter() 1601 + .filter_map(|v| { 1602 + if let CssValue::Keyword(k) = v { 1603 + Some(k.as_str()) 1604 + } else { 1605 + None 1606 + } 1607 + }) 1608 + .collect(); 1609 + if kws.contains(&"row") && kws.contains(&"dense") { 1610 + GridAutoFlow::RowDense 1611 + } else if kws.contains(&"column") && kws.contains(&"dense") { 1612 + GridAutoFlow::ColumnDense 1613 + } else if kws.contains(&"dense") { 1614 + GridAutoFlow::RowDense 1615 + } else if kws.contains(&"column") { 1616 + GridAutoFlow::Column 1617 + } else { 1618 + GridAutoFlow::Row 1619 + } 1620 + } 1621 + _ => style.grid_auto_flow, 1622 + }; 1623 + } 1624 + 1625 + // Grid item placement 1626 + "grid-column-start" => { 1627 + style.grid_column_start = parse_grid_placement(value); 1628 + } 1629 + "grid-column-end" => { 1630 + style.grid_column_end = parse_grid_placement(value); 1631 + } 1632 + "grid-row-start" => { 1633 + style.grid_row_start = parse_grid_placement(value); 1634 + } 1635 + "grid-row-end" => { 1636 + style.grid_row_end = parse_grid_placement(value); 1637 + } 1638 + 1639 + // Grid/flex alignment 1640 + "justify-items" => { 1641 + style.justify_items = match value { 1642 + CssValue::Keyword(k) => match k.as_str() { 1643 + "start" => JustifyItems::Start, 1644 + "end" => JustifyItems::End, 1645 + "center" => JustifyItems::Center, 1646 + "stretch" => JustifyItems::Stretch, 1647 + _ => style.justify_items, 1648 + }, 1649 + _ => style.justify_items, 1650 + }; 1651 + } 1652 + "justify-self" => { 1653 + style.justify_self = match value { 1654 + CssValue::Keyword(k) => match k.as_str() { 1655 + "start" => JustifySelf::Start, 1656 + "end" => JustifySelf::End, 1657 + "center" => JustifySelf::Center, 1658 + "stretch" => JustifySelf::Stretch, 1659 + _ => style.justify_self, 1660 + }, 1661 + CssValue::Auto => JustifySelf::Auto, 1662 + _ => style.justify_self, 1663 + }; 1664 + } 1665 + 1485 1666 _ => {} // Unknown property — ignore 1486 1667 } 1487 1668 } 1488 1669 1670 + /// Parse a CssValue into a GridPlacement. 1671 + fn parse_grid_placement(value: &CssValue) -> GridPlacement { 1672 + match value { 1673 + CssValue::Auto => GridPlacement::Auto, 1674 + CssValue::Number(n) => GridPlacement::Line(*n as i32), 1675 + CssValue::Zero => GridPlacement::Line(0), 1676 + CssValue::List(vals) => { 1677 + // "span N" pattern 1678 + if vals.len() == 2 { 1679 + if let CssValue::Keyword(k) = &vals[0] { 1680 + if k == "span" { 1681 + if let CssValue::Number(n) = &vals[1] { 1682 + return GridPlacement::Span((*n as i32).max(1)); 1683 + } 1684 + } 1685 + } 1686 + } 1687 + GridPlacement::Auto 1688 + } 1689 + _ => GridPlacement::Auto, 1690 + } 1691 + } 1692 + 1693 + /// Handle grid template properties from raw component values. 1694 + /// Returns `true` if the property was handled. 1695 + fn apply_grid_property( 1696 + style: &mut ComputedStyle, 1697 + property: &str, 1698 + values: &[ComponentValue], 1699 + current_fs: f32, 1700 + viewport: (f32, f32), 1701 + ) -> bool { 1702 + match property { 1703 + "grid-template-columns" => { 1704 + style.grid_template_columns = parse_grid_track_list(values, current_fs, viewport); 1705 + true 1706 + } 1707 + "grid-template-rows" => { 1708 + style.grid_template_rows = parse_grid_track_list(values, current_fs, viewport); 1709 + true 1710 + } 1711 + "grid-template-areas" => { 1712 + style.grid_template_areas = parse_grid_template_areas(values); 1713 + true 1714 + } 1715 + "grid-auto-columns" => { 1716 + if let Some(size) = parse_single_grid_track(values, current_fs, viewport) { 1717 + style.grid_auto_columns = size; 1718 + } 1719 + true 1720 + } 1721 + "grid-auto-rows" => { 1722 + if let Some(size) = parse_single_grid_track(values, current_fs, viewport) { 1723 + style.grid_auto_rows = size; 1724 + } 1725 + true 1726 + } 1727 + _ => false, 1728 + } 1729 + } 1730 + 1731 + /// Parse a grid track list from raw component values. 1732 + /// Handles `fr` units, `repeat()`, `minmax()`, `auto`, `min-content`, `max-content`. 1733 + fn parse_grid_track_list( 1734 + values: &[ComponentValue], 1735 + fs: f32, 1736 + viewport: (f32, f32), 1737 + ) -> Vec<GridTrackSize> { 1738 + let mut tracks = Vec::new(); 1739 + for cv in values 1740 + .iter() 1741 + .filter(|v| !matches!(v, ComponentValue::Whitespace)) 1742 + { 1743 + match cv { 1744 + ComponentValue::Dimension(n, _, unit) => { 1745 + let u = unit.to_ascii_lowercase(); 1746 + if u == "fr" { 1747 + tracks.push(GridTrackSize::Fr(*n as f32)); 1748 + } else { 1749 + let px = resolve_dimension_to_px(*n, &u, fs, viewport); 1750 + tracks.push(GridTrackSize::Length(px)); 1751 + } 1752 + } 1753 + ComponentValue::Percentage(n) => { 1754 + tracks.push(GridTrackSize::Percentage(*n as f32)); 1755 + } 1756 + ComponentValue::Number(n, _) => { 1757 + if *n == 0.0 { 1758 + tracks.push(GridTrackSize::Length(0.0)); 1759 + } 1760 + } 1761 + ComponentValue::Ident(s) => match s.to_ascii_lowercase().as_str() { 1762 + "auto" => tracks.push(GridTrackSize::Auto), 1763 + "min-content" => tracks.push(GridTrackSize::MinContent), 1764 + "max-content" => tracks.push(GridTrackSize::MaxContent), 1765 + _ => {} 1766 + }, 1767 + ComponentValue::Function(name, args) => match name.to_ascii_lowercase().as_str() { 1768 + "minmax" => { 1769 + if let Some(mm) = parse_grid_minmax(args, fs, viewport) { 1770 + tracks.push(mm); 1771 + } 1772 + } 1773 + "repeat" => { 1774 + tracks.extend(parse_grid_repeat(args, fs, viewport)); 1775 + } 1776 + _ => {} 1777 + }, 1778 + _ => {} 1779 + } 1780 + } 1781 + tracks 1782 + } 1783 + 1784 + /// Parse a single grid track size from component values (for grid-auto-columns/rows). 1785 + fn parse_single_grid_track( 1786 + values: &[ComponentValue], 1787 + fs: f32, 1788 + viewport: (f32, f32), 1789 + ) -> Option<GridTrackSize> { 1790 + let tracks = parse_grid_track_list(values, fs, viewport); 1791 + tracks.into_iter().next() 1792 + } 1793 + 1794 + /// Parse `minmax(min, max)` function arguments into a GridTrackSize. 1795 + fn parse_grid_minmax( 1796 + args: &[ComponentValue], 1797 + fs: f32, 1798 + viewport: (f32, f32), 1799 + ) -> Option<GridTrackSize> { 1800 + let non_ws: Vec<&ComponentValue> = args 1801 + .iter() 1802 + .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 1803 + .collect(); 1804 + if non_ws.len() != 2 { 1805 + return None; 1806 + } 1807 + let min = parse_grid_track_cv(non_ws[0], fs, viewport)?; 1808 + let max = parse_grid_track_cv(non_ws[1], fs, viewport)?; 1809 + Some(GridTrackSize::MinMax(Box::new(min), Box::new(max))) 1810 + } 1811 + 1812 + /// Parse `repeat(count, tracks...)` function arguments. 1813 + fn parse_grid_repeat(args: &[ComponentValue], fs: f32, viewport: (f32, f32)) -> Vec<GridTrackSize> { 1814 + let non_ws: Vec<&ComponentValue> = args 1815 + .iter() 1816 + .filter(|v| !matches!(v, ComponentValue::Whitespace)) 1817 + .collect(); 1818 + // Find the first comma to split count from track list 1819 + let comma_pos = non_ws 1820 + .iter() 1821 + .position(|v| matches!(v, ComponentValue::Comma)); 1822 + let comma_pos = match comma_pos { 1823 + Some(p) => p, 1824 + None => return Vec::new(), 1825 + }; 1826 + // Parse repeat count 1827 + let count = match non_ws.first() { 1828 + Some(ComponentValue::Number(n, _)) => *n as usize, 1829 + _ => return Vec::new(), 1830 + }; 1831 + if count == 0 || count > 10000 { 1832 + return Vec::new(); 1833 + } 1834 + // Parse the track list after the comma 1835 + let track_cvs: Vec<ComponentValue> = non_ws[comma_pos + 1..] 1836 + .iter() 1837 + .filter(|v| !matches!(v, ComponentValue::Comma)) 1838 + .map(|v| (*v).clone()) 1839 + .collect(); 1840 + let pattern = parse_grid_track_list(&track_cvs, fs, viewport); 1841 + let mut result = Vec::with_capacity(pattern.len() * count); 1842 + for _ in 0..count { 1843 + result.extend(pattern.iter().cloned()); 1844 + } 1845 + result 1846 + } 1847 + 1848 + /// Parse a single component value into a GridTrackSize. 1849 + fn parse_grid_track_cv( 1850 + cv: &ComponentValue, 1851 + fs: f32, 1852 + viewport: (f32, f32), 1853 + ) -> Option<GridTrackSize> { 1854 + match cv { 1855 + ComponentValue::Dimension(n, _, unit) => { 1856 + let u = unit.to_ascii_lowercase(); 1857 + if u == "fr" { 1858 + Some(GridTrackSize::Fr(*n as f32)) 1859 + } else { 1860 + Some(GridTrackSize::Length(resolve_dimension_to_px( 1861 + *n, &u, fs, viewport, 1862 + ))) 1863 + } 1864 + } 1865 + ComponentValue::Percentage(n) => Some(GridTrackSize::Percentage(*n as f32)), 1866 + ComponentValue::Number(n, _) if *n == 0.0 => Some(GridTrackSize::Length(0.0)), 1867 + ComponentValue::Ident(s) => match s.to_ascii_lowercase().as_str() { 1868 + "auto" => Some(GridTrackSize::Auto), 1869 + "min-content" => Some(GridTrackSize::MinContent), 1870 + "max-content" => Some(GridTrackSize::MaxContent), 1871 + _ => None, 1872 + }, 1873 + ComponentValue::Function(name, args) => { 1874 + if name.eq_ignore_ascii_case("minmax") { 1875 + parse_grid_minmax(args, fs, viewport) 1876 + } else { 1877 + None 1878 + } 1879 + } 1880 + _ => None, 1881 + } 1882 + } 1883 + 1884 + /// Resolve a CSS dimension (value + unit string) to px. 1885 + fn resolve_dimension_to_px(n: f64, unit: &str, fs: f32, viewport: (f32, f32)) -> f32 { 1886 + let n = n as f32; 1887 + match unit { 1888 + "px" => n, 1889 + "em" => n * fs, 1890 + "rem" => n * 16.0, 1891 + "pt" => n * 96.0 / 72.0, 1892 + "cm" => n * 96.0 / 2.54, 1893 + "mm" => n * 96.0 / 25.4, 1894 + "in" => n * 96.0, 1895 + "pc" => n * 96.0 / 6.0, 1896 + "vw" => n * viewport.0 / 100.0, 1897 + "vh" => n * viewport.1 / 100.0, 1898 + "vmin" => n * viewport.0.min(viewport.1) / 100.0, 1899 + "vmax" => n * viewport.0.max(viewport.1) / 100.0, 1900 + _ => n, 1901 + } 1902 + } 1903 + 1904 + /// Parse `grid-template-areas` from component values. 1905 + /// Each string value defines a row of named areas. 1906 + fn parse_grid_template_areas(values: &[ComponentValue]) -> Vec<Vec<String>> { 1907 + let mut rows = Vec::new(); 1908 + for cv in values { 1909 + if let ComponentValue::String(s) = cv { 1910 + let row: Vec<String> = s.split_whitespace().map(|w| w.to_string()).collect(); 1911 + if !row.is_empty() { 1912 + rows.push(row); 1913 + } 1914 + } 1915 + } 1916 + rows 1917 + } 1918 + 1489 1919 /// Handle transition-related properties from raw component values. 1490 1920 /// Returns `true` if the property was handled (caller should skip normal processing). 1491 1921 fn apply_transition_property( ··· 1765 2195 "flex-basis" => style.flex_basis = parent.flex_basis, 1766 2196 "align-self" => style.align_self = parent.align_self, 1767 2197 "order" => style.order = parent.order, 2198 + "grid-template-columns" => { 2199 + style.grid_template_columns = parent.grid_template_columns.clone() 2200 + } 2201 + "grid-template-rows" => style.grid_template_rows = parent.grid_template_rows.clone(), 2202 + "grid-template-areas" => style.grid_template_areas = parent.grid_template_areas.clone(), 2203 + "grid-auto-columns" => style.grid_auto_columns = parent.grid_auto_columns.clone(), 2204 + "grid-auto-rows" => style.grid_auto_rows = parent.grid_auto_rows.clone(), 2205 + "grid-auto-flow" => style.grid_auto_flow = parent.grid_auto_flow, 2206 + "justify-items" => style.justify_items = parent.justify_items, 2207 + "grid-column-start" => style.grid_column_start = parent.grid_column_start, 2208 + "grid-column-end" => style.grid_column_end = parent.grid_column_end, 2209 + "grid-row-start" => style.grid_row_start = parent.grid_row_start, 2210 + "grid-row-end" => style.grid_row_end = parent.grid_row_end, 2211 + "justify-self" => style.justify_self = parent.justify_self, 1768 2212 "transition" => style.transition = parent.transition.clone(), 1769 2213 "animation" => style.animation = parent.animation.clone(), 1770 2214 _ => {} ··· 1825 2269 "flex-basis" => style.flex_basis = initial.flex_basis, 1826 2270 "align-self" => style.align_self = initial.align_self, 1827 2271 "order" => style.order = initial.order, 2272 + "grid-template-columns" => { 2273 + style.grid_template_columns = initial.grid_template_columns.clone() 2274 + } 2275 + "grid-template-rows" => style.grid_template_rows = initial.grid_template_rows.clone(), 2276 + "grid-template-areas" => style.grid_template_areas = initial.grid_template_areas.clone(), 2277 + "grid-auto-columns" => style.grid_auto_columns = initial.grid_auto_columns.clone(), 2278 + "grid-auto-rows" => style.grid_auto_rows = initial.grid_auto_rows.clone(), 2279 + "grid-auto-flow" => style.grid_auto_flow = initial.grid_auto_flow, 2280 + "justify-items" => style.justify_items = initial.justify_items, 2281 + "grid-column-start" => style.grid_column_start = initial.grid_column_start, 2282 + "grid-column-end" => style.grid_column_end = initial.grid_column_end, 2283 + "grid-row-start" => style.grid_row_start = initial.grid_row_start, 2284 + "grid-row-end" => style.grid_row_end = initial.grid_row_end, 2285 + "justify-self" => style.justify_self = initial.justify_self, 1828 2286 "transition" => style.transition = initial.transition, 1829 2287 "animation" => style.animation = initial.animation, 1830 2288 _ => {} ··· 2245 2703 &decl.value 2246 2704 }; 2247 2705 2248 - // Handle transition/animation properties specially (need raw ComponentValues) 2706 + // Handle transition/animation/grid-template properties specially (need raw ComponentValues) 2249 2707 if apply_transition_property(&mut style, &decl.property, values) { 2250 2708 continue; 2251 2709 } 2252 2710 if apply_animation_property(&mut style, &decl.property, values) { 2711 + continue; 2712 + } 2713 + let fs = style.font_size; 2714 + if apply_grid_property(&mut style, &decl.property, values, fs, viewport) { 2253 2715 continue; 2254 2716 } 2255 2717 ··· 2295 2757 if apply_transition_property(&mut style, &decl.property, values) { 2296 2758 continue; 2297 2759 } 2760 + let fs = style.font_size; 2761 + if apply_grid_property(&mut style, &decl.property, values, fs, viewport) { 2762 + continue; 2763 + } 2298 2764 2299 2765 let property = decl.property.as_str(); 2300 2766 if let Some(longhands) = expand_shorthand(property, values, false) { ··· 2328 2794 }; 2329 2795 2330 2796 if apply_transition_property(&mut style, &decl.property, values) { 2797 + continue; 2798 + } 2799 + let fs = style.font_size; 2800 + if apply_grid_property(&mut style, &decl.property, values, fs, viewport) { 2331 2801 continue; 2332 2802 } 2333 2803 ··· 3856 4326 let div_node = &body_node.children[0]; 3857 4327 3858 4328 assert_eq!(div_node.style.line_height, 20.0); 4329 + } 4330 + 4331 + fn grid_doc(css: &str, body_html: &str) -> StyledNode { 4332 + let html = format!( 4333 + "<html><head><style>{}</style></head><body>{}</body></html>", 4334 + css, body_html 4335 + ); 4336 + let doc = we_html::parse_html(&html); 4337 + let sheets = extract_stylesheets(&doc); 4338 + resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap() 4339 + } 4340 + 4341 + #[test] 4342 + fn display_grid_parsing() { 4343 + let styled = grid_doc("div { display: grid; }", "<div>hello</div>"); 4344 + let div = &styled.children[0].children[0]; 4345 + assert_eq!(div.style.display, Display::Grid); 4346 + } 4347 + 4348 + #[test] 4349 + fn display_inline_grid_parsing() { 4350 + let styled = grid_doc("div { display: inline-grid; }", "<div>hello</div>"); 4351 + let div = &styled.children[0].children[0]; 4352 + assert_eq!(div.style.display, Display::InlineGrid); 4353 + } 4354 + 4355 + #[test] 4356 + fn grid_template_columns_fr() { 4357 + let styled = grid_doc( 4358 + "div { display: grid; grid-template-columns: 1fr 2fr 1fr; }", 4359 + "<div>hello</div>", 4360 + ); 4361 + let div = &styled.children[0].children[0]; 4362 + assert_eq!(div.style.grid_template_columns.len(), 3); 4363 + assert_eq!(div.style.grid_template_columns[0], GridTrackSize::Fr(1.0)); 4364 + assert_eq!(div.style.grid_template_columns[1], GridTrackSize::Fr(2.0)); 4365 + assert_eq!(div.style.grid_template_columns[2], GridTrackSize::Fr(1.0)); 4366 + } 4367 + 4368 + #[test] 4369 + fn grid_template_columns_mixed() { 4370 + let styled = grid_doc( 4371 + "div { display: grid; grid-template-columns: 100px auto 1fr; }", 4372 + "<div>hello</div>", 4373 + ); 4374 + let div = &styled.children[0].children[0]; 4375 + assert_eq!(div.style.grid_template_columns.len(), 3); 4376 + assert_eq!( 4377 + div.style.grid_template_columns[0], 4378 + GridTrackSize::Length(100.0) 4379 + ); 4380 + assert_eq!(div.style.grid_template_columns[1], GridTrackSize::Auto); 4381 + assert_eq!(div.style.grid_template_columns[2], GridTrackSize::Fr(1.0)); 4382 + } 4383 + 4384 + #[test] 4385 + fn grid_template_rows_parsing() { 4386 + let styled = grid_doc( 4387 + "div { display: grid; grid-template-rows: auto 100px auto; }", 4388 + "<div>hello</div>", 4389 + ); 4390 + let div = &styled.children[0].children[0]; 4391 + assert_eq!(div.style.grid_template_rows.len(), 3); 4392 + assert_eq!(div.style.grid_template_rows[0], GridTrackSize::Auto); 4393 + assert_eq!( 4394 + div.style.grid_template_rows[1], 4395 + GridTrackSize::Length(100.0) 4396 + ); 4397 + assert_eq!(div.style.grid_template_rows[2], GridTrackSize::Auto); 4398 + } 4399 + 4400 + #[test] 4401 + fn grid_template_columns_repeat() { 4402 + let styled = grid_doc( 4403 + "div { display: grid; grid-template-columns: repeat(3, 1fr); }", 4404 + "<div>hello</div>", 4405 + ); 4406 + let div = &styled.children[0].children[0]; 4407 + assert_eq!(div.style.grid_template_columns.len(), 3); 4408 + for track in &div.style.grid_template_columns { 4409 + assert_eq!(*track, GridTrackSize::Fr(1.0)); 4410 + } 4411 + } 4412 + 4413 + #[test] 4414 + fn grid_template_columns_minmax() { 4415 + let styled = grid_doc( 4416 + "div { display: grid; grid-template-columns: minmax(100px, 1fr); }", 4417 + "<div>hello</div>", 4418 + ); 4419 + let div = &styled.children[0].children[0]; 4420 + assert_eq!(div.style.grid_template_columns.len(), 1); 4421 + assert!(matches!( 4422 + &div.style.grid_template_columns[0], 4423 + GridTrackSize::MinMax(min, max) 4424 + if matches!(min.as_ref(), GridTrackSize::Length(px) if (*px - 100.0).abs() < 0.1) 4425 + && matches!(max.as_ref(), GridTrackSize::Fr(f) if (*f - 1.0).abs() < 0.1) 4426 + )); 4427 + } 4428 + 4429 + #[test] 4430 + fn grid_auto_flow_parsing() { 4431 + let styled = grid_doc( 4432 + "div { display: grid; grid-auto-flow: column; }", 4433 + "<div>hello</div>", 4434 + ); 4435 + let div = &styled.children[0].children[0]; 4436 + assert_eq!(div.style.grid_auto_flow, GridAutoFlow::Column); 4437 + } 4438 + 4439 + #[test] 4440 + fn grid_item_placement_parsing() { 4441 + let styled = grid_doc( 4442 + "div { display: grid; } span { grid-column: 1 / 3; grid-row: 2 / span 2; }", 4443 + "<div><span>A</span></div>", 4444 + ); 4445 + let div = &styled.children[0].children[0]; 4446 + let span = &div.children[0]; 4447 + assert_eq!(span.style.grid_column_start, GridPlacement::Line(1)); 4448 + assert_eq!(span.style.grid_column_end, GridPlacement::Line(3)); 4449 + assert_eq!(span.style.grid_row_start, GridPlacement::Line(2)); 4450 + assert_eq!(span.style.grid_row_end, GridPlacement::Span(2)); 4451 + } 4452 + 4453 + #[test] 4454 + fn grid_template_areas_parsing() { 4455 + let styled = grid_doc( 4456 + r#"div { display: grid; grid-template-areas: "a a b" "a a b" "c c c"; }"#, 4457 + "<div>hello</div>", 4458 + ); 4459 + let div = &styled.children[0].children[0]; 4460 + assert_eq!(div.style.grid_template_areas.len(), 3); 4461 + assert_eq!(div.style.grid_template_areas[0], vec!["a", "a", "b"]); 4462 + assert_eq!(div.style.grid_template_areas[1], vec!["a", "a", "b"]); 4463 + assert_eq!(div.style.grid_template_areas[2], vec!["c", "c", "c"]); 4464 + } 4465 + 4466 + #[test] 4467 + fn grid_gap_parsing() { 4468 + let styled = grid_doc("div { display: grid; gap: 10px 20px; }", "<div>hello</div>"); 4469 + let div = &styled.children[0].children[0]; 4470 + assert_eq!(div.style.row_gap, 10.0); 4471 + assert_eq!(div.style.column_gap, 20.0); 3859 4472 } 3860 4473 }