From 17203d6964e4ca2423afaa35b580d9182e1efe74 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sun, 22 Mar 2026 08:50:44 +0100 Subject: [PATCH] Implement CSS float and clear properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/layout/src/lib.rs | 745 +++++++++++++++++++++++++++++++++-- crates/style/src/computed.rs | 65 +++ 2 files changed, 782 insertions(+), 28 deletions(-) diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 558eb53..d18842f 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -8,8 +8,8 @@ use std::collections::HashMap; use we_css::values::Color; use we_dom::{Document, NodeData, NodeId}; use we_style::computed::{ - AlignContent, AlignItems, AlignSelf, BorderStyle, BoxSizing, ComputedStyle, Display, - FlexDirection, FlexWrap, JustifyContent, LengthOrAuto, Overflow, Position, StyledNode, + AlignContent, AlignItems, AlignSelf, BorderStyle, BoxSizing, Clear, ComputedStyle, Display, + FlexDirection, FlexWrap, Float, JustifyContent, LengthOrAuto, Overflow, Position, StyledNode, TextAlign, TextDecoration, Visibility, }; use we_text::font::Font; @@ -119,6 +119,10 @@ pub struct LayoutBox { pub sticky_constraint: Option, /// CSS `visibility` property. pub visibility: Visibility, + /// CSS `float` property. + pub float: Float, + /// CSS `clear` property. + pub clear: Clear, /// Natural content height before CSS height override. /// Used to determine overflow for scroll containers. pub content_height: f32, @@ -190,6 +194,8 @@ impl LayoutBox { css_offsets: [style.top, style.right, style.bottom, style.left], sticky_constraint: None, visibility: style.visibility, + float: style.float, + clear: style.clear, content_height: 0.0, flex_direction: style.flex_direction, flex_wrap: style.flex_wrap, @@ -378,13 +384,21 @@ fn build_box( } } - let box_type = match style.display { + // Per CSS2 §9.7: float forces display to block. + let effective_display = + if style.float != Float::None && style.display == Display::Inline { + Display::Block + } else { + style.display + }; + + let box_type = match effective_display { Display::Block | Display::Flex | Display::InlineFlex => BoxType::Block(node), Display::Inline => BoxType::Inline(node), Display::None => unreachable!(), }; - if style.display == Display::Block { + if effective_display == Display::Block { children = normalize_children(children, style); } @@ -489,9 +503,173 @@ fn is_block_level(b: &LayoutBox) -> bool { matches!(b.box_type, BoxType::Block(_) | BoxType::Anonymous) } -/// Returns `true` if this box is in normal flow (not absolutely or fixed positioned). +/// Returns `true` if this box is in normal flow (not absolutely/fixed positioned, not floated). fn is_in_flow(b: &LayoutBox) -> bool { - b.position != Position::Absolute && b.position != Position::Fixed + b.position != Position::Absolute && b.position != Position::Fixed && b.float == Float::None +} + +/// Returns `true` if this box is floated. +fn is_floated(b: &LayoutBox) -> bool { + b.float != Float::None +} + +// --------------------------------------------------------------------------- +// Float tracking +// --------------------------------------------------------------------------- + +/// A positioned float rectangle used for tracking placed floats. +#[derive(Debug, Clone, Copy)] +struct PlacedFloat { + /// Left edge of the float's margin box. + x: f32, + /// Top edge of the float's margin box. + y: f32, + /// Width of the float's margin box. + width: f32, + /// Height of the float's margin box. + height: f32, + /// Which side this float is on. + side: Float, +} + +/// Tracks active floats within a block formatting context. +#[derive(Debug, Default)] +struct FloatContext { + floats: Vec, +} + +impl FloatContext { + /// Get the available x-range at a given y position, narrowed by active floats. + /// Returns (left_edge, right_edge) within the containing block. + fn available_range( + &self, + y: f32, + line_height: f32, + container_x: f32, + container_width: f32, + ) -> (f32, f32) { + let mut left = container_x; + let mut right = container_x + container_width; + + for f in &self.floats { + let float_top = f.y; + let float_bottom = f.y + f.height; + + // Check if this float overlaps vertically with the line. + if y + line_height > float_top && y < float_bottom { + match f.side { + Float::Left => { + let float_right_edge = f.x + f.width; + if float_right_edge > left { + left = float_right_edge; + } + } + Float::Right => { + let float_left_edge = f.x; + if float_left_edge < right { + right = float_left_edge; + } + } + Float::None => {} + } + } + } + + (left, right) + } + + /// Find the y position below all floats that match the given clear side. + fn clear_y(&self, clear: Clear) -> f32 { + let mut y = 0.0f32; + for f in &self.floats { + let dominated = match clear { + Clear::Left => f.side == Float::Left, + Clear::Right => f.side == Float::Right, + Clear::Both => true, + Clear::None => false, + }; + if dominated { + let bottom = f.y + f.height; + if bottom > y { + y = bottom; + } + } + } + y + } + + /// Find the bottom edge of all placed floats. + fn max_float_bottom(&self) -> f32 { + let mut bottom = 0.0f32; + for f in &self.floats { + let fb = f.y + f.height; + if fb > bottom { + bottom = fb; + } + } + bottom + } + + /// Place a float and return its position. + fn place_float( + &mut self, + float_side: Float, + float_width: f32, + float_height: f32, + cursor_y: f32, + container_x: f32, + container_width: f32, + ) -> (f32, f32) { + // Start at cursor_y and find a position where the float fits. + let mut y = cursor_y; + + loop { + let (left, right) = self.available_range(y, float_height, container_x, container_width); + let available = right - left; + + if available >= float_width || available >= container_width { + let x = match float_side { + Float::Left => left, + Float::Right => right - float_width, + Float::None => unreachable!(), + }; + self.floats.push(PlacedFloat { + x, + y, + width: float_width, + height: float_height, + side: float_side, + }); + return (x, y); + } + + // Move down below the topmost interfering float and try again. + let mut next_y = f32::MAX; + for f in &self.floats { + let fb = f.y + f.height; + if fb > y && fb < next_y { + next_y = fb; + } + } + if next_y == f32::MAX { + // No more floats to clear, place at current position. + let x = match float_side { + Float::Left => container_x, + Float::Right => container_x + container_width - float_width, + Float::None => unreachable!(), + }; + self.floats.push(PlacedFloat { + x, + y, + width: float_width, + height: float_height, + side: float_side, + }); + return (x, y); + } + y = next_y; + } + } } // --------------------------------------------------------------------------- @@ -517,6 +695,7 @@ fn compute_layout( font: &Font, doc: &Document, abs_cb: Rect, + float_ctx: Option<&FloatContext>, ) { // Resolve percentage margins against containing block width. // Only re-resolve percentages — absolute margins may have been modified @@ -594,10 +773,10 @@ fn compute_layout( BoxType::Block(_) | BoxType::Anonymous => { if matches!(b.display, Display::Flex | Display::InlineFlex) { layout_flex_children(b, viewport_width, viewport_height, font, doc, abs_cb); - } else if has_block_children(b) { + } else if has_block_children(b) || has_float_children(b) { layout_block_children(b, viewport_width, viewport_height, font, doc, abs_cb); } else { - layout_inline_children(b, font, doc); + layout_inline_children(b, font, doc, float_ctx); } } BoxType::TextRun { .. } | BoxType::Inline(_) => { @@ -870,7 +1049,7 @@ fn layout_absolute_child( doc, child_abs_cb, ); - } else if has_block_children(child) { + } else if has_block_children(child) || has_float_children(child) { layout_block_children( child, viewport_width, @@ -880,7 +1059,7 @@ fn layout_absolute_child( child_abs_cb, ); } else { - layout_inline_children(child, font, doc); + layout_inline_children(child, font, doc, None); } } _ => {} @@ -962,6 +1141,10 @@ fn has_block_children(b: &LayoutBox) -> bool { .any(|c| is_in_flow(c) && is_block_level(c)) } +fn has_float_children(b: &LayoutBox) -> bool { + b.children.iter().any(is_floated) +} + /// Collapse two adjoining margins per CSS2 §8.3.1. /// /// Both non-negative → use the larger. @@ -980,7 +1163,9 @@ fn collapse_margins(a: f32, b: f32) -> f32 { /// Returns `true` if this box establishes a new block formatting context, /// which prevents its margins from collapsing with children. fn establishes_bfc(b: &LayoutBox) -> bool { - b.overflow != Overflow::Visible || matches!(b.display, Display::Flex | Display::InlineFlex) + b.overflow != Overflow::Visible + || matches!(b.display, Display::Flex | Display::InlineFlex) + || b.float != Float::None } /// Returns `true` if a block box has no in-flow content (empty block). @@ -1080,11 +1265,12 @@ fn last_block_bottom_margin(children: &[LayoutBox]) -> Option { } } -/// Lay out block-level children with vertical margin collapsing (CSS2 §8.3.1). +/// Lay out block-level children with vertical margin collapsing (CSS2 §8.3.1) +/// and float support. /// -/// Handles adjacent-sibling collapsing, empty-block collapsing, and +/// Handles adjacent-sibling collapsing, empty-block collapsing, /// parent-child internal spacing (the parent's external margins were already -/// updated by `pre_collapse_margins`). +/// updated by `pre_collapse_margins`), and float placement. fn layout_block_children( parent: &mut LayoutBox, viewport_width: f32, @@ -1107,17 +1293,47 @@ fn layout_block_children( let child_count = parent.children.len(); // Track whether we've seen any in-flow children (for parent_top_open). let mut first_in_flow = true; + let mut float_ctx = FloatContext::default(); for i in 0..child_count { // Skip out-of-flow children (absolute/fixed) — they are laid out // separately in layout_abspos_children. - if !is_in_flow(&parent.children[i]) { + if parent.children[i].position == Position::Absolute + || parent.children[i].position == Position::Fixed + { + continue; + } + + // --- Handle floated children --- + if is_floated(&parent.children[i]) { + layout_float_child( + &mut parent.children[i], + &mut float_ctx, + cursor_y, + content_x, + content_width, + viewport_width, + viewport_height, + font, + doc, + abs_cb, + ); continue; } let child_top_margin = parent.children[i].margin.top; let child_bottom_margin = parent.children[i].margin.bottom; + // --- Handle clear property --- + if parent.children[i].clear != Clear::None { + let clear_y = float_ctx.clear_y(parent.children[i].clear); + if clear_y > cursor_y { + cursor_y = clear_y; + // Clear resets pending margin. + pending_margin = None; + } + } + // --- Empty block: top+bottom margins self-collapse --- if is_empty_block(&parent.children[i]) { let self_collapsed = collapse_margins(child_top_margin, child_bottom_margin); @@ -1164,6 +1380,7 @@ fn layout_block_children( font, doc, abs_cb, + Some(&float_ctx), ); let child = &parent.children[i]; @@ -1189,6 +1406,178 @@ fn layout_block_children( } parent.rect.height = cursor_y - parent.rect.y; + + // BFC containment: if this box establishes a BFC, it must expand to + // contain all of its floated children. + if establishes_bfc(parent) { + let float_bottom = float_ctx.max_float_bottom(); + let needed = float_bottom - parent.rect.y; + if needed > parent.rect.height { + parent.rect.height = needed; + } + } +} + +/// Lay out a single floated child element. +#[allow(clippy::too_many_arguments)] +fn layout_float_child( + child: &mut LayoutBox, + float_ctx: &mut FloatContext, + cursor_y: f32, + container_x: f32, + container_width: f32, + viewport_width: f32, + viewport_height: f32, + font: &Font, + doc: &Document, + abs_cb: Rect, +) { + let float_side = child.float; + + // Resolve margins against containing block width. + child.margin = EdgeSizes { + top: resolve_length_against(child.css_margin[0], container_width), + right: resolve_length_against(child.css_margin[1], container_width), + bottom: resolve_length_against(child.css_margin[2], container_width), + left: resolve_length_against(child.css_margin[3], container_width), + }; + + // Resolve padding against containing block width. + child.padding = EdgeSizes { + top: resolve_length_against(child.css_padding[0], container_width), + right: resolve_length_against(child.css_padding[1], container_width), + bottom: resolve_length_against(child.css_padding[2], container_width), + left: resolve_length_against(child.css_padding[3], container_width), + }; + + let horiz_extra = + child.border.left + child.border.right + child.padding.left + child.padding.right; + + // Resolve content width. + let content_width = match child.css_width { + LengthOrAuto::Length(w) => match child.box_sizing { + BoxSizing::ContentBox => w.max(0.0), + BoxSizing::BorderBox => (w - horiz_extra).max(0.0), + }, + LengthOrAuto::Percentage(p) => { + let resolved = p / 100.0 * container_width; + match child.box_sizing { + BoxSizing::ContentBox => resolved.max(0.0), + BoxSizing::BorderBox => (resolved - horiz_extra).max(0.0), + } + } + LengthOrAuto::Auto => { + // Shrink-to-fit: measure content. + let max_content = measure_float_content_width(child, font); + let available = + (container_width - child.margin.left - child.margin.right - horiz_extra).max(0.0); + max_content.min(available) + } + }; + + child.rect.width = content_width; + + // Temporary position for layout. + child.rect.x = container_x + child.margin.left + child.border.left + child.padding.left; + child.rect.y = cursor_y + child.margin.top + child.border.top + child.padding.top; + + // Layout child content. + if let Some((rw, rh)) = child.replaced_size { + child.rect.width = rw.min(child.rect.width); + child.rect.height = rh; + } else { + match &child.box_type { + BoxType::Block(_) | BoxType::Anonymous => { + if matches!(child.display, Display::Flex | Display::InlineFlex) { + layout_flex_children(child, viewport_width, viewport_height, font, doc, abs_cb); + } else if has_block_children(child) || has_float_children(child) { + layout_block_children( + child, + viewport_width, + viewport_height, + font, + doc, + abs_cb, + ); + } else { + layout_inline_children(child, font, doc, None); + } + } + _ => {} + } + } + + // Resolve explicit CSS height. + child.content_height = child.rect.height; + match child.css_height { + LengthOrAuto::Length(h) => { + let vert_extra = + child.border.top + child.border.bottom + child.padding.top + child.padding.bottom; + child.rect.height = match child.box_sizing { + BoxSizing::ContentBox => h.max(0.0), + BoxSizing::BorderBox => (h - vert_extra).max(0.0), + }; + } + LengthOrAuto::Percentage(p) => { + let resolved = p / 100.0 * viewport_height; + let vert_extra = + child.border.top + child.border.bottom + child.padding.top + child.padding.bottom; + child.rect.height = match child.box_sizing { + BoxSizing::ContentBox => resolved.max(0.0), + BoxSizing::BorderBox => (resolved - vert_extra).max(0.0), + }; + } + LengthOrAuto::Auto => {} + } + + // Compute the float's margin box dimensions. + let margin_box_width = child.margin.left + + child.border.left + + child.padding.left + + child.rect.width + + child.padding.right + + child.border.right + + child.margin.right; + let margin_box_height = child.margin.top + + child.border.top + + child.padding.top + + child.rect.height + + child.padding.bottom + + child.border.bottom + + child.margin.bottom; + + // Place the float. + let (fx, fy) = float_ctx.place_float( + float_side, + margin_box_width, + margin_box_height, + cursor_y, + container_x, + container_width, + ); + + // Position the child's content box relative to the placed margin box. + let final_x = fx + child.margin.left + child.border.left + child.padding.left; + let final_y = fy + child.margin.top + child.border.top + child.padding.top; + + // Shift the entire box tree from its temporary position to the final one. + let dx = final_x - child.rect.x; + let dy = final_y - child.rect.y; + if dx != 0.0 || dy != 0.0 { + shift_box(child, dx, dy); + } + + // Set sticky constraints and handle abspos children. + set_sticky_constraints(child); + layout_abspos_children(child, abs_cb, viewport_width, viewport_height, font, doc); + apply_relative_offset(child, container_width, viewport_height); +} + +/// Measure the max-content width of a float's content (shrink-to-fit). +fn measure_float_content_width(b: &LayoutBox, font: &Font) -> f32 { + let mut max_width = 0.0f32; + measure_box_content_width(b, font, &mut max_width); + max_width } // --------------------------------------------------------------------------- @@ -1396,6 +1785,7 @@ fn layout_flex_children( font, doc, abs_cb, + None, ); child.rect.height } @@ -1535,6 +1925,7 @@ fn layout_flex_children( font, doc, abs_cb, + None, ); let cross = child.rect.height + items[i].outer_cross; if cross > max_cross { @@ -1552,6 +1943,7 @@ fn layout_flex_children( font, doc, abs_cb, + None, ); child.rect.height = target_main; let cross = child.rect.width @@ -1814,13 +2206,13 @@ fn reposition_lines(b: &mut LayoutBox, font: &Font, doc: &Document) { if !b.lines.is_empty() { // Re-run inline layout at the new position. b.lines.clear(); - layout_inline_children(b, font, doc); + layout_inline_children(b, font, doc, None); } // Recursively reposition children that have their own inline content. for child in &mut b.children { if !child.lines.is_empty() { child.lines.clear(); - layout_inline_children(child, font, doc); + layout_inline_children(child, font, doc, None); } } } @@ -1996,7 +2388,15 @@ fn split_into_words(text: &str) -> Vec { } /// Lay out inline children using a proper inline formatting context. -fn layout_inline_children(parent: &mut LayoutBox, font: &Font, doc: &Document) { +/// +/// If `float_ctx` is provided, line boxes are shortened to avoid overlapping +/// with active floats from the parent block formatting context. +fn layout_inline_children( + parent: &mut LayoutBox, + font: &Font, + doc: &Document, + float_ctx: Option<&FloatContext>, +) { let available_width = parent.rect.width; let text_align = parent.text_align; let line_height = parent.line_height; @@ -2009,10 +2409,26 @@ fn layout_inline_children(parent: &mut LayoutBox, font: &Font, doc: &Document) { return; } - // Process items into line boxes. - let mut all_lines: Vec> = Vec::new(); + // Build line boxes, respecting float-narrowed available widths. + let mut all_lines: Vec<(Vec, f32, f32)> = Vec::new(); // (fragments, line_left_offset, line_available_width) let mut current_line: Vec = Vec::new(); let mut cursor_x: f32 = 0.0; + let mut line_y = parent.rect.y; + + // Compute the available width for the current line, narrowed by floats. + let line_avail = |y: f32| -> (f32, f32) { + if let Some(fctx) = float_ctx { + let (left, right) = + fctx.available_range(y, line_height, parent.rect.x, available_width); + let offset = (left - parent.rect.x).max(0.0); + let width = (right - left).max(0.0); + (offset, width) + } else { + (0.0, available_width) + } + }; + + let (mut current_line_offset, mut current_line_width) = line_avail(line_y); for item in &items { match item { @@ -2026,8 +2442,16 @@ fn layout_inline_children(parent: &mut LayoutBox, font: &Font, doc: &Document) { let word_width = measure_text_width(font, text, *font_size); // If this word doesn't fit and the line isn't empty, break. - if cursor_x > 0.0 && cursor_x + word_width > available_width { - all_lines.push(std::mem::take(&mut current_line)); + if cursor_x > 0.0 && cursor_x + word_width > current_line_width { + all_lines.push(( + std::mem::take(&mut current_line), + current_line_offset, + current_line_width, + )); + line_y += line_height; + let (off, w) = line_avail(line_y); + current_line_offset = off; + current_line_width = w; cursor_x = 0.0; } @@ -2046,13 +2470,21 @@ fn layout_inline_children(parent: &mut LayoutBox, font: &Font, doc: &Document) { // Only add space if we have content on the line. if !current_line.is_empty() { let space_width = measure_text_width(font, " ", *font_size); - if cursor_x + space_width <= available_width { + if cursor_x + space_width <= current_line_width { cursor_x += space_width; } } } InlineItemKind::ForcedBreak => { - all_lines.push(std::mem::take(&mut current_line)); + all_lines.push(( + std::mem::take(&mut current_line), + current_line_offset, + current_line_width, + )); + line_y += line_height; + let (off, w) = line_avail(line_y); + current_line_offset = off; + current_line_width = w; cursor_x = 0.0; } InlineItemKind::InlineStart { @@ -2074,7 +2506,7 @@ fn layout_inline_children(parent: &mut LayoutBox, font: &Font, doc: &Document) { // Flush the last line. if !current_line.is_empty() { - all_lines.push(current_line); + all_lines.push((current_line, current_line_offset, current_line_width)); } if all_lines.is_empty() { @@ -2087,7 +2519,7 @@ fn layout_inline_children(parent: &mut LayoutBox, font: &Font, doc: &Document) { let mut y = parent.rect.y; let num_lines = all_lines.len(); - for (line_idx, line_fragments) in all_lines.iter().enumerate() { + for (line_idx, (line_fragments, line_offset, line_avail_w)) in all_lines.iter().enumerate() { if line_fragments.is_empty() { y += line_height; continue; @@ -2102,12 +2534,12 @@ fn layout_inline_children(parent: &mut LayoutBox, font: &Font, doc: &Document) { // Compute text-align offset. let is_last_line = line_idx == num_lines - 1; let align_offset = - compute_align_offset(text_align, available_width, line_width, is_last_line); + compute_align_offset(text_align, *line_avail_w, line_width, is_last_line); for frag in line_fragments { text_lines.push(TextLine { text: frag.text.clone(), - x: parent.rect.x + frag.x + align_offset, + x: parent.rect.x + line_offset + frag.x + align_offset, y, width: frag.width, font_size: frag.font_size, @@ -2209,6 +2641,7 @@ pub fn layout( font, doc, viewport_cb, + None, ); let height = root.margin_box_height(); @@ -4740,4 +5173,260 @@ body { margin: 0; } container_box.rect.height, ); } + + // ----------------------------------------------------------------------- + // Float layout tests + // ----------------------------------------------------------------------- + + #[test] + fn float_left_positioned_at_left_edge() { + // A float:left element should be placed at the left edge of the container. + let mut doc = Document::new(); + let root = doc.root(); + let html = doc.create_element("html"); + let body = doc.create_element("body"); + let container = doc.create_element("div"); + let float_elem = doc.create_element("div"); + doc.append_child(root, html); + doc.append_child(html, body); + doc.append_child(body, container); + doc.append_child(container, float_elem); + doc.set_attribute(container, "style", "width: 400px;"); + doc.set_attribute( + float_elem, + "style", + "float: left; width: 100px; height: 50px;", + ); + + let tree = layout_doc(&doc); + let body_box = &tree.root.children[0]; + let container_box = &body_box.children[0]; + let float_box = &container_box.children[0]; + + assert_eq!(float_box.float, Float::Left); + assert!( + (float_box.rect.width - 100.0).abs() < 0.01, + "float width: {}", + float_box.rect.width + ); + assert!( + (float_box.rect.height - 50.0).abs() < 0.01, + "float height: {}", + float_box.rect.height + ); + // Float should be at the left edge of the container (same x as container content). + assert!( + (float_box.rect.x - container_box.rect.x).abs() < 0.01, + "float x should be at container left edge: {} vs {}", + float_box.rect.x, + container_box.rect.x, + ); + } + + #[test] + fn float_right_positioned_at_right_edge() { + let mut doc = Document::new(); + let root = doc.root(); + let html = doc.create_element("html"); + let body = doc.create_element("body"); + let container = doc.create_element("div"); + let float_elem = doc.create_element("div"); + doc.append_child(root, html); + doc.append_child(html, body); + doc.append_child(body, container); + doc.append_child(container, float_elem); + doc.set_attribute(container, "style", "width: 400px;"); + doc.set_attribute( + float_elem, + "style", + "float: right; width: 100px; height: 50px;", + ); + + let tree = layout_doc(&doc); + let body_box = &tree.root.children[0]; + let container_box = &body_box.children[0]; + let float_box = &container_box.children[0]; + + assert_eq!(float_box.float, Float::Right); + // Float's right edge (content + padding + border + margin) should align + // with the container's right content edge. + let float_right = float_box.rect.x + float_box.rect.width; + let container_right = container_box.rect.x + container_box.rect.width; + assert!( + (float_right - container_right).abs() < 0.01, + "float right edge {} should match container right edge {}", + float_right, + container_right, + ); + } + + #[test] + fn two_left_floats_stack_horizontally() { + let mut doc = Document::new(); + let root = doc.root(); + let html = doc.create_element("html"); + let body = doc.create_element("body"); + let container = doc.create_element("div"); + let float1 = doc.create_element("div"); + let float2 = doc.create_element("div"); + doc.append_child(root, html); + doc.append_child(html, body); + doc.append_child(body, container); + doc.append_child(container, float1); + doc.append_child(container, float2); + doc.set_attribute(container, "style", "width: 400px;"); + doc.set_attribute(float1, "style", "float: left; width: 100px; height: 50px;"); + doc.set_attribute(float2, "style", "float: left; width: 120px; height: 50px;"); + + let tree = layout_doc(&doc); + let body_box = &tree.root.children[0]; + let container_box = &body_box.children[0]; + let f1 = &container_box.children[0]; + let f2 = &container_box.children[1]; + + // Second float should be placed to the right of the first. + let f1_right = + f1.rect.x + f1.rect.width + f1.padding.right + f1.border.right + f1.margin.right; + assert!( + f2.rect.x >= f1_right - 0.01, + "second float x ({}) should be >= first float right edge ({})", + f2.rect.x, + f1_right, + ); + // Both should be on the same row (same y). + assert!( + (f1.rect.y - f2.rect.y).abs() < 0.01, + "floats should be on the same row: {} vs {}", + f1.rect.y, + f2.rect.y, + ); + } + + #[test] + fn clear_both_moves_below_floats() { + let mut doc = Document::new(); + let root = doc.root(); + let html = doc.create_element("html"); + let body = doc.create_element("body"); + let container = doc.create_element("div"); + let float_elem = doc.create_element("div"); + let cleared = doc.create_element("div"); + let text = doc.create_text("After clear"); + doc.append_child(root, html); + doc.append_child(html, body); + doc.append_child(body, container); + doc.append_child(container, float_elem); + doc.append_child(container, cleared); + doc.append_child(cleared, text); + doc.set_attribute(container, "style", "width: 400px;"); + doc.set_attribute( + float_elem, + "style", + "float: left; width: 100px; height: 80px;", + ); + doc.set_attribute(cleared, "style", "clear: both;"); + + let tree = layout_doc(&doc); + let body_box = &tree.root.children[0]; + let container_box = &body_box.children[0]; + + // Find the cleared element (skip the float). + let float_box = &container_box.children[0]; + let cleared_box = &container_box.children[1]; + + let float_bottom = float_box.rect.y + + float_box.rect.height + + float_box.padding.bottom + + float_box.border.bottom + + float_box.margin.bottom; + assert!( + cleared_box.rect.y >= float_bottom - 0.01, + "cleared element y ({}) should be >= float bottom ({})", + cleared_box.rect.y, + float_bottom, + ); + } + + #[test] + fn bfc_contains_floats() { + // A container with overflow:hidden (establishes BFC) should expand + // to contain its floated children. + let mut doc = Document::new(); + let root = doc.root(); + let html = doc.create_element("html"); + let body = doc.create_element("body"); + let container = doc.create_element("div"); + let float_elem = doc.create_element("div"); + doc.append_child(root, html); + doc.append_child(html, body); + doc.append_child(body, container); + doc.append_child(container, float_elem); + doc.set_attribute(container, "style", "width: 400px; overflow: hidden;"); + doc.set_attribute( + float_elem, + "style", + "float: left; width: 100px; height: 150px;", + ); + + let tree = layout_doc(&doc); + let body_box = &tree.root.children[0]; + let container_box = &body_box.children[0]; + + // Container should be at least 150px tall to contain the float. + assert!( + container_box.rect.height >= 150.0 - 0.01, + "BFC container height ({}) should be >= float height (150)", + container_box.rect.height, + ); + } + + #[test] + fn inline_text_wraps_around_float() { + // Text in a block should flow around a float. + let mut doc = Document::new(); + let root = doc.root(); + let html = doc.create_element("html"); + let body = doc.create_element("body"); + let container = doc.create_element("div"); + let float_elem = doc.create_element("div"); + let text_p = doc.create_element("p"); + let text = doc.create_text("Some text content that wraps"); + doc.append_child(root, html); + doc.append_child(html, body); + doc.append_child(body, container); + doc.append_child(container, float_elem); + doc.append_child(container, text_p); + doc.append_child(text_p, text); + doc.set_attribute(container, "style", "width: 400px;"); + doc.set_attribute( + float_elem, + "style", + "float: left; width: 100px; height: 80px;", + ); + + let tree = layout_doc(&doc); + let body_box = &tree.root.children[0]; + let container_box = &body_box.children[0]; + + // Find the text paragraph (it should be the second child, after the float). + let text_box = &container_box.children[1]; + + // The text lines that overlap with the float should be offset to + // the right of the float. Check that the first line's x is shifted. + if !text_box.lines.is_empty() { + let first_line = &text_box.lines[0]; + let float_box = &container_box.children[0]; + let float_right = float_box.rect.x + + float_box.rect.width + + float_box.padding.right + + float_box.border.right + + float_box.margin.right; + assert!( + first_line.x >= float_right - 0.01, + "first line x ({}) should be >= float right edge ({})", + first_line.x, + float_right, + ); + } + } } diff --git a/crates/style/src/computed.rs b/crates/style/src/computed.rs index cc24a28..128af27 100644 --- a/crates/style/src/computed.rs +++ b/crates/style/src/computed.rs @@ -104,6 +104,33 @@ pub enum BoxSizing { BorderBox, } +// --------------------------------------------------------------------------- +// Float +// --------------------------------------------------------------------------- + +/// CSS `float` property values. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Float { + #[default] + None, + Left, + Right, +} + +// --------------------------------------------------------------------------- +// Clear +// --------------------------------------------------------------------------- + +/// CSS `clear` property values. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Clear { + #[default] + None, + Left, + Right, + Both, +} + // --------------------------------------------------------------------------- // Overflow // --------------------------------------------------------------------------- @@ -294,6 +321,10 @@ pub struct ComputedStyle { pub left: LengthOrAuto, pub z_index: Option, + // Float + pub float: Float, + pub clear: Clear, + // Overflow pub overflow: Overflow, @@ -370,6 +401,9 @@ impl Default for ComputedStyle { left: LengthOrAuto::Auto, z_index: None, + float: Float::None, + clear: Clear::None, + overflow: Overflow::Visible, visibility: Visibility::Visible, @@ -874,6 +908,33 @@ fn apply_property( }; } + // Float + "float" => { + style.float = match value { + CssValue::Keyword(k) => match k.as_str() { + "none" => Float::None, + "left" => Float::Left, + "right" => Float::Right, + _ => style.float, + }, + _ => style.float, + }; + } + + // Clear + "clear" => { + style.clear = match value { + CssValue::Keyword(k) => match k.as_str() { + "none" => Clear::None, + "left" => Clear::Left, + "right" => Clear::Right, + "both" => Clear::Both, + _ => style.clear, + }, + _ => style.clear, + }; + } + // Overflow "overflow" => { style.overflow = match value { @@ -1089,6 +1150,8 @@ fn inherit_property(style: &mut ComputedStyle, property: &str, parent: &Computed "box-sizing" => style.box_sizing = parent.box_sizing, "background-color" => style.background_color = parent.background_color, "position" => style.position = parent.position, + "float" => style.float = parent.float, + "clear" => style.clear = parent.clear, "overflow" => style.overflow = parent.overflow, "flex-direction" => style.flex_direction = parent.flex_direction, "flex-wrap" => style.flex_wrap = parent.flex_wrap, @@ -1142,6 +1205,8 @@ fn reset_property_to_initial(style: &mut ComputedStyle, property: &str) { "right" => style.right = initial.right, "bottom" => style.bottom = initial.bottom, "left" => style.left = initial.left, + "float" => style.float = initial.float, + "clear" => style.clear = initial.clear, "overflow" => style.overflow = initial.overflow, "visibility" => style.visibility = initial.visibility, "flex-direction" => style.flex_direction = initial.flex_direction, -- 2.51.2