Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
21 changes: 21 additions & 0 deletions benches/src/yoga_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ fn items_into_align(align: Option<tf::AlignSelf>) -> yg::Align {
Some(tf::AlignSelf::Stretch) => yg::Align::Stretch,
Some(tf::AlignSelf::Start) => unimplemented!(),
Some(tf::AlignSelf::End) => unimplemented!(),
Some(
tf::AlignSelf::SafeStart
| tf::AlignSelf::SafeEnd
| tf::AlignSelf::SafeFlexStart
| tf::AlignSelf::SafeFlexEnd
| tf::AlignSelf::SafeCenter,
) => unimplemented!(),
}
}

Expand All @@ -167,6 +174,13 @@ fn content_into_align(align: Option<tf::AlignContent>) -> yg::Align {
Some(tf::AlignContent::SpaceBetween) => yg::Align::SpaceBetween,
Some(tf::AlignContent::SpaceAround) => yg::Align::SpaceAround,
Some(tf::AlignContent::SpaceEvenly) => unimplemented!(),
Some(
tf::AlignContent::SafeStart
| tf::AlignContent::SafeEnd
| tf::AlignContent::SafeFlexStart
| tf::AlignContent::SafeFlexEnd
| tf::AlignContent::SafeCenter,
) => unimplemented!(),
}
}

Expand All @@ -182,6 +196,13 @@ fn content_into_justify(align: Option<tf::JustifyContent>) -> yg::Justify {
Some(tf::JustifyContent::SpaceAround) => yg::Justify::SpaceAround,
Some(tf::JustifyContent::Stretch) => unimplemented!(),
Some(tf::JustifyContent::SpaceEvenly) => unimplemented!(),
Some(
tf::JustifyContent::SafeStart
| tf::JustifyContent::SafeEnd
| tf::JustifyContent::SafeFlexStart
| tf::JustifyContent::SafeFlexEnd
| tf::JustifyContent::SafeCenter,
) => unimplemented!(),
}
}

Expand Down
23 changes: 23 additions & 0 deletions docs/style-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,26 @@ N = Supported in spec but not implemented in Taffy
| `grid_row` | - | Y | `Line<GridPlacement>` | 8 | - | The vertical (row) placement of a grid item |
| `grid_column` | - | Y | `Line<GridPlacement>` | 8 | - | The horizontal (row) placement of a grid item |
| `grid_area` | - | 5 | - | - | - | Accepts either shorthand row/column-start/end or a named grid area |

## Safe and unsafe overflow alignment

