diff --git a/crates/blitz-dom/src/layout/construct.rs b/crates/blitz-dom/src/layout/construct.rs index a1b07ff..fb6a1de 100644 --- a/crates/blitz-dom/src/layout/construct.rs +++ b/crates/blitz-dom/src/layout/construct.rs @@ -1159,7 +1159,12 @@ pub(crate) fn build_inline_layout_into( let float = style.map(|s| s.clone_float()).unwrap_or(Float::None); let box_kind = if position.is_absolutely_positioned() { InlineBoxKind::OutOfFlow - } else if float.is_floating() { + } else if cfg!(feature = "floats") && float.is_floating() { + // Custom boxes yield until the caller places them. Only the + // float-aware line breaker places them; otherwise Parley + // appends them with zero size, so the float would overlap the + // text after it. Without floats, lay these boxes out in flow + // like other inline boxes. InlineBoxKind::CustomOutOfFlow } else { InlineBoxKind::InFlow diff --git a/crates/blitz-dom/src/layout/inline.rs b/crates/blitz-dom/src/layout/inline.rs index 1f98570..74c93cb 100644 --- a/crates/blitz-dom/src/layout/inline.rs +++ b/crates/blitz-dom/src/layout/inline.rs @@ -16,7 +16,7 @@ use taffy::{ #[cfg(feature = "floats")] use parley::YieldData; #[cfg(feature = "floats")] -use taffy::{BlockItemStyle as _, Clear, Float, prelude::TaffyMaxContent}; +use taffy::{BlockItemStyle as _, Clear, Float}; use stylo_taffy::StyleFlags; @@ -391,16 +391,24 @@ impl LayoutPassState<'_> { }; #[cfg(feature = "floats")] let float_child_inputs = taffy::tree::LayoutInput { - available_space: Size::MAX_CONTENT, + // Auto-width floats shrink to the available inline width. Asking + // for max-content here lets long text force a newsletter wider + // than its containing cell, even though that text could wrap. + available_space: Size { + width: available_space.width, + height: AvailableSpace::MaxContent, + }, ..child_inputs }; // Update inline boxes for ibox in inline_layout.layout.inline_boxes_mut() { let style = self.child_layout_style(&self.nodes[NodeId::from_u64(ibox.id)]); + // Margin and padding percentages resolve against the containing block's + // width on every side (CSS 2.1 §8.3, §8.4). let margin = style .margin() - .resolve_or_zero(inputs.parent_size, resolve_calc_value); + .resolve_or_zero(child_inputs.parent_size.width, resolve_calc_value); #[cfg(feature = "floats")] let is_floated = style.float().is_floated(); @@ -509,7 +517,6 @@ impl LayoutPassState<'_> { #[cfg(feature = "floats")] let float_width = match available_space.width { - AvailableSpace::Definite(_) => 0.0, AvailableSpace::MinContent => { let mut width: f32 = 0.0; for ibox in inline_layout.layout.inline_boxes_mut() { @@ -518,9 +525,10 @@ impl LayoutPassState<'_> { self.child_layout_style(&self.nodes[NodeId::from_u64(ibox.id)]); ( style.float().is_floated(), - style - .margin() - .resolve_or_zero(inputs.parent_size, resolve_calc_value), + style.margin().resolve_or_zero( + child_inputs.parent_size.width, + resolve_calc_value, + ), ) }; @@ -535,7 +543,10 @@ impl LayoutPassState<'_> { width * scale } - AvailableSpace::MaxContent => { + // A definite available width only occurs here when shrink-to-fit sizing, + // which needs the max-content contribution of the floats (backported from + // DioxusLabs/blitz#980). + AvailableSpace::MaxContent | AvailableSpace::Definite(_) => { // When computing a max-content size the available width is effectively // infinite, so floats never wrap onto a new "band" due to a lack of // horizontal space. They only move below preceding floats when the `clear` @@ -554,9 +565,10 @@ impl LayoutPassState<'_> { ( style.float(), style.clear(), - style - .margin() - .resolve_or_zero(inputs.parent_size, resolve_calc_value), + style.margin().resolve_or_zero( + child_inputs.parent_size.width, + resolve_calc_value, + ), ) }; @@ -671,6 +683,12 @@ impl LayoutPassState<'_> { // Out-of-flow candidates bubbled up from this container and its in-flow subtree. // These are laid out by the out-of-flow positioning pass (`compute_oof_layout`). let mut oof_candidates = OofCandidates::new(); + // Floats placed in this formatting context, and out-of-flow candidates bubbled + // up from them. They move with the content when `align-content` shifts it. + #[cfg(feature = "floats")] + let mut placed_floats = Vec::new(); + #[cfg(feature = "floats")] + let mut float_oof_candidates = OofCandidates::new(); // Perform inline layout #[cfg(feature = "floats")] @@ -680,7 +698,13 @@ impl LayoutPassState<'_> { let mut has_active_floats = initial_slot.segment_id.is_some(); let state = breaker.state_mut(); state.set_layout_max_advance(width); - state.set_line_max_advance(initial_slot.width * scale); + // Lines have no height limit. An infinite one, unlike Parley's `f32::MAX` + // default, can never be exceeded, even by an infinitely tall box. + state.set_line_max_height(f32::INFINITY); + // With huge insets, f32 rounding can make a float slot slightly wider than + // the layout, which Parley's line breaker rejects (DioxusLabs/blitz#1443). + let layout_max_advance = state.layout_max_advance(); + state.set_line_max_advance((initial_slot.width * scale).min(layout_max_advance)); state.set_line_x(initial_slot.x * scale); state.set_line_y((initial_slot.y * scale) as f64); @@ -705,7 +729,9 @@ impl LayoutPassState<'_> { block_ctx.find_content_slot(min_y as f32, Clear::None, None); has_active_floats = next_slot.segment_id.is_some(); - state.set_line_max_advance(next_slot.width * scale); + state.set_line_max_advance( + (next_slot.width * scale).min(layout_max_advance), + ); state.set_line_x(next_slot.x * scale); state.set_line_y((next_slot.y * scale) as f64); } else { @@ -715,15 +741,18 @@ impl LayoutPassState<'_> { continue; } - YieldData::MaxHeightExceeded(_data) => { - // TODO + YieldData::MaxHeightExceeded(_) => { + // Unreachable with an infinite height limit. Should it happen, + // breaking the line again without a limit still makes progress. + debug_assert!(false, "line breaker yielded MaxHeightExceeded"); + breaker.state_mut().set_line_max_height(f32::INFINITY); continue; } YieldData::InlineBoxBreak(box_break_data) => { let state = breaker.state_mut(); let node_id = NodeId::from_u64(box_break_data.inline_box_id); - let (direction, clear, margin) = { + let (direction, clear, margin, width) = { let style = self.child_layout_style(&self.nodes[node_id]); // We can assume that the box is a float because we only set `break_on_box: true` for floats let direction = match style.float() { @@ -734,17 +763,21 @@ impl LayoutPassState<'_> { ( direction, style.clear(), - style - .margin() - .resolve_or_zero(inputs.parent_size, resolve_calc_value), + style.margin().resolve_or_zero( + child_inputs.parent_size.width, + resolve_calc_value, + ), + style.size().width, ) }; let margin_sum = margin.sum_axes(); + // The float's margin box shrinks to the available width, so its + // border box gets that width less its margins (CSS 2.1 §10.3.5). let mut output = self.compute_child_layout( crate::taffy_node_id(node_id), - float_child_inputs, + inline_box_inputs(width, margin, float_child_inputs), ); let min_y = state.line_y() as f32 / scale; @@ -762,7 +795,9 @@ impl LayoutPassState<'_> { block_ctx.find_content_slot(min_y as f32, Clear::None, None); has_active_floats = next_slot.segment_id.is_some(); - state.set_line_max_advance(next_slot.width * scale); + state.set_line_max_advance( + (next_slot.width * scale).min(layout_max_advance), + ); state.set_line_x(next_slot.x * scale); state.set_line_y((next_slot.y * scale) as f64); @@ -777,9 +812,10 @@ impl LayoutPassState<'_> { // Translate anchors from item-relative to container-relative // coordinates and collect candidates bubbled from the float's subtree + placed_floats.push(node_id); if !output.oof_candidates.is_empty() { output.oof_candidates.translate(location); - oof_candidates.append(&mut output.oof_candidates); + float_oof_candidates.append(&mut output.oof_candidates); } // dbg!(&layout.size); @@ -902,9 +938,10 @@ impl LayoutPassState<'_> { let container_direction = self.nodes[node_id].layout_style().direction(); - // `align-content` aligns the line boxes (as a single unit) within the content box in - // the block axis. Floats are positioned relative to the formatting context and are not - // moved. The offset is stored on the text layout for painting and hit-testing. + // `align-content` aligns the entire contents (as a single unit) within the content box + // in the block axis (https://drafts.csswg.org/css-align-3/#distribution-block). This + // includes table cells' `vertical-align`. The offset is stored on the text layout for + // painting and hit-testing. let align_content = BlockContainerStyle::align_content(&self.nodes[node_id].layout_style()); let block_offset = if align_content.keyword() == taffy::AlignContentKeyword::Normal { 0.0 @@ -916,6 +953,21 @@ impl LayoutPassState<'_> { inline_layout.block_offset = block_offset; let line_box_top = container_pb.top + block_offset; + // Floats belong to the contents when this box roots their formatting context. + #[cfg(feature = "floats")] + { + if is_bfc_root && block_offset != 0.0 { + for &float_id in &placed_floats { + self.nodes[float_id].unrounded_layout_mut().location.y += block_offset; + } + float_oof_candidates.translate(taffy::Point { + x: 0.0, + y: block_offset, + }); + } + oof_candidates.append(&mut float_oof_candidates); + } + // Store sizes and positions of inline boxes let mut ibox_order: u32 = 0; let mut span_cb_flags = StyleFlags::empty(); @@ -932,13 +984,13 @@ impl LayoutPassState<'_> { let style = self.child_layout_style(node); let padding = style .padding() - .resolve_or_zero(child_inputs.parent_size, resolve_calc_value); + .resolve_or_zero(child_inputs.parent_size.width, resolve_calc_value); let border = style .border() - .resolve_or_zero(child_inputs.parent_size, resolve_calc_value); + .resolve_or_zero(child_inputs.parent_size.width, resolve_calc_value); let margin = style .margin() - .resolve_or_zero(child_inputs.parent_size, resolve_calc_value); + .resolve_or_zero(child_inputs.parent_size.width, resolve_calc_value); #[cfg(feature = "floats")] let is_floated = style.float() != Float::None;