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 box-sizing property (content-box and border-box)

Add BoxSizing enum and box_sizing field to ComputedStyle with CSS
parsing support. Layout engine now respects explicit CSS width/height
and adjusts content dimensions based on box-sizing mode:
- content-box (default): width/height apply to content area only
- border-box: width/height include padding and border

Content area clamps to 0 when padding+border exceeds specified size
in border-box mode. Property is not inherited per spec.

9 new tests covering both modes, clamping, non-inheritance, and height.

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

+274 -10
+203 -10
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, ComputedStyle, Display, LengthOrAuto, Overflow, Position, StyledNode, TextAlign, 12 - TextDecoration, 11 + BorderStyle, BoxSizing, ComputedStyle, Display, LengthOrAuto, Overflow, Position, StyledNode, 12 + TextAlign, TextDecoration, 13 13 }; 14 14 use we_text::font::Font; 15 15 ··· 95 95 pub relative_offset: (f32, f32), 96 96 /// CSS `overflow` property. 97 97 pub overflow: Overflow, 98 + /// CSS `box-sizing` property. 99 + pub box_sizing: BoxSizing, 100 + /// CSS `width` property (explicit or auto). 101 + pub css_width: LengthOrAuto, 102 + /// CSS `height` property (explicit or auto). 103 + pub css_height: LengthOrAuto, 98 104 } 99 105 100 106 impl LayoutBox { ··· 129 135 position: style.position, 130 136 relative_offset: (0.0, 0.0), 131 137 overflow: style.overflow, 138 + box_sizing: style.box_sizing, 139 + css_width: style.width, 140 + css_height: style.height, 132 141 } 133 142 } 134 143 ··· 419 428 ) { 420 429 let content_x = x + b.margin.left + b.border.left + b.padding.left; 421 430 let content_y = y + b.margin.top + b.border.top + b.padding.top; 422 - let content_width = (available_width 423 - - b.margin.left 424 - - b.margin.right 425 - - b.border.left 426 - - b.border.right 427 - - b.padding.left 428 - - b.padding.right) 429 - .max(0.0); 431 + 432 + let horizontal_extra = b.border.left + b.border.right + b.padding.left + b.padding.right; 433 + 434 + // Resolve content width: explicit CSS width (adjusted for box-sizing) or auto. 435 + let content_width = match b.css_width { 436 + LengthOrAuto::Length(w) => match b.box_sizing { 437 + BoxSizing::ContentBox => w.max(0.0), 438 + BoxSizing::BorderBox => (w - horizontal_extra).max(0.0), 439 + }, 440 + LengthOrAuto::Auto => { 441 + (available_width - b.margin.left - b.margin.right - horizontal_extra).max(0.0) 442 + } 443 + }; 430 444 431 445 b.rect.x = content_x; 432 446 b.rect.y = content_y; ··· 453 467 BoxType::TextRun { .. } | BoxType::Inline(_) => { 454 468 // Handled by the parent's inline layout. 455 469 } 470 + } 471 + 472 + // Apply explicit CSS height (adjusted for box-sizing), overriding auto height. 473 + if let LengthOrAuto::Length(h) = b.css_height { 474 + let vertical_extra = b.border.top + b.border.bottom + b.padding.top + b.padding.bottom; 475 + b.rect.height = match b.box_sizing { 476 + BoxSizing::ContentBox => h.max(0.0), 477 + BoxSizing::BorderBox => (h - vertical_extra).max(0.0), 478 + }; 456 479 } 457 480 458 481 apply_relative_offset(b); ··· 2192 2215 assert_eq!(collapse_margins(20.0, -5.0), 15.0); 2193 2216 assert_eq!(collapse_margins(-5.0, 20.0), 15.0); 2194 2217 assert_eq!(collapse_margins(0.0, 0.0), 0.0); 2218 + } 2219 + 2220 + // --- Box-sizing tests --- 2221 + 2222 + #[test] 2223 + fn content_box_default_width_applies_to_content() { 2224 + // Default box-sizing (content-box): width = content width only. 2225 + let html_str = r#"<!DOCTYPE html> 2226 + <html> 2227 + <head><style> 2228 + body { margin: 0; } 2229 + div { width: 200px; padding: 10px; border: 5px solid black; } 2230 + </style></head> 2231 + <body> 2232 + <div>Content</div> 2233 + </body> 2234 + </html>"#; 2235 + let doc = we_html::parse_html(html_str); 2236 + let font = test_font(); 2237 + let sheets = extract_stylesheets(&doc); 2238 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2239 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2240 + 2241 + let body_box = &tree.root.children[0]; 2242 + let div_box = &body_box.children[0]; 2243 + 2244 + // content-box: rect.width = 200 (the specified width IS the content) 2245 + assert_eq!(div_box.rect.width, 200.0); 2246 + assert_eq!(div_box.padding.left, 10.0); 2247 + assert_eq!(div_box.padding.right, 10.0); 2248 + assert_eq!(div_box.border.left, 5.0); 2249 + assert_eq!(div_box.border.right, 5.0); 2250 + } 2251 + 2252 + #[test] 2253 + fn border_box_width_includes_padding_and_border() { 2254 + // box-sizing: border-box: width includes padding and border. 2255 + let html_str = r#"<!DOCTYPE html> 2256 + <html> 2257 + <head><style> 2258 + body { margin: 0; } 2259 + div { box-sizing: border-box; width: 200px; padding: 10px; border: 5px solid black; } 2260 + </style></head> 2261 + <body> 2262 + <div>Content</div> 2263 + </body> 2264 + </html>"#; 2265 + let doc = we_html::parse_html(html_str); 2266 + let font = test_font(); 2267 + let sheets = extract_stylesheets(&doc); 2268 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2269 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2270 + 2271 + let body_box = &tree.root.children[0]; 2272 + let div_box = &body_box.children[0]; 2273 + 2274 + // border-box: content width = 200 - 10*2 (padding) - 5*2 (border) = 170 2275 + assert_eq!(div_box.rect.width, 170.0); 2276 + assert_eq!(div_box.padding.left, 10.0); 2277 + assert_eq!(div_box.padding.right, 10.0); 2278 + assert_eq!(div_box.border.left, 5.0); 2279 + assert_eq!(div_box.border.right, 5.0); 2280 + } 2281 + 2282 + #[test] 2283 + fn border_box_padding_exceeds_width_clamps_to_zero() { 2284 + // border-box with padding+border > specified width: content clamps to 0. 2285 + let html_str = r#"<!DOCTYPE html> 2286 + <html> 2287 + <head><style> 2288 + body { margin: 0; } 2289 + div { box-sizing: border-box; width: 20px; padding: 15px; border: 5px solid black; } 2290 + </style></head> 2291 + <body> 2292 + <div>X</div> 2293 + </body> 2294 + </html>"#; 2295 + let doc = we_html::parse_html(html_str); 2296 + let font = test_font(); 2297 + let sheets = extract_stylesheets(&doc); 2298 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2299 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2300 + 2301 + let body_box = &tree.root.children[0]; 2302 + let div_box = &body_box.children[0]; 2303 + 2304 + // border-box: content = 20 - 15*2 - 5*2 = 20 - 40 = -20 → clamped to 0 2305 + assert_eq!(div_box.rect.width, 0.0); 2306 + } 2307 + 2308 + #[test] 2309 + fn box_sizing_is_not_inherited() { 2310 + // box-sizing is not inherited: child should use default content-box. 2311 + let html_str = r#"<!DOCTYPE html> 2312 + <html> 2313 + <head><style> 2314 + body { margin: 0; } 2315 + .parent { box-sizing: border-box; width: 300px; padding: 10px; border: 5px solid black; } 2316 + .child { width: 100px; padding: 10px; border: 5px solid black; } 2317 + </style></head> 2318 + <body> 2319 + <div class="parent"><div class="child">Inner</div></div> 2320 + </body> 2321 + </html>"#; 2322 + let doc = we_html::parse_html(html_str); 2323 + let font = test_font(); 2324 + let sheets = extract_stylesheets(&doc); 2325 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2326 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2327 + 2328 + let body_box = &tree.root.children[0]; 2329 + let parent_box = &body_box.children[0]; 2330 + let child_box = &parent_box.children[0]; 2331 + 2332 + // Parent: border-box → content = 300 - 20 - 10 = 270 2333 + assert_eq!(parent_box.rect.width, 270.0); 2334 + // Child: default content-box → content = 100 (not reduced by padding/border) 2335 + assert_eq!(child_box.rect.width, 100.0); 2336 + } 2337 + 2338 + #[test] 2339 + fn border_box_height() { 2340 + // box-sizing: border-box also applies to height. 2341 + let html_str = r#"<!DOCTYPE html> 2342 + <html> 2343 + <head><style> 2344 + body { margin: 0; } 2345 + div { box-sizing: border-box; height: 100px; padding: 10px; border: 5px solid black; } 2346 + </style></head> 2347 + <body> 2348 + <div>Content</div> 2349 + </body> 2350 + </html>"#; 2351 + let doc = we_html::parse_html(html_str); 2352 + let font = test_font(); 2353 + let sheets = extract_stylesheets(&doc); 2354 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2355 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2356 + 2357 + let body_box = &tree.root.children[0]; 2358 + let div_box = &body_box.children[0]; 2359 + 2360 + // border-box: content height = 100 - 10*2 (padding) - 5*2 (border) = 70 2361 + assert_eq!(div_box.rect.height, 70.0); 2362 + } 2363 + 2364 + #[test] 2365 + fn content_box_explicit_height() { 2366 + // content-box: height applies to content only. 2367 + let html_str = r#"<!DOCTYPE html> 2368 + <html> 2369 + <head><style> 2370 + body { margin: 0; } 2371 + div { height: 100px; padding: 10px; border: 5px solid black; } 2372 + </style></head> 2373 + <body> 2374 + <div>Content</div> 2375 + </body> 2376 + </html>"#; 2377 + let doc = we_html::parse_html(html_str); 2378 + let font = test_font(); 2379 + let sheets = extract_stylesheets(&doc); 2380 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2381 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2382 + 2383 + let body_box = &tree.root.children[0]; 2384 + let div_box = &body_box.children[0]; 2385 + 2386 + // content-box: rect.height = 100 (specified height IS content height) 2387 + assert_eq!(div_box.rect.height, 100.0); 2195 2388 } 2196 2389 }
+71
crates/style/src/computed.rs
··· 90 90 } 91 91 92 92 // --------------------------------------------------------------------------- 93 + // BoxSizing 94 + // --------------------------------------------------------------------------- 95 + 96 + /// CSS `box-sizing` property values. 97 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 98 + pub enum BoxSizing { 99 + #[default] 100 + ContentBox, 101 + BorderBox, 102 + } 103 + 104 + // --------------------------------------------------------------------------- 93 105 // Overflow 94 106 // --------------------------------------------------------------------------- 95 107 ··· 189 201 pub width: LengthOrAuto, 190 202 pub height: LengthOrAuto, 191 203 204 + // Box model: sizing 205 + pub box_sizing: BoxSizing, 206 + 192 207 // Text / inherited 193 208 pub color: Color, 194 209 pub font_size: f32, ··· 249 264 width: LengthOrAuto::Auto, 250 265 height: LengthOrAuto::Auto, 251 266 267 + box_sizing: BoxSizing::ContentBox, 268 + 252 269 color: Color::rgb(0, 0, 0), 253 270 font_size: 16.0, 254 271 font_weight: FontWeight(400.0), ··· 593 610 style.height = resolve_length_or_auto(value, parent_fs, current_fs); 594 611 } 595 612 613 + // Box sizing 614 + "box-sizing" => { 615 + style.box_sizing = match value { 616 + CssValue::Keyword(k) => match k.as_str() { 617 + "content-box" => BoxSizing::ContentBox, 618 + "border-box" => BoxSizing::BorderBox, 619 + _ => style.box_sizing, 620 + }, 621 + _ => style.box_sizing, 622 + }; 623 + } 624 + 596 625 // Color (inherited) 597 626 "color" => { 598 627 if let Some(c) = resolve_color(value, parent.color) { ··· 832 861 "padding-left" => style.padding_left = parent.padding_left, 833 862 "width" => style.width = parent.width, 834 863 "height" => style.height = parent.height, 864 + "box-sizing" => style.box_sizing = parent.box_sizing, 835 865 "background-color" => style.background_color = parent.background_color, 836 866 "position" => style.position = parent.position, 837 867 "overflow" => style.overflow = parent.overflow, ··· 857 887 "border-left-width" => style.border_left_width = initial.border_left_width, 858 888 "width" => style.width = initial.width, 859 889 "height" => style.height = initial.height, 890 + "box-sizing" => style.box_sizing = initial.box_sizing, 860 891 "color" => style.color = initial.color, 861 892 "font-size" => { 862 893 style.font_size = initial.font_size; ··· 1978 2009 1979 2010 // Later stylesheet wins. 1980 2011 assert_eq!(p_node.style.color, Color::rgb(0, 0, 255)); 2012 + } 2013 + 2014 + #[test] 2015 + fn box_sizing_content_box_default() { 2016 + let html_str = r#"<!DOCTYPE html> 2017 + <html><head></head><body><div>Test</div></body></html>"#; 2018 + let doc = we_html::parse_html(html_str); 2019 + let sheets = extract_stylesheets(&doc); 2020 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2021 + let body = &styled.children[0]; 2022 + let div = &body.children[0]; 2023 + assert_eq!(div.style.box_sizing, BoxSizing::ContentBox); 2024 + } 2025 + 2026 + #[test] 2027 + fn box_sizing_border_box_parsed() { 2028 + let html_str = r#"<!DOCTYPE html> 2029 + <html><head><style>div { box-sizing: border-box; }</style></head> 2030 + <body><div>Test</div></body></html>"#; 2031 + let doc = we_html::parse_html(html_str); 2032 + let sheets = extract_stylesheets(&doc); 2033 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2034 + let body = &styled.children[0]; 2035 + let div = &body.children[0]; 2036 + assert_eq!(div.style.box_sizing, BoxSizing::BorderBox); 2037 + } 2038 + 2039 + #[test] 2040 + fn box_sizing_not_inherited() { 2041 + let html_str = r#"<!DOCTYPE html> 2042 + <html><head><style>.parent { box-sizing: border-box; }</style></head> 2043 + <body><div class="parent"><p>Child</p></div></body></html>"#; 2044 + let doc = we_html::parse_html(html_str); 2045 + let sheets = extract_stylesheets(&doc); 2046 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2047 + let body = &styled.children[0]; 2048 + let parent = &body.children[0]; 2049 + let child = &parent.children[0]; 2050 + assert_eq!(parent.style.box_sizing, BoxSizing::BorderBox); 2051 + assert_eq!(child.style.box_sizing, BoxSizing::ContentBox); 1981 2052 } 1982 2053 }