The `AlignItems` and `AlignContent` enums (and their `AlignSelf`, `JustifyItems`, `JustifySelf`,
and `JustifyContent` aliases) carry a set of `Safe*` variants that pair the spec's
[overflow-position keyword](https://www.w3.org/TR/css-align-3/#overflow-values) `safe` with the
underlying position keyword. The variants are:

- `SafeStart`, `SafeEnd`, `SafeFlexStart`, `SafeFlexEnd`, `SafeCenter`

When the alignment subject would overflow its alignment container, a `Safe*` value falls back
to logical `Start` so the start edge of the content stays visible. When the subject fits, the
`Safe*` variant behaves identically to its underlying position keyword. The plain (non-`Safe*`)
variants are equivalent to the spec's `unsafe` keyword and keep their requested position even
when that causes overflow at the start edge.

The CSS modifier is only valid against the position keywords listed above. Combinations such
as `safe stretch`, `safe baseline`, and `safe space-between` are not representable and are
rejected by the parser when the `parse` feature is enabled.

The `safe`/`unsafe` overflow-position keyword is supported by both the Flexbox and CSS Grid
layout algorithms for both content-level (`align-content`, `justify-content`) and self-level
(`align-self`, `justify-self`) alignment.
30 changes: 19 additions & 11 deletions src/compute/common/alignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ pub(crate) fn apply_alignment_fallback(
free_space: f32,
num_items: usize,
mut alignment_mode: AlignContent,
mut is_safe: bool,
) -> AlignContent {
// Pick up the overflow-position modifier from the alignment style itself, then continue
// with the underlying position keyword.
let mut is_safe = alignment_mode.is_safe();
if is_safe {
alignment_mode = alignment_mode.position();
}

// Fallback occurs in two cases:

// 1. If there is only a single item being aligned and alignment is a distributed alignment keyword
Expand Down Expand Up @@ -47,25 +53,27 @@ pub(crate) fn compute_alignment_offset(
layout_is_flex_reversed: bool,
is_first: bool,
) -> f32 {
// Safe* variants share the offset math of their underlying position keyword. The
// overflow-aware fallback to Start is applied upstream by `apply_alignment_fallback`.
if is_first {
match alignment_mode {
AlignContent::Start => 0.0,
AlignContent::FlexStart => {
AlignContent::Start | AlignContent::SafeStart => 0.0,
AlignContent::FlexStart | AlignContent::SafeFlexStart => {
if layout_is_flex_reversed {
free_space
} else {
0.0
}
}
AlignContent::End => free_space,
AlignContent::FlexEnd => {
AlignContent::End | AlignContent::SafeEnd => free_space,
AlignContent::FlexEnd | AlignContent::SafeFlexEnd => {
if layout_is_flex_reversed {
0.0
} else {
free_space
}
}
AlignContent::Center => free_space / 2.0,
AlignContent::Center | AlignContent::SafeCenter => free_space / 2.0,
AlignContent::Stretch => 0.0,
AlignContent::SpaceBetween => 0.0,
AlignContent::SpaceAround => {
Expand All @@ -86,11 +94,11 @@ pub(crate) fn compute_alignment_offset(
} else {
let free_space = free_space.max(0.0);
gap + match alignment_mode {
AlignContent::Start => 0.0,
AlignContent::FlexStart => 0.0,
AlignContent::End => 0.0,
AlignContent::FlexEnd => 0.0,
AlignContent::Center => 0.0,
AlignContent::Start | AlignContent::SafeStart => 0.0,
AlignContent::FlexStart | AlignContent::SafeFlexStart => 0.0,
AlignContent::End | AlignContent::SafeEnd => 0.0,
AlignContent::FlexEnd | AlignContent::SafeFlexEnd => 0.0,
AlignContent::Center | AlignContent::SafeCenter => 0.0,
AlignContent::Stretch => 0.0,
AlignContent::SpaceBetween => free_space / (num_items - 1) as f32,
AlignContent::SpaceAround => free_space / num_items as f32,
Expand Down
57 changes: 50 additions & 7 deletions src/compute/flexbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1698,9 +1698,8 @@ fn distribute_remaining_free_space(flex_lines: &mut [FlexLine], constants: &Algo
let num_items = line.items.len();
let layout_reverse = constants.dir.is_reverse();
let gap = constants.gap.main(constants.dir);
let is_safe = false; // TODO: Implement safe alignment
let raw_justify_content_mode = constants.justify_content.unwrap_or(JustifyContent::FlexStart);
let justify_content_mode = apply_alignment_fallback(free_space, num_items, raw_justify_content_mode, is_safe);
let justify_content_mode = apply_alignment_fallback(free_space, num_items, raw_justify_content_mode);

let justify_item = |(i, child): (usize, &mut FlexItem)| {
child.offset_main =
Expand Down Expand Up @@ -1779,7 +1778,17 @@ fn align_flex_items_along_cross_axis(
) -> f32 {
let cross_axis_should_reverse = constants.is_column && matches!(constants.layout_direction, Direction::Rtl);

match child.align_self {
// If align-self uses a "safe" overflow-position keyword and the item would overflow its
// line cross size, fall back to logical Start to avoid data loss. See CSS Box Alignment 3
// §4.3 <https://www.w3.org/TR/css-align-3/#overflow-values>. Otherwise, strip the Safe*
// modifier so the regular match below sees only the position keyword.
let align_self = if child.align_self.is_safe() && free_space < 0.0 {
AlignSelf::Start
} else {
child.align_self.position()
};

match align_self {
AlignSelf::Start => {
if cross_axis_should_reverse {
free_space
Expand Down Expand Up @@ -1830,6 +1839,14 @@ fn align_flex_items_along_cross_axis(
0.0
}
}
AlignSelf::SafeStart
| AlignSelf::SafeEnd
| AlignSelf::SafeFlexStart
| AlignSelf::SafeFlexEnd
| AlignSelf::SafeCenter => {
// Unreachable: Safe* variants are stripped above into the underlying position keyword.
unreachable!()
}
}
}

Expand Down Expand Up @@ -1880,9 +1897,8 @@ fn align_flex_lines_per_align_content(flex_lines: &mut [FlexLine], constants: &A
let gap = constants.gap.cross(constants.dir);
let total_cross_axis_gap = sum_axis_gaps(gap, num_lines);
let free_space = constants.inner_container_size.cross(constants.dir) - total_cross_size - total_cross_axis_gap;
let is_safe = false; // TODO: Implement safe alignment

let align_content_mode = apply_alignment_fallback(free_space, num_lines, constants.align_content, is_safe);
let align_content_mode = apply_alignment_fallback(free_space, num_lines, constants.align_content);

let align_line = |(i, line): (usize, &mut FlexLine)| {
line.offset_cross =
Expand Down Expand Up @@ -2326,7 +2342,10 @@ fn perform_absolute_layout_on_absolute_children(
} else {
// Stretch is an invalid value for justify_content in the flexbox algorithm, so we
// treat it as if it wasn't set (and thus we default to FlexStart behaviour)
match (constants.justify_content.unwrap_or(JustifyContent::Start), main_axis_flex_start_reversed) {
// TODO (safe alignment): apply Start fallback when justify_content.is_safe() and the
// resolved size overflows the containing block. Wired in a follow-up commit.
match (constants.justify_content.unwrap_or(JustifyContent::Start).position(), main_axis_flex_start_reversed)
{
(JustifyContent::SpaceBetween, _)
| (JustifyContent::Stretch, false)
| (JustifyContent::FlexStart, false)
Expand Down Expand Up @@ -2368,6 +2387,17 @@ fn perform_absolute_layout_on_absolute_children(
- resolved_margin.main_end(constants.dir))
/ 2.0
}
(
JustifyContent::SafeStart
| JustifyContent::SafeEnd
| JustifyContent::SafeFlexStart
| JustifyContent::SafeFlexEnd
| JustifyContent::SafeCenter,
_,
) => {
// Unreachable: `position()` strips Safe* into the underlying position keyword.
unreachable!()
}
}
};

Expand Down Expand Up @@ -2395,7 +2425,9 @@ fn perform_absolute_layout_on_absolute_children(
- resolved_margin.cross_end(constants.dir)
}
} else {
match (align_self, cross_axis_flex_start_reversed) {
// TODO (safe alignment): apply Start fallback when align_self.is_safe() and the
// resolved size overflows the containing block. Wired in a follow-up commit.
match (align_self.position(), cross_axis_flex_start_reversed) {
// Stretch alignment does not apply to absolutely positioned items
// See "Example 3" at https://www.w3.org/TR/css-flexbox-1/#abspos-items
// Note: Stretch should be FlexStart not Start when we support both
Expand Down Expand Up @@ -2437,6 +2469,17 @@ fn perform_absolute_layout_on_absolute_children(
- resolved_margin.cross_end(constants.dir))
/ 2.0
}
(
AlignSelf::SafeStart
| AlignSelf::SafeEnd
| AlignSelf::SafeFlexStart
| AlignSelf::SafeFlexEnd
| AlignSelf::SafeCenter,
_,
) => {
// Unreachable: `position()` strips Safe* into the underlying position keyword.
unreachable!()
}
}
};

Expand Down
22 changes: 20 additions & 2 deletions src/compute/grid/alignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ pub(super) fn align_tracks(
// simply pass zero here. Grid layout is never reversed.
let gap = 0.0;
let layout_is_reversed = false;
let is_safe = false; // TODO: Implement safe alignment
let track_alignment = apply_alignment_fallback(free_space, num_tracks, track_alignment_style, is_safe);
let track_alignment = apply_alignment_fallback(free_space, num_tracks, track_alignment_style);
let track_alignment = if axis_is_reversed { track_alignment.reversed() } else { track_alignment };

// Compute offsets
Expand Down Expand Up @@ -315,6 +314,17 @@ pub(super) fn align_item_within_area(
end: margin.end.unwrap_or(auto_margin_size),
};

// If the alignment uses a "safe" overflow-position keyword and the item would overflow
// its grid area, fall back to logical Start to avoid data loss. See CSS Box Alignment 3
// §4.3 <https://www.w3.org/TR/css-align-3/#overflow-values>. Otherwise, strip the
// Safe* modifier so the regular match below sees only the position keyword.
let overflows = resolved_size + non_auto_margin.sum() > grid_area_size;
let alignment_style = if alignment_style.is_safe() && overflows {
AlignSelf::Start
} else {
alignment_style.position()
};

// Compute offset in the axis
let alignment_based_offset = match alignment_style {
// TODO: Add support for baseline alignment. For now we treat it as "start".
Expand All @@ -333,6 +343,14 @@ pub(super) fn align_item_within_area(
}
}
AlignSelf::Center => (grid_area_size - resolved_size + resolved_margin.start - resolved_margin.end) / 2.0,
AlignSelf::SafeStart
| AlignSelf::SafeEnd
| AlignSelf::SafeFlexStart
| AlignSelf::SafeFlexEnd
| AlignSelf::SafeCenter => {
// Unreachable: Safe* variants are stripped above into the underlying position keyword.
unreachable!()
}
};

let offset_within_area = if position == Position::Absolute {
Expand Down
23 changes: 12 additions & 11 deletions src/compute/grid/track_sizing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,25 +189,26 @@ pub(super) fn compute_alignment_gutter_adjustment(
}

// As items never cross the outermost gutters in a grid, we can simplify our calculations by treating
// AlignContent::Start and AlignContent::End the same
// AlignContent::Start and AlignContent::End the same. Safe* variants share the gutter weight
// of their underlying position; overflow fallback is handled when offsets are computed.
let outer_gutter_weight = match alignment {
AlignContent::Start => 1,
AlignContent::FlexStart => 1,
AlignContent::End => 1,
AlignContent::FlexEnd => 1,
AlignContent::Center => 1,
AlignContent::Start | AlignContent::SafeStart => 1,
AlignContent::FlexStart | AlignContent::SafeFlexStart => 1,
AlignContent::End | AlignContent::SafeEnd => 1,
AlignContent::FlexEnd | AlignContent::SafeFlexEnd => 1,
AlignContent::Center | AlignContent::SafeCenter => 1,
AlignContent::Stretch => 0,
AlignContent::SpaceBetween => 0,
AlignContent::SpaceAround => 1,
AlignContent::SpaceEvenly => 1,
};

let inner_gutter_weight = match alignment {
AlignContent::FlexStart => 0,
AlignContent::Start => 0,
AlignContent::FlexEnd => 0,
AlignContent::End => 0,
AlignContent::Center => 0,
AlignContent::FlexStart | AlignContent::SafeFlexStart => 0,
AlignContent::Start | AlignContent::SafeStart => 0,
AlignContent::FlexEnd | AlignContent::SafeFlexEnd => 0,
AlignContent::End | AlignContent::SafeEnd => 0,
AlignContent::Center | AlignContent::SafeCenter => 0,
AlignContent::Stretch => 0,
AlignContent::SpaceBetween => 1,
AlignContent::SpaceAround => 2,
Expand Down
Loading
Loading