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 float and clear properties

Add float layout support to the layout engine:
- Parse float (none/left/right) and clear (none/left/right/both) CSS properties
- Float positioning: left floats at left edge, right floats at right edge
- Float stacking: multiple floats stack horizontally, wrapping when full
- Line box shortening: inline content flows around active floats
- Clear property: elements move below cleared floats
- BFC containment: overflow:hidden/scroll containers expand to contain floats
- Floated inline elements are blockified per CSS2 §9.7

Implements issue 3mhlhnjkxcr2x

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

+782 -28
+717 -28
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 - AlignContent, AlignItems, AlignSelf, BorderStyle, BoxSizing, ComputedStyle, Display, 12 - FlexDirection, FlexWrap, JustifyContent, LengthOrAuto, Overflow, Position, StyledNode, 11 + AlignContent, AlignItems, AlignSelf, BorderStyle, BoxSizing, Clear, ComputedStyle, Display, 12 + FlexDirection, FlexWrap, Float, JustifyContent, LengthOrAuto, Overflow, Position, StyledNode, 13 13 TextAlign, TextDecoration, Visibility, 14 14 }; 15 15 use we_text::font::Font; ··· 119 119 pub sticky_constraint: Option<Rect>, 120 120 /// CSS `visibility` property. 121 121 pub visibility: Visibility, 122 + /// CSS `float` property. 123 + pub float: Float, 124 + /// CSS `clear` property. 125 + pub clear: Clear, 122 126 /// Natural content height before CSS height override. 123 127 /// Used to determine overflow for scroll containers. 124 128 pub content_height: f32, ··· 190 194 css_offsets: [style.top, style.right, style.bottom, style.left], 191 195 sticky_constraint: None, 192 196 visibility: style.visibility, 197 + float: style.float, 198 + clear: style.clear, 193 199 content_height: 0.0, 194 200 flex_direction: style.flex_direction, 195 201 flex_wrap: style.flex_wrap, ··· 378 384 } 379 385 } 380 386 381 - let box_type = match style.display { 387 + // Per CSS2 §9.7: float forces display to block. 388 + let effective_display = 389 + if style.float != Float::None && style.display == Display::Inline { 390 + Display::Block 391 + } else { 392 + style.display 393 + }; 394 + 395 + let box_type = match effective_display { 382 396 Display::Block | Display::Flex | Display::InlineFlex => BoxType::Block(node), 383 397 Display::Inline => BoxType::Inline(node), 384 398 Display::None => unreachable!(), 385 399 }; 386 400 387 - if style.display == Display::Block { 401 + if effective_display == Display::Block { 388 402 children = normalize_children(children, style); 389 403 } 390 404 ··· 489 503 matches!(b.box_type, BoxType::Block(_) | BoxType::Anonymous) 490 504 } 491 505 492 - /// Returns `true` if this box is in normal flow (not absolutely or fixed positioned). 506 + /// Returns `true` if this box is in normal flow (not absolutely/fixed positioned, not floated). 493 507 fn is_in_flow(b: &LayoutBox) -> bool { 494 - b.position != Position::Absolute && b.position != Position::Fixed 508 + b.position != Position::Absolute && b.position != Position::Fixed && b.float == Float::None 509 + } 510 + 511 + /// Returns `true` if this box is floated. 512 + fn is_floated(b: &LayoutBox) -> bool { 513 + b.float != Float::None 514 + } 515 + 516 + // --------------------------------------------------------------------------- 517 + // Float tracking 518 + // --------------------------------------------------------------------------- 519 + 520 + /// A positioned float rectangle used for tracking placed floats. 521 + #[derive(Debug, Clone, Copy)] 522 + struct PlacedFloat { 523 + /// Left edge of the float's margin box. 524 + x: f32, 525 + /// Top edge of the float's margin box. 526 + y: f32, 527 + /// Width of the float's margin box. 528 + width: f32, 529 + /// Height of the float's margin box. 530 + height: f32, 531 + /// Which side this float is on. 532 + side: Float, 533 + } 534 + 535 + /// Tracks active floats within a block formatting context. 536 + #[derive(Debug, Default)] 537 + struct FloatContext { 538 + floats: Vec<PlacedFloat>, 539 + } 540 + 541 + impl FloatContext { 542 + /// Get the available x-range at a given y position, narrowed by active floats. 543 + /// Returns (left_edge, right_edge) within the containing block. 544 + fn available_range( 545 + &self, 546 + y: f32, 547 + line_height: f32, 548 + container_x: f32, 549 + container_width: f32, 550 + ) -> (f32, f32) { 551 + let mut left = container_x; 552 + let mut right = container_x + container_width; 553 + 554 + for f in &self.floats { 555 + let float_top = f.y; 556 + let float_bottom = f.y + f.height; 557 + 558 + // Check if this float overlaps vertically with the line. 559 + if y + line_height > float_top && y < float_bottom { 560 + match f.side { 561 + Float::Left => { 562 + let float_right_edge = f.x + f.width; 563 + if float_right_edge > left { 564 + left = float_right_edge; 565 + } 566 + } 567 + Float::Right => { 568 + let float_left_edge = f.x; 569 + if float_left_edge < right { 570 + right = float_left_edge; 571 + } 572 + } 573 + Float::None => {} 574 + } 575 + } 576 + } 577 + 578 + (left, right) 579 + } 580 + 581 + /// Find the y position below all floats that match the given clear side. 582 + fn clear_y(&self, clear: Clear) -> f32 { 583 + let mut y = 0.0f32; 584 + for f in &self.floats { 585 + let dominated = match clear { 586 + Clear::Left => f.side == Float::Left, 587 + Clear::Right => f.side == Float::Right, 588 + Clear::Both => true, 589 + Clear::None => false, 590 + }; 591 + if dominated { 592 + let bottom = f.y + f.height; 593 + if bottom > y { 594 + y = bottom; 595 + } 596 + } 597 + } 598 + y 599 + } 600 + 601 + /// Find the bottom edge of all placed floats. 602 + fn max_float_bottom(&self) -> f32 { 603 + let mut bottom = 0.0f32; 604 + for f in &self.floats { 605 + let fb = f.y + f.height; 606 + if fb > bottom { 607 + bottom = fb; 608 + } 609 + } 610 + bottom 611 + } 612 + 613 + /// Place a float and return its position. 614 + fn place_float( 615 + &mut self, 616 + float_side: Float, 617 + float_width: f32, 618 + float_height: f32, 619 + cursor_y: f32, 620 + container_x: f32, 621 + container_width: f32, 622 + ) -> (f32, f32) { 623 + // Start at cursor_y and find a position where the float fits. 624 + let mut y = cursor_y; 625 + 626 + loop { 627 + let (left, right) = self.available_range(y, float_height, container_x, container_width); 628 + let available = right - left; 629 + 630 + if available >= float_width || available >= container_width { 631 + let x = match float_side { 632 + Float::Left => left, 633 + Float::Right => right - float_width, 634 + Float::None => unreachable!(), 635 + }; 636 + self.floats.push(PlacedFloat { 637 + x, 638 + y, 639 + width: float_width, 640 + height: float_height, 641 + side: float_side, 642 + }); 643 + return (x, y); 644 + } 645 + 646 + // Move down below the topmost interfering float and try again. 647 + let mut next_y = f32::MAX; 648 + for f in &self.floats { 649 + let fb = f.y + f.height; 650 + if fb > y && fb < next_y { 651 + next_y = fb; 652 + } 653 + } 654 + if next_y == f32::MAX { 655 + // No more floats to clear, place at current position. 656 + let x = match float_side { 657 + Float::Left => container_x, 658 + Float::Right => container_x + container_width - float_width, 659 + Float::None => unreachable!(), 660 + }; 661 + self.floats.push(PlacedFloat { 662 + x, 663 + y, 664 + width: float_width, 665 + height: float_height, 666 + side: float_side, 667 + }); 668 + return (x, y); 669 + } 670 + y = next_y; 671 + } 672 + } 495 673 } 496 674 497 675 // --------------------------------------------------------------------------- ··· 517 695 font: &Font, 518 696 doc: &Document, 519 697 abs_cb: Rect, 698 + float_ctx: Option<&FloatContext>, 520 699 ) { 521 700 // Resolve percentage margins against containing block width. 522 701 // Only re-resolve percentages — absolute margins may have been modified ··· 594 773 BoxType::Block(_) | BoxType::Anonymous => { 595 774 if matches!(b.display, Display::Flex | Display::InlineFlex) { 596 775 layout_flex_children(b, viewport_width, viewport_height, font, doc, abs_cb); 597 - } else if has_block_children(b) { 776 + } else if has_block_children(b) || has_float_children(b) { 598 777 layout_block_children(b, viewport_width, viewport_height, font, doc, abs_cb); 599 778 } else { 600 - layout_inline_children(b, font, doc); 779 + layout_inline_children(b, font, doc, float_ctx); 601 780 } 602 781 } 603 782 BoxType::TextRun { .. } | BoxType::Inline(_) => { ··· 870 1049 doc, 871 1050 child_abs_cb, 872 1051 ); 873 - } else if has_block_children(child) { 1052 + } else if has_block_children(child) || has_float_children(child) { 874 1053 layout_block_children( 875 1054 child, 876 1055 viewport_width, ··· 880 1059 child_abs_cb, 881 1060 ); 882 1061 } else { 883 - layout_inline_children(child, font, doc); 1062 + layout_inline_children(child, font, doc, None); 884 1063 } 885 1064 } 886 1065 _ => {} ··· 962 1141 .any(|c| is_in_flow(c) && is_block_level(c)) 963 1142 } 964 1143 1144 + fn has_float_children(b: &LayoutBox) -> bool { 1145 + b.children.iter().any(is_floated) 1146 + } 1147 + 965 1148 /// Collapse two adjoining margins per CSS2 §8.3.1. 966 1149 /// 967 1150 /// Both non-negative → use the larger. ··· 980 1163 /// Returns `true` if this box establishes a new block formatting context, 981 1164 /// which prevents its margins from collapsing with children. 982 1165 fn establishes_bfc(b: &LayoutBox) -> bool { 983 - b.overflow != Overflow::Visible || matches!(b.display, Display::Flex | Display::InlineFlex) 1166 + b.overflow != Overflow::Visible 1167 + || matches!(b.display, Display::Flex | Display::InlineFlex) 1168 + || b.float != Float::None 984 1169 } 985 1170 986 1171 /// Returns `true` if a block box has no in-flow content (empty block). ··· 1080 1265 } 1081 1266 } 1082 1267 1083 - /// Lay out block-level children with vertical margin collapsing (CSS2 §8.3.1). 1268 + /// Lay out block-level children with vertical margin collapsing (CSS2 §8.3.1) 1269 + /// and float support. 1084 1270 /// 1085 - /// Handles adjacent-sibling collapsing, empty-block collapsing, and 1271 + /// Handles adjacent-sibling collapsing, empty-block collapsing, 1086 1272 /// parent-child internal spacing (the parent's external margins were already 1087 - /// updated by `pre_collapse_margins`). 1273 + /// updated by `pre_collapse_margins`), and float placement. 1088 1274 fn layout_block_children( 1089 1275 parent: &mut LayoutBox, 1090 1276 viewport_width: f32, ··· 1107 1293 let child_count = parent.children.len(); 1108 1294 // Track whether we've seen any in-flow children (for parent_top_open). 1109 1295 let mut first_in_flow = true; 1296 + let mut float_ctx = FloatContext::default(); 1110 1297 1111 1298 for i in 0..child_count { 1112 1299 // Skip out-of-flow children (absolute/fixed) — they are laid out 1113 1300 // separately in layout_abspos_children. 1114 - if !is_in_flow(&parent.children[i]) { 1301 + if parent.children[i].position == Position::Absolute 1302 + || parent.children[i].position == Position::Fixed 1303 + { 1304 + continue; 1305 + } 1306 + 1307 + // --- Handle floated children --- 1308 + if is_floated(&parent.children[i]) { 1309 + layout_float_child( 1310 + &mut parent.children[i], 1311 + &mut float_ctx, 1312 + cursor_y, 1313 + content_x, 1314 + content_width, 1315 + viewport_width, 1316 + viewport_height, 1317 + font, 1318 + doc, 1319 + abs_cb, 1320 + ); 1115 1321 continue; 1116 1322 } 1117 1323 1118 1324 let child_top_margin = parent.children[i].margin.top; 1119 1325 let child_bottom_margin = parent.children[i].margin.bottom; 1326 + 1327 + // --- Handle clear property --- 1328 + if parent.children[i].clear != Clear::None { 1329 + let clear_y = float_ctx.clear_y(parent.children[i].clear); 1330 + if clear_y > cursor_y { 1331 + cursor_y = clear_y; 1332 + // Clear resets pending margin. 1333 + pending_margin = None; 1334 + } 1335 + } 1120 1336 1121 1337 // --- Empty block: top+bottom margins self-collapse --- 1122 1338 if is_empty_block(&parent.children[i]) { ··· 1164 1380 font, 1165 1381 doc, 1166 1382 abs_cb, 1383 + Some(&float_ctx), 1167 1384 ); 1168 1385 1169 1386 let child = &parent.children[i]; ··· 1189 1406 } 1190 1407 1191 1408 parent.rect.height = cursor_y - parent.rect.y; 1409 + 1410 + // BFC containment: if this box establishes a BFC, it must expand to 1411 + // contain all of its floated children. 1412 + if establishes_bfc(parent) { 1413 + let float_bottom = float_ctx.max_float_bottom(); 1414 + let needed = float_bottom - parent.rect.y; 1415 + if needed > parent.rect.height { 1416 + parent.rect.height = needed; 1417 + } 1418 + } 1419 + } 1420 + 1421 + /// Lay out a single floated child element. 1422 + #[allow(clippy::too_many_arguments)] 1423 + fn layout_float_child( 1424 + child: &mut LayoutBox, 1425 + float_ctx: &mut FloatContext, 1426 + cursor_y: f32, 1427 + container_x: f32, 1428 + container_width: f32, 1429 + viewport_width: f32, 1430 + viewport_height: f32, 1431 + font: &Font, 1432 + doc: &Document, 1433 + abs_cb: Rect, 1434 + ) { 1435 + let float_side = child.float; 1436 + 1437 + // Resolve margins against containing block width. 1438 + child.margin = EdgeSizes { 1439 + top: resolve_length_against(child.css_margin[0], container_width), 1440 + right: resolve_length_against(child.css_margin[1], container_width), 1441 + bottom: resolve_length_against(child.css_margin[2], container_width), 1442 + left: resolve_length_against(child.css_margin[3], container_width), 1443 + }; 1444 + 1445 + // Resolve padding against containing block width. 1446 + child.padding = EdgeSizes { 1447 + top: resolve_length_against(child.css_padding[0], container_width), 1448 + right: resolve_length_against(child.css_padding[1], container_width), 1449 + bottom: resolve_length_against(child.css_padding[2], container_width), 1450 + left: resolve_length_against(child.css_padding[3], container_width), 1451 + }; 1452 + 1453 + let horiz_extra = 1454 + child.border.left + child.border.right + child.padding.left + child.padding.right; 1455 + 1456 + // Resolve content width. 1457 + let content_width = match child.css_width { 1458 + LengthOrAuto::Length(w) => match child.box_sizing { 1459 + BoxSizing::ContentBox => w.max(0.0), 1460 + BoxSizing::BorderBox => (w - horiz_extra).max(0.0), 1461 + }, 1462 + LengthOrAuto::Percentage(p) => { 1463 + let resolved = p / 100.0 * container_width; 1464 + match child.box_sizing { 1465 + BoxSizing::ContentBox => resolved.max(0.0), 1466 + BoxSizing::BorderBox => (resolved - horiz_extra).max(0.0), 1467 + } 1468 + } 1469 + LengthOrAuto::Auto => { 1470 + // Shrink-to-fit: measure content. 1471 + let max_content = measure_float_content_width(child, font); 1472 + let available = 1473 + (container_width - child.margin.left - child.margin.right - horiz_extra).max(0.0); 1474 + max_content.min(available) 1475 + } 1476 + }; 1477 + 1478 + child.rect.width = content_width; 1479 + 1480 + // Temporary position for layout. 1481 + child.rect.x = container_x + child.margin.left + child.border.left + child.padding.left; 1482 + child.rect.y = cursor_y + child.margin.top + child.border.top + child.padding.top; 1483 + 1484 + // Layout child content. 1485 + if let Some((rw, rh)) = child.replaced_size { 1486 + child.rect.width = rw.min(child.rect.width); 1487 + child.rect.height = rh; 1488 + } else { 1489 + match &child.box_type { 1490 + BoxType::Block(_) | BoxType::Anonymous => { 1491 + if matches!(child.display, Display::Flex | Display::InlineFlex) { 1492 + layout_flex_children(child, viewport_width, viewport_height, font, doc, abs_cb); 1493 + } else if has_block_children(child) || has_float_children(child) { 1494 + layout_block_children( 1495 + child, 1496 + viewport_width, 1497 + viewport_height, 1498 + font, 1499 + doc, 1500 + abs_cb, 1501 + ); 1502 + } else { 1503 + layout_inline_children(child, font, doc, None); 1504 + } 1505 + } 1506 + _ => {} 1507 + } 1508 + } 1509 + 1510 + // Resolve explicit CSS height. 1511 + child.content_height = child.rect.height; 1512 + match child.css_height { 1513 + LengthOrAuto::Length(h) => { 1514 + let vert_extra = 1515 + child.border.top + child.border.bottom + child.padding.top + child.padding.bottom; 1516 + child.rect.height = match child.box_sizing { 1517 + BoxSizing::ContentBox => h.max(0.0), 1518 + BoxSizing::BorderBox => (h - vert_extra).max(0.0), 1519 + }; 1520 + } 1521 + LengthOrAuto::Percentage(p) => { 1522 + let resolved = p / 100.0 * viewport_height; 1523 + let vert_extra = 1524 + child.border.top + child.border.bottom + child.padding.top + child.padding.bottom; 1525 + child.rect.height = match child.box_sizing { 1526 + BoxSizing::ContentBox => resolved.max(0.0), 1527 + BoxSizing::BorderBox => (resolved - vert_extra).max(0.0), 1528 + }; 1529 + } 1530 + LengthOrAuto::Auto => {} 1531 + } 1532 + 1533 + // Compute the float's margin box dimensions. 1534 + let margin_box_width = child.margin.left 1535 + + child.border.left 1536 + + child.padding.left 1537 + + child.rect.width 1538 + + child.padding.right 1539 + + child.border.right 1540 + + child.margin.right; 1541 + let margin_box_height = child.margin.top 1542 + + child.border.top 1543 + + child.padding.top 1544 + + child.rect.height 1545 + + child.padding.bottom 1546 + + child.border.bottom 1547 + + child.margin.bottom; 1548 + 1549 + // Place the float. 1550 + let (fx, fy) = float_ctx.place_float( 1551 + float_side, 1552 + margin_box_width, 1553 + margin_box_height, 1554 + cursor_y, 1555 + container_x, 1556 + container_width, 1557 + ); 1558 + 1559 + // Position the child's content box relative to the placed margin box. 1560 + let final_x = fx + child.margin.left + child.border.left + child.padding.left; 1561 + let final_y = fy + child.margin.top + child.border.top + child.padding.top; 1562 + 1563 + // Shift the entire box tree from its temporary position to the final one. 1564 + let dx = final_x - child.rect.x; 1565 + let dy = final_y - child.rect.y; 1566 + if dx != 0.0 || dy != 0.0 { 1567 + shift_box(child, dx, dy); 1568 + } 1569 + 1570 + // Set sticky constraints and handle abspos children. 1571 + set_sticky_constraints(child); 1572 + layout_abspos_children(child, abs_cb, viewport_width, viewport_height, font, doc); 1573 + apply_relative_offset(child, container_width, viewport_height); 1574 + } 1575 + 1576 + /// Measure the max-content width of a float's content (shrink-to-fit). 1577 + fn measure_float_content_width(b: &LayoutBox, font: &Font) -> f32 { 1578 + let mut max_width = 0.0f32; 1579 + measure_box_content_width(b, font, &mut max_width); 1580 + max_width 1192 1581 } 1193 1582 1194 1583 // --------------------------------------------------------------------------- ··· 1396 1785 font, 1397 1786 doc, 1398 1787 abs_cb, 1788 + None, 1399 1789 ); 1400 1790 child.rect.height 1401 1791 } ··· 1535 1925 font, 1536 1926 doc, 1537 1927 abs_cb, 1928 + None, 1538 1929 ); 1539 1930 let cross = child.rect.height + items[i].outer_cross; 1540 1931 if cross > max_cross { ··· 1552 1943 font, 1553 1944 doc, 1554 1945 abs_cb, 1946 + None, 1555 1947 ); 1556 1948 child.rect.height = target_main; 1557 1949 let cross = child.rect.width ··· 1814 2206 if !b.lines.is_empty() { 1815 2207 // Re-run inline layout at the new position. 1816 2208 b.lines.clear(); 1817 - layout_inline_children(b, font, doc); 2209 + layout_inline_children(b, font, doc, None); 1818 2210 } 1819 2211 // Recursively reposition children that have their own inline content. 1820 2212 for child in &mut b.children { 1821 2213 if !child.lines.is_empty() { 1822 2214 child.lines.clear(); 1823 - layout_inline_children(child, font, doc); 2215 + layout_inline_children(child, font, doc, None); 1824 2216 } 1825 2217 } 1826 2218 } ··· 1996 2388 } 1997 2389 1998 2390 /// Lay out inline children using a proper inline formatting context. 1999 - fn layout_inline_children(parent: &mut LayoutBox, font: &Font, doc: &Document) { 2391 + /// 2392 + /// If `float_ctx` is provided, line boxes are shortened to avoid overlapping 2393 + /// with active floats from the parent block formatting context. 2394 + fn layout_inline_children( 2395 + parent: &mut LayoutBox, 2396 + font: &Font, 2397 + doc: &Document, 2398 + float_ctx: Option<&FloatContext>, 2399 + ) { 2000 2400 let available_width = parent.rect.width; 2001 2401 let text_align = parent.text_align; 2002 2402 let line_height = parent.line_height; ··· 2009 2409 return; 2010 2410 } 2011 2411 2012 - // Process items into line boxes. 2013 - let mut all_lines: Vec<Vec<PendingFragment>> = Vec::new(); 2412 + // Build line boxes, respecting float-narrowed available widths. 2413 + let mut all_lines: Vec<(Vec<PendingFragment>, f32, f32)> = Vec::new(); // (fragments, line_left_offset, line_available_width) 2014 2414 let mut current_line: Vec<PendingFragment> = Vec::new(); 2015 2415 let mut cursor_x: f32 = 0.0; 2416 + let mut line_y = parent.rect.y; 2417 + 2418 + // Compute the available width for the current line, narrowed by floats. 2419 + let line_avail = |y: f32| -> (f32, f32) { 2420 + if let Some(fctx) = float_ctx { 2421 + let (left, right) = 2422 + fctx.available_range(y, line_height, parent.rect.x, available_width); 2423 + let offset = (left - parent.rect.x).max(0.0); 2424 + let width = (right - left).max(0.0); 2425 + (offset, width) 2426 + } else { 2427 + (0.0, available_width) 2428 + } 2429 + }; 2430 + 2431 + let (mut current_line_offset, mut current_line_width) = line_avail(line_y); 2016 2432 2017 2433 for item in &items { 2018 2434 match item { ··· 2026 2442 let word_width = measure_text_width(font, text, *font_size); 2027 2443 2028 2444 // If this word doesn't fit and the line isn't empty, break. 2029 - if cursor_x > 0.0 && cursor_x + word_width > available_width { 2030 - all_lines.push(std::mem::take(&mut current_line)); 2445 + if cursor_x > 0.0 && cursor_x + word_width > current_line_width { 2446 + all_lines.push(( 2447 + std::mem::take(&mut current_line), 2448 + current_line_offset, 2449 + current_line_width, 2450 + )); 2451 + line_y += line_height; 2452 + let (off, w) = line_avail(line_y); 2453 + current_line_offset = off; 2454 + current_line_width = w; 2031 2455 cursor_x = 0.0; 2032 2456 } 2033 2457 ··· 2046 2470 // Only add space if we have content on the line. 2047 2471 if !current_line.is_empty() { 2048 2472 let space_width = measure_text_width(font, " ", *font_size); 2049 - if cursor_x + space_width <= available_width { 2473 + if cursor_x + space_width <= current_line_width { 2050 2474 cursor_x += space_width; 2051 2475 } 2052 2476 } 2053 2477 } 2054 2478 InlineItemKind::ForcedBreak => { 2055 - all_lines.push(std::mem::take(&mut current_line)); 2479 + all_lines.push(( 2480 + std::mem::take(&mut current_line), 2481 + current_line_offset, 2482 + current_line_width, 2483 + )); 2484 + line_y += line_height; 2485 + let (off, w) = line_avail(line_y); 2486 + current_line_offset = off; 2487 + current_line_width = w; 2056 2488 cursor_x = 0.0; 2057 2489 } 2058 2490 InlineItemKind::InlineStart { ··· 2074 2506 2075 2507 // Flush the last line. 2076 2508 if !current_line.is_empty() { 2077 - all_lines.push(current_line); 2509 + all_lines.push((current_line, current_line_offset, current_line_width)); 2078 2510 } 2079 2511 2080 2512 if all_lines.is_empty() { ··· 2087 2519 let mut y = parent.rect.y; 2088 2520 let num_lines = all_lines.len(); 2089 2521 2090 - for (line_idx, line_fragments) in all_lines.iter().enumerate() { 2522 + for (line_idx, (line_fragments, line_offset, line_avail_w)) in all_lines.iter().enumerate() { 2091 2523 if line_fragments.is_empty() { 2092 2524 y += line_height; 2093 2525 continue; ··· 2102 2534 // Compute text-align offset. 2103 2535 let is_last_line = line_idx == num_lines - 1; 2104 2536 let align_offset = 2105 - compute_align_offset(text_align, available_width, line_width, is_last_line); 2537 + compute_align_offset(text_align, *line_avail_w, line_width, is_last_line); 2106 2538 2107 2539 for frag in line_fragments { 2108 2540 text_lines.push(TextLine { 2109 2541 text: frag.text.clone(), 2110 - x: parent.rect.x + frag.x + align_offset, 2542 + x: parent.rect.x + line_offset + frag.x + align_offset, 2111 2543 y, 2112 2544 width: frag.width, 2113 2545 font_size: frag.font_size, ··· 2209 2641 font, 2210 2642 doc, 2211 2643 viewport_cb, 2644 + None, 2212 2645 ); 2213 2646 2214 2647 let height = root.margin_box_height(); ··· 4739 5172 constraint.height, 4740 5173 container_box.rect.height, 4741 5174 ); 5175 + } 5176 + 5177 + // ----------------------------------------------------------------------- 5178 + // Float layout tests 5179 + // ----------------------------------------------------------------------- 5180 + 5181 + #[test] 5182 + fn float_left_positioned_at_left_edge() { 5183 + // A float:left element should be placed at the left edge of the container. 5184 + let mut doc = Document::new(); 5185 + let root = doc.root(); 5186 + let html = doc.create_element("html"); 5187 + let body = doc.create_element("body"); 5188 + let container = doc.create_element("div"); 5189 + let float_elem = doc.create_element("div"); 5190 + doc.append_child(root, html); 5191 + doc.append_child(html, body); 5192 + doc.append_child(body, container); 5193 + doc.append_child(container, float_elem); 5194 + doc.set_attribute(container, "style", "width: 400px;"); 5195 + doc.set_attribute( 5196 + float_elem, 5197 + "style", 5198 + "float: left; width: 100px; height: 50px;", 5199 + ); 5200 + 5201 + let tree = layout_doc(&doc); 5202 + let body_box = &tree.root.children[0]; 5203 + let container_box = &body_box.children[0]; 5204 + let float_box = &container_box.children[0]; 5205 + 5206 + assert_eq!(float_box.float, Float::Left); 5207 + assert!( 5208 + (float_box.rect.width - 100.0).abs() < 0.01, 5209 + "float width: {}", 5210 + float_box.rect.width 5211 + ); 5212 + assert!( 5213 + (float_box.rect.height - 50.0).abs() < 0.01, 5214 + "float height: {}", 5215 + float_box.rect.height 5216 + ); 5217 + // Float should be at the left edge of the container (same x as container content). 5218 + assert!( 5219 + (float_box.rect.x - container_box.rect.x).abs() < 0.01, 5220 + "float x should be at container left edge: {} vs {}", 5221 + float_box.rect.x, 5222 + container_box.rect.x, 5223 + ); 5224 + } 5225 + 5226 + #[test] 5227 + fn float_right_positioned_at_right_edge() { 5228 + let mut doc = Document::new(); 5229 + let root = doc.root(); 5230 + let html = doc.create_element("html"); 5231 + let body = doc.create_element("body"); 5232 + let container = doc.create_element("div"); 5233 + let float_elem = doc.create_element("div"); 5234 + doc.append_child(root, html); 5235 + doc.append_child(html, body); 5236 + doc.append_child(body, container); 5237 + doc.append_child(container, float_elem); 5238 + doc.set_attribute(container, "style", "width: 400px;"); 5239 + doc.set_attribute( 5240 + float_elem, 5241 + "style", 5242 + "float: right; width: 100px; height: 50px;", 5243 + ); 5244 + 5245 + let tree = layout_doc(&doc); 5246 + let body_box = &tree.root.children[0]; 5247 + let container_box = &body_box.children[0]; 5248 + let float_box = &container_box.children[0]; 5249 + 5250 + assert_eq!(float_box.float, Float::Right); 5251 + // Float's right edge (content + padding + border + margin) should align 5252 + // with the container's right content edge. 5253 + let float_right = float_box.rect.x + float_box.rect.width; 5254 + let container_right = container_box.rect.x + container_box.rect.width; 5255 + assert!( 5256 + (float_right - container_right).abs() < 0.01, 5257 + "float right edge {} should match container right edge {}", 5258 + float_right, 5259 + container_right, 5260 + ); 5261 + } 5262 + 5263 + #[test] 5264 + fn two_left_floats_stack_horizontally() { 5265 + let mut doc = Document::new(); 5266 + let root = doc.root(); 5267 + let html = doc.create_element("html"); 5268 + let body = doc.create_element("body"); 5269 + let container = doc.create_element("div"); 5270 + let float1 = doc.create_element("div"); 5271 + let float2 = doc.create_element("div"); 5272 + doc.append_child(root, html); 5273 + doc.append_child(html, body); 5274 + doc.append_child(body, container); 5275 + doc.append_child(container, float1); 5276 + doc.append_child(container, float2); 5277 + doc.set_attribute(container, "style", "width: 400px;"); 5278 + doc.set_attribute(float1, "style", "float: left; width: 100px; height: 50px;"); 5279 + doc.set_attribute(float2, "style", "float: left; width: 120px; height: 50px;"); 5280 + 5281 + let tree = layout_doc(&doc); 5282 + let body_box = &tree.root.children[0]; 5283 + let container_box = &body_box.children[0]; 5284 + let f1 = &container_box.children[0]; 5285 + let f2 = &container_box.children[1]; 5286 + 5287 + // Second float should be placed to the right of the first. 5288 + let f1_right = 5289 + f1.rect.x + f1.rect.width + f1.padding.right + f1.border.right + f1.margin.right; 5290 + assert!( 5291 + f2.rect.x >= f1_right - 0.01, 5292 + "second float x ({}) should be >= first float right edge ({})", 5293 + f2.rect.x, 5294 + f1_right, 5295 + ); 5296 + // Both should be on the same row (same y). 5297 + assert!( 5298 + (f1.rect.y - f2.rect.y).abs() < 0.01, 5299 + "floats should be on the same row: {} vs {}", 5300 + f1.rect.y, 5301 + f2.rect.y, 5302 + ); 5303 + } 5304 + 5305 + #[test] 5306 + fn clear_both_moves_below_floats() { 5307 + let mut doc = Document::new(); 5308 + let root = doc.root(); 5309 + let html = doc.create_element("html"); 5310 + let body = doc.create_element("body"); 5311 + let container = doc.create_element("div"); 5312 + let float_elem = doc.create_element("div"); 5313 + let cleared = doc.create_element("div"); 5314 + let text = doc.create_text("After clear"); 5315 + doc.append_child(root, html); 5316 + doc.append_child(html, body); 5317 + doc.append_child(body, container); 5318 + doc.append_child(container, float_elem); 5319 + doc.append_child(container, cleared); 5320 + doc.append_child(cleared, text); 5321 + doc.set_attribute(container, "style", "width: 400px;"); 5322 + doc.set_attribute( 5323 + float_elem, 5324 + "style", 5325 + "float: left; width: 100px; height: 80px;", 5326 + ); 5327 + doc.set_attribute(cleared, "style", "clear: both;"); 5328 + 5329 + let tree = layout_doc(&doc); 5330 + let body_box = &tree.root.children[0]; 5331 + let container_box = &body_box.children[0]; 5332 + 5333 + // Find the cleared element (skip the float). 5334 + let float_box = &container_box.children[0]; 5335 + let cleared_box = &container_box.children[1]; 5336 + 5337 + let float_bottom = float_box.rect.y 5338 + + float_box.rect.height 5339 + + float_box.padding.bottom 5340 + + float_box.border.bottom 5341 + + float_box.margin.bottom; 5342 + assert!( 5343 + cleared_box.rect.y >= float_bottom - 0.01, 5344 + "cleared element y ({}) should be >= float bottom ({})", 5345 + cleared_box.rect.y, 5346 + float_bottom, 5347 + ); 5348 + } 5349 + 5350 + #[test] 5351 + fn bfc_contains_floats() { 5352 + // A container with overflow:hidden (establishes BFC) should expand 5353 + // to contain its floated children. 5354 + let mut doc = Document::new(); 5355 + let root = doc.root(); 5356 + let html = doc.create_element("html"); 5357 + let body = doc.create_element("body"); 5358 + let container = doc.create_element("div"); 5359 + let float_elem = doc.create_element("div"); 5360 + doc.append_child(root, html); 5361 + doc.append_child(html, body); 5362 + doc.append_child(body, container); 5363 + doc.append_child(container, float_elem); 5364 + doc.set_attribute(container, "style", "width: 400px; overflow: hidden;"); 5365 + doc.set_attribute( 5366 + float_elem, 5367 + "style", 5368 + "float: left; width: 100px; height: 150px;", 5369 + ); 5370 + 5371 + let tree = layout_doc(&doc); 5372 + let body_box = &tree.root.children[0]; 5373 + let container_box = &body_box.children[0]; 5374 + 5375 + // Container should be at least 150px tall to contain the float. 5376 + assert!( 5377 + container_box.rect.height >= 150.0 - 0.01, 5378 + "BFC container height ({}) should be >= float height (150)", 5379 + container_box.rect.height, 5380 + ); 5381 + } 5382 + 5383 + #[test] 5384 + fn inline_text_wraps_around_float() { 5385 + // Text in a block should flow around a float. 5386 + let mut doc = Document::new(); 5387 + let root = doc.root(); 5388 + let html = doc.create_element("html"); 5389 + let body = doc.create_element("body"); 5390 + let container = doc.create_element("div"); 5391 + let float_elem = doc.create_element("div"); 5392 + let text_p = doc.create_element("p"); 5393 + let text = doc.create_text("Some text content that wraps"); 5394 + doc.append_child(root, html); 5395 + doc.append_child(html, body); 5396 + doc.append_child(body, container); 5397 + doc.append_child(container, float_elem); 5398 + doc.append_child(container, text_p); 5399 + doc.append_child(text_p, text); 5400 + doc.set_attribute(container, "style", "width: 400px;"); 5401 + doc.set_attribute( 5402 + float_elem, 5403 + "style", 5404 + "float: left; width: 100px; height: 80px;", 5405 + ); 5406 + 5407 + let tree = layout_doc(&doc); 5408 + let body_box = &tree.root.children[0]; 5409 + let container_box = &body_box.children[0]; 5410 + 5411 + // Find the text paragraph (it should be the second child, after the float). 5412 + let text_box = &container_box.children[1]; 5413 + 5414 + // The text lines that overlap with the float should be offset to 5415 + // the right of the float. Check that the first line's x is shifted. 5416 + if !text_box.lines.is_empty() { 5417 + let first_line = &text_box.lines[0]; 5418 + let float_box = &container_box.children[0]; 5419 + let float_right = float_box.rect.x 5420 + + float_box.rect.width 5421 + + float_box.padding.right 5422 + + float_box.border.right 5423 + + float_box.margin.right; 5424 + assert!( 5425 + first_line.x >= float_right - 0.01, 5426 + "first line x ({}) should be >= float right edge ({})", 5427 + first_line.x, 5428 + float_right, 5429 + ); 5430 + } 4742 5431 } 4743 5432 }
+65
crates/style/src/computed.rs
··· 105 105 } 106 106 107 107 // --------------------------------------------------------------------------- 108 + // Float 109 + // --------------------------------------------------------------------------- 110 + 111 + /// CSS `float` property values. 112 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 113 + pub enum Float { 114 + #[default] 115 + None, 116 + Left, 117 + Right, 118 + } 119 + 120 + // --------------------------------------------------------------------------- 121 + // Clear 122 + // --------------------------------------------------------------------------- 123 + 124 + /// CSS `clear` property values. 125 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 126 + pub enum Clear { 127 + #[default] 128 + None, 129 + Left, 130 + Right, 131 + Both, 132 + } 133 + 134 + // --------------------------------------------------------------------------- 108 135 // Overflow 109 136 // --------------------------------------------------------------------------- 110 137 ··· 294 321 pub left: LengthOrAuto, 295 322 pub z_index: Option<i32>, 296 323 324 + // Float 325 + pub float: Float, 326 + pub clear: Clear, 327 + 297 328 // Overflow 298 329 pub overflow: Overflow, 299 330 ··· 370 401 left: LengthOrAuto::Auto, 371 402 z_index: None, 372 403 404 + float: Float::None, 405 + clear: Clear::None, 406 + 373 407 overflow: Overflow::Visible, 374 408 visibility: Visibility::Visible, 375 409 ··· 874 908 }; 875 909 } 876 910 911 + // Float 912 + "float" => { 913 + style.float = match value { 914 + CssValue::Keyword(k) => match k.as_str() { 915 + "none" => Float::None, 916 + "left" => Float::Left, 917 + "right" => Float::Right, 918 + _ => style.float, 919 + }, 920 + _ => style.float, 921 + }; 922 + } 923 + 924 + // Clear 925 + "clear" => { 926 + style.clear = match value { 927 + CssValue::Keyword(k) => match k.as_str() { 928 + "none" => Clear::None, 929 + "left" => Clear::Left, 930 + "right" => Clear::Right, 931 + "both" => Clear::Both, 932 + _ => style.clear, 933 + }, 934 + _ => style.clear, 935 + }; 936 + } 937 + 877 938 // Overflow 878 939 "overflow" => { 879 940 style.overflow = match value { ··· 1089 1150 "box-sizing" => style.box_sizing = parent.box_sizing, 1090 1151 "background-color" => style.background_color = parent.background_color, 1091 1152 "position" => style.position = parent.position, 1153 + "float" => style.float = parent.float, 1154 + "clear" => style.clear = parent.clear, 1092 1155 "overflow" => style.overflow = parent.overflow, 1093 1156 "flex-direction" => style.flex_direction = parent.flex_direction, 1094 1157 "flex-wrap" => style.flex_wrap = parent.flex_wrap, ··· 1142 1205 "right" => style.right = initial.right, 1143 1206 "bottom" => style.bottom = initial.bottom, 1144 1207 "left" => style.left = initial.left, 1208 + "float" => style.float = initial.float, 1209 + "clear" => style.clear = initial.clear, 1145 1210 "overflow" => style.overflow = initial.overflow, 1146 1211 "visibility" => style.visibility = initial.visibility, 1147 1212 "flex-direction" => style.flex_direction = initial.flex_direction,