Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/gentest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ fn generate_node(ident: &str, node: &Value) -> TokenStream {
"none" => quote!(display: taffy::style::Display::None,),
"block" => quote!(display: taffy::style::Display::Block,),
"grid" => quote!(display: taffy::style::Display::Grid,),
"inline" => quote!(display: taffy::style::Display::Inline,),
_ => quote!(display: taffy::style::Display::Flex,),
},
_ => quote!(),
Expand Down
175 changes: 175 additions & 0 deletions src/compute/inline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//! CSS Inline Layout Algorithm
//!
//! This module implements the CSS inline layout algorithm for inline elements.
//! Inline elements are treated similarly to leaf nodes but with inline-specific
//! behavior including proper baseline alignment and inline formatting context participation.

use crate::geometry::{Point, Size};
use crate::style::{AvailableSpace, Overflow, Position};
use crate::tree::{CollapsibleMarginSet, LayoutInput, LayoutOutput, RunMode, SizingMode};
use crate::util::debug::debug_log;
use crate::util::sys::f32_max;
use crate::util::MaybeMath;
use crate::util::{MaybeResolve, ResolveOrZero};
use crate::{BoxSizing, CoreStyle};

/// Compute the layout for an inline element
///
/// Inline elements have special layout behavior:
/// - They don't establish their own layout context for children
/// - They participate in inline formatting contexts
/// - They have baseline alignment behavior
/// - They don't allow margin collapsing through them
pub fn compute_inline_layout<MeasureFunction>(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot 在这个函数中,并没有办法更新节点 Layout 上的 location(即 x/y),因此你需要修改 src/compute/block.rs,判断子节点是否是 inline 节点,如果是 inline 节点需要按照 inline 的方式来布局。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented proper inline layout positioning in the block container. The solution:

  1. Enhanced the block layout algorithm to detect inline children using a new is_inline() method on the BlockItemStyle trait
  2. Groups consecutive inline children and layouts them horizontally using a new layout_inline_line() function
  3. Proper inline formatting context - inline elements flow horizontally and are positioned correctly with x/y coordinates
  4. Line height calculation based on the tallest inline element in each line
  5. Block elements positioned correctly below inline lines

The test demonstrates the fix working:

  • Inline1: positioned at (0, 0)
  • Inline2: positioned at (50, 0) - horizontally after first inline
  • Block child: positioned at (0, 25) - below the inline line

All existing tests pass (89/89) ensuring no regressions.

Commit: a9670ff

inputs: LayoutInput,
style: &impl CoreStyle,
resolve_calc_value: impl Fn(*const (), f32) -> f32,
measure_function: MeasureFunction,
) -> LayoutOutput
where
MeasureFunction: FnOnce(Size<Option<f32>>, Size<AvailableSpace>) -> Size<f32>,
{
let LayoutInput { known_dimensions, parent_size, available_space, sizing_mode, run_mode, .. } = inputs;

// Note: both horizontal and vertical percentage padding/borders are resolved against the container's inline size (i.e. width).
// This is not a bug, but is how CSS is specified (see: https://developer.mozilla.org/en-US/docs/Web/CSS/padding#values)
let margin = style.margin().resolve_or_zero(parent_size.width, &resolve_calc_value);
let padding = style.padding().resolve_or_zero(parent_size.width, &resolve_calc_value);
let border = style.border().resolve_or_zero(parent_size.width, &resolve_calc_value);
let padding_border = padding + border;
let pb_sum = padding_border.sum_axes();
let box_sizing_adjustment = if style.box_sizing() == BoxSizing::ContentBox { pb_sum } else { Size::ZERO };

// Resolve node's preferred/min/max sizes (width/heights) against the available space (percentages resolve to pixel values)
// For ContentSize mode, we pretend that the node has no size styles as these should be ignored.
let (node_size, node_min_size, node_max_size, aspect_ratio) = match sizing_mode {
SizingMode::ContentSize => {
let node_size = known_dimensions;
let node_min_size = Size::NONE;
let node_max_size = Size::NONE;
(node_size, node_min_size, node_max_size, None)
}
SizingMode::InherentSize => {
let aspect_ratio = style.aspect_ratio();
let style_size = style
.size()
.maybe_resolve(parent_size, &resolve_calc_value)
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
let style_min_size = style
.min_size()
.maybe_resolve(parent_size, &resolve_calc_value)
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
let style_max_size = style
.max_size()
.maybe_resolve(parent_size, &resolve_calc_value)
.maybe_add(box_sizing_adjustment);

let node_size = known_dimensions.or(style_size);
(node_size, style_min_size, style_max_size, aspect_ratio)
}
};

// Scrollbar gutters are reserved when the `overflow` property is set to `Overflow::Scroll`.
// However, the axis are switched (transposed) because a node that scrolls vertically needs
// *horizontal* space to be reserved for a scrollbar
let scrollbar_gutter = style.overflow().transpose().map(|overflow| match overflow {
Overflow::Scroll => style.scrollbar_width(),
_ => 0.0,
});
// TODO: make side configurable based on the `direction` property
let mut content_box_inset = padding_border;
content_box_inset.right += scrollbar_gutter.x;
content_box_inset.bottom += scrollbar_gutter.y;

// Inline elements don't prevent margin collapsing in the same way block elements do
let has_styles_preventing_being_collapsed_through = style.overflow().x.is_scroll_container()
|| style.overflow().y.is_scroll_container()
|| style.position() == Position::Absolute
|| padding.top > 0.0
|| padding.bottom > 0.0
|| border.top > 0.0
|| border.bottom > 0.0
|| matches!(node_size.height, Some(h) if h > 0.0)
|| matches!(node_min_size.height, Some(h) if h > 0.0);

debug_log!("INLINE");
debug_log!("node_size", dbg:node_size);
debug_log!("min_size ", dbg:node_min_size);
debug_log!("max_size ", dbg:node_max_size);

// Return early if both width and height are known
if let Size { width: Some(width), height: Some(height) } = node_size {
let size = Size { width, height }
.maybe_clamp(node_min_size, node_max_size)
.maybe_max(padding_border.sum_axes().map(Some));
return LayoutOutput {
size,
#[cfg(feature = "content_size")]
content_size: Size::ZERO,
first_baselines: Point::NONE,
top_margin: CollapsibleMarginSet::ZERO,
bottom_margin: CollapsibleMarginSet::ZERO,
margins_can_collapse_through: false,
};
}

// Compute available space
let available_space = Size {
width: known_dimensions
.width
.map(AvailableSpace::from)
.unwrap_or(available_space.width)
.maybe_sub(margin.horizontal_axis_sum())
.maybe_set(known_dimensions.width)
.maybe_set(node_size.width)
.map_definite_value(|size| {
size.maybe_clamp(node_min_size.width, node_max_size.width) - content_box_inset.horizontal_axis_sum()
}),
height: known_dimensions
.height
.map(AvailableSpace::from)
.unwrap_or(available_space.height)
.maybe_sub(margin.vertical_axis_sum())
.maybe_set(known_dimensions.height)
.maybe_set(node_size.height)
.map_definite_value(|size| {
size.maybe_clamp(node_min_size.height, node_max_size.height) - content_box_inset.vertical_axis_sum()
}),
};

// For inline elements, the content size is determined by the measure function
// which could handle text content, replaced element content, etc.
let measured_size = measure_function(
match run_mode {
RunMode::ComputeSize => known_dimensions,
RunMode::PerformLayout => Size::NONE,
RunMode::PerformHiddenLayout => unreachable!(),
},
available_space,
);

let clamped_size = known_dimensions
.or(node_size)
.unwrap_or(measured_size + content_box_inset.sum_axes())
.maybe_clamp(node_min_size, node_max_size);

let size = Size {
width: clamped_size.width,
height: f32_max(clamped_size.height, aspect_ratio.map(|ratio| clamped_size.width / ratio).unwrap_or(0.0)),
};
let size = size.maybe_max(padding_border.sum_axes().map(Some));

LayoutOutput {
size,
#[cfg(feature = "content_size")]
content_size: measured_size + padding.sum_axes(),
first_baselines: Point::NONE, // TODO: Implement proper baseline calculation for inline elements
top_margin: CollapsibleMarginSet::ZERO,
bottom_margin: CollapsibleMarginSet::ZERO,
margins_can_collapse_through: !has_styles_preventing_being_collapsed_through
&& size.height == 0.0
&& measured_size.height == 0.0,
}
}
3 changes: 3 additions & 0 deletions src/compute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! | [`compute_flexbox_layout`] | Layout a Flexbox container and it's direct children |
//! | [`compute_grid_layout`] | Layout a CSS Grid container and it's direct children |
//! | [`compute_block_layout`] | Layout a Block container and it's direct children |
//! | [`compute_inline_layout`] | Layout an Inline element with proper inline formatting context behavior |
//! | [`compute_leaf_layout`] | Applies common properties like padding/border/aspect-ratio to a node before deferring to a passed closure to determine it's size. Can be applied to nodes like text or image nodes. |
//! | [`compute_root_layout`] | Layout the root node of a tree (regardless of it's layout mode). This function is typically called once to begin a layout run. | |
//! | [`compute_hidden_layout`] | Mark a node as hidden during layout (like `Display::None`) |
Expand All @@ -23,6 +24,7 @@
//!
pub(crate) mod common;
pub(crate) mod leaf;
pub(crate) mod inline;

