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 Flexbox layout (Level 1)

Add full flexbox support with flex container and item properties:

CSS parsing:
- Parse display: flex/inline-flex
- Parse flex container props: flex-direction, flex-wrap, justify-content,
align-items, align-content, gap/row-gap/column-gap
- Parse flex item props: flex-grow, flex-shrink, flex-basis, align-self, order
- Expand flex, flex-flow, and gap shorthands

Layout algorithm (CSS Flexbox Level 1 §9):
- Determine main/cross axes from flex-direction
- Content-based flex-basis sizing for auto basis
- Distribute free space via flex-grow/flex-shrink
- Line wrapping (flex-wrap: wrap/wrap-reverse)
- Main axis justification (all justify-content modes)
- Cross axis alignment (all align-items/align-self modes)
- Multi-line align-content distribution
- Visual ordering via order property
- Gap spacing between items
- Reverse direction support (row-reverse, column-reverse)

Tests: 10 new flex layout tests covering row/column direction,
grow/shrink, justify-content, align-items, wrap, gap, and order.

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

+1415 -4
+111
crates/css/src/values.rs
··· 359 359 .collect(), 360 360 ), 361 361 "background" => Some(expand_background(values, important)), 362 + "flex" => Some(expand_flex(values, important)), 363 + "flex-flow" => Some(expand_flex_flow(values, important)), 364 + "gap" => Some(expand_gap(values, important)), 362 365 _ => Option::None, 363 366 } 367 + } 368 + 369 + /// Expand the `flex` shorthand into `flex-grow`, `flex-shrink`, `flex-basis`. 370 + fn expand_flex(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 371 + let parsed: Vec<CssValue> = values 372 + .iter() 373 + .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 374 + .map(parse_single_value) 375 + .collect(); 376 + 377 + let (grow, shrink, basis) = match parsed.as_slice() { 378 + [CssValue::None] => (CssValue::Number(0.0), CssValue::Number(0.0), CssValue::Auto), 379 + [CssValue::Auto] => (CssValue::Number(1.0), CssValue::Number(1.0), CssValue::Auto), 380 + [CssValue::Number(g)] => (CssValue::Number(*g), CssValue::Number(1.0), CssValue::Zero), 381 + [CssValue::Zero] => (CssValue::Number(0.0), CssValue::Number(1.0), CssValue::Zero), 382 + [CssValue::Number(g), CssValue::Number(s)] => { 383 + (CssValue::Number(*g), CssValue::Number(*s), CssValue::Zero) 384 + } 385 + [CssValue::Number(g), CssValue::Number(s), basis] => { 386 + (CssValue::Number(*g), CssValue::Number(*s), basis.clone()) 387 + } 388 + _ => (CssValue::Number(0.0), CssValue::Number(1.0), CssValue::Auto), 389 + }; 390 + 391 + vec![ 392 + LonghandDeclaration { 393 + property: "flex-grow".to_string(), 394 + value: grow, 395 + important, 396 + }, 397 + LonghandDeclaration { 398 + property: "flex-shrink".to_string(), 399 + value: shrink, 400 + important, 401 + }, 402 + LonghandDeclaration { 403 + property: "flex-basis".to_string(), 404 + value: basis, 405 + important, 406 + }, 407 + ] 408 + } 409 + 410 + /// Expand the `flex-flow` shorthand into `flex-direction` and `flex-wrap`. 411 + fn expand_flex_flow(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 412 + let parsed: Vec<CssValue> = values 413 + .iter() 414 + .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 415 + .map(parse_single_value) 416 + .collect(); 417 + 418 + let mut direction = CssValue::Keyword("row".to_string()); 419 + let mut wrap = CssValue::Keyword("nowrap".to_string()); 420 + 421 + for val in &parsed { 422 + if let CssValue::Keyword(k) = val { 423 + match k.as_str() { 424 + "row" | "row-reverse" | "column" | "column-reverse" => { 425 + direction = val.clone(); 426 + } 427 + "nowrap" | "wrap" | "wrap-reverse" => { 428 + wrap = val.clone(); 429 + } 430 + _ => {} 431 + } 432 + } 433 + } 434 + 435 + vec![ 436 + LonghandDeclaration { 437 + property: "flex-direction".to_string(), 438 + value: direction, 439 + important, 440 + }, 441 + LonghandDeclaration { 442 + property: "flex-wrap".to_string(), 443 + value: wrap, 444 + important, 445 + }, 446 + ] 447 + } 448 + 449 + /// Expand the `gap` shorthand into `row-gap` and `column-gap`. 450 + fn expand_gap(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 451 + let parsed: Vec<CssValue> = values 452 + .iter() 453 + .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 454 + .map(parse_single_value) 455 + .collect(); 456 + 457 + let (row, col) = match parsed.as_slice() { 458 + [single] => (single.clone(), single.clone()), 459 + [r, c] => (r.clone(), c.clone()), 460 + _ => (CssValue::Zero, CssValue::Zero), 461 + }; 462 + 463 + vec![ 464 + LonghandDeclaration { 465 + property: "row-gap".to_string(), 466 + value: row, 467 + important, 468 + }, 469 + LonghandDeclaration { 470 + property: "column-gap".to_string(), 471 + value: col, 472 + important, 473 + }, 474 + ] 364 475 } 365 476 366 477 /// Expand a box-model shorthand (margin, padding) using the 1-to-4 value pattern.
+1062 -4
crates/layout/src/lib.rs
··· 8 8 use we_css::values::Color; 9 9 use we_dom::{Document, NodeData, NodeId}; 10 10 use we_style::computed::{ 11 - BorderStyle, BoxSizing, ComputedStyle, Display, LengthOrAuto, Overflow, Position, StyledNode, 11 + AlignContent, AlignItems, AlignSelf, BorderStyle, BoxSizing, ComputedStyle, Display, 12 + FlexDirection, FlexWrap, JustifyContent, LengthOrAuto, Overflow, Position, StyledNode, 12 13 TextAlign, TextDecoration, Visibility, 13 14 }; 14 15 use we_text::font::Font; ··· 68 69 #[derive(Debug)] 69 70 pub struct LayoutBox { 70 71 pub box_type: BoxType, 72 + pub display: Display, 71 73 pub rect: Rect, 72 74 pub margin: EdgeSizes, 73 75 pub padding: EdgeSizes, ··· 115 117 /// Natural content height before CSS height override. 116 118 /// Used to determine overflow for scroll containers. 117 119 pub content_height: f32, 120 + // Flex container properties 121 + pub flex_direction: FlexDirection, 122 + pub flex_wrap: FlexWrap, 123 + pub justify_content: JustifyContent, 124 + pub align_items: AlignItems, 125 + pub align_content: AlignContent, 126 + pub row_gap: f32, 127 + pub column_gap: f32, 128 + // Flex item properties 129 + pub flex_grow: f32, 130 + pub flex_shrink: f32, 131 + pub flex_basis: LengthOrAuto, 132 + pub align_self: AlignSelf, 133 + pub order: i32, 118 134 } 119 135 120 136 impl LayoutBox { 121 137 fn new(box_type: BoxType, style: &ComputedStyle) -> Self { 122 138 LayoutBox { 123 139 box_type, 140 + display: style.display, 124 141 rect: Rect::default(), 125 142 margin: EdgeSizes::default(), 126 143 padding: EdgeSizes::default(), ··· 167 184 css_offsets: [style.top, style.right, style.bottom, style.left], 168 185 visibility: style.visibility, 169 186 content_height: 0.0, 187 + flex_direction: style.flex_direction, 188 + flex_wrap: style.flex_wrap, 189 + justify_content: style.justify_content, 190 + align_items: style.align_items, 191 + align_content: style.align_content, 192 + row_gap: style.row_gap, 193 + column_gap: style.column_gap, 194 + flex_grow: style.flex_grow, 195 + flex_shrink: style.flex_shrink, 196 + flex_basis: style.flex_basis, 197 + align_self: style.align_self, 198 + order: style.order, 170 199 } 171 200 } 172 201 ··· 343 372 } 344 373 345 374 let box_type = match style.display { 346 - Display::Block => BoxType::Block(node), 375 + Display::Block | Display::Flex | Display::InlineFlex => BoxType::Block(node), 347 376 Display::Inline => BoxType::Inline(node), 348 377 Display::None => unreachable!(), 349 378 }; ··· 539 568 540 569 match &b.box_type { 541 570 BoxType::Block(_) | BoxType::Anonymous => { 542 - if has_block_children(b) { 571 + if matches!(b.display, Display::Flex | Display::InlineFlex) { 572 + layout_flex_children(b, viewport_height, font, doc); 573 + } else if has_block_children(b) { 543 574 layout_block_children(b, viewport_height, font, doc); 544 575 } else { 545 576 layout_inline_children(b, font, doc); ··· 632 663 /// Returns `true` if this box establishes a new block formatting context, 633 664 /// which prevents its margins from collapsing with children. 634 665 fn establishes_bfc(b: &LayoutBox) -> bool { 635 - b.overflow != Overflow::Visible 666 + b.overflow != Overflow::Visible || matches!(b.display, Display::Flex | Display::InlineFlex) 636 667 } 637 668 638 669 /// Returns `true` if a block box has no in-flow content (empty block). ··· 821 852 } 822 853 823 854 parent.rect.height = cursor_y - parent.rect.y; 855 + } 856 + 857 + // --------------------------------------------------------------------------- 858 + // Flex layout 859 + // --------------------------------------------------------------------------- 860 + 861 + /// Measure the intrinsic max-content width of a flex item's content. 862 + /// Returns the width needed to lay out content without wrapping. 863 + fn measure_flex_item_max_content_width(child: &LayoutBox, font: &Font, _doc: &Document) -> f32 { 864 + // Measure the widest text line from all inline content. 865 + let mut max_width = 0.0f32; 866 + measure_box_content_width(child, font, &mut max_width); 867 + max_width 868 + } 869 + 870 + /// Recursively measure max content width of a layout box tree. 871 + fn measure_box_content_width(b: &LayoutBox, font: &Font, max_w: &mut f32) { 872 + match &b.box_type { 873 + BoxType::TextRun { text, .. } => { 874 + let w = measure_text_width(font, text, b.font_size); 875 + if w > *max_w { 876 + *max_w = w; 877 + } 878 + } 879 + _ => { 880 + let horiz = b.border.left + b.border.right + b.padding.left + b.padding.right; 881 + let mut child_max = 0.0f32; 882 + for child in &b.children { 883 + measure_box_content_width(child, font, &mut child_max); 884 + } 885 + let total = child_max + horiz; 886 + if total > *max_w { 887 + *max_w = total; 888 + } 889 + } 890 + } 891 + } 892 + 893 + /// 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) { 895 + let flex_direction = parent.flex_direction; 896 + let flex_wrap = parent.flex_wrap; 897 + let justify_content = parent.justify_content; 898 + let align_items = parent.align_items; 899 + let align_content = parent.align_content; 900 + let row_gap = parent.row_gap; 901 + let column_gap = parent.column_gap; 902 + 903 + let container_main_size = match flex_direction { 904 + FlexDirection::Row | FlexDirection::RowReverse => parent.rect.width, 905 + FlexDirection::Column | FlexDirection::ColumnReverse => { 906 + match parent.css_height { 907 + LengthOrAuto::Length(h) => h, 908 + LengthOrAuto::Percentage(p) => p / 100.0 * viewport_height, 909 + LengthOrAuto::Auto => viewport_height, // fallback 910 + } 911 + } 912 + }; 913 + let container_cross_size = match flex_direction { 914 + FlexDirection::Row | FlexDirection::RowReverse => match parent.css_height { 915 + LengthOrAuto::Length(h) => Some(h), 916 + LengthOrAuto::Percentage(p) => Some(p / 100.0 * viewport_height), 917 + LengthOrAuto::Auto => None, 918 + }, 919 + FlexDirection::Column | FlexDirection::ColumnReverse => Some(parent.rect.width), 920 + }; 921 + 922 + let is_row = matches!( 923 + flex_direction, 924 + FlexDirection::Row | FlexDirection::RowReverse 925 + ); 926 + let is_reverse = matches!( 927 + flex_direction, 928 + FlexDirection::RowReverse | FlexDirection::ColumnReverse 929 + ); 930 + 931 + if parent.children.is_empty() { 932 + parent.rect.height = 0.0; 933 + return; 934 + } 935 + 936 + // Sort children by `order` (stable sort preserves DOM order for equal values). 937 + let child_count = parent.children.len(); 938 + let mut order_indices: Vec<usize> = (0..child_count).collect(); 939 + order_indices.sort_by_key(|&i| parent.children[i].order); 940 + 941 + // Step 1: Determine flex base sizes and hypothetical main sizes. 942 + struct FlexItemInfo { 943 + index: usize, 944 + base_size: f32, 945 + hypothetical_main: f32, 946 + flex_grow: f32, 947 + flex_shrink: f32, 948 + frozen: bool, 949 + target_main: f32, 950 + outer_main: f32, // margin+border+padding on main axis 951 + outer_cross: f32, // margin+border+padding on cross axis 952 + } 953 + 954 + let mut items: Vec<FlexItemInfo> = Vec::with_capacity(child_count); 955 + 956 + for &idx in &order_indices { 957 + let child = &mut parent.children[idx]; 958 + 959 + // Resolve margin/padding percentages for the child. 960 + let cb_width = if is_row { 961 + container_main_size 962 + } else { 963 + container_cross_size.unwrap_or(parent.rect.width) 964 + }; 965 + 966 + // Resolve percentage margins and padding against containing block width. 967 + for i in 0..4 { 968 + if matches!(child.css_margin[i], LengthOrAuto::Percentage(_)) { 969 + let resolved = resolve_length_against(child.css_margin[i], cb_width); 970 + match i { 971 + 0 => child.margin.top = resolved, 972 + 1 => child.margin.right = resolved, 973 + 2 => child.margin.bottom = resolved, 974 + 3 => child.margin.left = resolved, 975 + _ => {} 976 + } 977 + } 978 + if matches!(child.css_padding[i], LengthOrAuto::Percentage(_)) { 979 + let resolved = resolve_length_against(child.css_padding[i], cb_width); 980 + match i { 981 + 0 => child.padding.top = resolved, 982 + 1 => child.padding.right = resolved, 983 + 2 => child.padding.bottom = resolved, 984 + 3 => child.padding.left = resolved, 985 + _ => {} 986 + } 987 + } 988 + } 989 + 990 + let margin_main = if is_row { 991 + child.margin.left + child.margin.right 992 + } else { 993 + child.margin.top + child.margin.bottom 994 + }; 995 + let border_padding_main = if is_row { 996 + child.border.left + child.border.right + child.padding.left + child.padding.right 997 + } else { 998 + child.border.top + child.border.bottom + child.padding.top + child.padding.bottom 999 + }; 1000 + let margin_cross = if is_row { 1001 + child.margin.top + child.margin.bottom 1002 + } else { 1003 + child.margin.left + child.margin.right 1004 + }; 1005 + let border_padding_cross = if is_row { 1006 + child.border.top + child.border.bottom + child.padding.top + child.padding.bottom 1007 + } else { 1008 + child.border.left + child.border.right + child.padding.left + child.padding.right 1009 + }; 1010 + 1011 + // Determine flex-basis. 1012 + let flex_basis = child.flex_basis; 1013 + let specified_main = if is_row { 1014 + child.css_width 1015 + } else { 1016 + child.css_height 1017 + }; 1018 + 1019 + let base_size = match flex_basis { 1020 + LengthOrAuto::Length(px) => px, 1021 + LengthOrAuto::Percentage(p) => p / 100.0 * container_main_size, 1022 + LengthOrAuto::Auto => { 1023 + // Use specified main size if set, otherwise content size. 1024 + match specified_main { 1025 + LengthOrAuto::Length(px) => px, 1026 + LengthOrAuto::Percentage(p) => p / 100.0 * container_main_size, 1027 + LengthOrAuto::Auto => { 1028 + // Content-based sizing: use max-content size. 1029 + if is_row { 1030 + measure_flex_item_max_content_width(child, font, doc) 1031 + } else { 1032 + // For column direction, measure content height. 1033 + let avail = container_cross_size.unwrap_or(parent.rect.width); 1034 + compute_layout(child, 0.0, 0.0, avail, viewport_height, font, doc); 1035 + child.rect.height 1036 + } 1037 + } 1038 + } 1039 + } 1040 + }; 1041 + 1042 + let hypothetical_main = base_size.max(0.0); 1043 + let outer = margin_main + border_padding_main; 1044 + 1045 + items.push(FlexItemInfo { 1046 + index: idx, 1047 + base_size, 1048 + hypothetical_main, 1049 + flex_grow: child.flex_grow, 1050 + flex_shrink: child.flex_shrink, 1051 + frozen: false, 1052 + target_main: hypothetical_main, 1053 + outer_main: outer, 1054 + outer_cross: margin_cross + border_padding_cross, 1055 + }); 1056 + } 1057 + 1058 + // Step 2: Collect items into flex lines. 1059 + let mut lines: Vec<Vec<usize>> = Vec::new(); // indices into `items` 1060 + 1061 + if flex_wrap == FlexWrap::Nowrap { 1062 + // All items on one line. 1063 + lines.push((0..items.len()).collect()); 1064 + } else { 1065 + let mut current_line: Vec<usize> = Vec::new(); 1066 + let mut line_main_size = 0.0f32; 1067 + 1068 + for (i, item) in items.iter().enumerate() { 1069 + let item_outer = item.hypothetical_main + item.outer_main; 1070 + let gap = if current_line.is_empty() { 1071 + 0.0 1072 + } else { 1073 + column_gap 1074 + }; 1075 + 1076 + if !current_line.is_empty() && line_main_size + gap + item_outer > container_main_size { 1077 + lines.push(std::mem::take(&mut current_line)); 1078 + line_main_size = 0.0; 1079 + } 1080 + 1081 + if !current_line.is_empty() { 1082 + line_main_size += column_gap; 1083 + } 1084 + line_main_size += item_outer; 1085 + current_line.push(i); 1086 + } 1087 + 1088 + if !current_line.is_empty() { 1089 + lines.push(current_line); 1090 + } 1091 + } 1092 + 1093 + if flex_wrap == FlexWrap::WrapReverse { 1094 + lines.reverse(); 1095 + } 1096 + 1097 + // Step 3: Resolve flexible lengths for each line. 1098 + for line in &lines { 1099 + // Total hypothetical main sizes + gaps. 1100 + let total_gaps = if line.len() > 1 { 1101 + (line.len() - 1) as f32 * column_gap 1102 + } else { 1103 + 0.0 1104 + }; 1105 + let total_outer_hypo: f32 = line 1106 + .iter() 1107 + .map(|&i| items[i].hypothetical_main + items[i].outer_main) 1108 + .sum(); 1109 + let used_space = total_outer_hypo + total_gaps; 1110 + let free_space = container_main_size - used_space; 1111 + 1112 + // Reset frozen state. 1113 + for &i in line { 1114 + items[i].frozen = false; 1115 + items[i].target_main = items[i].hypothetical_main; 1116 + } 1117 + 1118 + if free_space > 0.0 { 1119 + // Grow items. 1120 + let total_grow: f32 = line.iter().map(|&i| items[i].flex_grow).sum(); 1121 + if total_grow > 0.0 { 1122 + for &i in line { 1123 + if items[i].flex_grow > 0.0 { 1124 + items[i].target_main += free_space * (items[i].flex_grow / total_grow); 1125 + } 1126 + } 1127 + } 1128 + } else if free_space < 0.0 { 1129 + // Shrink items. 1130 + let total_shrink_scaled: f32 = line 1131 + .iter() 1132 + .map(|&i| items[i].flex_shrink * items[i].base_size) 1133 + .sum(); 1134 + if total_shrink_scaled > 0.0 { 1135 + for &i in line { 1136 + let scaled = items[i].flex_shrink * items[i].base_size; 1137 + let ratio = scaled / total_shrink_scaled; 1138 + items[i].target_main = 1139 + (items[i].hypothetical_main + free_space * ratio).max(0.0); 1140 + } 1141 + } 1142 + } 1143 + } 1144 + 1145 + // Step 4: Determine cross sizes of items by laying them out at their target main size. 1146 + struct LineCrossInfo { 1147 + max_cross: f32, 1148 + } 1149 + 1150 + let mut line_infos: Vec<LineCrossInfo> = Vec::new(); 1151 + 1152 + for line in &lines { 1153 + let mut max_cross = 0.0f32; 1154 + 1155 + for &i in line { 1156 + let idx = items[i].index; 1157 + let child = &mut parent.children[idx]; 1158 + let target_main = items[i].target_main; 1159 + 1160 + // Set up the child for layout at the resolved main size. 1161 + if is_row { 1162 + child.css_width = LengthOrAuto::Length(target_main); 1163 + compute_layout(child, 0.0, 0.0, target_main, viewport_height, font, doc); 1164 + let cross = child.rect.height + items[i].outer_cross; 1165 + if cross > max_cross { 1166 + max_cross = cross; 1167 + } 1168 + } else { 1169 + let avail = container_cross_size.unwrap_or(parent.rect.width); 1170 + compute_layout(child, 0.0, 0.0, avail, viewport_height, font, doc); 1171 + child.rect.height = target_main; 1172 + let cross = child.rect.width 1173 + + child.border.left 1174 + + child.border.right 1175 + + child.padding.left 1176 + + child.padding.right 1177 + + child.margin.left 1178 + + child.margin.right; 1179 + if cross > max_cross { 1180 + max_cross = cross; 1181 + } 1182 + } 1183 + } 1184 + 1185 + line_infos.push(LineCrossInfo { max_cross }); 1186 + } 1187 + 1188 + // Per CSS Flexbox §9.4: If the flex container is single-line and has a 1189 + // definite cross size, the cross size of the flex line is the container's 1190 + // cross size (clamped to min/max). This ensures alignment works relative 1191 + // to the full container. 1192 + if lines.len() == 1 { 1193 + if let Some(cs) = container_cross_size { 1194 + if line_infos[0].max_cross < cs { 1195 + line_infos[0].max_cross = cs; 1196 + } 1197 + } 1198 + } 1199 + 1200 + // Step 5: Handle align-items: stretch — stretch items to fill the line cross size. 1201 + for (line_idx, line) in lines.iter().enumerate() { 1202 + let line_cross = line_infos[line_idx].max_cross; 1203 + 1204 + for &i in line { 1205 + let idx = items[i].index; 1206 + let child = &mut parent.children[idx]; 1207 + 1208 + let effective_align = match child.align_self { 1209 + AlignSelf::Auto => align_items, 1210 + AlignSelf::FlexStart => AlignItems::FlexStart, 1211 + AlignSelf::FlexEnd => AlignItems::FlexEnd, 1212 + AlignSelf::Center => AlignItems::Center, 1213 + AlignSelf::Baseline => AlignItems::Baseline, 1214 + AlignSelf::Stretch => AlignItems::Stretch, 1215 + }; 1216 + 1217 + if effective_align == AlignItems::Stretch { 1218 + let item_cross_space = line_cross - items[i].outer_cross; 1219 + if is_row { 1220 + if matches!(child.css_height, LengthOrAuto::Auto) { 1221 + child.rect.height = item_cross_space.max(0.0); 1222 + } 1223 + } else if matches!(child.css_width, LengthOrAuto::Auto) { 1224 + child.rect.width = item_cross_space.max(0.0); 1225 + } 1226 + } 1227 + } 1228 + } 1229 + 1230 + // Step 6: Position items on main and cross axes. 1231 + let total_cross_gaps = if lines.len() > 1 { 1232 + (lines.len() - 1) as f32 * row_gap 1233 + } else { 1234 + 0.0 1235 + }; 1236 + let total_line_cross: f32 = line_infos.iter().map(|li| li.max_cross).sum(); 1237 + let total_cross_used = total_line_cross + total_cross_gaps; 1238 + 1239 + // For definite cross size containers, compute align-content offsets. 1240 + let cross_free_space = container_cross_size 1241 + .map(|cs| (cs - total_cross_used).max(0.0)) 1242 + .unwrap_or(0.0); 1243 + 1244 + let (ac_initial_offset, ac_between_offset) = compute_content_distribution( 1245 + align_content_to_justify(align_content), 1246 + cross_free_space, 1247 + lines.len(), 1248 + ); 1249 + 1250 + let mut cross_cursor = if is_row { parent.rect.y } else { parent.rect.x } + ac_initial_offset; 1251 + 1252 + for (line_idx, line) in lines.iter().enumerate() { 1253 + let line_cross = line_infos[line_idx].max_cross; 1254 + 1255 + // Main-axis justification. 1256 + let total_main_gaps = if line.len() > 1 { 1257 + (line.len() - 1) as f32 * column_gap 1258 + } else { 1259 + 0.0 1260 + }; 1261 + let items_main: f32 = line 1262 + .iter() 1263 + .map(|&i| items[i].target_main + items[i].outer_main) 1264 + .sum(); 1265 + let main_free = (container_main_size - items_main - total_main_gaps).max(0.0); 1266 + 1267 + let (jc_initial, jc_between) = 1268 + compute_content_distribution(justify_content, main_free, line.len()); 1269 + 1270 + // Determine the starting main position. 1271 + let mut main_cursor = if is_row { parent.rect.x } else { parent.rect.y }; 1272 + 1273 + if is_reverse { 1274 + // For reverse directions, start from the end. 1275 + main_cursor += container_main_size; 1276 + } 1277 + 1278 + main_cursor += if is_reverse { -jc_initial } else { jc_initial }; 1279 + 1280 + let line_items: Vec<usize> = if is_reverse { 1281 + line.iter().rev().copied().collect() 1282 + } else { 1283 + line.to_vec() 1284 + }; 1285 + 1286 + for (item_pos, &i) in line_items.iter().enumerate() { 1287 + let idx = items[i].index; 1288 + let child = &mut parent.children[idx]; 1289 + let target_main = items[i].target_main; 1290 + 1291 + // Main-axis position. 1292 + let child_main_margin_start = if is_row { 1293 + child.margin.left 1294 + } else { 1295 + child.margin.top 1296 + }; 1297 + let child_main_margin_end = if is_row { 1298 + child.margin.right 1299 + } else { 1300 + child.margin.bottom 1301 + }; 1302 + let child_main_bp_start = if is_row { 1303 + child.border.left + child.padding.left 1304 + } else { 1305 + child.border.top + child.padding.top 1306 + }; 1307 + let child_main_bp_end = if is_row { 1308 + child.border.right + child.padding.right 1309 + } else { 1310 + child.border.bottom + child.padding.bottom 1311 + }; 1312 + 1313 + let outer_size = child_main_margin_start 1314 + + child_main_bp_start 1315 + + target_main 1316 + + child_main_bp_end 1317 + + child_main_margin_end; 1318 + 1319 + if is_reverse { 1320 + main_cursor -= outer_size; 1321 + } 1322 + 1323 + let content_main = main_cursor + child_main_margin_start + child_main_bp_start; 1324 + 1325 + // Cross-axis position. 1326 + let child_cross_margin_start = if is_row { 1327 + child.margin.top 1328 + } else { 1329 + child.margin.left 1330 + }; 1331 + let child_cross_bp_start = if is_row { 1332 + child.border.top + child.padding.top 1333 + } else { 1334 + child.border.left + child.padding.left 1335 + }; 1336 + let child_cross_total = if is_row { 1337 + child.rect.height + items[i].outer_cross 1338 + } else { 1339 + child.rect.width + items[i].outer_cross 1340 + }; 1341 + 1342 + let effective_align = match child.align_self { 1343 + AlignSelf::Auto => align_items, 1344 + AlignSelf::FlexStart => AlignItems::FlexStart, 1345 + AlignSelf::FlexEnd => AlignItems::FlexEnd, 1346 + AlignSelf::Center => AlignItems::Center, 1347 + AlignSelf::Baseline => AlignItems::Baseline, 1348 + AlignSelf::Stretch => AlignItems::Stretch, 1349 + }; 1350 + 1351 + let cross_offset = match effective_align { 1352 + AlignItems::FlexStart | AlignItems::Stretch | AlignItems::Baseline => 0.0, 1353 + AlignItems::FlexEnd => line_cross - child_cross_total, 1354 + AlignItems::Center => (line_cross - child_cross_total) / 2.0, 1355 + }; 1356 + 1357 + let content_cross = 1358 + cross_cursor + cross_offset + child_cross_margin_start + child_cross_bp_start; 1359 + 1360 + // Set child position. 1361 + if is_row { 1362 + child.rect.x = content_main; 1363 + child.rect.y = content_cross; 1364 + child.rect.width = target_main; 1365 + } else { 1366 + child.rect.x = content_cross; 1367 + child.rect.y = content_main; 1368 + child.rect.height = target_main; 1369 + } 1370 + 1371 + // Shift any text lines to match new position. 1372 + reposition_lines(child, font, doc); 1373 + 1374 + if !is_reverse { 1375 + main_cursor += outer_size; 1376 + } 1377 + 1378 + // Add gap between items (not after last). 1379 + if item_pos < line_items.len() - 1 { 1380 + if is_reverse { 1381 + main_cursor -= column_gap; 1382 + } else { 1383 + main_cursor += column_gap; 1384 + } 1385 + } 1386 + 1387 + // Also add justify-content between spacing. 1388 + if item_pos < line_items.len() - 1 { 1389 + if is_reverse { 1390 + main_cursor -= jc_between; 1391 + } else { 1392 + main_cursor += jc_between; 1393 + } 1394 + } 1395 + } 1396 + 1397 + cross_cursor += line_cross + row_gap + ac_between_offset; 1398 + } 1399 + 1400 + // Set parent height based on children. 1401 + if is_row { 1402 + if matches!(parent.css_height, LengthOrAuto::Auto) { 1403 + parent.rect.height = total_cross_used; 1404 + } 1405 + } else if matches!(parent.css_height, LengthOrAuto::Auto) { 1406 + // For column flex, height is the main axis. 1407 + let total_main: f32 = lines 1408 + .iter() 1409 + .map(|line| { 1410 + let items_main: f32 = line 1411 + .iter() 1412 + .map(|&i| items[i].target_main + items[i].outer_main) 1413 + .sum(); 1414 + let gaps = if line.len() > 1 { 1415 + (line.len() - 1) as f32 * column_gap 1416 + } else { 1417 + 0.0 1418 + }; 1419 + items_main + gaps 1420 + }) 1421 + .fold(0.0f32, f32::max); 1422 + parent.rect.height = total_main; 1423 + } 1424 + } 1425 + 1426 + /// Reposition inline text lines inside a layout box after moving it. 1427 + /// Re-runs inline layout if the box has inline children. 1428 + fn reposition_lines(b: &mut LayoutBox, font: &Font, doc: &Document) { 1429 + if !b.lines.is_empty() { 1430 + // Re-run inline layout at the new position. 1431 + b.lines.clear(); 1432 + layout_inline_children(b, font, doc); 1433 + } 1434 + // Recursively reposition children that have their own inline content. 1435 + for child in &mut b.children { 1436 + if !child.lines.is_empty() { 1437 + child.lines.clear(); 1438 + layout_inline_children(child, font, doc); 1439 + } 1440 + } 1441 + } 1442 + 1443 + /// Convert AlignContent to JustifyContent for reuse of distribution logic. 1444 + fn align_content_to_justify(ac: AlignContent) -> JustifyContent { 1445 + match ac { 1446 + AlignContent::FlexStart | AlignContent::Stretch => JustifyContent::FlexStart, 1447 + AlignContent::FlexEnd => JustifyContent::FlexEnd, 1448 + AlignContent::Center => JustifyContent::Center, 1449 + AlignContent::SpaceBetween => JustifyContent::SpaceBetween, 1450 + AlignContent::SpaceAround => JustifyContent::SpaceAround, 1451 + } 1452 + } 1453 + 1454 + /// Compute the initial offset and between-item spacing for content distribution. 1455 + /// Returns (initial_offset, between_spacing). 1456 + fn compute_content_distribution( 1457 + mode: JustifyContent, 1458 + free_space: f32, 1459 + item_count: usize, 1460 + ) -> (f32, f32) { 1461 + if item_count == 0 { 1462 + return (0.0, 0.0); 1463 + } 1464 + match mode { 1465 + JustifyContent::FlexStart => (0.0, 0.0), 1466 + JustifyContent::FlexEnd => (free_space, 0.0), 1467 + JustifyContent::Center => (free_space / 2.0, 0.0), 1468 + JustifyContent::SpaceBetween => { 1469 + if item_count <= 1 { 1470 + (0.0, 0.0) 1471 + } else { 1472 + (0.0, free_space / (item_count - 1) as f32) 1473 + } 1474 + } 1475 + JustifyContent::SpaceAround => { 1476 + let per_item = free_space / item_count as f32; 1477 + (per_item / 2.0, per_item) 1478 + } 1479 + JustifyContent::SpaceEvenly => { 1480 + let spacing = free_space / (item_count + 1) as f32; 1481 + (spacing, spacing) 1482 + } 1483 + } 824 1484 } 825 1485 826 1486 // --------------------------------------------------------------------------- ··· 2910 3570 "height: 50% should be 300px on 600px viewport, got {}", 2911 3571 div_box.rect.height 2912 3572 ); 3573 + } 3574 + 3575 + // ----------------------------------------------------------------------- 3576 + // Flexbox tests 3577 + // ----------------------------------------------------------------------- 3578 + 3579 + #[test] 3580 + fn flex_row_default_items_laid_out_horizontally() { 3581 + let html_str = r#"<!DOCTYPE html> 3582 + <html><head><style> 3583 + body { margin: 0; } 3584 + .container { display: flex; width: 600px; } 3585 + .item { width: 100px; height: 50px; } 3586 + </style></head> 3587 + <body> 3588 + <div class="container"> 3589 + <div class="item">A</div> 3590 + <div class="item">B</div> 3591 + <div class="item">C</div> 3592 + </div> 3593 + </body></html>"#; 3594 + let doc = we_html::parse_html(html_str); 3595 + let tree = layout_doc(&doc); 3596 + 3597 + let body = &tree.root.children[0]; 3598 + let container = &body.children[0]; 3599 + assert_eq!(container.children.len(), 3); 3600 + 3601 + // Items should be at x=0, 100, 200 3602 + assert!( 3603 + (container.children[0].rect.x - 0.0).abs() < 1.0, 3604 + "first item x should be 0, got {}", 3605 + container.children[0].rect.x 3606 + ); 3607 + assert!( 3608 + (container.children[1].rect.x - 100.0).abs() < 1.0, 3609 + "second item x should be 100, got {}", 3610 + container.children[1].rect.x 3611 + ); 3612 + assert!( 3613 + (container.children[2].rect.x - 200.0).abs() < 1.0, 3614 + "third item x should be 200, got {}", 3615 + container.children[2].rect.x 3616 + ); 3617 + 3618 + // All items should have the same y. 3619 + let y0 = container.children[0].rect.y; 3620 + assert!((container.children[1].rect.y - y0).abs() < 1.0); 3621 + assert!((container.children[2].rect.y - y0).abs() < 1.0); 3622 + } 3623 + 3624 + #[test] 3625 + fn flex_direction_column() { 3626 + let html_str = r#"<!DOCTYPE html> 3627 + <html><head><style> 3628 + body { margin: 0; } 3629 + .container { display: flex; flex-direction: column; width: 600px; } 3630 + .item { height: 50px; } 3631 + </style></head> 3632 + <body> 3633 + <div class="container"> 3634 + <div class="item">A</div> 3635 + <div class="item">B</div> 3636 + <div class="item">C</div> 3637 + </div> 3638 + </body></html>"#; 3639 + let doc = we_html::parse_html(html_str); 3640 + let tree = layout_doc(&doc); 3641 + 3642 + let body = &tree.root.children[0]; 3643 + let container = &body.children[0]; 3644 + assert_eq!(container.children.len(), 3); 3645 + 3646 + // Items should be stacked vertically at y offsets: 0, 50, 100 (relative to container). 3647 + let cy = container.rect.y; 3648 + assert!((container.children[0].rect.y - cy).abs() < 1.0); 3649 + assert!( 3650 + (container.children[1].rect.y - cy - 50.0).abs() < 1.0, 3651 + "second item y should be {}, got {}", 3652 + cy + 50.0, 3653 + container.children[1].rect.y 3654 + ); 3655 + assert!( 3656 + (container.children[2].rect.y - cy - 100.0).abs() < 1.0, 3657 + "third item y should be {}, got {}", 3658 + cy + 100.0, 3659 + container.children[2].rect.y 3660 + ); 3661 + } 3662 + 3663 + #[test] 3664 + fn flex_grow_distributes_space() { 3665 + let html_str = r#"<!DOCTYPE html> 3666 + <html><head><style> 3667 + body { margin: 0; } 3668 + .container { display: flex; width: 600px; } 3669 + .a { flex-grow: 1; flex-basis: 0; height: 50px; } 3670 + .b { flex-grow: 2; flex-basis: 0; height: 50px; } 3671 + .c { flex-grow: 1; flex-basis: 0; height: 50px; } 3672 + </style></head> 3673 + <body> 3674 + <div class="container"> 3675 + <div class="a">A</div> 3676 + <div class="b">B</div> 3677 + <div class="c">C</div> 3678 + </div> 3679 + </body></html>"#; 3680 + let doc = we_html::parse_html(html_str); 3681 + let tree = layout_doc(&doc); 3682 + 3683 + let body = &tree.root.children[0]; 3684 + let container = &body.children[0]; 3685 + 3686 + // flex-grow 1:2:1 should split 600px as 150:300:150 3687 + let a = &container.children[0]; 3688 + let b = &container.children[1]; 3689 + let c = &container.children[2]; 3690 + 3691 + assert!( 3692 + (a.rect.width - 150.0).abs() < 1.0, 3693 + "item A width should be 150, got {}", 3694 + a.rect.width 3695 + ); 3696 + assert!( 3697 + (b.rect.width - 300.0).abs() < 1.0, 3698 + "item B width should be 300, got {}", 3699 + b.rect.width 3700 + ); 3701 + assert!( 3702 + (c.rect.width - 150.0).abs() < 1.0, 3703 + "item C width should be 150, got {}", 3704 + c.rect.width 3705 + ); 3706 + } 3707 + 3708 + #[test] 3709 + fn flex_justify_content_center() { 3710 + let html_str = r#"<!DOCTYPE html> 3711 + <html><head><style> 3712 + body { margin: 0; } 3713 + .container { display: flex; width: 600px; justify-content: center; } 3714 + .item { width: 100px; height: 50px; } 3715 + </style></head> 3716 + <body> 3717 + <div class="container"> 3718 + <div class="item">A</div> 3719 + <div class="item">B</div> 3720 + </div> 3721 + </body></html>"#; 3722 + let doc = we_html::parse_html(html_str); 3723 + let tree = layout_doc(&doc); 3724 + 3725 + let body = &tree.root.children[0]; 3726 + let container = &body.children[0]; 3727 + 3728 + // 600px container, 200px of items, 400px free space, offset = 200. 3729 + let a = &container.children[0]; 3730 + let b = &container.children[1]; 3731 + 3732 + assert!( 3733 + (a.rect.x - 200.0).abs() < 1.0, 3734 + "first item x should be 200, got {}", 3735 + a.rect.x 3736 + ); 3737 + assert!( 3738 + (b.rect.x - 300.0).abs() < 1.0, 3739 + "second item x should be 300, got {}", 3740 + b.rect.x 3741 + ); 3742 + } 3743 + 3744 + #[test] 3745 + fn flex_justify_content_space_between() { 3746 + let html_str = r#"<!DOCTYPE html> 3747 + <html><head><style> 3748 + body { margin: 0; } 3749 + .container { display: flex; width: 600px; justify-content: space-between; } 3750 + .item { width: 100px; height: 50px; } 3751 + </style></head> 3752 + <body> 3753 + <div class="container"> 3754 + <div class="item">A</div> 3755 + <div class="item">B</div> 3756 + <div class="item">C</div> 3757 + </div> 3758 + </body></html>"#; 3759 + let doc = we_html::parse_html(html_str); 3760 + let tree = layout_doc(&doc); 3761 + 3762 + let body = &tree.root.children[0]; 3763 + let container = &body.children[0]; 3764 + 3765 + // 3 items of 100px each = 300px, 300px free, between = 150 3766 + // Positions: 0, 250, 500 3767 + let a = &container.children[0]; 3768 + let b = &container.children[1]; 3769 + let c = &container.children[2]; 3770 + 3771 + assert!( 3772 + (a.rect.x - 0.0).abs() < 1.0, 3773 + "first item x should be 0, got {}", 3774 + a.rect.x 3775 + ); 3776 + assert!( 3777 + (b.rect.x - 250.0).abs() < 1.0, 3778 + "second item x should be 250, got {}", 3779 + b.rect.x 3780 + ); 3781 + assert!( 3782 + (c.rect.x - 500.0).abs() < 1.0, 3783 + "third item x should be 500, got {}", 3784 + c.rect.x 3785 + ); 3786 + } 3787 + 3788 + #[test] 3789 + fn flex_align_items_center() { 3790 + let html_str = r#"<!DOCTYPE html> 3791 + <html><head><style> 3792 + body { margin: 0; } 3793 + .container { display: flex; width: 600px; height: 200px; align-items: center; } 3794 + .item { width: 100px; height: 50px; } 3795 + </style></head> 3796 + <body> 3797 + <div class="container"> 3798 + <div class="item">A</div> 3799 + </div> 3800 + </body></html>"#; 3801 + let doc = we_html::parse_html(html_str); 3802 + let tree = layout_doc(&doc); 3803 + 3804 + let body = &tree.root.children[0]; 3805 + let container = &body.children[0]; 3806 + let a = &container.children[0]; 3807 + 3808 + // Container height=200, item height=50, centered at y = container.y + 75 3809 + let expected_y = container.rect.y + 75.0; 3810 + assert!( 3811 + (a.rect.y - expected_y).abs() < 1.0, 3812 + "item y should be {}, got {}", 3813 + expected_y, 3814 + a.rect.y 3815 + ); 3816 + } 3817 + 3818 + #[test] 3819 + fn flex_wrap_wraps_to_new_line() { 3820 + let html_str = r#"<!DOCTYPE html> 3821 + <html><head><style> 3822 + body { margin: 0; } 3823 + .container { display: flex; flex-wrap: wrap; width: 250px; } 3824 + .item { width: 100px; height: 50px; } 3825 + </style></head> 3826 + <body> 3827 + <div class="container"> 3828 + <div class="item">A</div> 3829 + <div class="item">B</div> 3830 + <div class="item">C</div> 3831 + </div> 3832 + </body></html>"#; 3833 + let doc = we_html::parse_html(html_str); 3834 + let tree = layout_doc(&doc); 3835 + 3836 + let body = &tree.root.children[0]; 3837 + let container = &body.children[0]; 3838 + 3839 + // 250px container, 100px items: A and B fit on line 1, C wraps to line 2 3840 + let a = &container.children[0]; 3841 + let b = &container.children[1]; 3842 + let c = &container.children[2]; 3843 + 3844 + // A and B should be on the same row 3845 + assert!((a.rect.y - b.rect.y).abs() < 1.0); 3846 + // C should be on a different row (50px below) 3847 + assert!( 3848 + (c.rect.y - a.rect.y - 50.0).abs() < 1.0, 3849 + "C should be 50px below A, got y diff {}", 3850 + c.rect.y - a.rect.y 3851 + ); 3852 + } 3853 + 3854 + #[test] 3855 + fn flex_gap_adds_spacing() { 3856 + let html_str = r#"<!DOCTYPE html> 3857 + <html><head><style> 3858 + body { margin: 0; } 3859 + .container { display: flex; width: 600px; gap: 20px; } 3860 + .item { width: 100px; height: 50px; } 3861 + </style></head> 3862 + <body> 3863 + <div class="container"> 3864 + <div class="item">A</div> 3865 + <div class="item">B</div> 3866 + <div class="item">C</div> 3867 + </div> 3868 + </body></html>"#; 3869 + let doc = we_html::parse_html(html_str); 3870 + let tree = layout_doc(&doc); 3871 + 3872 + let body = &tree.root.children[0]; 3873 + let container = &body.children[0]; 3874 + 3875 + let a = &container.children[0]; 3876 + let b = &container.children[1]; 3877 + let c = &container.children[2]; 3878 + 3879 + // With gap: 20px, positions should be: 0, 120, 240 3880 + assert!((a.rect.x - 0.0).abs() < 1.0); 3881 + assert!( 3882 + (b.rect.x - 120.0).abs() < 1.0, 3883 + "B x should be 120, got {}", 3884 + b.rect.x 3885 + ); 3886 + assert!( 3887 + (c.rect.x - 240.0).abs() < 1.0, 3888 + "C x should be 240, got {}", 3889 + c.rect.x 3890 + ); 3891 + } 3892 + 3893 + #[test] 3894 + fn flex_order_changes_visual_order() { 3895 + let html_str = r#"<!DOCTYPE html> 3896 + <html><head><style> 3897 + body { margin: 0; } 3898 + .container { display: flex; width: 600px; } 3899 + .a { width: 100px; height: 50px; order: 2; } 3900 + .b { width: 100px; height: 50px; order: 1; } 3901 + .c { width: 100px; height: 50px; order: 3; } 3902 + </style></head> 3903 + <body> 3904 + <div class="container"> 3905 + <div class="a">A</div> 3906 + <div class="b">B</div> 3907 + <div class="c">C</div> 3908 + </div> 3909 + </body></html>"#; 3910 + let doc = we_html::parse_html(html_str); 3911 + let tree = layout_doc(&doc); 3912 + 3913 + let body = &tree.root.children[0]; 3914 + let container = &body.children[0]; 3915 + 3916 + // DOM order: A(order:2), B(order:1), C(order:3) 3917 + // Visual order: B(1), A(2), C(3) 3918 + // B is at index 1 in DOM, A at 0, C at 2. 3919 + // B (index 1) should be first visually (x ≈ 0) 3920 + // A (index 0) should be second visually (x ≈ 100) 3921 + // C (index 2) should be third visually (x ≈ 200) 3922 + let a = &container.children[0]; // DOM index 0, order 2 3923 + let b = &container.children[1]; // DOM index 1, order 1 3924 + let c = &container.children[2]; // DOM index 2, order 3 3925 + 3926 + assert!( 3927 + b.rect.x < a.rect.x, 3928 + "B (order:1) should be before A (order:2), B.x={} A.x={}", 3929 + b.rect.x, 3930 + a.rect.x 3931 + ); 3932 + assert!( 3933 + a.rect.x < c.rect.x, 3934 + "A (order:2) should be before C (order:3), A.x={} C.x={}", 3935 + a.rect.x, 3936 + c.rect.x 3937 + ); 3938 + } 3939 + 3940 + #[test] 3941 + fn flex_shrink_items() { 3942 + let html_str = r#"<!DOCTYPE html> 3943 + <html><head><style> 3944 + body { margin: 0; } 3945 + .container { display: flex; width: 200px; } 3946 + .item { width: 100px; height: 50px; flex-shrink: 1; } 3947 + </style></head> 3948 + <body> 3949 + <div class="container"> 3950 + <div class="item">A</div> 3951 + <div class="item">B</div> 3952 + <div class="item">C</div> 3953 + </div> 3954 + </body></html>"#; 3955 + let doc = we_html::parse_html(html_str); 3956 + let tree = layout_doc(&doc); 3957 + 3958 + let body = &tree.root.children[0]; 3959 + let container = &body.children[0]; 3960 + 3961 + // 3 items * 100px = 300px in 200px container, shrink evenly 3962 + // Each should be ~66.67px 3963 + for (i, child) in container.children.iter().enumerate() { 3964 + assert!( 3965 + (child.rect.width - 200.0 / 3.0).abs() < 1.0, 3966 + "item {} width should be ~66.67, got {}", 3967 + i, 3968 + child.rect.width 3969 + ); 3970 + } 2913 3971 } 2914 3972 }
+242
crates/style/src/computed.rs
··· 20 20 Block, 21 21 #[default] 22 22 Inline, 23 + Flex, 24 + InlineFlex, 23 25 None, 24 26 } 25 27 ··· 127 129 } 128 130 129 131 // --------------------------------------------------------------------------- 132 + // Flex enums 133 + // --------------------------------------------------------------------------- 134 + 135 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 136 + pub enum FlexDirection { 137 + #[default] 138 + Row, 139 + RowReverse, 140 + Column, 141 + ColumnReverse, 142 + } 143 + 144 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 145 + pub enum FlexWrap { 146 + #[default] 147 + Nowrap, 148 + Wrap, 149 + WrapReverse, 150 + } 151 + 152 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 153 + pub enum JustifyContent { 154 + #[default] 155 + FlexStart, 156 + FlexEnd, 157 + Center, 158 + SpaceBetween, 159 + SpaceAround, 160 + SpaceEvenly, 161 + } 162 + 163 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 164 + pub enum AlignItems { 165 + #[default] 166 + Stretch, 167 + FlexStart, 168 + FlexEnd, 169 + Center, 170 + Baseline, 171 + } 172 + 173 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 174 + pub enum AlignContent { 175 + #[default] 176 + Stretch, 177 + FlexStart, 178 + FlexEnd, 179 + Center, 180 + SpaceBetween, 181 + SpaceAround, 182 + } 183 + 184 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 185 + pub enum AlignSelf { 186 + #[default] 187 + Auto, 188 + FlexStart, 189 + FlexEnd, 190 + Center, 191 + Baseline, 192 + Stretch, 193 + } 194 + 195 + // --------------------------------------------------------------------------- 130 196 // BorderStyle 131 197 // --------------------------------------------------------------------------- 132 198 ··· 231 297 232 298 // Visibility (inherited) 233 299 pub visibility: Visibility, 300 + 301 + // Flex container properties 302 + pub flex_direction: FlexDirection, 303 + pub flex_wrap: FlexWrap, 304 + pub justify_content: JustifyContent, 305 + pub align_items: AlignItems, 306 + pub align_content: AlignContent, 307 + pub row_gap: f32, 308 + pub column_gap: f32, 309 + 310 + // Flex item properties 311 + pub flex_grow: f32, 312 + pub flex_shrink: f32, 313 + pub flex_basis: LengthOrAuto, 314 + pub align_self: AlignSelf, 315 + pub order: i32, 234 316 } 235 317 236 318 impl Default for ComputedStyle { ··· 287 369 288 370 overflow: Overflow::Visible, 289 371 visibility: Visibility::Visible, 372 + 373 + flex_direction: FlexDirection::Row, 374 + flex_wrap: FlexWrap::Nowrap, 375 + justify_content: JustifyContent::FlexStart, 376 + align_items: AlignItems::Stretch, 377 + align_content: AlignContent::Stretch, 378 + row_gap: 0.0, 379 + column_gap: 0.0, 380 + 381 + flex_grow: 0.0, 382 + flex_shrink: 1.0, 383 + flex_basis: LengthOrAuto::Auto, 384 + align_self: AlignSelf::Auto, 385 + order: 0, 290 386 } 291 387 } 292 388 } ··· 498 594 CssValue::Keyword(k) => match k.as_str() { 499 595 "block" => Display::Block, 500 596 "inline" => Display::Inline, 597 + "flex" => Display::Flex, 598 + "inline-flex" => Display::InlineFlex, 501 599 _ => Display::Block, 502 600 }, 503 601 CssValue::None => Display::None, ··· 790 888 }; 791 889 } 792 890 891 + // Flex container properties 892 + "flex-direction" => { 893 + style.flex_direction = match value { 894 + CssValue::Keyword(k) => match k.as_str() { 895 + "row" => FlexDirection::Row, 896 + "row-reverse" => FlexDirection::RowReverse, 897 + "column" => FlexDirection::Column, 898 + "column-reverse" => FlexDirection::ColumnReverse, 899 + _ => style.flex_direction, 900 + }, 901 + _ => style.flex_direction, 902 + }; 903 + } 904 + "flex-wrap" => { 905 + style.flex_wrap = match value { 906 + CssValue::Keyword(k) => match k.as_str() { 907 + "nowrap" => FlexWrap::Nowrap, 908 + "wrap" => FlexWrap::Wrap, 909 + "wrap-reverse" => FlexWrap::WrapReverse, 910 + _ => style.flex_wrap, 911 + }, 912 + _ => style.flex_wrap, 913 + }; 914 + } 915 + "justify-content" => { 916 + style.justify_content = match value { 917 + CssValue::Keyword(k) => match k.as_str() { 918 + "flex-start" => JustifyContent::FlexStart, 919 + "flex-end" => JustifyContent::FlexEnd, 920 + "center" => JustifyContent::Center, 921 + "space-between" => JustifyContent::SpaceBetween, 922 + "space-around" => JustifyContent::SpaceAround, 923 + "space-evenly" => JustifyContent::SpaceEvenly, 924 + _ => style.justify_content, 925 + }, 926 + _ => style.justify_content, 927 + }; 928 + } 929 + "align-items" => { 930 + style.align_items = match value { 931 + CssValue::Keyword(k) => match k.as_str() { 932 + "stretch" => AlignItems::Stretch, 933 + "flex-start" => AlignItems::FlexStart, 934 + "flex-end" => AlignItems::FlexEnd, 935 + "center" => AlignItems::Center, 936 + "baseline" => AlignItems::Baseline, 937 + _ => style.align_items, 938 + }, 939 + _ => style.align_items, 940 + }; 941 + } 942 + "align-content" => { 943 + style.align_content = match value { 944 + CssValue::Keyword(k) => match k.as_str() { 945 + "stretch" => AlignContent::Stretch, 946 + "flex-start" => AlignContent::FlexStart, 947 + "flex-end" => AlignContent::FlexEnd, 948 + "center" => AlignContent::Center, 949 + "space-between" => AlignContent::SpaceBetween, 950 + "space-around" => AlignContent::SpaceAround, 951 + _ => style.align_content, 952 + }, 953 + _ => style.align_content, 954 + }; 955 + } 956 + "row-gap" => { 957 + if let CssValue::Length(n, unit) = value { 958 + style.row_gap = resolve_length_unit(*n, *unit, current_fs, viewport); 959 + } else if let CssValue::Zero = value { 960 + style.row_gap = 0.0; 961 + } 962 + } 963 + "column-gap" => { 964 + if let CssValue::Length(n, unit) = value { 965 + style.column_gap = resolve_length_unit(*n, *unit, current_fs, viewport); 966 + } else if let CssValue::Zero = value { 967 + style.column_gap = 0.0; 968 + } 969 + } 970 + 971 + // Flex item properties 972 + "flex-grow" => { 973 + if let CssValue::Number(n) = value { 974 + style.flex_grow = *n as f32; 975 + } else if let CssValue::Zero = value { 976 + style.flex_grow = 0.0; 977 + } 978 + } 979 + "flex-shrink" => { 980 + if let CssValue::Number(n) = value { 981 + style.flex_shrink = *n as f32; 982 + } else if let CssValue::Zero = value { 983 + style.flex_shrink = 0.0; 984 + } 985 + } 986 + "flex-basis" => { 987 + style.flex_basis = resolve_layout_length_or_auto(value, current_fs, viewport); 988 + } 989 + "align-self" => { 990 + style.align_self = match value { 991 + CssValue::Keyword(k) => match k.as_str() { 992 + "flex-start" => AlignSelf::FlexStart, 993 + "flex-end" => AlignSelf::FlexEnd, 994 + "center" => AlignSelf::Center, 995 + "baseline" => AlignSelf::Baseline, 996 + "stretch" => AlignSelf::Stretch, 997 + _ => style.align_self, 998 + }, 999 + CssValue::Auto => AlignSelf::Auto, 1000 + _ => style.align_self, 1001 + }; 1002 + } 1003 + "order" => { 1004 + if let CssValue::Number(n) = value { 1005 + style.order = *n as i32; 1006 + } else if let CssValue::Zero = value { 1007 + style.order = 0; 1008 + } 1009 + } 1010 + 793 1011 _ => {} // Unknown property — ignore 794 1012 } 795 1013 } ··· 859 1077 "background-color" => style.background_color = parent.background_color, 860 1078 "position" => style.position = parent.position, 861 1079 "overflow" => style.overflow = parent.overflow, 1080 + "flex-direction" => style.flex_direction = parent.flex_direction, 1081 + "flex-wrap" => style.flex_wrap = parent.flex_wrap, 1082 + "justify-content" => style.justify_content = parent.justify_content, 1083 + "align-items" => style.align_items = parent.align_items, 1084 + "align-content" => style.align_content = parent.align_content, 1085 + "row-gap" => style.row_gap = parent.row_gap, 1086 + "column-gap" => style.column_gap = parent.column_gap, 1087 + "flex-grow" => style.flex_grow = parent.flex_grow, 1088 + "flex-shrink" => style.flex_shrink = parent.flex_shrink, 1089 + "flex-basis" => style.flex_basis = parent.flex_basis, 1090 + "align-self" => style.align_self = parent.align_self, 1091 + "order" => style.order = parent.order, 862 1092 _ => {} 863 1093 } 864 1094 } ··· 901 1131 "left" => style.left = initial.left, 902 1132 "overflow" => style.overflow = initial.overflow, 903 1133 "visibility" => style.visibility = initial.visibility, 1134 + "flex-direction" => style.flex_direction = initial.flex_direction, 1135 + "flex-wrap" => style.flex_wrap = initial.flex_wrap, 1136 + "justify-content" => style.justify_content = initial.justify_content, 1137 + "align-items" => style.align_items = initial.align_items, 1138 + "align-content" => style.align_content = initial.align_content, 1139 + "row-gap" => style.row_gap = initial.row_gap, 1140 + "column-gap" => style.column_gap = initial.column_gap, 1141 + "flex-grow" => style.flex_grow = initial.flex_grow, 1142 + "flex-shrink" => style.flex_shrink = initial.flex_shrink, 1143 + "flex-basis" => style.flex_basis = initial.flex_basis, 1144 + "align-self" => style.align_self = initial.align_self, 1145 + "order" => style.order = initial.order, 904 1146 _ => {} 905 1147 } 906 1148 }