safe and unsafe overflow-position alignment keywords (#952)
* feat(style): add Safe* alignment variants and parser
Add SafeStart/SafeEnd/SafeFlexStart/SafeFlexEnd/SafeCenter on AlignItems
and AlignContent with is_safe()/position() helpers, RTL-aware reversed()
extension, and a hand-rolled FromCss parser for `safe X` / `unsafe X`
that rejects spec-invalid combinations. apply_alignment_fallback now
folds the safe modifier into its is_safe flag, so content alignment in
grid tracks and flex justify/align-content honor the Start fallback when
overflowing. Self-alignment paths accept the new variants but defer the
overflow fallback to a follow-up commit.
* feat(compute): wire safe-overflow fallback for self alignment
Implement the Start fallback for "safe" overflow-position keywords on
align-self and justify-self in both grid and flex layouts. When an item
overflows its alignment container and the alignment value is one of the
Safe* variants, the offset is anchored to the logical Start edge to
avoid data loss; an item that fits its container keeps its requested
position.
In grid `align_item_within_area`, overflow is detected from the resolved
size and non-auto margins versus the grid area size, and the fallback
respects the container `direction` so RTL anchors at the right edge.
In flex `align_flex_items_along_cross_axis`, the fallback short-circuits
when the cross-axis free space is negative, accounting for
`cross_axis_should_reverse` and `is_wrap_reverse` so the Start edge
matches the underlying writing-mode-relative start.
The three previously-stale `// TODO: Implement safe alignment` comments
at the grid track, flex justify-content, and flex align-content sites
are removed; the modifier-folding inside `apply_alignment_fallback`
already routes Safe* keywords through the Start fallback path on
overflow.
Add ten integration tests covering the new self-alignment behavior, the
content-alignment behavior wired by the previous commit, the no-overflow
case, the unsafe (default) overflow regression, the RTL fallback edge,
and a multi-line flex `align-content` overflow.
The flex absolutely-positioned alignment paths still strip Safe* via
`.position()` without invoking the Start fallback; this is left for a
follow-up commit.
* test(safe-alignment): add gentest fixtures for safe and unsafe overflow alignment
Add 17 HTML fixtures under test_fixtures/grid/ and test_fixtures/flex/
that exercise the `safe` and `unsafe` overflow-position keywords across
container-level (justify-content, align-content) and item-level
(justify-self, align-self) alignment in both grid and flex layouts. The
fixtures pair overflow and no-overflow geometries so that the Start
fallback path and the normal positional path are both verified, and
include unsafe-end regressions to prove the default behavior is
unchanged for plain end alignment.
Run the gentest pipeline against these fixtures to render each in
headless Chrome and capture the expected layout into 68 generated XML
expectation files (4 variants per fixture: border-box LTR, border-box
RTL, content-box LTR, content-box RTL). Register the new entries in
tests/xml/mod.rs.
The full suite passes — taffy's safe-alignment implementation matches
Chrome byte-for-byte on every fixture.
* docs: document safe and unsafe overflow alignment keywords
Add a section to docs/style-properties.md that lists the new Safe*
variants on AlignItems and AlignContent, describes the overflow
fallback to logical Start, and clarifies which position keywords are
valid pairings under the spec.
* refactor(safe-alignment): address review feedback
- Drop dead `is_safe` parameter from `apply_alignment_fallback`; infer
it from the alignment style itself.
- In `align_item_within_area` (grid) and `align_flex_items_along_cross_axis`
(flex), mutate `alignment_style` to `Start` on safe + overflow and
reuse the existing match, removing a duplicated RTL branch.
- Add `Safe*` arms to `benches/yoga_helpers.rs` so the bench crate builds
on CI; Yoga has no overflow-position equivalent, so they map to
`unimplemented!()` like the other unsupported variants.
* refactor(safe-alignment): collapse Safe* variants into struct { keyword, safety }
Addresses review feedback from @nicoburns. Replaces the combinatorial Safe*
variants on AlignContent / AlignItems with a struct holding a position-keyword
enum and an orthogonal AlignmentSafety { Safe, Unsafe } modifier. Every
pre-existing CSS spelling (Start, End, FlexStart, FlexEnd, Center, Stretch,
SpaceBetween, SpaceEvenly, SpaceAround, Baseline) and every Safe* spelling
(SafeStart, SafeEnd, SafeFlexStart, SafeFlexEnd, SafeCenter) is exposed as a
PascalCase associated constant, so call sites read identically to the previous
enum form. Compute paths take the struct at the API boundary, fold safety into
the keyword via the fallback step, and then match on the bare *Keyword enum —
exhaustive, with zero unreachable!() arms in src/compute/.
- public API: AlignContent / AlignItems are now `pub struct { keyword, safety }`
with associated consts for every prior spelling
- new public types: AlignContentKeyword, AlignItemsKeyword, AlignmentSafety
(added to the prelude)
- `position()` accessor renamed to `keyword()` (returns the new *Keyword enum)
- compute/common/alignment::apply_alignment_fallback returns
AlignContentKeyword; compute_alignment_offset takes one — internal matches
lose every `| Self::Safe*` sibling
- custom Serialize / Deserialize preserves the single-tag wire format
("Start", "SafeFlexEnd", …) so deserializing data produced by older builds
still works
- drive-by: fix pre-existing rand::distributions → rand::distr drift and
Dimension/LengthPercentage enum-variant → tag-based API drift in
benches/src/taffy_03_helpers.rs so `cargo test --workspace --all-features`
passes again
Size impact (src/style/mod.rs::style_sizes asserts these now):
AlignContent / AlignItems : 1 → 2 bytes
Option<AlignContent> / <AlignItems>: 1 → 2 bytes (niche-packed in safety byte)
Style<String> : 536 → 544 bytes
Style<Arc<str>> : 504 → 512 bytes
No behaviour change: test_fixtures byte-identical, all 4345 generated + 107 lib
+ 45 hand-written + 5 doc tests pass with `--all-features` and
`--no-default-features`.
* style: drop drive-by _f32 literal suffixes
Reverts 208 _f32 type-annotation suffixes that were inserted by an
IDE auto-fixer responding to a nightly-only `f32: From<f64>` lint
that's not yet a hard error. None of them are necessary on stable;
several landed inside string literals or doc comments where there
was no float to annotate at all (e.g. "Taffy 0.3" became
"Taffy 0.3_f32"). Per @nicoburns review feedback: keep the diff
minimal and let upstream address the lint when it actually fires.
No behavior change: test_fixtures byte-identical, all 4345 generated
+ 107 lib + 45 hand-written + 5 doc tests pass with --all-features
and --no-default-features.
Taffy is a flexible, high-performance, cross-platform UI layout library written in Rust.
It currently implements the CSS Block, Flexbox and CSS Grid layout algorithms. Support for other paradigms is planned. For more information on this and other future development plans see the roadmap issue.
This crate is a collaborative, cross-team project, and is designed to be used as a dependency for other UI and GUI libraries. Right now, it powers:
- Servo: an alternative web browser
- Blitz: a radically modular web engine
- Bevy: an ergonomic, ECS-first Rust game engine
- Takumi: Renders your React components to images
- iocraft: crafting beautiful interfaces for the terminal
- Slint: a declarative GUI toolkit for building native user interfaces
- The Lapce text editor via the Floem UI framework
- The Zed text editor via the GPUI UI framework
Usage
use taffy::prelude::*;
// First create an instance of TaffyTree
let mut tree : TaffyTree<()> = TaffyTree::new();
// Create a tree of nodes using `TaffyTree.new_leaf` and `TaffyTree.new_with_children`.
// These functions both return a node id which can be used to refer to that node
// The Style struct is used to specify styling information
let header_node = tree
.new_leaf(
Style {
size: Size { width: length(800.0), height: length(100.0) },
..Default::default()
},
).unwrap();
let body_node = tree
.new_leaf(
Style {
size: Size { width: length(800.0), height: auto() },
flex_grow: 1.0,
..Default::default()
},
).unwrap();
let root_node = tree
.new_with_children(
Style {
flex_direction: FlexDirection::Column,
size: Size { width: length(800.0), height: length(600.0) },
..Default::default()
},
&[header_node, body_node],
)
.unwrap();
// Call compute_layout on the root of your tree to run the layout algorithm
tree.compute_layout(root_node, Size::MAX_CONTENT).unwrap();
// Inspect the computed layout using `TaffyTree.layout`
assert_eq!(tree.layout(root_node).unwrap().size.width, 800.0);
assert_eq!(tree.layout(root_node).unwrap().size.height, 600.0);
assert_eq!(tree.layout(header_node).unwrap().size.width, 800.0);
assert_eq!(tree.layout(header_node).unwrap().size.height, 100.0);
assert_eq!(tree.layout(body_node).unwrap().size.width, 800.0);
assert_eq!(tree.layout(body_node).unwrap().size.height, 500.0); // This value was not set explicitly, but was computed by Taffy
Bindings to other languages
- Python via stretchable
- WIP C bindings
- WIP WASM bindings
Learning Resources
Taffy implements the Flexbox and CSS Grid specifications faithfully, so documentation designed for the web should translate cleanly to Taffy's implementation. For reference documentation on individual style properties we recommend the MDN documentation (for example this page on the width property). Such pages can usually be found by searching for "MDN property-name" using a search engine.
If you are interested in guide-level documentation on CSS layout, then we recommend the following resources:
Flexbox
- Flexbox Froggy. This is an interactive tutorial/game that allows you to learn the essential parts of Flexbox in a fun engaging way.
- A Complete Guide To Flexbox by CSS Tricks. This is detailed guide with illustrations and comprehensive written explanation of the different Flexbox properties and how they work.
CSS Grid
- CSS Grid Garden. This is an interactive tutorial/game that allows you to learn the essential parts of CSS Grid in a fun engaging way.
- A Complete Guide To CSS Grid by CSS Tricks. This is detailed guide with illustrations and comprehensive written explanation of the different CSS Grid properties and how they work.
Benchmarks (vs. Yoga)
- Run on a 2021 MacBook Pro with M1 Pro processor using criterion
- The benchmarks measure layout computation only. They do not measure tree creation.
- Yoga benchmarks were run via the yoga crate (Rust bindings)
- Most popular websites seem to have between 3,000 and 10,000 nodes (although they also require text layout, which neither yoga nor taffy implement).
Note that the table below contains multiple different units (milliseconds vs. microseconds)
| Benchmark | Node Count | Depth | Yoga (ba27f9d) | Taffy (71027a8) |
|---|---|---|---|---|
| yoga 'huge nested' | 1,000 | 3 | 364.60 µs | 329.04 µs |
| yoga 'huge nested' | 10,000 | 4 | 4.1988 ms | 4.3486 ms |
| yoga 'huge nested' | 100,000 | 5 | 45.804 ms | 38.559 ms |
| big trees (wide) | 1,000 | 1 | 737.77 µs | 505.99 µs |
| big trees (wide) | 10,000 | 1 | 7.1007 ms | 8.3395 ms |
| big trees (wide) | 100,000 | 1 | 135.78 ms | 247.42 ms |
| big trees (deep) | 4,000 | 12 | 2.2333 ms | 1.7400 ms |
| big trees (deep) | 10,000 | 14 | 5.9477 ms | 4.4445 ms |
| big trees (deep) | 100,000 | 17 | 76.755 ms | 63.778 ms |
| super deep | 1,000 | 1,000 | 555.32 µs | 472.85 µs |
Contributions
Contributions welcome:
if you'd like to use, improve or build taffy, feel free to join the conversation, open an issue or submit a PR.
If you have questions about how to use taffy, open a discussion so we can answer your questions in a way that others can find.