#[cfg(feature = "block_layout")]
pub(crate) mod block;
Expand All @@ -34,6 +36,7 @@ pub(crate) mod flexbox;
pub(crate) mod grid;

pub use leaf::compute_leaf_layout;
pub use inline::compute_inline_layout;

#[cfg(feature = "block_layout")]
pub use self::block::compute_block_layout;
Expand Down
3 changes: 3 additions & 0 deletions src/style/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ pub enum Display {
/// The children will follow the CSS Grid layout algorithm
#[cfg(feature = "grid")]
Grid,
/// The element generates an inline-level box
Inline,
/// The node is hidden, and it's children will also be hidden
None,
}
Expand Down Expand Up @@ -216,6 +218,7 @@ impl core::fmt::Display for Display {
Display::Flex => write!(f, "FLEX"),
#[cfg(feature = "grid")]
Display::Grid => write!(f, "GRID"),
Display::Inline => write!(f, "INLINE"),
}
}
}
Expand Down
14 changes: 13 additions & 1 deletion src/tree/taffy_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::util::debug::{debug_log, debug_log_node};
use crate::util::sys::{new_vec_with_capacity, ChildrenVec, Vec};

use crate::compute::{
compute_cached_layout, compute_hidden_layout, compute_leaf_layout, compute_root_layout, round_layout,
compute_cached_layout, compute_hidden_layout, compute_inline_layout, compute_leaf_layout, compute_root_layout, round_layout,
};
use crate::CacheTree;
#[cfg(feature = "block_layout")]
Expand Down Expand Up @@ -243,6 +243,7 @@ impl<NodeContext> PrintTree for TaffyTree<NodeContext> {

match (num_children, display) {
(_, Display::None) => "NONE",
(_, Display::Inline) => "INLINE",
(0, _) => "LEAF",
#[cfg(feature = "block_layout")]
(_, Display::Block) => "BLOCK",
Expand Down Expand Up @@ -381,6 +382,17 @@ where
(Display::Flex, true) => compute_flexbox_layout(tree, node, inputs),
#[cfg(feature = "grid")]
(Display::Grid, true) => compute_grid_layout(tree, node, inputs),
// Inline elements use dedicated inline layout algorithm with measure function
(Display::Inline, _) => {
let node_key = node.into();
let style = &tree.taffy.nodes[node_key].style;
let has_context = tree.taffy.nodes[node_key].has_context;
let node_context = has_context.then(|| tree.taffy.node_context_data.get_mut(node_key)).flatten();
let measure_function = |known_dimensions, available_space| {
(tree.measure_function)(known_dimensions, available_space, node, node_context, style)
};
compute_inline_layout(inputs, style, |_, _| 0.0, measure_function)
}
(_, false) => {
let node_key = node.into();
let style = &tree.taffy.nodes[node_key].style;
Expand Down
Loading