Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix sizing of flexbox nodes under a min-content constraint #291

Merged
merged 9 commits into from
Dec 24, 2022
54 changes: 54 additions & 0 deletions benches/generated/grid_min_content_flex_column.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions benches/generated/grid_min_content_flex_row.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions benches/generated/mod.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 48 additions & 49 deletions src/compute/flexbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::geometry::{Point, Rect, Size};
use crate::layout::{Layout, RunMode, SizingMode};
use crate::math::MaybeMath;
use crate::node::Node;
use crate::prelude::TaffyMaxContent;
use crate::prelude::{TaffyMaxContent, TaffyMinContent};
use crate::resolve::{MaybeResolve, ResolveOrZero};
use crate::style::{
AlignContent, AlignItems, AlignSelf, AvailableSpace, Dimension, Display, FlexWrap, JustifyContent,
Expand All @@ -36,6 +36,10 @@ struct FlexItem {
/// The cross-alignment of this item
align_self: AlignSelf,

/// The minimum size of the item. This differs from min_size above because it also
/// takes into account content based automatic minimum sizes
resolved_minimum_size: Size<f32>,

/// The final offset of this item
position: Rect<Option<f32>>,
/// The margin of this item
Expand Down Expand Up @@ -202,6 +206,15 @@ fn compute_preliminary(
NODE_LOGGER.log("determine_flex_base_size");
determine_flex_base_size(tree, known_dimensions, &constants, available_space, &mut flex_items);

#[cfg(feature = "debug")]
for item in flex_items.iter() {
NODE_LOGGER.labelled_log("item.flex_basis", item.flex_basis);
NODE_LOGGER.labelled_log("item.inner_flex_basis", item.inner_flex_basis);
NODE_LOGGER.labelled_debug_log("item.hypothetical_outer_size", item.hypothetical_outer_size);
NODE_LOGGER.labelled_debug_log("item.hypothetical_inner_size", item.hypothetical_inner_size);
NODE_LOGGER.labelled_debug_log("item.resolved_minimum_size", item.resolved_minimum_size);
}

// TODO: Add step 4 according to spec: https://www.w3.org/TR/css-flexbox-1/#algo-main-container
// 9.3. Main Size Determination

Expand Down Expand Up @@ -431,6 +444,7 @@ fn generate_anonymous_flex_items(tree: &impl LayoutTree, node: Node, constants:
violation: 0.0,
frozen: false,

resolved_minimum_size: Size::zero(),
hypothetical_inner_size: Size::zero(),
hypothetical_outer_size: Size::zero(),
target_size: Size::zero(),
Expand Down Expand Up @@ -565,27 +579,22 @@ fn determine_flex_base_size(

let child_known_dimensions = {
let mut ckd = child.size;
if child.align_self == AlignSelf::Stretch {
if constants.is_column && ckd.width.is_none() {
ckd.width = available_space.width.into_option();
}
if constants.is_row && ckd.height.is_none() {
ckd.height = available_space.height.into_option();
}
if child.align_self == AlignSelf::Stretch && ckd.cross(constants.dir).is_none() {
ckd.set_cross(constants.dir, available_space.cross(constants.dir).into_option());
}
ckd
};

child.flex_basis = compute_node_layout(
tree,
child.node,
child_known_dimensions.maybe_min(child.max_size),
child_known_dimensions,
available_space,
RunMode::ComputeSize,
SizingMode::ContentSize,
)
.main(constants.dir)
.maybe_min(child.max_size.main(constants.dir));
.main(constants.dir);
// .maybe_min(child.max_size.main(constants.dir))
}

// The hypothetical main size is the item’s flex base size clamped according to its
Expand All @@ -595,26 +604,36 @@ fn determine_flex_base_size(
child.inner_flex_basis =
child.flex_basis - child.padding.main_axis_sum(constants.dir) - child.border.main_axis_sum(constants.dir);

// TODO - not really spec abiding but needs to be done somewhere. probably somewhere else though.
// The following logic was developed not from the spec but by trail and error looking into how
// webkit handled various scenarios. Can probably be solved better by passing in
// min-content max-content constraints from the top
let min_main = compute_node_layout(
let child_known_dimensions = {
let mut ckd = Size::NONE;
if child.align_self == AlignSelf::Stretch && ckd.cross(constants.dir).is_none() {
ckd.set_cross(constants.dir, available_space.cross(constants.dir).into_option());
}
ckd
};

let min_content_size = compute_node_layout(
tree,
child.node,
Size::NONE,
available_space,
child_known_dimensions, // Should possibly also be Size::NONE
Size::MIN_CONTENT,
RunMode::ComputeSize,
SizingMode::ContentSize,
)
.main(constants.dir)
.maybe_clamp(child.min_size.main(constants.dir), child.size.main(constants.dir))
.into();
);

child
.hypothetical_inner_size
.set_main(constants.dir, child.flex_basis.maybe_clamp(min_main, child.max_size.main(constants.dir)));
// 4.5. Automatic Minimum Size of Flex Items
// https://www.w3.org/TR/css-flexbox-1/#min-size-auto
let specified = child.size.maybe_min(child.max_size);
child.resolved_minimum_size = child.min_size.unwrap_or(min_content_size.maybe_min(specified));

let hypothetical_inner_min_main = min_content_size
.main(constants.dir)
.maybe_clamp(child.resolved_minimum_size.main(constants.dir).into(), child.size.main(constants.dir))
.into();
child.hypothetical_inner_size.set_main(
constants.dir,
child.flex_basis.maybe_clamp(hypothetical_inner_min_main, child.max_size.main(constants.dir)),
);
child.hypothetical_outer_size.set_main(
constants.dir,
child.hypothetical_inner_size.main(constants.dir) + child.margin.main_axis_sum(constants.dir),
Expand Down Expand Up @@ -770,7 +789,7 @@ fn resolve_flexible_lengths(
.iter()
.map(|child| {
child.margin.main_axis_sum(constants.dir)
+ if child.frozen { child.target_size.main(constants.dir) } else { child.flex_basis }
+ if child.frozen { child.outer_target_size.main(constants.dir) } else { child.flex_basis }
})
.sum::<f32>();

Expand Down Expand Up @@ -798,7 +817,7 @@ fn resolve_flexible_lengths(
.iter()
.map(|child| {
child.margin.main_axis_sum(constants.dir)
+ if child.frozen { child.target_size.main(constants.dir) } else { child.flex_basis }
+ if child.frozen { child.outer_target_size.main(constants.dir) } else { child.flex_basis }
})
.sum::<f32>();

Expand Down Expand Up @@ -870,29 +889,9 @@ fn resolve_flexible_lengths(
// If the item’s target main size was made larger by this, it’s a min violation.

let total_violation = unfrozen.iter_mut().fold(0.0, |acc, child| -> f32 {
// TODO - not really spec abiding but needs to be done somewhere. probably somewhere else though.
// The following logic was developed not from the spec but by trial and error looking into how
// webkit handled various scenarios. Can probably be solved better by passing in
// min-content max-content constraints from the top. Need to figure out correct thing to do here as
// just piling on more conditionals.
let min_main = if constants.is_row && !tree.needs_measure(child.node) {
compute_node_layout(
tree,
child.node,
Size::NONE,
available_space,
RunMode::ComputeSize,
SizingMode::ContentSize,
)
.width
.maybe_clamp(child.min_size.width, child.size.width)
.into()
} else {
child.min_size.main(constants.dir)
};

let resolved_min_main: Option<f32> = child.resolved_minimum_size.main(constants.dir).into();
let max_main = child.max_size.main(constants.dir);
let clamped = child.target_size.main(constants.dir).maybe_clamp(min_main, max_main).max(0.0);
let clamped = child.target_size.main(constants.dir).maybe_clamp(resolved_min_main, max_main).max(0.0);
child.violation = clamped - child.target_size.main(constants.dir);
child.target_size.set_main(constants.dir, clamped);
child.outer_target_size.set_main(
Expand Down
17 changes: 15 additions & 2 deletions src/compute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub(crate) mod leaf;
#[cfg(feature = "experimental_grid")]
pub(crate) mod grid;

use crate::data::CACHE_SIZE;
use crate::error::TaffyError;
use crate::geometry::{Point, Size};
use crate::layout::{Cache, Layout, RunMode, SizingMode};
Expand Down Expand Up @@ -129,8 +130,20 @@ fn compute_node_layout(
}
};

// Compute cache slot
let has_known_width = known_dimensions.width.is_some();
let has_known_height = known_dimensions.height.is_some();
let cache_slot = if has_known_width && has_known_height {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The logic here is hard to follow, and seems likely to result in surprising bugs if altered unsuspectingly. Is there a clearer or more robust approach we can use? It feels like a match statement may help as a stopgap change.

Failing that, more comments.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think the logic itself is good (for the time being at least), but I agree that it could do with some explaining / documenting. I remember trying to understand the caching implementation when I first started working on Taffy, and it made no sense to me at all.

0
} else if has_known_width {
1 + (available_space.height == AvailableSpace::MinContent) as usize
} else if has_known_height {
1 + (available_space.width == AvailableSpace::MinContent) as usize
} else {
3 + (available_space.width == AvailableSpace::MinContent) as usize
};

// Cache result
let cache_slot = (known_dimensions.width.is_some() as usize) + (known_dimensions.height.is_some() as usize * 2);
*tree.cache_mut(node, cache_slot) =
Some(Cache { known_dimensions, available_space, run_mode: cache_run_mode, cached_size: computed_size });

Expand All @@ -152,7 +165,7 @@ fn compute_from_cache(
run_mode: RunMode,
sizing_mode: SizingMode,
) -> Option<Size<f32>> {
for idx in 0..4 {
for idx in 0..CACHE_SIZE {
let entry = tree.cache_mut(node, idx);
#[cfg(feature = "debug")]
NODE_LOGGER.labelled_debug_log("cache_entry", &entry);
Expand Down
9 changes: 6 additions & 3 deletions src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
use crate::layout::{Cache, Layout};
use crate::style::Style;

/// The number of cache entries for each node in the tree
pub(crate) const CACHE_SIZE: usize = 5;

/// Layout information for a given [`Node`](crate::node::Node)
///
/// Stored in a [`Taffy`].
Expand All @@ -18,21 +21,21 @@ pub(crate) struct NodeData {
pub(crate) needs_measure: bool,

/// The primary cached results of the layout computation
pub(crate) size_cache: [Option<Cache>; 4],
pub(crate) size_cache: [Option<Cache>; CACHE_SIZE],
}

impl NodeData {
/// Create the data for a new node
#[must_use]
pub const fn new(style: Style) -> Self {
Self { style, size_cache: [None; 4], layout: Layout::new(), needs_measure: false }
Self { style, size_cache: [None; CACHE_SIZE], layout: Layout::new(), needs_measure: false }
}

/// Marks a node and all of its parents (recursively) as dirty
///
/// This clears any cached data and signals that the data must be recomputed.
#[inline]
pub fn mark_dirty(&mut self) {
self.size_cache = [None; 4];
self.size_cache = [None; CACHE_SIZE];
}
}
Loading