render: constrain shrink-wrapped grid items

This commit is contained in:
SGavrl committed 2026-07-26 20:26:42 +00:00
1 parent 9dfc67af74
commit d6be828d17
56 files changed
+22573 -2

No files matched your search

Generated
-2
View File
@@ -3227,8 +3227,6 @@ dependencies = [
[[package]] [[package]]
name = "taffy" name = "taffy"
version = "0.12.1" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73afc801dd6bd47529eaa7c7e90557f107527d1b7c9c7ed7d7803c7b8d0c357f"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"grid", "grid",
+4
View File
@@ -56,6 +56,10 @@ thiserror = "2"
anyhow = "1" anyhow = "1"
base64 = "0.22" base64 = "0.22"
[patch.crates-io]
# Keep the grid shrink-to-fit correction local until it lands upstream.
taffy = { path = "vendor/taffy" }
# Unwinding (the Rust default) is REQUIRED for the anti-panic protocol: ops wrap # Unwinding (the Rust default) is REQUIRED for the anti-panic protocol: ops wrap
# their bodies in std::panic::catch_unwind so a panic degrades to an error return # their bodies in std::panic::catch_unwind so a panic degrades to an error return
# instead of unwinding into V8's FFI frame and aborting the process. panic="abort" # instead of unwinding into V8's FFI frame and aborting the process. panic="abort"
@@ -856,6 +856,80 @@ fn item_self_alignment_places_flex_and_grid_items() {
); );
} }
#[test]
fn non_stretched_auto_grid_item_shrink_wraps_inside_grid_area() {
let tree = parse_html(
r#"
<style>
body { margin: 0 }
.panel {
display: grid;
align-items: start;
justify-items: center;
box-sizing: border-box;
}
demo-wrap { display: block }
#wide-panel {
margin-left: 112px;
width: 1168px;
padding: 16px 48px;
}
#wide-inner {
display: grid;
grid-template-columns: 1fr 1fr;
box-sizing: border-box;
width: 100%;
max-width: 1200px;
padding: 32px;
}
#wide-inner span {
display: inline-block;
width: 56px;
height: 20px;
}
#narrow-panel { width: 400px }
#narrow-inner { width: 200px; height: 20px }
</style>
<div id="wide-panel" class="panel">
<demo-wrap id="wide-wrapper">
<div id="wide-inner">
<div>
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
</div>
<div>
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
</div>
</div>
</demo-wrap>
</div>
<div id="narrow-panel" class="panel">
<demo-wrap id="narrow-wrapper"><div id="narrow-inner"></div></demo-wrap>
</div>
"#,
);
let layout = layout_dom(&tree, (1280.0, 1000.0));
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let wide_panel = rect("wide-panel");
let wide_wrapper = rect("wide-wrapper");
let wide_inner = rect("wide-inner");
let narrow_wrapper = rect("narrow-wrapper");
assert!(
(wide_wrapper.x - 160.0).abs() < 0.01
&& (wide_wrapper.width - 1072.0).abs() < 0.01
&& (wide_inner.x - 160.0).abs() < 0.01
&& (wide_inner.width - 1072.0).abs() < 0.01,
"wide shrink-wrapped item should be clamped to the grid area: \
panel={wide_panel:?} wrapper={wide_wrapper:?} inner={wide_inner:?}"
);
assert!(
(narrow_wrapper.x - 100.0).abs() < 0.01
&& (narrow_wrapper.width - 200.0).abs() < 0.01,
"a narrower item should keep its intrinsic width and remain centered: {narrow_wrapper:?}"
);
}
#[test] #[test]
fn opposing_floats_share_header_band_through_inline_wrapper() { fn opposing_floats_share_header_band_through_inline_wrapper() {
let tree = parse_html(include_str!("../../../render-repros/opposing-header-floats.html")); let tree = parse_html(include_str!("../../../render-repros/opposing-header-floats.html"));
+171
View File
@@ -0,0 +1,171 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.71"
name = "taffy"
version = "0.12.1"
authors = [
"Alice Cecile <[email protected]>",
"Johnathan Kelley <[email protected]>",
"Nico Burns <[email protected]>",
]
build = false
include = [
"src/**/*",
"examples/**/*",
"Cargo.toml",
"README.md",
]
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "A flexible UI layout library "
readme = "README.md"
keywords = [
"cross-platform",
"layout",
"flexbox",
"css-grid",
"grid",
]
categories = ["gui"]
license = "MIT"
repository = "https://github.com/DioxusLabs/taffy"
[package.metadata.docs.rs]
all-features = true
cargo-args = [
"-Zunstable-options",
"-Zrustdoc-scrape-examples",
]
rustdoc-args = [
"--cfg",
"docsrs",
]
[features]
alloc = ["serde?/alloc"]
block_layout = []
calc = []
content_size = []
debug = ["std"]
default = [
"std",
"taffy_tree",
"flexbox",
"grid",
"block_layout",
"float_layout",
"calc",
"content_size",
"detailed_layout_info",
]
detailed_layout_info = []
flexbox = []
float_layout = []
grid = [
"alloc",
"dep:grid",
]
parse = ["dep:cssparser"]
parse_faster = [
"parse",
"cssparser/fast_match_byte",
]
profile = ["std"]
serde = ["dep:serde"]
std = [
"grid?/std",
"serde?/std",
"slotmap?/std",
]
strict_provenance = []
taffy_tree = ["dep:slotmap"]
[lib]
name = "taffy"
path = "src/lib.rs"
[[example]]
name = "basic"
path = "examples/basic.rs"
doc-scrape-examples = true
[[example]]
name = "custom_tree_owned_partial"
path = "examples/custom_tree_owned_partial.rs"
[[example]]
name = "custom_tree_owned_unsafe"
path = "examples/custom_tree_owned_unsafe.rs"
[[example]]
name = "custom_tree_vec"
path = "examples/custom_tree_vec.rs"
[[example]]
name = "flexbox_gap"
path = "examples/flexbox_gap.rs"
[[example]]
name = "grid_holy_grail"
path = "examples/grid_holy_grail.rs"
[[example]]
name = "measure"
path = "examples/measure.rs"
[[example]]
name = "nested"
path = "examples/nested.rs"
[dependencies.arrayvec]
version = "0.7"
default-features = false
[dependencies.cssparser]
version = "0.37.0"
optional = true
default-features = false
[dependencies.document-features]
version = "0.2.7"
optional = true
[dependencies.grid]
version = "1.0.0"
optional = true
default-features = false
[dependencies.serde]
version = "1.0"
features = ["serde_derive"]
optional = true
default-features = false
[dependencies.slotmap]
version = "1.0.6"
optional = true
default-features = false
[dev-dependencies.roxmltree]
version = "0.21.1"
[dev-dependencies.serde_json]
version = "1.0.93"
[profile.release]
lto = true
panic = "abort"
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2018 Visly Inc.
Copyright (c) 2026 Taffy Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+132
View File
@@ -0,0 +1,132 @@
<!-- markdownlint-disable-next-line MD041 -->
<p>
<picture>
<img src="assets/logo.svg" alt="Taffy" height="70">
</picture>
</p>
[![GitHub CI](https://github.com/DioxusLabs/taffy/actions/workflows/ci.yml/badge.svg)](https://github.com/DioxusLabs/taffy/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/taffy.svg)](https://crates.io/crates/taffy)
[![docs.rs](https://img.shields.io/docsrs/taffy)](https://docs.rs/taffy)
![Crates.io MSRV](https://img.shields.io/crates/msrv/taffy)
Taffy is a flexible, high-performance, cross-platform UI layout library written in [Rust](https://www.rust-lang.org).
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](https://github.com/DioxusLabs/taffy/issues/345).
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](https://github.com/servo/servo): an alternative web browser
- [Blitz](https://github.com/DioxusLabs/blitz): a radically modular web engine
- [Bevy](https://bevyengine.org/): an ergonomic, ECS-first Rust game engine
- [Takumi](https://github.com/kane50613/takumi): Renders your React components to images
- [iocraft](https://github.com/ccbrown/iocraft): crafting beautiful interfaces for the terminal
- [Slint](https://github.com/slint-ui/slint): a declarative GUI toolkit for building native user interfaces
- The [Lapce](https://lapce.dev/) text editor via the [Floem](https://github.com/lapce/floem) UI framework
- The [Zed](https://zed.dev/) text editor via the [GPUI](https://github.com/zed-industries/zed/tree/main/crates/gpui) UI framework
## Usage
```rust
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](https://github.com/mortencombat/stretchable)
- [WIP C bindings](https://github.com/DioxusLabs/taffy/pull/404)
- [WIP WASM bindings](https://github.com/DioxusLabs/taffy/pull/394)
## 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](https://developer.mozilla.org/en-US/docs/Web/CSS/width) 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](https://flexboxfroggy.com/). 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](https://css-tricks.com/snippets/css/a-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](https://cssgridgarden.com/). 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](https://css-tricks.com/snippets/css/complete-guide-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](https://github.com/facebook/yoga))
- Run on a 2021 MacBook Pro with M1 Pro processor using [criterion](https://github.com/bheisler/criterion.rs)
- The benchmarks measure layout computation only. They do not measure tree creation.
- Yoga benchmarks were run via the [yoga](https://github.com/bschwind/yoga-rs) 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 |
[ba27f9d]: https://github.com/facebook/yoga/commit/ba27f9d1ecfa7518019845b84b035d3d4a2a6658
[71027a8]: https://github.com/DioxusLabs/taffy/commit/71027a8de03b343e120852b84bb7dca9fb4651c5
## Contributions
[Contributions welcome](https://github.com/DioxusLabs/taffy/blob/main/CONTRIBUTING.md):
if you'd like to use, improve or build `taffy`, feel free to join the conversation, open an [issue](https://github.com/DioxusLabs/taffy/issues) or submit a [PR](https://github.com/DioxusLabs/taffy/pulls).
If you have questions about how to use `taffy`, open a [discussion](https://github.com/DioxusLabs/taffy/discussions) so we can answer your questions in a way that others can find.
+1397
View File
File diff suppressed because it is too large. Load diff
+117
View File
@@ -0,0 +1,117 @@
//! Generic CSS alignment code that is shared between both the Flexbox and CSS Grid algorithms.
use crate::style::{AlignContent, AlignContentKeyword, AlignItems, AlignItemsKeyword, AlignmentSafety};
/// Resolve the `safe`/`unsafe` overflow-position fallback for a self-level alignment value
/// (used by `align-self` / `justify-self`-style sites and by absolutely-positioned items in
/// flex/grid). If the alignment subject overflows its alignment container and the requested
/// alignment is `safe`, fall back to logical `Start` per CSS Box Alignment
/// <https://www.w3.org/TR/css-align-3/#overflow-values>. Otherwise drop the safety modifier
/// and return the bare keyword.
#[inline]
pub(crate) fn resolve_self_alignment_safety(alignment: AlignItems, overflows: bool) -> AlignItemsKeyword {
if matches!(alignment.safety, AlignmentSafety::Safe) && overflows {
AlignItemsKeyword::Start
} else {
alignment.keyword
}
}
/// Resolve any spec-defined fallbacks for the given [`AlignContent`] value, returning the
/// bare position keyword the alignment math should use.
///
/// In addition to the spec at <https://www.w3.org/TR/css-align-3/> this implementation follows
/// the resolution of <https://github.com/w3c/csswg-drafts/issues/10154>.
pub(crate) fn apply_alignment_fallback(
free_space: f32,
num_items: usize,
alignment_mode: AlignContent,
) -> AlignContentKeyword {
let mut keyword = alignment_mode.keyword;
let mut is_safe = matches!(alignment_mode.safety, AlignmentSafety::Safe);
// 1. If there is only a single item being aligned or the items overflow the container, the
// distributed alignment keywords (`stretch`, `space-*`) fall back to a positional keyword
// and gain implicit `safe` semantics so step 2 can flip them to `Start` on overflow.
// https://www.w3.org/TR/css-align-3/#distribution-values
if num_items <= 1 || free_space <= 0.0 {
(keyword, is_safe) = match keyword {
AlignContentKeyword::Stretch | AlignContentKeyword::SpaceBetween => (AlignContentKeyword::FlexStart, true),
AlignContentKeyword::SpaceAround | AlignContentKeyword::SpaceEvenly => (AlignContentKeyword::Center, true),
other => (other, is_safe),
};
}
// 2. Safe alignment falls back to `Start` whenever the alignment subject would overflow the
// alignment container.
if free_space <= 0.0 && is_safe {
keyword = AlignContentKeyword::Start;
}
keyword
}
/// Generic alignment function that is used:
/// - For both align-content and justify-content alignment
/// - For both the Flexbox and CSS Grid algorithms
///
/// CSS Grid does not apply gaps as part of alignment, so the gap parameter should
/// always be set to zero for CSS Grid.
pub(crate) fn compute_alignment_offset(
free_space: f32,
num_items: usize,
gap: f32,
alignment_mode: AlignContentKeyword,
layout_is_flex_reversed: bool,
is_first: bool,
) -> f32 {
if is_first {
match alignment_mode {
AlignContentKeyword::Start => 0.0,
AlignContentKeyword::FlexStart => {
if layout_is_flex_reversed {
free_space
} else {
0.0
}
}
AlignContentKeyword::End => free_space,
AlignContentKeyword::FlexEnd => {
if layout_is_flex_reversed {
0.0
} else {
free_space
}
}
AlignContentKeyword::Center => free_space / 2.0,
AlignContentKeyword::Stretch => 0.0,
AlignContentKeyword::SpaceBetween => 0.0,
AlignContentKeyword::SpaceAround => {
if free_space >= 0.0 {
(free_space / num_items as f32) / 2.0
} else {
free_space / 2.0
}
}
AlignContentKeyword::SpaceEvenly => {
if free_space >= 0.0 {
free_space / (num_items + 1) as f32
} else {
free_space / 2.0
}
}
}
} else {
let free_space = free_space.max(0.0);
gap + match alignment_mode {
AlignContentKeyword::Start
| AlignContentKeyword::FlexStart
| AlignContentKeyword::End
| AlignContentKeyword::FlexEnd
| AlignContentKeyword::Center
| AlignContentKeyword::Stretch => 0.0,
AlignContentKeyword::SpaceBetween => free_space / (num_items - 1) as f32,
AlignContentKeyword::SpaceAround => free_space / num_items as f32,
AlignContentKeyword::SpaceEvenly => free_space / (num_items + 1) as f32,
}
}
}
+34
View File
@@ -0,0 +1,34 @@
//! Generic CSS content size code that is shared between all CSS algorithms.
use crate::geometry::{Point, Size};
use crate::style::Overflow;
use crate::util::sys::{f32_max, f32_min};
#[inline(always)]
/// Determine how much width/height a given node contributes to it's parent's content size
pub(crate) fn compute_content_size_contribution(
location: Point<f32>,
size: Size<f32>,
content_size: Size<f32>,
overflow: Point<Overflow>,
) -> Size<f32> {
let size_content_size_contribution = Size {
width: match overflow.x {
Overflow::Visible => f32_max(size.width, content_size.width),
_ => size.width,
},
height: match overflow.y {
Overflow::Visible => f32_max(size.height, content_size.height),
_ => size.height,
},
};
if size_content_size_contribution.width > 0.0 && size_content_size_contribution.height > 0.0 {
let max_x = f32_max(location.x + size_content_size_contribution.width, 0.0);
let min_x = f32_min(location.x, 0.0);
let max_y = f32_max(location.y + size_content_size_contribution.height, 0.0);
let min_y = f32_min(location.y, 0.0);
Size { width: max_x - min_x, height: max_y - min_y }
} else {
Size::ZERO
}
}
+5
View File
@@ -0,0 +1,5 @@
//! Generic code that is shared between multiple layout algorithms
pub(crate) mod alignment;
#[cfg(feature = "content_size")]
pub(crate) mod content_size;
File diff suppressed because it is too large. Load diff
+552
View File
@@ -0,0 +1,552 @@
//! Computes the position of floats in a block formatting context
//!
//! Here are the precise rules that govern the behavior of floats:
//!
//! 1. The left outer edge of a left-floating box may not be to the left of the left edge of its containing block. An analogous rule
//! holds for right-floating elements.
//! 2. If the current box is left-floating, and there are any left-floating boxes generated by elements earlier in the source document,
//! then for each such earlier box, either the left outer edge of the current box must be to the right of the right outer edge of the earlier box,
//! or its top must be lower than the bottom of the earlier box. Analogous rules hold for right-floating boxes.
//! 3. The right outer edge of a left-floating box may not be to the right of the left outer edge of any right-floating box that is next to it.
//! Analogous rules hold for right-floating elements.
//! 4. A floating box's outer top may not be higher than the top of its containing block. When the float occurs between two collapsing margins,
//! the float is positioned as if it had an otherwise empty anonymous block parent taking part in the flow. The position of such a parent is defined
//! by the rules in the section on margin collapsing.
//! 5. The outer top of a floating box may not be higher than the outer top of any block or floated box generated by an element earlier in the source document.
//! 6. The outer top of an element's floating box may not be higher than the top of any line-box containing a box generated by
//! an element earlier in the source document.
//! 7. A left-floating box that has another left-floating box to its left may not have its right outer edge to the right of its containing block's
//! right edge. (Loosely: a left float may not stick out at the right edge, unless it is already as far to the left as possible.) An analogous rule
//! holds for right-floating elements.
//! 8. A floating box must be placed as high as possible.
//! 9. A left-floating box must be put as far to the left as possible, a right-floating box as far to the right as possible. A higher position is
//! preferred over one that is further to the left/right.
//!
//! But in CSS 2.2, if, within the block formatting context, there is an in-flow negative vertical margin such that the float's position is above the position it would be at were all such negative margins set to zero, the position of the float is undefined.
//!
//! <https://www.w3.org/TR/CSS22/visuren.html#floats>
use core::ops::Range;
use crate::{debug::debug_log, sys::Vec, AvailableSpace, Clear, FloatDirection, Point, Size};
/// An empty "slot" that avoids floats that is suitable for non-floated content
/// to be laid out into
#[derive(Debug, Clone, Copy, Default)]
pub struct ContentSlot {
/// The id of the segment that the slot starts in
pub segment_id: Option<usize>,
/// The x position of the start of the slot
pub x: f32,
/// The y position of the start of the slot
pub y: f32,
/// The width of the slot
pub width: f32,
/// The height of the slot
pub height: f32,
}
/// A floated box
#[derive(Debug, Clone, Default)]
pub struct PlacedFloatedBox {
/// A user defined ID for the box
// id: u64,
/// The width of the box
pub width: f32,
/// The height of the box
pub height: f32,
/// Horizontal distance from the edge of the container that the box is floated towards
/// (distance from the left for left floats, from the right for right floats)
pub x_inset: f32,
/// Vertical distance from top edge of the container
pub y: f32,
}
/// A non-overlapping horizontal segment of the Block Formatting Context container
#[derive(Debug, Clone)]
struct Segment {
/// The vertical start and end points of the segment
y: Range<f32>,
/// Left inset in slot 0. Right inset in slot 1.
insets: [f32; 2],
}
impl Segment {
/// Whether the segment can fit the passed floated box (in the horizontal axis)
fn fits_float_width(&self, floated_box: Size<f32>, direction: FloatDirection, bfc_width: f32) -> bool {
let slot = direction as usize;
self.insets[slot] == 0.0 || (bfc_width - floated_box.width - self.inset_sum()) >= 0.0
}
/// The total space taken up by both insets
#[inline(always)]
fn inset_sum(&self) -> f32 {
self.insets[0] + self.insets[1]
}
}
/// Helper type for placing a single floated box
///
/// Given a pinned starting y position, this type helps determine if there is any x position
/// such that there is sufficient horizontal space for the box accross it's entire height.
#[derive(Debug, Clone)]
struct FloatFitter {
/// The overall width of the Block Formatting Context
bfc_width: f32,
/// The total height of the set of segments currently being considered
slot_height: f64,
/// The union of the insets of the set of segments currently being considered
insets: [f32; 2],
}
impl FloatFitter {
/// Create a new `FloatFitter`
fn new(bfc_width: f32, slot_height: f32, insets: [f32; 2]) -> Self {
Self { bfc_width, slot_height: slot_height as f64, insets }
}
// Horizontal fitting
/// Union the insets of another segment. This is a "max" of the insets on each side.
fn union_insets(&mut self, insets: [f32; 2]) {
self.insets[0] = self.insets[0].max(insets[0]);
self.insets[1] = self.insets[1].max(insets[1]);
}
/// Given the currently accounted for insets, check whether there is an x position
/// such that the box fits horizontally
fn fits_horiontally(&self, width: f32) -> bool {
self.insets == [0.0, 0.0] || self.bfc_width - self.insets[0] - self.insets[1] - width >= 0.0
}
// Vertical fitting
/// Add the height of another segment.
fn add_height(&mut self, height: f32) {
self.slot_height += height as f64;
}
/// Given the currently accounted for height, check whether the box fits vertically
fn fits_vertically(&mut self, height: f32) -> bool {
self.slot_height >= height as f64
}
}
/// A context for placing floated boxes
#[derive(Debug, Clone)]
pub struct FloatContext {
/// The available space constraint that applies to the root Block Formatting Context
/// for which this `FloatContext` manages floats.
available_width: f32,
/// Whether the float context contains any floats
has_floats: bool,
/// A list of left-floated boxes within the context
left_floats: Vec<PlacedFloatedBox>,
/// A list of right-floated boxes within the context
right_floats: Vec<PlacedFloatedBox>,
/// A list of non-overlapping horizontal "segments" within the context.
/// Each segment has the same available width for it's entire height.
segments: Vec<Segment>,
/// A closed-open range indicating which segment the last placed float
/// was placed(on each side).
last_placed_floats: [Range<usize>; 2],
// Left hwm in slot 0. Right hwm in slot 1.
// high_water_marks: [usize; 2],
// left_float_high_water_mark: usize,
// right_float_high_water_mark: usize,
}
impl Default for FloatContext {
fn default() -> Self {
Self {
available_width: 0.0,
has_floats: false,
left_floats: Vec::new(),
right_floats: Vec::new(),
segments: Vec::new(),
last_placed_floats: [0..0, 0..0], // high_water_marks: [0, 0],
}
}
}
impl FloatContext {
/// Create a new empty `FloatContext`
pub fn new() -> Self {
Default::default()
}
/// Whether the float context contains any floats
#[inline(always)]
pub fn has_floats(&self) -> bool {
self.has_floats
}
/// Whether the float context contains any floats that extend to or below min_y
#[inline(always)]
pub fn has_active_floats(&self, min_y: f32) -> bool {
self.has_floats && self.segments.last().map(|seg| seg.y.end).unwrap_or(0.0) > min_y
}
/// Set the width of the `FloatContext`
pub fn set_width(&mut self, available_width: f32) {
self.available_width = available_width;
}
/// Returns a slice of placed left floats
pub fn left_floats(&self) -> &[PlacedFloatedBox] {
&self.left_floats
}
/// Returns a slice of placed right floats
pub fn right_floats(&self) -> &[PlacedFloatedBox] {
&self.right_floats
}
/// Divide a segment into two segments so that a new float can be placed and have it's
/// vertical start and end at exact segment boundaries
fn subdivide_segment(&mut self, idx: usize, divide_at_y: f32) {
let old_segment = &mut self.segments[idx];
let new_segment = Segment { insets: old_segment.insets, y: divide_at_y..old_segment.y.end };
if !old_segment.y.contains(&divide_at_y) || old_segment.y.start == divide_at_y {
debug_log!("old_segment", dbg:&mut *old_segment);
debug_log!("divide_at_y", dbg:divide_at_y);
assert!(old_segment.y.contains(&divide_at_y) && old_segment.y.start != divide_at_y);
}
old_segment.y.end = divide_at_y;
self.segments.splice((idx + 1)..(idx + 1), core::iter::once(new_segment));
}
/// Update the last placed float start and end values
fn update_last_placed_float(&mut self, direction: FloatDirection, placement: Range<usize>) {
let slot = direction as usize;
self.last_placed_floats[slot].start = self.last_placed_floats[slot].start.max(placement.start);
self.last_placed_floats[slot].end = self.last_placed_floats[slot].end.max(placement.end);
}
/// Position a floated box with the context, returning the (x, y) coordinates
pub fn place_floated_box(
&mut self,
floated_box: Size<f32>,
min_y: f32,
containing_block_insets: [f32; 2],
direction: FloatDirection,
clear: Clear,
) -> Point<f32> {
self.has_floats = true;
let placed_floated_box =
self.place_floated_box_inner(floated_box, min_y, containing_block_insets, direction, clear);
let x_inset = placed_floated_box.x_inset;
let y = placed_floated_box.y;
match direction {
FloatDirection::Left => {
self.left_floats.push(placed_floated_box);
Point { x: x_inset, y }
}
FloatDirection::Right => {
self.right_floats.push(placed_floated_box);
Point { x: self.available_width - x_inset - floated_box.width, y }
}
}
}
/// Inner implementation of float placement, split into a separate function so that it can early-return
fn place_floated_box_inner(
&mut self,
floated_box: Size<f32>,
min_y: f32,
containing_block_insets: [f32; 2],
direction: FloatDirection,
clear: Clear,
) -> PlacedFloatedBox {
let slot = direction as usize;
// Ensure that float:
// - Starts at or after the last placed float that was floated in the same direction as it
// - Respects "clear"
let hwm = match clear {
Clear::Left => {
let float_dir_start = self.last_placed_floats[slot].start;
let left_end = self.last_placed_floats[0].end;
float_dir_start.max(left_end + 1)
}
Clear::Right => {
let float_dir_start = self.last_placed_floats[slot].start;
let right_end = self.last_placed_floats[1].end;
float_dir_start.max(right_end + 1)
}
Clear::Both => {
let left_end = self.last_placed_floats[0].end;
let right_end = self.last_placed_floats[1].end;
left_end.max(right_end) + 1
}
Clear::None => {
// float_dir_start
self.last_placed_floats[slot].start
}
};
// Ensure that float is placed in a segment at or below "min_y"
// (ensuring that it is placed at or below min_y within it's segment happens below)
let start_idx = self
.segments
.get(hwm..)
.and_then(|segments| segments.iter().position(|segment| segment.y.end > min_y).map(|idx| idx + hwm));
let mut start_idx = start_idx.unwrap_or(self.segments.len());
let mut start_y = min_y;
let mut end_idx = start_idx;
// let mut end_y = min_y;
// Loop over remaining segments, trying to place the float in a position
// that has space to accomodate it.
let (start, mut end, placed_inset) = 'outer: loop {
// Start segment does not exist:
//
// This means no existing segment can accomodate the float so we must create a new
// segment below all existing segments. A new segment will always have space for
// the float, so we can exit the loop at this point.
let Some(start_segment) = self.segments.get(start_idx) else {
break (None, None, containing_block_insets[slot]);
};
// Candidate start segment doesn't have (horizontal) space for the float:
// => retry with the next segment
if !start_segment.fits_float_width(floated_box, direction, self.available_width) {
start_idx += 1;
end_idx = end_idx.max(start_idx);
continue;
}
start_y = start_y.max(start_segment.y.start);
let available_height = start_segment.y.end - start_y;
let mut fitter = FloatFitter::new(self.available_width, available_height, containing_block_insets);
fitter.union_insets(start_segment.insets);
// Pinning the start segment, loop over segments starting with the start segment
// to find the end segment:
// - The selected segment range must have enough height to contain the float
// - All of the segments in the range must have enough horizontal width to contain the float
// TODO: must ensure that width is in the correct place.
loop {
// End segment does not exist:
//
// This means no existing segment can accomodate the float so we must create a new
// segment below all existing segments
let Some(end_segment) = self.segments.get(end_idx) else {
let inset = fitter.insets[slot];
break 'outer (Some(start_idx), None, inset);
};
// Check horizontal fit
//
// If it does not fit horizontally then it will never fit in this position, so
// continue the outer loop to find and check a new position
fitter.union_insets(end_segment.insets);
if !fitter.fits_horiontally(floated_box.width) {
start_idx += 1;
end_idx = end_idx.max(start_idx);
continue 'outer;
}
// Check vertical fit
//
// If it does not (yet) fit vertically then continue the inner loop to add another
// segment to the range of segments we are placing the float in
if end_idx != start_idx {
fitter.add_height(end_segment.y.end - end_segment.y.start);
}
if !fitter.fits_vertically(floated_box.height) {
end_idx += 1;
continue;
}
let inset = fitter.insets[slot];
break 'outer (Some(start_idx), Some(end_idx), inset);
}
};
// Short-circuit for zero-sized boxes
if floated_box.width == 0.0 || floated_box.height == 0.0 {
// TODO: need to update last_placed_float?
return PlacedFloatedBox {
width: floated_box.width,
height: floated_box.height,
y: start_y,
x_inset: placed_inset,
};
}
// Handle case where floated box is placed after all existing segments
if start.is_none() {
let last_y_end = self.segments.last().map(|seg| seg.y.end).unwrap_or(0.0);
if start_y > last_y_end {
self.segments.push(Segment { y: last_y_end..start_y, insets: [0.0, 0.0] });
}
let start_y = last_y_end.max(start_y);
let mut insets = containing_block_insets;
insets[slot] += floated_box.width;
self.segments.push(Segment { y: start_y..(start_y + floated_box.height), insets });
// Update last_placed_float
let start_idx = self.segments.len() - 1;
let end_idx = start_idx + 1;
self.update_last_placed_float(direction, start_idx..end_idx);
return PlacedFloatedBox {
width: floated_box.width,
height: floated_box.height,
y: start_y,
x_inset: containing_block_insets[slot],
};
}
// Else unwrap the index of the segment that the start of the floating box is placed in
let mut start_idx = start.unwrap();
// If the floated box doesn't start at the exact same y-offset as the segment it starts in, then
// subdivide that segment into two segments at the y-offset that the floated box starts at, and increment
// `start_idx` so that the floating box is placed in the second of the two segments.
if start_y != self.segments[start_idx].y.start {
self.subdivide_segment(start_idx, start_y);
start_idx += 1;
if let Some(end_idx) = end.as_mut() {
*end_idx += 1;
}
}
let end_idx = match end {
None => {
let last_y_end = self.segments.last().map(|seg| seg.y.end).unwrap_or(0.0);
if min_y > last_y_end {
self.segments.push(Segment { y: last_y_end..min_y, insets: [0.0, 0.0] });
}
self.segments.len() - 1
}
Some(end_idx) => {
let end_y = start_y + floated_box.height;
if end_y != self.segments[end_idx].y.end {
self.subdivide_segment(end_idx, end_y);
}
end_idx
}
};
// Update inset for the range of segments that the float is placed in
let placed_inset_plus_width = placed_inset + floated_box.width;
for segment in &mut self.segments[start_idx..=end_idx] {
segment.insets[slot] = placed_inset_plus_width;
}
// Update last_placed_float
self.update_last_placed_float(direction, start_idx..(end_idx + 1));
PlacedFloatedBox { width: floated_box.width, height: floated_box.height, y: start_y, x_inset: placed_inset }
}
/// Get the end segment of the last float on side(s) specified by the clear parameter (if any)
fn cleared_segment(&self, clear: Clear) -> Option<usize> {
match clear {
Clear::Left => Some(self.last_placed_floats[0].end),
Clear::Right => Some(self.last_placed_floats[1].end),
Clear::Both => {
let left_end = self.last_placed_floats[0].end;
let right_end = self.last_placed_floats[1].end;
Some(left_end.max(right_end))
}
Clear::None => None,
}
}
/// Get the bottom of lowest relevant float for the specific clear property
pub fn cleared_threshold(&self, clear: Clear) -> Option<f32> {
self.cleared_segment(clear).and_then(|idx| self.segments.get(idx.max(1) - 1)).map(|seg| seg.y.end)
}
/// Search for a space suitable for laying out non-floated content into
pub fn find_content_slot(
&self,
min_y: f32,
containing_block_insets: [f32; 2],
clear: Clear,
after: Option<usize>,
) -> ContentSlot {
if !self.has_active_floats(min_y) {
return ContentSlot {
segment_id: None,
x: containing_block_insets[0],
y: min_y,
width: self.available_width - containing_block_insets[0] - containing_block_insets[1],
height: f32::INFINITY,
};
}
// The min starting segment index
let at_least = after.map(|idx| idx + 1).unwrap_or(0);
let hwm = at_least.max(self.cleared_segment(clear).map(|idx| idx + 1).unwrap_or(0));
let start_idx = self
.segments
.get(hwm..)
.and_then(|segments| segments.iter().position(|segment| segment.y.end > min_y).map(|idx| idx + hwm));
let start_idx = start_idx.unwrap_or(self.segments.len());
let segment = self.segments.get(start_idx);
match segment {
Some(segment) => {
let inset_left = segment.insets[0].max(containing_block_insets[0]);
let inset_right = segment.insets[1].max(containing_block_insets[1]);
ContentSlot {
segment_id: Some(start_idx),
x: inset_left,
y: segment.y.start.max(min_y),
width: self.available_width - inset_left - inset_right,
height: f32::INFINITY,
}
}
None => ContentSlot {
segment_id: None,
x: containing_block_insets[0],
y: min_y,
width: self.available_width - containing_block_insets[0] - containing_block_insets[1],
height: f32::INFINITY,
},
}
}
}
/// Context for computing the intrinsic width contribution of a set of floats
pub struct FloatIntrinsicWidthCalculator {
/// The available width of the container
available_width: AvailableSpace,
/// The running total intrinsic width contribution
contribution: f32,
}
impl FloatIntrinsicWidthCalculator {
/// Create a new `FloatIntrinsicWidthCalculator`
pub fn new(available_width: AvailableSpace) -> Self {
Self { available_width, contribution: 0.0 }
}
/// Add a float to the computation
pub fn add_float(&mut self, width: f32, _direction: FloatDirection, _clear: Clear) {
match self.available_width {
AvailableSpace::Definite(_) => {
// We will never hit this code path with definite available space
}
AvailableSpace::MinContent => self.contribution = self.contribution.max(width),
AvailableSpace::MaxContent => self.contribution += width,
};
}
/// Get the computed float contribution to intrinsic width
pub fn result(&self) -> f32 {
self.contribution
}
}
+405
View File
@@ -0,0 +1,405 @@
//! Alignment of tracks and final positioning of items
use super::types::GridTrack;
use crate::compute::common::alignment::{
apply_alignment_fallback, compute_alignment_offset, resolve_self_alignment_safety,
};
use crate::geometry::{InBothAbsAxis, Line, Point, Rect, Size};
use crate::style::{
AlignContent, AlignItems, AlignItemsKeyword, AlignSelf, AvailableSpace, CoreStyle, GridItemStyle, Overflow,
Position,
};
use crate::tree::{Layout, LayoutPartialTreeExt, NodeId, SizingMode};
use crate::util::sys::f32_max;
use crate::util::{MaybeMath, MaybeResolve, ResolveOrZero};
#[cfg(feature = "content_size")]
use crate::compute::common::content_size::compute_content_size_contribution;
use crate::{BoxSizing, Direction, LayoutGridContainer};
/// Align the grid tracks within the grid according to the align-content (rows) or
/// justify-content (columns) property. This only does anything if the size of the
/// grid is not equal to the size of the grid container in the axis being aligned.
pub(super) fn align_tracks(
grid_container_content_box_size: f32,
padding: Line<f32>,
border: Line<f32>,
tracks: &mut [GridTrack],
track_alignment_style: AlignContent,
axis_is_reversed: bool,
) {
let used_size: f32 = tracks.iter().map(|track| track.base_size).sum();
let free_space = grid_container_content_box_size - used_size;
let origin = padding.start + border.start;
// Count the number of non-collapsed tracks (not counting gutters)
let num_tracks = tracks.iter().skip(1).step_by(2).filter(|track| !track.is_collapsed).count();
// Grid layout treats gaps as full tracks rather than applying them at alignment so we
// simply pass zero here. Grid layout is never reversed.
let gap = 0.0;
let layout_is_reversed = false;
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
let mut total_offset = origin;
let mut seen_non_collapsed_track = false;
tracks.iter_mut().enumerate().for_each(|(i, track)| {
// Odd tracks are gutters (but slices are zero-indexed, so odd tracks have even indices)
let is_gutter = i % 2 == 0;
let is_non_collapsed_track = !is_gutter && !track.is_collapsed;
// Alignment offsets should be applied only to non-collapsed tracks.
let is_first = is_non_collapsed_track && !seen_non_collapsed_track;
let offset = if is_non_collapsed_track {
compute_alignment_offset(free_space, num_tracks, gap, track_alignment, layout_is_reversed, is_first)
} else {
0.0
};
track.offset = total_offset + offset;
total_offset = total_offset + offset + track.base_size;
if is_non_collapsed_track {
seen_non_collapsed_track = true;
}
});
}
/// Align and size a grid item into it's final position
pub(super) fn align_and_position_item(
tree: &mut impl LayoutGridContainer,
node: NodeId,
order: u32,
grid_area: Rect<f32>,
container_alignment_styles: InBothAbsAxis<Option<AlignItems>>,
baseline_shim: f32,
direction: Direction,
) -> (Size<f32>, f32, f32) {
let grid_area_size = Size { width: grid_area.right - grid_area.left, height: grid_area.bottom - grid_area.top };
let style = tree.get_grid_child_style(node);
let overflow = style.overflow();
let scrollbar_width = style.scrollbar_width();
let aspect_ratio = style.aspect_ratio();
let justify_self = style.justify_self();
let align_self = style.align_self();
let position = style.position();
let inset_horizontal = style
.inset()
.horizontal_components()
.map(|size| size.resolve_to_option(grid_area_size.width, |val, basis| tree.calc(val, basis)));
let inset_vertical = style
.inset()
.vertical_components()
.map(|size| size.resolve_to_option(grid_area_size.height, |val, basis| tree.calc(val, basis)));
let padding =
style.padding().map(|p| p.resolve_or_zero(Some(grid_area_size.width), |val, basis| tree.calc(val, basis)));
let border =
style.border().map(|p| p.resolve_or_zero(Some(grid_area_size.width), |val, basis| tree.calc(val, basis)));
let padding_border_size = (padding + border).sum_axes();
let box_sizing_adjustment =
if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
let inherent_size = style
.size()
.maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
let min_size = style
.min_size()
.maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
.maybe_add(box_sizing_adjustment)
.or(padding_border_size.map(Some))
.maybe_max(padding_border_size)
.maybe_apply_aspect_ratio(aspect_ratio);
let max_size = style
.max_size()
.maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
// Resolve default alignment styles if they are set on neither the parent or the node itself
// Note: if the child has a preferred aspect ratio but neither width or height are set, then the width is stretched
// and the then height is calculated from the width according the aspect ratio
// See: https://www.w3.org/TR/css-grid-1/#grid-item-sizing
let alignment_styles = InBothAbsAxis {
horizontal: justify_self.or(container_alignment_styles.horizontal).unwrap_or_else(|| {
if inherent_size.width.is_some() {
AlignSelf::START
} else {
AlignSelf::STRETCH
}
}),
vertical: align_self.or(container_alignment_styles.vertical).unwrap_or_else(|| {
if inherent_size.height.is_some() || aspect_ratio.is_some() {
AlignSelf::START
} else {
AlignSelf::STRETCH
}
}),
};
// Note: This is not a bug. It is part of the CSS spec that both horizontal and vertical margins
// resolve against the WIDTH of the grid area.
let margin =
style.margin().map(|margin| margin.resolve_to_option(grid_area_size.width, |val, basis| tree.calc(val, basis)));
let grid_area_minus_item_margins_size = Size {
width: grid_area_size.width.maybe_sub(margin.left).maybe_sub(margin.right),
height: grid_area_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - baseline_shim,
};
// If node is absolutely positioned and width is not set explicitly, then deduce it
// from left, right and container_content_box if both are set.
let width = inherent_size.width.or_else(|| {
// Apply width derived from both the left and right properties of an absolutely
// positioned element being set
if position == Position::Absolute {
if let (Some(left), Some(right)) = (inset_horizontal.start, inset_horizontal.end) {
return Some(f32_max(grid_area_minus_item_margins_size.width - left - right, 0.0));
}
}
// Apply width based on stretch alignment if:
// - Alignment style is "stretch"
// - The node is not absolutely positioned
// - The node does not have auto margins in this axis.
if margin.left.is_some()
&& margin.right.is_some()
&& alignment_styles.horizontal == AlignSelf::STRETCH
&& position != Position::Absolute
{
return Some(grid_area_minus_item_margins_size.width);
}
None
});
// Reapply aspect ratio after stretch and absolute position width adjustments
let Size { width, height } = Size { width, height: inherent_size.height }.maybe_apply_aspect_ratio(aspect_ratio);
let height = height.or_else(|| {
if position == Position::Absolute {
if let (Some(top), Some(bottom)) = (inset_vertical.start, inset_vertical.end) {
return Some(f32_max(grid_area_minus_item_margins_size.height - top - bottom, 0.0));
}
}
// Apply height based on stretch alignment if:
// - Alignment style is "stretch"
// - The node is not absolutely positioned
// - The node does not have auto margins in this axis.
if margin.top.is_some()
&& margin.bottom.is_some()
&& alignment_styles.vertical == AlignSelf::STRETCH
&& position != Position::Absolute
{
return Some(grid_area_minus_item_margins_size.height);
}
None
});
// Reapply aspect ratio after stretch and absolute position height adjustments
let Size { width, height } = Size { width, height }.maybe_apply_aspect_ratio(aspect_ratio);
// Clamp size by min and max width/height
let Size { width, height } = Size { width, height }.maybe_clamp(min_size, max_size);
// Layout node
drop(style);
let size = if position == Position::Absolute && (width.is_none() || height.is_none()) {
tree.measure_child_size_both(
node,
Size { width, height },
grid_area_size.map(Option::Some),
grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
SizingMode::InherentSize,
Line::FALSE,
)
.map(Some)
} else {
Size { width, height }
};
let mut layout_output = tree.perform_child_layout(
node,
size,
grid_area_size.map(Option::Some),
grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
SizingMode::InherentSize,
Line::FALSE,
);
// Resolve final size
let mut resolved_size = size.unwrap_or(layout_output.size).maybe_clamp(min_size, max_size);
// An auto-sized grid item that is not stretched uses fit-content sizing
// in the inline axis. The inherent-size layout above obtains its
// max-content size; if that exceeds the definite grid area, lay it out
// again at the available size so descendants resolve against the actual
// shrink-to-fit width.
if size.width.is_none()
&& alignment_styles.horizontal != AlignSelf::STRETCH
&& resolved_size.width > grid_area_minus_item_margins_size.width
{
let constrained_size = Size {
width: Some(grid_area_minus_item_margins_size.width)
.maybe_clamp(min_size.width, max_size.width),
height: size.height,
};
layout_output = tree.perform_child_layout(
node,
constrained_size,
grid_area_size.map(Option::Some),
grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
SizingMode::InherentSize,
Line::FALSE,
);
resolved_size = constrained_size.unwrap_or(layout_output.size).maybe_clamp(min_size, max_size);
}
let Size { width, height } = resolved_size;
let (x, x_margin) = align_item_within_area(
Line { start: grid_area.left, end: grid_area.right },
justify_self.unwrap_or(alignment_styles.horizontal),
width,
position,
inset_horizontal,
margin.horizontal_components(),
0.0,
direction,
);
let (y, y_margin) = align_item_within_area(
Line { start: grid_area.top, end: grid_area.bottom },
align_self.unwrap_or(alignment_styles.vertical),
height,
position,
inset_vertical,
margin.vertical_components(),
baseline_shim,
Direction::Ltr,
);
let scrollbar_size = Size {
width: if overflow.y == Overflow::Scroll { scrollbar_width } else { 0.0 },
height: if overflow.x == Overflow::Scroll { scrollbar_width } else { 0.0 },
};
let resolved_margin = Rect { left: x_margin.start, right: x_margin.end, top: y_margin.start, bottom: y_margin.end };
tree.set_unrounded_layout(
node,
&Layout {
order,
location: Point { x, y },
size: Size { width, height },
#[cfg(feature = "content_size")]
content_size: layout_output.content_size,
scrollbar_size,
padding,
border,
margin: resolved_margin,
},
);
#[cfg(feature = "content_size")]
let contribution = compute_content_size_contribution(
Point { x: x - grid_area.left, y: y - grid_area.top },
Size { width, height },
layout_output.content_size,
overflow,
);
#[cfg(not(feature = "content_size"))]
let contribution = Size::ZERO;
(contribution, y, height)
}
/// Align and size a grid item along a single axis
#[allow(clippy::too_many_arguments)]
pub(super) fn align_item_within_area(
grid_area: Line<f32>,
alignment_style: AlignSelf,
resolved_size: f32,
position: Position,
inset: Line<Option<f32>>,
margin: Line<Option<f32>>,
baseline_shim: f32,
direction: Direction,
) -> (f32, Line<f32>) {
// Calculate grid area dimension in the axis
let non_auto_margin = Line { start: margin.start.unwrap_or(0.0) + baseline_shim, end: margin.end.unwrap_or(0.0) };
let grid_area_size = f32_max(grid_area.end - grid_area.start, 0.0);
let free_space = f32_max(grid_area_size - resolved_size - non_auto_margin.sum(), 0.0);
// Expand auto margins to fill available space
let auto_margin_count = margin.start.is_none() as u8 + margin.end.is_none() as u8;
let auto_margin_size = if auto_margin_count > 0 { free_space / auto_margin_count as f32 } else { 0.0 };
let resolved_margin = Line {
start: margin.start.unwrap_or(auto_margin_size) + baseline_shim,
end: margin.end.unwrap_or(auto_margin_size),
};
let overflows = resolved_size + non_auto_margin.sum() > grid_area_size;
let alignment_keyword = resolve_self_alignment_safety(alignment_style, overflows);
// Compute offset in the axis
let alignment_based_offset = match alignment_keyword {
// TODO: Add support for baseline alignment. For now we treat it as "start".
AlignItemsKeyword::Start
| AlignItemsKeyword::FlexStart
| AlignItemsKeyword::Baseline
| AlignItemsKeyword::Stretch => {
if direction.is_rtl() {
grid_area_size - resolved_size - resolved_margin.end
} else {
resolved_margin.start
}
}
AlignItemsKeyword::End | AlignItemsKeyword::FlexEnd => {
if direction.is_rtl() {
resolved_margin.start
} else {
grid_area_size - resolved_size - resolved_margin.end
}
}
AlignItemsKeyword::Center => {
(grid_area_size - resolved_size + resolved_margin.start - resolved_margin.end) / 2.0
}
};
let offset_within_area = if position == Position::Absolute {
match (inset.start, inset.end) {
(Some(start), Some(end)) => {
if direction.is_rtl() {
grid_area_size - end - resolved_size - non_auto_margin.end
} else {
start + non_auto_margin.start
}
}
(Some(start), None) => start + non_auto_margin.start,
(None, Some(end)) => grid_area_size - end - resolved_size - non_auto_margin.end,
(None, None) => alignment_based_offset,
}
} else {
alignment_based_offset
};
let mut start = grid_area.start + offset_within_area;
if position == Position::Relative {
let relative_inset = if direction.is_rtl() {
inset.end.map(|pos| -pos).or(inset.start)
} else {
inset.start.or(inset.end.map(|pos| -pos))
};
start += relative_inset.unwrap_or(0.0);
}
(start, resolved_margin)
}
+732
View File
@@ -0,0 +1,732 @@
//! Helper functions for initialising GridTrack's from styles
//! This mainly consists of evaluating GridAutoTracks
use super::types::{GridTrack, GridTrackKind, TrackCounts};
use crate::geometry::AbsoluteAxis;
use crate::style::{LengthPercentage, RepetitionCount, TrackSizingFunction};
use crate::style_helpers::TaffyAuto;
use crate::util::sys::{ceil, floor, Vec};
use crate::util::MaybeMath;
use crate::util::ResolveOrZero;
use crate::{GenericGridTemplateComponent, GenericRepetition, GridContainerStyle};
/// The auto-repeat fit strategy to use
pub(crate) enum AutoRepeatStrategy {
/// If the grid container has a definite size or max size in the relevant axis:
/// - then the number of repetitions is the largest possible positive integer that does not cause the grid to overflow the content
/// box of its grid container.
MaxRepetitionsThatDoNotOverflow,
/// Otherwise, if the grid container has a definite min size in the relevant axis:
/// - then the number of repetitions is the smallest possible positive integer that fulfills that minimum requirement
MinRepetitionsThatDoOverflow,
}
/// Compute the number of rows and columns in the explicit grid
pub(crate) fn compute_explicit_grid_size_in_axis(
style: &impl GridContainerStyle,
auto_fit_container_size: Option<f32>,
auto_fit_strategy: AutoRepeatStrategy,
resolve_calc_value: impl Fn(*const (), f32) -> f32,
axis: AbsoluteAxis,
) -> (u16, u16) {
let template = match axis {
AbsoluteAxis::Horizontal => style.grid_template_columns(),
AbsoluteAxis::Vertical => style.grid_template_rows(),
};
let Some(template) = template else {
return (0, 0);
};
// If template contains no tracks, then there are trivially zero explicit tracks
let track_count = template.len();
if track_count == 0 {
return (0, 0);
}
// If there are any repetitions that contains no tracks, then the whole definition should be considered invalid
// and we default to no explicit tracks
let template_has_repetitions_with_zero_tracks = template.clone().any(|track_def| match track_def {
GenericGridTemplateComponent::Single(_) => false,
GenericGridTemplateComponent::Repeat(repeat) => repeat.track_count() == 0,
});
if template_has_repetitions_with_zero_tracks {
return (0, 0);
}
// Compute that number of track generated by single track definition and repetitions with a fixed repetition count
let non_auto_repeating_track_count = template
.clone()
.map(|track_def| match track_def {
GenericGridTemplateComponent::Single(_) => 1,
GenericGridTemplateComponent::Repeat(repeat) => match repeat.count() {
RepetitionCount::Count(count) => count * repeat.track_count(),
RepetitionCount::AutoFit | RepetitionCount::AutoFill => 0,
},
})
.sum::<u16>();
let auto_repetition_count: u16 = template.clone().filter(|track_def| track_def.is_auto_repetition()).count() as u16;
let all_track_defs_have_fixed_component = template.clone().all(|track_def| match track_def {
GenericGridTemplateComponent::Single(sizing_function) => sizing_function.has_fixed_component(),
GenericGridTemplateComponent::Repeat(repeat) => {
repeat.tracks().all(|sizing_function| sizing_function.has_fixed_component())
}
});
let template_is_valid =
auto_repetition_count == 0 || (auto_repetition_count == 1 && all_track_defs_have_fixed_component);
// If the template is invalid because it contains multiple auto-repetition definitions or it combines an auto-repetition
// definition with non-fixed-size track sizing functions, then disregard it entirely and default to zero explicit tracks
if !template_is_valid {
return (0, 0);
}
// If there are no repetitions, then the number of explicit tracks is simply equal to the lengths of the track definition
// vector (as each item in the Vec represents one track).
if auto_repetition_count == 0 {
return (0, non_auto_repeating_track_count);
}
let repetition_definition = template
.clone()
.find_map(|def| match def {
GenericGridTemplateComponent::Single(_) => None,
GenericGridTemplateComponent::Repeat(repeat) => match repeat.count() {
RepetitionCount::Count(_) => None,
RepetitionCount::AutoFit | RepetitionCount::AutoFill => Some(repeat),
},
})
.unwrap();
let repetition_definition_iter = repetition_definition.tracks();
let repetition_track_count = repetition_definition_iter.len() as u16;
// Determine the number of repetitions
let num_repetitions: u16 = match auto_fit_container_size {
None => 1,
Some(inner_container_size) => {
let parent_size = Some(inner_container_size);
/// ...treating each track as its max track sizing function if that is definite or as its minimum track sizing function
/// otherwise, flooring the max track sizing function by the min track sizing function if both are definite
fn track_definite_value(
sizing_function: TrackSizingFunction,
parent_size: Option<f32>,
calc_resolver: impl Fn(*const (), f32) -> f32,
) -> f32 {
let max_size = sizing_function.max.definite_value(parent_size, &calc_resolver);
let min_size = sizing_function.min.definite_value(parent_size, &calc_resolver);
max_size.map(|max| max.maybe_max(min_size)).or(min_size).unwrap()
}
let non_repeating_track_used_space: f32 = template
.clone()
.map(|track_def| match track_def {
GenericGridTemplateComponent::Single(sizing_function) => {
track_definite_value(sizing_function, parent_size, &resolve_calc_value)
}
GenericGridTemplateComponent::Repeat(repeat) => match repeat.count() {
RepetitionCount::Count(count) => {
let sum = repeat
.tracks()
.map(|sizing_function| {
track_definite_value(sizing_function, parent_size, &resolve_calc_value)
})
.sum::<f32>();
sum * (count as f32)
}
RepetitionCount::AutoFit | RepetitionCount::AutoFill => 0.0,
},
})
.sum();
let gap_size = style.gap().get_abs(axis).resolve_or_zero(Some(inner_container_size), &resolve_calc_value);
// Compute the amount of space that a single repetition of the repeated track list takes
let per_repetition_track_used_space: f32 = repetition_definition_iter
.map(|sizing_function| track_definite_value(sizing_function, parent_size, &resolve_calc_value))
.sum::<f32>();
// We special case the first repetition here because the number of gaps in the first repetition
// depends on the number of non-repeating tracks in the template
let first_repetition_and_non_repeating_tracks_used_space = non_repeating_track_used_space
+ per_repetition_track_used_space
+ ((non_auto_repeating_track_count + repetition_track_count).saturating_sub(1) as f32 * gap_size);
// If a single repetition already overflows the container then we return 1 as the repetition count
// (the number of repetitions is floored at 1)
if first_repetition_and_non_repeating_tracks_used_space > inner_container_size {
1u16
} else {
let per_repetition_gap_used_space = (repetition_track_count as f32) * gap_size;
let per_repetition_used_space = per_repetition_track_used_space + per_repetition_gap_used_space;
let num_repetition_that_fit = (inner_container_size
- first_repetition_and_non_repeating_tracks_used_space)
/ per_repetition_used_space;
// If the container size is a preferred or maximum size:
// Then we return the maximum number of repetitions that fit into the container without overflowing.
// If the container size is a minimum size:
// - Then we return the minimum number of repetitions required to overflow the size.
//
// In all cases we add the additional repetition that was already accounted for in the special-case computation above
match auto_fit_strategy {
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow => (floor(num_repetition_that_fit) as u16) + 1,
AutoRepeatStrategy::MinRepetitionsThatDoOverflow => (ceil(num_repetition_that_fit) as u16) + 1,
}
}
}
};
let grid_template_track_count = non_auto_repeating_track_count + (repetition_track_count * num_repetitions);
(num_repetitions, grid_template_track_count)
}
/// Resolve the track sizing functions of explicit tracks, automatically created tracks, and gutters
/// given a set of track counts and all of the relevant styles
pub(super) fn initialize_grid_tracks(
tracks: &mut Vec<GridTrack>,
counts: TrackCounts,
style: &impl GridContainerStyle,
axis: AbsoluteAxis,
track_has_items: impl Fn(usize) -> bool,
) {
// Extract styles
let track_template;
let auto_tracks;
let gap;
match axis {
AbsoluteAxis::Horizontal => {
track_template = style.grid_template_columns();
auto_tracks = style.grid_auto_columns();
gap = style.gap().width;
}
AbsoluteAxis::Vertical => {
track_template = style.grid_template_rows();
auto_tracks = style.grid_auto_rows();
gap = style.gap().height;
}
};
// Clear vector (in case this is a re-layout), reserve space for all tracks ahead of time to reduce allocations,
// and push the initial gutter
tracks.clear();
tracks.reserve((counts.len() * 2) + 1);
tracks.push(GridTrack::gutter(gap));
let auto_track_count = auto_tracks.len();
let non_auto_repeating_track_count = track_template
.clone()
.map(|track_template| {
track_template
.map(|track_def| match track_def {
GenericGridTemplateComponent::Single(_) => 1,
GenericGridTemplateComponent::Repeat(repeat) => match repeat.count() {
RepetitionCount::Count(count) => count * repeat.track_count(),
RepetitionCount::AutoFit | RepetitionCount::AutoFill => 0,
},
})
.sum::<u16>()
})
.unwrap_or(0);
// Create negative implicit tracks
if counts.negative_implicit > 0 {
if auto_track_count == 0 {
let iter = core::iter::repeat(TrackSizingFunction::AUTO);
create_implicit_tracks(tracks, counts.negative_implicit, iter, gap)
} else {
let offset = auto_track_count - (counts.negative_implicit as usize % auto_track_count);
let iter = auto_tracks.clone().cycle().skip(offset);
create_implicit_tracks(tracks, counts.negative_implicit, iter, gap)
}
}
let mut current_track_index = (counts.negative_implicit) as usize;
// Create explicit tracks
// An explicit check against the count (rather than just relying on track_template being empty) is required here
// because a count of zero can result from the track_template being invalid, in which case it should be ignored.
if counts.explicit > 0 {
if let Some(track_template) = track_template {
track_template.for_each(|track_sizing_function| {
match track_sizing_function {
GenericGridTemplateComponent::Single(sizing_function) => {
tracks.push(GridTrack::new(
sizing_function.min_sizing_function(),
sizing_function.max_sizing_function(),
));
tracks.push(GridTrack::gutter(gap));
current_track_index += 1;
}
GenericGridTemplateComponent::Repeat(repeat) => match repeat.count() {
RepetitionCount::Count(count) => {
let track_iter = repeat.tracks();
let track_iter = track_iter.cycle().take(repeat.track_count() as usize * count as usize);
track_iter.for_each(|sizing_function| {
tracks.push(GridTrack::new(
sizing_function.min_sizing_function(),
sizing_function.max_sizing_function(),
));
tracks.push(GridTrack::gutter(gap));
current_track_index += 1;
});
}
RepetitionCount::AutoFit | RepetitionCount::AutoFill => {
let auto_repeated_track_count = (counts.explicit - non_auto_repeating_track_count) as usize;
let iter = repeat.tracks().cycle();
for track_def in iter.take(auto_repeated_track_count) {
let mut track =
GridTrack::new(track_def.min_sizing_function(), track_def.max_sizing_function());
let mut gutter = GridTrack::gutter(gap);
// Auto-fit tracks that don't contain should be collapsed.
if repeat.count() == RepetitionCount::AutoFit && !track_has_items(current_track_index) {
track.collapse();
gutter.collapse();
}
tracks.push(track);
tracks.push(gutter);
current_track_index += 1;
}
// Is this auto-repeat at the very end of the track list?
let is_last = current_track_index == counts.len();
// When collapsing auto-fit tracks (in the loop just above), we collapse the gutter after collapsed
// tracks but not the gutter before. This is correct so long as there is another non-collapsed track
// somewhere in the track list after the collapsed track.
//
// However, if the auto-fit repeat is at the very end of the track list, then there will be no such track.
// In this case we need to iterate backwards through the track list until we find a non-collapsed track and
// collapse the gutter just after that track.
//
// As all of the other tracks and gutters we find along the way will already be collapsed, we simplify the
// implementation to collapsing all tracks and gutters until we find the first non-collapsed track.
if repeat.count() == RepetitionCount::AutoFit && is_last {
for previous_track in tracks.iter_mut().rev() {
if previous_track.kind == GridTrackKind::Track && !previous_track.is_collapsed {
break;
}
previous_track.collapse();
}
}
}
},
}
});
}
}
let grid_area_tracks = (counts.negative_implicit + counts.explicit) - current_track_index as u16;
// Create positive implicit tracks
if auto_track_count == 0 {
let iter = core::iter::repeat(TrackSizingFunction::AUTO);
create_implicit_tracks(tracks, counts.positive_implicit + grid_area_tracks, iter, gap)
} else {
let iter = auto_tracks.clone().cycle();
create_implicit_tracks(tracks, counts.positive_implicit + grid_area_tracks, iter, gap)
}
// Mark first and last grid lines as collapsed
tracks.first_mut().unwrap().collapse();
tracks.last_mut().unwrap().collapse();
}
/// Utility function for repeating logic of creating implicit tracks
fn create_implicit_tracks(
tracks: &mut Vec<GridTrack>,
count: u16,
mut auto_tracks_iter: impl Iterator<Item = TrackSizingFunction>,
gap: LengthPercentage,
) {
for _ in 0..count {
let track_def = auto_tracks_iter.next().unwrap();
tracks.push(GridTrack::new(track_def.min_sizing_function(), track_def.max_sizing_function()));
tracks.push(GridTrack::gutter(gap));
}
}
#[cfg(test)]
mod test {
use super::compute_explicit_grid_size_in_axis;
use super::initialize_grid_tracks;
use crate::compute::grid::explicit_grid::AutoRepeatStrategy;
use crate::compute::grid::types::GridTrackKind;
use crate::compute::grid::types::TrackCounts;
use crate::compute::grid::util::*;
use crate::geometry::AbsoluteAxis;
use crate::prelude::*;
use crate::sys::DefaultCheapStr;
#[test]
fn explicit_grid_sizing_no_repeats() {
let grid_style = (600.0, 600.0, 2, 4).into_grid();
let preferred_size = grid_style.size.map(|s| s.into_option());
let (auto_col_reps, col_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Horizontal),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Horizontal,
);
let (auto_row_reps, row_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Vertical),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Vertical,
);
assert_eq!(col_count, 2);
assert_eq!(row_count, 4);
assert_eq!(auto_col_reps, 0);
assert_eq!(auto_row_reps, 0);
}
#[test]
fn explicit_grid_sizing_auto_fill_exact_fit() {
use RepetitionCount::AutoFill;
let grid_style: Style<DefaultCheapStr> = Style {
display: Display::Grid,
size: Size { width: length(120.0), height: length(80.0) },
grid_template_columns: vec![repeat(AutoFill, vec![length(40.0)])],
grid_template_rows: vec![repeat(AutoFill, vec![length(20.0)])],
..Default::default()
};
let preferred_size = grid_style.size.map(|s| s.into_option());
let (auto_col_reps, col_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Horizontal),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Horizontal,
);
let (auto_row_reps, row_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Vertical),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Vertical,
);
assert_eq!(col_count, 3);
assert_eq!(row_count, 4);
assert_eq!(auto_col_reps, 3);
assert_eq!(auto_row_reps, 4);
}
#[test]
fn explicit_grid_sizing_auto_fill_non_exact_fit() {
use RepetitionCount::AutoFill;
let grid_style: Style<DefaultCheapStr> = Style {
display: Display::Grid,
size: Size { width: length(140.0), height: length(90.0) },
grid_template_columns: vec![repeat(AutoFill, vec![length(40.0)])],
grid_template_rows: vec![repeat(AutoFill, vec![length(20.0)])],
..Default::default()
};
let preferred_size = grid_style.size.map(|s| s.into_option());
let (auto_col_reps, col_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Horizontal),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Horizontal,
);
let (auto_row_reps, row_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Vertical),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Vertical,
);
assert_eq!(col_count, 3);
assert_eq!(row_count, 4);
assert_eq!(auto_col_reps, 3);
assert_eq!(auto_row_reps, 4);
}
#[test]
fn explicit_grid_sizing_auto_fill_min_size_exact_fit() {
use RepetitionCount::AutoFill;
let grid_style: Style<DefaultCheapStr> = Style {
display: Display::Grid,
min_size: Size { width: length(120.0), height: length(80.0) },
grid_template_columns: vec![repeat(AutoFill, vec![length(40.0)])],
grid_template_rows: vec![repeat(AutoFill, vec![length(20.0)])],
..Default::default()
};
let inner_container_size = Size { width: Some(120.0), height: Some(80.0) };
let (auto_col_reps, col_count) = compute_explicit_grid_size_in_axis(
&grid_style,
inner_container_size.get_abs(AbsoluteAxis::Horizontal),
AutoRepeatStrategy::MinRepetitionsThatDoOverflow,
|_, _| 42.42,
AbsoluteAxis::Horizontal,
);
let (auto_row_reps, row_count) = compute_explicit_grid_size_in_axis(
&grid_style,
inner_container_size.get_abs(AbsoluteAxis::Vertical),
AutoRepeatStrategy::MinRepetitionsThatDoOverflow,
|_, _| 42.42,
AbsoluteAxis::Vertical,
);
assert_eq!(col_count, 3);
assert_eq!(row_count, 4);
assert_eq!(auto_col_reps, 3);
assert_eq!(auto_row_reps, 4);
}
#[test]
fn explicit_grid_sizing_auto_fill_min_size_non_exact_fit() {
use RepetitionCount::AutoFill;
let grid_style: Style<DefaultCheapStr> = Style {
display: Display::Grid,
min_size: Size { width: length(140.0), height: length(90.0) },
grid_template_columns: vec![repeat(AutoFill, vec![length(40.0)])],
grid_template_rows: vec![repeat(AutoFill, vec![length(20.0)])],
..Default::default()
};
let inner_container_size = Size { width: Some(140.0), height: Some(90.0) };
let (auto_col_reps, col_count) = compute_explicit_grid_size_in_axis(
&grid_style,
inner_container_size.get_abs(AbsoluteAxis::Horizontal),
AutoRepeatStrategy::MinRepetitionsThatDoOverflow,
|_, _| 42.42,
AbsoluteAxis::Horizontal,
);
let (auto_row_reps, row_count) = compute_explicit_grid_size_in_axis(
&grid_style,
inner_container_size.get_abs(AbsoluteAxis::Vertical),
AutoRepeatStrategy::MinRepetitionsThatDoOverflow,
|_, _| 42.42,
AbsoluteAxis::Vertical,
);
assert_eq!(col_count, 4);
assert_eq!(row_count, 5);
assert_eq!(auto_col_reps, 4);
assert_eq!(auto_row_reps, 5);
}
#[test]
fn explicit_grid_sizing_auto_fill_multiple_repeated_tracks() {
use RepetitionCount::AutoFill;
let grid_style: Style<DefaultCheapStr> = Style {
display: Display::Grid,
size: Size { width: length(140.0), height: length(100.0) },
grid_template_columns: vec![repeat(AutoFill, vec![length(40.0), length(20.0)])],
grid_template_rows: vec![repeat(AutoFill, vec![length(20.0), length(10.0)])],
..Default::default()
};
let preferred_size = grid_style.size.map(|s| s.into_option());
let (auto_col_reps, col_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Horizontal),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Horizontal,
);
let (auto_row_reps, row_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Vertical),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Vertical,
);
assert_eq!(col_count, 4); // 2 repetitions * 2 repeated tracks = 4 tracks in total
assert_eq!(row_count, 6); // 3 repetitions * 2 repeated tracks = 4 tracks in total
assert_eq!(auto_col_reps, 2);
assert_eq!(auto_row_reps, 3);
}
#[test]
fn explicit_grid_sizing_auto_fill_gap() {
use RepetitionCount::AutoFill;
let grid_style: Style<DefaultCheapStr> = Style {
display: Display::Grid,
size: Size { width: length(140.0), height: length(100.0) },
grid_template_columns: vec![repeat(AutoFill, vec![length(40.0)])],
grid_template_rows: vec![repeat(AutoFill, vec![length(20.0)])],
gap: length(20.0),
..Default::default()
};
let preferred_size = grid_style.size.map(|s| s.into_option());
let (auto_col_reps, col_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Horizontal),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Horizontal,
);
let (auto_row_reps, row_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Vertical),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Vertical,
);
assert_eq!(col_count, 2); // 2 tracks + 1 gap
assert_eq!(row_count, 3); // 3 tracks + 2 gaps
assert_eq!(auto_col_reps, 2);
assert_eq!(auto_row_reps, 3);
}
#[test]
fn explicit_grid_sizing_no_defined_size() {
use RepetitionCount::AutoFill;
let grid_style: Style<DefaultCheapStr> = Style {
display: Display::Grid,
grid_template_columns: vec![repeat(AutoFill, vec![length(40.0), percent(0.5), length(20.0)])],
grid_template_rows: vec![repeat(AutoFill, vec![length(20.0)])],
gap: length(20.0),
..Default::default()
};
let preferred_size = grid_style.size.map(|s| s.into_option());
let (auto_col_reps, col_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Horizontal),
AutoRepeatStrategy::MinRepetitionsThatDoOverflow,
|_, _| 42.42,
AbsoluteAxis::Horizontal,
);
let (auto_row_reps, row_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Vertical),
AutoRepeatStrategy::MinRepetitionsThatDoOverflow,
|_, _| 42.42,
AbsoluteAxis::Vertical,
);
assert_eq!(col_count, 3);
assert_eq!(row_count, 1);
assert_eq!(auto_col_reps, 1);
assert_eq!(auto_row_reps, 1);
}
#[test]
fn explicit_grid_sizing_mix_repeated_and_non_repeated() {
use RepetitionCount::AutoFill;
let grid_style: Style<DefaultCheapStr> = Style {
display: Display::Grid,
size: Size { width: length(140.0), height: length(100.0) },
grid_template_columns: vec![length(20.0), repeat(AutoFill, vec![length(40.0)])],
grid_template_rows: vec![length(40.0), repeat(AutoFill, vec![length(20.0)])],
gap: length(20.0),
..Default::default()
};
let preferred_size = grid_style.size.map(|s| s.into_option());
let (auto_col_reps, col_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Horizontal),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Horizontal,
);
let (auto_row_reps, row_count) = compute_explicit_grid_size_in_axis(
&grid_style,
preferred_size.get_abs(AbsoluteAxis::Vertical),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Vertical,
);
assert_eq!(col_count, 3); // 3 tracks + 2 gaps
assert_eq!(row_count, 2); // 2 tracks + 1 gap
assert_eq!(auto_col_reps, 2);
assert_eq!(auto_row_reps, 1);
}
#[test]
fn explicit_grid_sizing_mix_with_padding() {
use RepetitionCount::AutoFill;
let grid_style: Style<DefaultCheapStr> = Style {
display: Display::Grid,
size: Size { width: length(120.0), height: length(120.0) },
padding: Rect { left: length(10.0), right: length(10.0), top: length(20.0), bottom: length(20.0) },
grid_template_columns: vec![repeat(AutoFill, vec![length(20.0)])],
grid_template_rows: vec![repeat(AutoFill, vec![length(20.0)])],
..Default::default()
};
let inner_container_size = Size { width: Some(100.0), height: Some(80.0) };
let (auto_col_reps, col_count) = compute_explicit_grid_size_in_axis(
&grid_style,
inner_container_size.get_abs(AbsoluteAxis::Horizontal),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Horizontal,
);
let (auto_row_reps, row_count) = compute_explicit_grid_size_in_axis(
&grid_style,
inner_container_size.get_abs(AbsoluteAxis::Vertical),
AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
|_, _| 42.42,
AbsoluteAxis::Vertical,
);
assert_eq!(col_count, 5); // 40px horizontal padding
assert_eq!(row_count, 4); // 20px vertical padding
assert_eq!(auto_col_reps, 5);
assert_eq!(auto_row_reps, 4);
}
#[test]
fn test_initialize_grid_tracks() {
let minpx0 = MinTrackSizingFunction::from_length(0.0);
let minpx20 = MinTrackSizingFunction::from_length(20.0);
let minpx100 = MinTrackSizingFunction::from_length(100.0);
let maxpx0 = MaxTrackSizingFunction::from_length(0.0);
let maxpx20 = MaxTrackSizingFunction::from_length(20.0);
let maxpx100 = MaxTrackSizingFunction::from_length(100.0);
// Setup test
let grid_style: Style<DefaultCheapStr> = Style {
display: Display::Grid,
gap: length(20.0),
grid_template_columns: vec![length(100.0), minmax(length(100.0), fr(2.0)), fr(1.0)],
grid_auto_columns: vec![auto(), length(100.0)],
..Default::default()
};
let track_counts = TrackCounts {
negative_implicit: 3,
explicit: grid_style.grid_template_columns.len() as u16,
positive_implicit: 3,
};
// Call function
let mut tracks = Vec::new();
initialize_grid_tracks(&mut tracks, track_counts, &grid_style, AbsoluteAxis::Horizontal, |_| false);
// Assertions
let expected = vec![
// Gutter
(GridTrackKind::Gutter, minpx0, maxpx0),
// Negative implicit tracks
(GridTrackKind::Track, minpx100, maxpx100),
(GridTrackKind::Gutter, minpx20, maxpx20),
(GridTrackKind::Track, auto(), auto()),
(GridTrackKind::Gutter, minpx20, maxpx20),
(GridTrackKind::Track, minpx100, maxpx100),
(GridTrackKind::Gutter, minpx20, maxpx20),
// Explicit tracks
(GridTrackKind::Track, minpx100, maxpx100),
(GridTrackKind::Gutter, minpx20, maxpx20),
(GridTrackKind::Track, minpx100, MaxTrackSizingFunction::from_fr(2.0)), // Note: separate min-max functions
(GridTrackKind::Gutter, minpx20, maxpx20),
(GridTrackKind::Track, auto(), MaxTrackSizingFunction::from_fr(1.0)), // Note: min sizing function of flex sizing functions is AUTO
(GridTrackKind::Gutter, minpx20, maxpx20),
// Positive implicit tracks
(GridTrackKind::Track, auto(), auto()),
(GridTrackKind::Gutter, minpx20, maxpx20),
(GridTrackKind::Track, minpx100, maxpx100),
(GridTrackKind::Gutter, minpx20, maxpx20),
(GridTrackKind::Track, auto(), auto()),
(GridTrackKind::Gutter, minpx0, maxpx0),
];
assert_eq!(tracks.len(), expected.len(), "Number of tracks doesn't match");
for (idx, (actual, (kind, min, max))) in tracks.into_iter().zip(expected).enumerate() {
assert_eq!(actual.kind, kind, "Track {idx} (0-based index)");
assert_eq!(actual.min_track_sizing_function, min, "Track {idx} (0-based index)");
assert_eq!(actual.max_track_sizing_function, max, "Track {idx} (0-based index)");
}
}
}
+253
View File
@@ -0,0 +1,253 @@
//! This module is not required for spec compliance, but is used as a performance optimisation
//! to reduce the number of allocations required when creating a grid.
use crate::geometry::Line;
use crate::style::{GenericGridPlacement, GridPlacement};
use crate::{CheapCloneStr, Direction, GridItemStyle};
use core::cmp::{max, min};
use super::types::TrackCounts;
use super::OriginZeroLine;
/// Estimate the number of rows and columns in the grid
/// This is used as a performance optimisation to pre-size vectors and reduce allocations. It also forms a necessary step
/// in the auto-placement
/// - The estimates for the explicit and negative implicit track counts are exact.
/// - However, the estimates for the positive explicit track count is a lower bound as auto-placement can affect this
/// in ways which are impossible to predict until the auto-placement algorithm is run.
///
/// Note that this function internally mixes use of grid track numbers and grid line numbers
pub(crate) fn compute_grid_size_estimate<'a, S: GridItemStyle + 'a>(
explicit_col_count: u16,
explicit_row_count: u16,
direction: Direction,
child_styles_iter: impl Iterator<Item = S>,
) -> (TrackCounts, TrackCounts) {
// Iterate over children, producing an estimate of the min and max grid lines (in origin-zero coordinates where)
// along with the span of each item
let (col_min, col_max, col_max_span, row_min, row_max, row_max_span) =
get_known_child_positions(child_styles_iter, explicit_col_count, explicit_row_count, direction);
// Compute *track* count estimates for each axis from:
// - The explicit track counts
// - The origin-zero coordinate min and max grid line variables
let negative_implicit_inline_tracks = col_min.implied_negative_implicit_tracks();
let explicit_inline_tracks = explicit_col_count;
let mut positive_implicit_inline_tracks = col_max.implied_positive_implicit_tracks(explicit_col_count);
let negative_implicit_block_tracks = row_min.implied_negative_implicit_tracks();
let explicit_block_tracks = explicit_row_count;
let mut positive_implicit_block_tracks = row_max.implied_positive_implicit_tracks(explicit_row_count);
// In each axis, adjust positive track estimate if any items have a span that does not fit within
// the total number of tracks in the estimate
let tot_inline_tracks = negative_implicit_inline_tracks + explicit_inline_tracks + positive_implicit_inline_tracks;
if tot_inline_tracks < col_max_span {
positive_implicit_inline_tracks = col_max_span - explicit_inline_tracks - negative_implicit_inline_tracks;
}
let tot_block_tracks = negative_implicit_block_tracks + explicit_block_tracks + positive_implicit_block_tracks;
if tot_block_tracks < row_max_span {
positive_implicit_block_tracks = row_max_span - explicit_block_tracks - negative_implicit_block_tracks;
}
let column_counts =
TrackCounts::from_raw(negative_implicit_inline_tracks, explicit_inline_tracks, positive_implicit_inline_tracks);
let row_counts =
TrackCounts::from_raw(negative_implicit_block_tracks, explicit_block_tracks, positive_implicit_block_tracks);
(column_counts, row_counts)
}
/// Iterate over children, producing an estimate of the min and max grid *lines* along with the span of each item
///
/// Min and max grid lines are returned in origin-zero coordinates)
/// The span is measured in tracks spanned
fn get_known_child_positions<'a, S: GridItemStyle + 'a>(
children_iter: impl Iterator<Item = S>,
explicit_col_count: u16,
explicit_row_count: u16,
direction: Direction,
) -> (OriginZeroLine, OriginZeroLine, u16, OriginZeroLine, OriginZeroLine, u16) {
let (mut col_min, mut col_max, mut col_max_span) = (OriginZeroLine(0), OriginZeroLine(0), 0);
let (mut row_min, mut row_max, mut row_max_span) = (OriginZeroLine(0), OriginZeroLine(0), 0);
children_iter.for_each(|child_style| {
let col_line = child_style.grid_column();
let row_line = child_style.grid_row();
// Note: that the children reference the lines in between (and around) the tracks not tracks themselves,
// and thus we must subtract 1 to get an accurate estimate of the number of tracks
let (mut child_col_min, mut child_col_max, child_col_span) =
child_min_line_max_line_span::<S::CustomIdent>(col_line, explicit_col_count);
let (child_row_min, child_row_max, child_row_span) =
child_min_line_max_line_span::<S::CustomIdent>(row_line, explicit_row_count);
// Placement mirrors horizontal spans in RTL, so mirror known column line bounds here
// to keep implicit-grid pre-sizing consistent with actual placement.
if direction.is_rtl() && (child_col_min != OriginZeroLine(0) || child_col_max != OriginZeroLine(0)) {
let explicit_col_end_line = explicit_col_count as i16;
let mirrored_min = OriginZeroLine(explicit_col_end_line - child_col_max.0);
let mirrored_max = OriginZeroLine(explicit_col_end_line - child_col_min.0);
child_col_min = mirrored_min;
child_col_max = mirrored_max;
}
col_min = min(col_min, child_col_min);
col_max = max(col_max, child_col_max);
col_max_span = max(col_max_span, child_col_span);
row_min = min(row_min, child_row_min);
row_max = max(row_max, child_row_max);
row_max_span = max(row_max_span, child_row_span);
});
(col_min, col_max, col_max_span, row_min, row_max, row_max_span)
}
/// Helper function for `compute_grid_size_estimate`
/// Produces a conservative estimate of the greatest and smallest grid lines used by a single grid item
///
/// Values are returned in origin-zero coordinates
#[inline]
fn child_min_line_max_line_span<S: CheapCloneStr>(
line: Line<GridPlacement<S>>,
explicit_track_count: u16,
) -> (OriginZeroLine, OriginZeroLine, u16) {
use GenericGridPlacement::*;
// 8.3.1. Grid Placement Conflict Handling
// A. If the placement for a grid item contains two lines, and the start line is further end-ward than the end line, swap the two lines.
// B. If the start line is equal to the end line, remove the end line.
// C. If the placement contains two spans, remove the one contributed by the end grid-placement property.
// D. If the placement contains only a span for a named line, replace it with a span of 1.
// Convert line into origin-zero coordinates before attempting to analyze
// We ignore named lines here as they are accounted for separately
let oz_line = line.into_origin_zero_ignoring_named(explicit_track_count);
let min = match (oz_line.start, oz_line.end) {
// Both tracks specified
(Line(track1), Line(track2)) => {
// See rules A and B above
if track1 == track2 {
track1
} else {
min(track1, track2)
}
}
// Start track specified
(Line(track), Auto) => track,
(Line(track), Span(_)) => track,
// End track specified
(Auto, Line(track)) => track,
(Span(span), Line(track)) => track - span,
// Only spans or autos
// We ignore spans here by returning 0 which never effect the estimate as these are accounted for separately
(Auto | Span(_), Auto | Span(_)) => OriginZeroLine(0),
};
let max = match (oz_line.start, oz_line.end) {
// Both tracks specified
(Line(track1), Line(track2)) => {
// See rules A and B above
if track1 == track2 {
track1 + 1
} else {
max(track1, track2)
}
}
// Start track specified
(Line(track), Auto) => track + 1,
(Line(track), Span(span)) => track + span,
// End track specified
(Auto, Line(track)) => track,
(Span(_), Line(track)) => track,
// Only spans or autos
// We ignore spans here by returning 0 which never effect the estimate as these are accounted for separately
(Auto | Span(_), Auto | Span(_)) => OriginZeroLine(0),
};
// Calculate span only for indefinitely placed items as we don't need for other items (whose required space will
// be taken into account by min and max)
let span = match (oz_line.start, oz_line.end) {
(Auto | Span(_), Auto | Span(_)) => oz_line.indefinite_span(),
_ => 1,
};
(min, max, span)
}
#[allow(clippy::bool_assert_comparison)]
#[cfg(test)]
mod tests {
mod test_child_min_max_line {
type S = String;
use super::super::child_min_line_max_line_span;
use super::super::OriginZeroLine;
use crate::geometry::Line;
use crate::style_helpers::*;
#[test]
fn child_min_max_line_auto() {
let (min_col, max_col, span) = child_min_line_max_line_span::<S>(Line { start: line(5), end: span(6) }, 6);
assert_eq!(min_col, OriginZeroLine(4));
assert_eq!(max_col, OriginZeroLine(10));
assert_eq!(span, 1);
}
#[test]
fn child_min_max_line_negative_track() {
let (min_col, max_col, span) = child_min_line_max_line_span::<S>(Line { start: line(-5), end: span(3) }, 6);
assert_eq!(min_col, OriginZeroLine(2));
assert_eq!(max_col, OriginZeroLine(5));
assert_eq!(span, 1);
}
}
mod test_initial_grid_sizing {
use super::super::compute_grid_size_estimate;
use crate::compute::grid::util::test_helpers::*;
use crate::style_helpers::*;
use crate::Direction;
#[test]
fn explicit_grid_sizing_with_children() {
let explicit_col_count = 6;
let explicit_row_count = 8;
let child_styles = [
(line(1), span(2), line(2), auto()).into_grid_child(),
(line(-4), auto(), line(-2), auto()).into_grid_child(),
];
let (inline, block) =
compute_grid_size_estimate(explicit_col_count, explicit_row_count, Direction::Ltr, child_styles.iter());
assert_eq!(inline.negative_implicit, 0);
assert_eq!(inline.explicit, explicit_col_count);
assert_eq!(inline.positive_implicit, 0);
assert_eq!(block.negative_implicit, 0);
assert_eq!(block.explicit, explicit_row_count);
assert_eq!(block.positive_implicit, 0);
}
#[test]
fn negative_implicit_grid_sizing() {
let explicit_col_count = 4;
let explicit_row_count = 4;
let child_styles = [
(line(-6), span(2), line(-8), auto()).into_grid_child(),
(line(4), auto(), line(3), auto()).into_grid_child(),
];
let (inline, block) =
compute_grid_size_estimate(explicit_col_count, explicit_row_count, Direction::Ltr, child_styles.iter());
assert_eq!(inline.negative_implicit, 1);
assert_eq!(inline.explicit, explicit_col_count);
assert_eq!(inline.positive_implicit, 0);
assert_eq!(block.negative_implicit, 3);
assert_eq!(block.explicit, explicit_row_count);
assert_eq!(block.positive_implicit, 0);
}
}
}
+897
View File
@@ -0,0 +1,897 @@
//! This module is a partial implementation of the CSS Grid Level 1 specification
//! <https://www.w3.org/TR/css-grid-1>
use crate::geometry::{AbsoluteAxis, AbstractAxis, InBothAbsAxis};
use crate::geometry::{Line, Point, Rect, Size};
use crate::style::{AlignItems, AlignSelf, AvailableSpace, Overflow, Position};
use crate::tree::{Layout, LayoutInput, LayoutOutput, LayoutPartialTreeExt, NodeId, RunMode, SizingMode};
use crate::util::debug::debug_log;
use crate::util::sys::{f32_max, f32_min, GridTrackVec, Vec};
use crate::util::MaybeMath;
use crate::util::{MaybeResolve, ResolveOrZero};
use crate::{
style_helpers::*, AlignContent, BoxGenerationMode, BoxSizing, CoreStyle, Direction, GridContainerStyle,
GridItemStyle, JustifyContent, LayoutGridContainer, RequestedAxis,
};
use alignment::{align_and_position_item, align_tracks};
use explicit_grid::{compute_explicit_grid_size_in_axis, initialize_grid_tracks, AutoRepeatStrategy};
use implicit_grid::compute_grid_size_estimate;
use placement::place_grid_items;
use track_sizing::{
determine_if_item_crosses_flexible_or_intrinsic_tracks, resolve_item_track_indexes, track_sizing_algorithm,
};
use types::{CellOccupancyMatrix, GridTrack, NamedLineResolver, TrackCounts};
#[cfg(feature = "detailed_layout_info")]
use types::{GridItem, GridTrackKind};
pub(crate) use types::{GridCoordinate, GridLine, OriginZeroLine};
mod alignment;
mod explicit_grid;
mod implicit_grid;
mod placement;
mod track_sizing;
mod types;
mod util;
/// Grid layout algorithm
/// This consists of a few phases:
/// - Resolving the explicit grid
/// - Placing items (which also resolves the implicit grid)
/// - Track (row/column) sizing
/// - Alignment & Final item placement
pub fn compute_grid_layout<Tree: LayoutGridContainer>(
tree: &mut Tree,
node: NodeId,
inputs: LayoutInput,
) -> LayoutOutput {
let LayoutInput { known_dimensions, parent_size, available_space, run_mode, .. } = inputs;
let style = tree.get_grid_container_style(node);
let direction = style.direction();
// 1. Compute "available grid space"
// https://www.w3.org/TR/css-grid-1/#available-grid-space
let aspect_ratio = style.aspect_ratio();
let padding = style.padding().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
let border = style.border().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
let padding_border = padding + border;
let padding_border_size = padding_border.sum_axes();
let box_sizing_adjustment =
if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
let min_size = style
.min_size()
.maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
let max_size = style
.max_size()
.maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
let preferred_size = if inputs.sizing_mode == SizingMode::InherentSize {
style
.size()
.maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(style.aspect_ratio())
.maybe_add(box_sizing_adjustment)
} else {
Size::NONE
};
// 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,
});
let mut content_box_inset = padding_border;
content_box_inset.bottom += scrollbar_gutter.y;
match direction {
Direction::Ltr => content_box_inset.right += scrollbar_gutter.x,
Direction::Rtl => content_box_inset.left += scrollbar_gutter.x,
};
let align_content = style.align_content().unwrap_or(AlignContent::STRETCH);
let justify_content = style.justify_content().unwrap_or(JustifyContent::STRETCH);
let align_items = style.align_items();
let justify_items = style.justify_items();
// Note: we avoid accessing the grid rows/columns methods more than once as this can
// cause an expensive-ish computation
let grid_template_columns = style.grid_template_columns();
let grid_template_rows = style.grid_template_rows();
let grid_auto_columns = style.grid_auto_columns();
let grid_auto_rows = style.grid_auto_rows();
let constrained_available_space = known_dimensions
.or(preferred_size)
.map(|size| size.map(AvailableSpace::Definite))
.unwrap_or(available_space)
.maybe_clamp(min_size, max_size)
.maybe_max(padding_border_size);
let available_grid_space = Size {
width: constrained_available_space
.width
.map_definite_value(|space| space - content_box_inset.horizontal_axis_sum()),
height: constrained_available_space
.height
.map_definite_value(|space| space - content_box_inset.vertical_axis_sum()),
};
let outer_node_size =
known_dimensions.or(preferred_size).maybe_clamp(min_size, max_size).maybe_max(padding_border_size);
let mut inner_node_size = Size {
width: outer_node_size.width.map(|space| space - content_box_inset.horizontal_axis_sum()),
height: outer_node_size.height.map(|space| space - content_box_inset.vertical_axis_sum()),
};
debug_log!("parent_size", dbg:parent_size);
debug_log!("outer_node_size", dbg:outer_node_size);
debug_log!("inner_node_size", dbg:inner_node_size);
// Short-circuit layout if the container's size is fully determined by the container's size and the run mode
// is ComputeSize (and thus the container's size is all that we're interested in)
if run_mode == RunMode::ComputeSize {
if let Size { width: Some(width), height: Some(height) } = outer_node_size {
return LayoutOutput::from_outer_size(Size { width, height });
}
// We can also short-circuit if the width is known and only the width has been requested.
if inputs.axis == RequestedAxis::Horizontal {
if let Some(width) = outer_node_size.width {
return LayoutOutput::from_outer_size(Size { width, height: 0.0 });
}
}
}
let get_child_styles_iter =
|node| tree.child_ids(node).map(|child_node: NodeId| tree.get_grid_child_style(child_node));
let child_styles_iter = get_child_styles_iter(node);
// 2. Resolve the explicit grid
// This is very similar to the inner_node_size except if the inner_node_size is not definite but the node
// has a min- or max- size style then that will be used in it's place.
let auto_fit_container_size = outer_node_size
.or(max_size)
.or(min_size)
.maybe_clamp(min_size, max_size)
.maybe_max(padding_border_size)
.maybe_sub(content_box_inset.sum_axes());
// If the grid container has a definite size or max size in the relevant axis:
// - then the number of repetitions is the largest possible positive integer that does not cause the grid to overflow the content
// box of its grid container.
// Otherwise, if the grid container has a definite min size in the relevant axis:
// - then the number of repetitions is the smallest possible positive integer that fulfills that minimum requirement
// Otherwise, the specified track list repeats only once.
let auto_repeat_fit_strategy = outer_node_size.or(max_size).map(|val| match val {
Some(_) => AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
None => AutoRepeatStrategy::MinRepetitionsThatDoOverflow,
});
// Compute the number of rows and columns in the explicit grid *template*
// (explicit tracks from grid_areas are computed separately below)
let (col_auto_repetition_count, grid_template_col_count) = compute_explicit_grid_size_in_axis(
&style,
auto_fit_container_size.width,
auto_repeat_fit_strategy.width,
|val, basis| tree.calc(val, basis),
AbsoluteAxis::Horizontal,
);
let (row_auto_repetition_count, grid_template_row_count) = compute_explicit_grid_size_in_axis(
&style,
auto_fit_container_size.height,
auto_repeat_fit_strategy.height,
|val, basis| tree.calc(val, basis),
AbsoluteAxis::Vertical,
);
// type CustomIdent<'a> = <<Tree as LayoutPartialTree>::CoreContainerStyle<'_> as CoreStyle>::CustomIdent;
let mut name_resolver = NamedLineResolver::new(&style, col_auto_repetition_count, row_auto_repetition_count);
let explicit_col_count = grid_template_col_count.max(name_resolver.area_column_count());
let explicit_row_count = grid_template_row_count.max(name_resolver.area_row_count());
name_resolver.set_explicit_column_count(explicit_col_count);
name_resolver.set_explicit_row_count(explicit_row_count);
// 3. Implicit Grid: Estimate Track Counts
// Estimate the number of rows and columns in the implicit grid (= the entire grid)
// This is necessary as part of placement. Doing it early here is a perf optimisation to reduce allocations.
let (est_col_counts, est_row_counts) =
compute_grid_size_estimate(explicit_col_count, explicit_row_count, direction, child_styles_iter);
// 4. Grid Item Placement
// Match items (children) to a definite grid position (row start/end and column start/end position)
let mut items = Vec::with_capacity(tree.child_count(node));
let mut cell_occupancy_matrix = CellOccupancyMatrix::with_track_counts(est_col_counts, est_row_counts);
let in_flow_children_iter = || {
tree.child_ids(node)
.enumerate()
.map(|(index, child_node)| (index, child_node, tree.get_grid_child_style(child_node)))
.filter(|(_, _, style)| {
style.box_generation_mode() != BoxGenerationMode::None && style.position() != Position::Absolute
})
};
place_grid_items(
&mut cell_occupancy_matrix,
&mut items,
in_flow_children_iter,
direction,
style.grid_auto_flow(),
align_items.unwrap_or(AlignItems::STRETCH),
justify_items.unwrap_or(AlignItems::STRETCH),
&name_resolver,
);
// Extract track counts from previous step (auto-placement can expand the number of tracks)
let final_col_counts = *cell_occupancy_matrix.track_counts(AbsoluteAxis::Horizontal);
let final_row_counts = *cell_occupancy_matrix.track_counts(AbsoluteAxis::Vertical);
// 5. Initialize Tracks
// Initialize (explicit and implicit) grid tracks (and gutters)
// This resolves the min and max track sizing functions for all tracks and gutters
let mut columns = GridTrackVec::new();
let mut rows = GridTrackVec::new();
let mut column_track_counts_for_init = final_col_counts;
if direction.is_rtl() && final_col_counts.explicit <= 1 {
column_track_counts_for_init.negative_implicit = final_col_counts.positive_implicit;
column_track_counts_for_init.positive_implicit = final_col_counts.negative_implicit;
}
initialize_grid_tracks(
&mut columns,
column_track_counts_for_init,
&style,
AbsoluteAxis::Horizontal,
|column_index| {
let occupancy_index = if direction.is_rtl() {
rtl_column_occupancy_index_for_initialization(column_index, final_col_counts)
} else {
column_index
};
cell_occupancy_matrix.column_is_occupied(occupancy_index)
},
);
initialize_grid_tracks(&mut rows, final_row_counts, &style, AbsoluteAxis::Vertical, |row_index| {
cell_occupancy_matrix.row_is_occupied(row_index)
});
if direction.is_rtl() {
reverse_non_gutter_tracks(&mut columns, final_col_counts);
}
drop(grid_template_rows);
drop(grid_template_columns);
drop(grid_auto_rows);
drop(grid_auto_columns);
drop(style);
// 6. Track Sizing
// Convert grid placements in origin-zero coordinates to indexes into the GridTrack (rows and columns) vectors
// This computation is relatively trivial, but it requires the final number of negative (implicit) tracks in
// each axis, and doing it up-front here means we don't have to keep repeating that calculation
resolve_item_track_indexes(&mut items, final_col_counts, final_row_counts);
// For each item, and in each axis, determine whether the item crosses any flexible (fr) tracks
// Record this as a boolean (per-axis) on each item for later use in the track-sizing algorithm
determine_if_item_crosses_flexible_or_intrinsic_tracks(&mut items, &columns, &rows);
// Determine if the grid has any baseline aligned items
let has_baseline_aligned_item = items.iter().any(|item| item.align_self == AlignSelf::BASELINE);
// Run track sizing algorithm for Inline axis
track_sizing_algorithm(
tree,
AbstractAxis::Inline,
min_size.get(AbstractAxis::Inline),
max_size.get(AbstractAxis::Inline),
justify_content,
align_content,
available_grid_space,
inner_node_size,
&mut columns,
&mut rows,
&mut items,
|track: &GridTrack, parent_size: Option<f32>, tree: &Tree| {
track.max_track_sizing_function.definite_value(parent_size, |val, basis| tree.calc(val, basis))
},
has_baseline_aligned_item,
);
let initial_column_sum = columns.iter().map(|track| track.base_size).sum::<f32>();
inner_node_size.width = inner_node_size.width.or_else(|| initial_column_sum.into());
items.iter_mut().for_each(|item| item.grid_area_size_cache = None);
// Run track sizing algorithm for Block axis
track_sizing_algorithm(
tree,
AbstractAxis::Block,
min_size.get(AbstractAxis::Block),
max_size.get(AbstractAxis::Block),
align_content,
justify_content,
available_grid_space,
inner_node_size,
&mut rows,
&mut columns,
&mut items,
|track: &GridTrack, _, _| Some(track.base_size),
false, // TODO: Support baseline alignment in the vertical axis
);
let initial_row_sum = rows.iter().map(|track| track.base_size).sum::<f32>();
inner_node_size.height = inner_node_size.height.or_else(|| initial_row_sum.into());
debug_log!("initial_column_sum", dbg:initial_column_sum);
debug_log!(dbg: columns.iter().map(|track| track.base_size).collect::<Vec<_>>());
debug_log!("initial_row_sum", dbg:initial_row_sum);
debug_log!(dbg: rows.iter().map(|track| track.base_size).collect::<Vec<_>>());
// 6. Compute container size
let resolved_style_size = known_dimensions.or(preferred_size);
let mut container_border_box = Size {
width: resolved_style_size
.get(AbstractAxis::Inline)
.unwrap_or_else(|| initial_column_sum + content_box_inset.horizontal_axis_sum())
.maybe_clamp(min_size.width, max_size.width)
.max(padding_border_size.width),
height: resolved_style_size
.get(AbstractAxis::Block)
.unwrap_or_else(|| initial_row_sum + content_box_inset.vertical_axis_sum())
.maybe_clamp(min_size.height, max_size.height)
.max(padding_border_size.height),
};
let mut container_content_box = Size {
width: f32_max(0.0, container_border_box.width - content_box_inset.horizontal_axis_sum()),
height: f32_max(0.0, container_border_box.height - content_box_inset.vertical_axis_sum()),
};
// If only the container's size has been requested
if run_mode == RunMode::ComputeSize {
return LayoutOutput::from_outer_size(container_border_box);
}
// 7. Resolve percentage track base sizes
// In the case of an indefinitely sized container these resolve to zero during the "Initialise Tracks" step
// and therefore need to be re-resolved here based on the content-sized content box of the container
if !available_grid_space.width.is_definite() {
for column in &mut columns {
let min: Option<f32> = column
.min_track_sizing_function
.resolved_percentage_size(container_content_box.width, |val, basis| tree.calc(val, basis));
let max: Option<f32> = column
.max_track_sizing_function
.resolved_percentage_size(container_content_box.width, |val, basis| tree.calc(val, basis));
column.base_size = column.base_size.maybe_clamp(min, max);
}
}
if !available_grid_space.height.is_definite() {
for row in &mut rows {
let min: Option<f32> = row
.min_track_sizing_function
.resolved_percentage_size(container_content_box.height, |val, basis| tree.calc(val, basis));
let max: Option<f32> = row
.max_track_sizing_function
.resolved_percentage_size(container_content_box.height, |val, basis| tree.calc(val, basis));
row.base_size = row.base_size.maybe_clamp(min, max);
}
}
// Column sizing must be re-run (once) if:
// - The grid container's width was initially indefinite and there are any columns with percentage track sizing functions
// - Any grid item crossing an intrinsically sized track's min content contribution width has changed
// TODO: Only rerun sizing for tracks that actually require it rather than for all tracks if any need it.
let mut rerun_column_sizing;
let mut intrinsic_column_contribution_changed = false;
let has_percentage_column = columns.iter().any(|track| track.uses_percentage());
let has_percentage_row = rows.iter().any(|track| track.uses_percentage());
let parent_width_indefinite = !available_space.width.is_definite();
rerun_column_sizing = parent_width_indefinite && has_percentage_column;
if !rerun_column_sizing {
intrinsic_column_contribution_changed =
items.iter_mut().filter(|item| item.crosses_intrinsic_column).any(|item| {
let grid_area_size = item.grid_area_size(
AbstractAxis::Inline,
&columns,
&rows,
inner_node_size,
|track: &GridTrack, _| Some(track.base_size),
&|val, basis| tree.calc(val, basis),
);
let available_space = grid_area_size.with(AbstractAxis::Inline, None);
let new_min_content_contribution =
item.min_content_contribution(AbstractAxis::Inline, tree, grid_area_size, available_space);
let has_changed = Some(new_min_content_contribution) != item.min_content_contribution_cache.width;
item.grid_area_size_cache = Some(grid_area_size);
item.min_content_contribution_cache.width = Some(new_min_content_contribution);
item.max_content_contribution_cache.width = None;
item.minimum_contribution_cache.width = None;
has_changed
});
rerun_column_sizing = intrinsic_column_contribution_changed;
} else {
// Clear intrinsic width caches
items.iter_mut().for_each(|item| {
item.grid_area_size_cache = None;
item.min_content_contribution_cache.width = None;
item.max_content_contribution_cache.width = None;
item.minimum_contribution_cache.width = None;
});
}
let mut intrinsic_row_contribution_changed = false;
if rerun_column_sizing {
// Re-run track sizing algorithm for Inline axis
track_sizing_algorithm(
tree,
AbstractAxis::Inline,
min_size.get(AbstractAxis::Inline),
max_size.get(AbstractAxis::Inline),
justify_content,
align_content,
available_grid_space,
inner_node_size,
&mut columns,
&mut rows,
&mut items,
|track: &GridTrack, _, _| Some(track.base_size),
has_baseline_aligned_item,
);
// Row sizing must be re-run (once) if:
// - The grid container's height was initially indefinite and there are any rows with percentage track sizing functions
// - Any grid item crossing an intrinsically sized track's min content contribution height has changed
// TODO: Only rerun sizing for tracks that actually require it rather than for all tracks if any need it.
let mut rerun_row_sizing;
let parent_height_indefinite = !available_space.height.is_definite();
rerun_row_sizing = parent_height_indefinite && has_percentage_row;
if !rerun_row_sizing {
intrinsic_row_contribution_changed =
items.iter_mut().filter(|item| item.crosses_intrinsic_column).any(|item| {
let grid_area_size = item.grid_area_size(
AbstractAxis::Block,
&rows,
&columns,
inner_node_size,
|track: &GridTrack, _| Some(track.base_size),
&|val, basis| tree.calc(val, basis),
);
let available_space = grid_area_size.with(AbstractAxis::Block, None);
let new_min_content_contribution =
item.min_content_contribution(AbstractAxis::Block, tree, grid_area_size, available_space);
let has_changed = Some(new_min_content_contribution) != item.min_content_contribution_cache.height;
item.grid_area_size_cache = Some(grid_area_size);
item.min_content_contribution_cache.height = Some(new_min_content_contribution);
item.max_content_contribution_cache.height = None;
item.minimum_contribution_cache.height = None;
has_changed
});
rerun_row_sizing = intrinsic_row_contribution_changed;
} else {
items.iter_mut().for_each(|item| {
// Clear intrinsic height caches
item.grid_area_size_cache = None;
item.min_content_contribution_cache.height = None;
item.max_content_contribution_cache.height = None;
item.minimum_contribution_cache.height = None;
});
}
if rerun_row_sizing {
// Re-run track sizing algorithm for Block axis
track_sizing_algorithm(
tree,
AbstractAxis::Block,
min_size.get(AbstractAxis::Block),
max_size.get(AbstractAxis::Block),
align_content,
justify_content,
available_grid_space,
inner_node_size,
&mut rows,
&mut columns,
&mut items,
|track: &GridTrack, _, _| Some(track.base_size),
false, // TODO: Support baseline alignment in the vertical axis
);
}
}
if (intrinsic_column_contribution_changed && !has_percentage_column)
|| (intrinsic_row_contribution_changed && !has_percentage_row)
{
let final_column_sum = columns.iter().map(|track| track.base_size).sum::<f32>();
let final_row_sum = rows.iter().map(|track| track.base_size).sum::<f32>();
if intrinsic_column_contribution_changed && !has_percentage_column {
container_border_box.width = resolved_style_size
.get(AbstractAxis::Inline)
.unwrap_or_else(|| final_column_sum + content_box_inset.horizontal_axis_sum())
.maybe_clamp(min_size.width, max_size.width)
.max(padding_border_size.width);
container_content_box.width =
f32_max(0.0, container_border_box.width - content_box_inset.horizontal_axis_sum());
}
if intrinsic_row_contribution_changed && !has_percentage_row {
container_border_box.height = resolved_style_size
.get(AbstractAxis::Block)
.unwrap_or_else(|| final_row_sum + content_box_inset.vertical_axis_sum())
.maybe_clamp(min_size.height, max_size.height)
.max(padding_border_size.height);
container_content_box.height =
f32_max(0.0, container_border_box.height - content_box_inset.vertical_axis_sum());
}
}
// If only the container's size has been requested
if run_mode == RunMode::ComputeSize {
return LayoutOutput::from_outer_size(container_border_box);
}
// 8. Track Alignment
// Align columns
let inline_size_without_scrollbar = f32_max(container_border_box.width - padding_border_size.width, 0.0);
let inline_scrollbar_gutter_for_alignment = f32_min(scrollbar_gutter.x, inline_size_without_scrollbar);
align_tracks(
container_content_box.get(AbstractAxis::Inline),
Line {
start: padding.left + if direction.is_rtl() { inline_scrollbar_gutter_for_alignment } else { 0.0 },
end: padding.right + if direction.is_rtl() { 0.0 } else { inline_scrollbar_gutter_for_alignment },
},
Line { start: border.left, end: border.right },
&mut columns,
justify_content,
direction.is_rtl(),
);
// Align rows
align_tracks(
container_content_box.get(AbstractAxis::Block),
Line { start: padding.top, end: padding.bottom },
Line { start: border.top, end: border.bottom },
&mut rows,
align_content,
false,
);
// 9. Size, Align, and Position Grid Items
#[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
let mut item_content_size_contribution = Size::ZERO;
// Sort items back into original order to allow them to be matched up with styles
items.sort_by_key(|item| item.source_order);
let container_alignment_styles = InBothAbsAxis { horizontal: justify_items, vertical: align_items };
// Position in-flow children (stored in items vector)
for (index, item) in items.iter_mut().enumerate() {
let grid_area = Rect {
top: rows[item.row_indexes.start as usize + 1].offset,
bottom: rows[item.row_indexes.end as usize].offset,
left: columns[item.column_indexes.start as usize + 1].offset,
right: columns[item.column_indexes.end as usize].offset,
};
#[cfg_attr(not(feature = "content_size"), allow(unused_variables))]
let (content_size_contribution, y_position, height) = align_and_position_item(
tree,
item.node,
index as u32,
grid_area,
container_alignment_styles,
item.baseline_shim,
direction,
);
item.y_position = y_position;
item.height = height;
#[cfg(feature = "content_size")]
{
item_content_size_contribution = item_content_size_contribution.f32_max(content_size_contribution);
}
}
// Position hidden and absolutely positioned children
let mut order = items.len() as u32;
(0..tree.child_count(node)).for_each(|index| {
let child = tree.get_child_id(node, index);
let child_style = tree.get_grid_child_style(child);
// Position hidden child
if child_style.box_generation_mode() == BoxGenerationMode::None {
drop(child_style);
tree.set_unrounded_layout(child, &Layout::with_order(order));
tree.perform_child_layout(
child,
Size::NONE,
Size::NONE,
Size::MAX_CONTENT,
SizingMode::InherentSize,
Line::FALSE,
);
order += 1;
return;
}
// Position absolutely positioned child
if child_style.position() == Position::Absolute {
// Convert grid-col-{start/end} into Option's of indexes into the columns vector
// The Option is None if the style property is Auto and an unresolvable Span
let maybe_col_indexes = name_resolver
.resolve_column_names(&child_style.grid_column())
.into_origin_zero(final_col_counts.explicit)
.resolve_absolutely_positioned_grid_tracks()
.map(|maybe_grid_line| {
maybe_grid_line
.map(|line: OriginZeroLine| {
if direction.is_rtl() {
OriginZeroLine(final_col_counts.explicit as i16 - line.0)
} else {
line
}
})
.and_then(|line| line.try_into_track_vec_index(final_col_counts))
});
let maybe_col_indexes = if direction.is_rtl() {
Line { start: maybe_col_indexes.end, end: maybe_col_indexes.start }
} else {
maybe_col_indexes
};
// Convert grid-row-{start/end} into Option's of indexes into the row vector
// The Option is None if the style property is Auto and an unresolvable Span
let maybe_row_indexes = name_resolver
.resolve_row_names(&child_style.grid_row())
.into_origin_zero(final_row_counts.explicit)
.resolve_absolutely_positioned_grid_tracks()
.map(|maybe_grid_line| {
maybe_grid_line.and_then(|line: OriginZeroLine| line.try_into_track_vec_index(final_row_counts))
});
let grid_area = Rect {
top: maybe_row_indexes.start.map(|index| rows[index].offset).unwrap_or(border.top),
bottom: maybe_row_indexes
.end
.map(|index| rows[index].offset)
.unwrap_or(container_border_box.height - border.bottom - scrollbar_gutter.y),
left: maybe_col_indexes.start.map(|index| columns[index].offset).unwrap_or_else(|| {
if direction.is_rtl() {
border.left + scrollbar_gutter.x
} else {
border.left
}
}),
right: maybe_col_indexes.end.map(|index| columns[index].offset).unwrap_or_else(|| {
if direction.is_rtl() {
container_border_box.width - border.right
} else {
container_border_box.width - border.right - scrollbar_gutter.x
}
}),
};
drop(child_style);
// TODO: Baseline alignment support for absolutely positioned items (should check if is actually specified)
#[cfg_attr(not(feature = "content_size"), allow(unused_variables))]
let (content_size_contribution, _, _) =
align_and_position_item(tree, child, order, grid_area, container_alignment_styles, 0.0, direction);
#[cfg(feature = "content_size")]
{
item_content_size_contribution = item_content_size_contribution.f32_max(content_size_contribution);
}
order += 1;
}
});
// Set detailed grid information
#[cfg(feature = "detailed_layout_info")]
tree.set_detailed_grid_info(
node,
DetailedGridInfo {
rows: DetailedGridTracksInfo::from_grid_tracks_and_track_count(final_row_counts, rows),
columns: DetailedGridTracksInfo::from_grid_tracks_and_track_count(final_col_counts, columns),
items: items.iter().map(DetailedGridItemsInfo::from_grid_item).collect(),
},
);
// If there are not items then return just the container size (no baseline)
if items.is_empty() {
return LayoutOutput::from_outer_size(container_border_box);
}
// Determine the grid container baseline(s) (currently we only compute the first baseline)
let grid_container_baseline: f32 = {
// Sort items by row start position so that we can iterate items in groups which are in the same row
items.sort_by_key(|item| item.row_indexes.start);
// Get the row index of the first row containing items
let first_row = items[0].row_indexes.start;
// Create a slice of all of the items start in this row (taking advantage of the fact that we have just sorted the array)
let first_row_items = &items[0..].split(|item| item.row_indexes.start != first_row).next().unwrap();
// Check if any items in *this row* are baseline aligned
let row_has_baseline_item = first_row_items.iter().any(|item| item.align_self == AlignSelf::BASELINE);
let item = if row_has_baseline_item {
first_row_items.iter().find(|item| item.align_self == AlignSelf::BASELINE).unwrap()
} else {
&first_row_items[0]
};
item.y_position + item.baseline.unwrap_or(item.height)
};
LayoutOutput::from_sizes_and_baselines(
container_border_box,
item_content_size_contribution,
Point { x: None, y: Some(grid_container_baseline) },
)
}
/// Reverses only non-gutter column tracks in-place while preserving line/gutter slots.
fn reverse_non_gutter_tracks(tracks: &mut [GridTrack], track_counts: TrackCounts) {
// When the explicit grid has 0/1 tracks, visual RTL mirroring is entirely determined by implicit tracks.
// Reverse all non-gutter tracks in that case.
if track_counts.explicit <= 1 {
const MIN_TRACK_VEC_LEN_TO_REVERSE_COLUMNS: usize = 5;
if tracks.len() < MIN_TRACK_VEC_LEN_TO_REVERSE_COLUMNS {
return;
}
let mut left = 1;
let mut right = tracks.len() - 2;
while left < right {
tracks.swap(left, right);
left += 2;
right = right.saturating_sub(2);
}
return;
}
let explicit_track_count = track_counts.explicit as usize;
if explicit_track_count < 2 {
return;
}
let mut left = track_counts.negative_implicit as usize;
let mut right = left + explicit_track_count - 1;
while left < right {
tracks.swap((2 * left) + 1, (2 * right) + 1);
left += 1;
right = right.saturating_sub(1);
}
}
/// Maps initialized column indexes to occupancy-matrix indexes for auto-fit collapsing in RTL.
fn rtl_column_occupancy_index_for_initialization(column_index: usize, track_counts: TrackCounts) -> usize {
if track_counts.explicit <= 1 {
return track_counts.len() - column_index - 1;
}
let explicit_start = track_counts.negative_implicit as usize;
let explicit_end = explicit_start + track_counts.explicit as usize;
if (explicit_start..explicit_end).contains(&column_index) {
explicit_start + (explicit_end - column_index - 1)
} else {
column_index
}
}
/// Information from the computation of grid
#[derive(Debug, Clone, PartialEq)]
#[cfg(feature = "detailed_layout_info")]
pub struct DetailedGridInfo {
/// <https://drafts.csswg.org/css-grid-1/#grid-row>
pub rows: DetailedGridTracksInfo,
/// <https://drafts.csswg.org/css-grid-1/#grid-column>
pub columns: DetailedGridTracksInfo,
/// <https://drafts.csswg.org/css-grid-1/#grid-items>
pub items: Vec<DetailedGridItemsInfo>,
}
/// Information from the computation of grids tracks
#[derive(Debug, Clone, PartialEq)]
#[cfg(feature = "detailed_layout_info")]
pub struct DetailedGridTracksInfo {
/// Number of leading implicit grid tracks
pub negative_implicit_tracks: u16,
/// Number of explicit grid tracks
pub explicit_tracks: u16,
/// Number of trailing implicit grid tracks
pub positive_implicit_tracks: u16,
/// Gutters between tracks
pub gutters: Vec<f32>,
/// The used size of the tracks
pub sizes: Vec<f32>,
}
#[cfg(feature = "detailed_layout_info")]
impl DetailedGridTracksInfo {
/// Get the base_size of [`GridTrack`] with a kind [`types::GridTrackKind`]
#[inline(always)]
fn grid_track_base_size_of_kind(grid_tracks: &[GridTrack], kind: GridTrackKind) -> Vec<f32> {
grid_tracks
.iter()
.filter_map(|track| match track.kind == kind {
true => Some(track.base_size),
false => None,
})
.collect()
}
/// Get the sizes of the gutters
fn gutters_from_grid_track_layout(grid_tracks: &[GridTrack]) -> Vec<f32> {
DetailedGridTracksInfo::grid_track_base_size_of_kind(grid_tracks, GridTrackKind::Gutter)
}
/// Get the sizes of the tracks
fn sizes_from_grid_track_layout(grid_tracks: &[GridTrack]) -> Vec<f32> {
DetailedGridTracksInfo::grid_track_base_size_of_kind(grid_tracks, GridTrackKind::Track)
}
/// Construct DetailedGridTracksInfo from TrackCounts and GridTracks
fn from_grid_tracks_and_track_count(track_count: TrackCounts, grid_tracks: Vec<GridTrack>) -> Self {
DetailedGridTracksInfo {
negative_implicit_tracks: track_count.negative_implicit,
explicit_tracks: track_count.explicit,
positive_implicit_tracks: track_count.positive_implicit,
gutters: DetailedGridTracksInfo::gutters_from_grid_track_layout(&grid_tracks),
sizes: DetailedGridTracksInfo::sizes_from_grid_track_layout(&grid_tracks),
}
}
}
/// Grid area information from the placement algorithm
///
/// The values is 1-indexed grid line numbers bounding the area.
/// This matches the Chrome and Firefox's format as of 2nd Jan 2024.
#[derive(Debug, Clone, PartialEq)]
#[cfg(feature = "detailed_layout_info")]
pub struct DetailedGridItemsInfo {
/// row-start with 1-indexed grid line numbers
pub row_start: u16,
/// row-end with 1-indexed grid line numbers
pub row_end: u16,
/// column-start with 1-indexed grid line numbers
pub column_start: u16,
/// column-end with 1-indexed grid line numbers
pub column_end: u16,
}
/// Grid area information from the placement algorithm
#[cfg(feature = "detailed_layout_info")]
impl DetailedGridItemsInfo {
/// Construct from GridItems
#[inline(always)]
fn from_grid_item(grid_item: &GridItem) -> Self {
/// Conversion from the indexes of Vec<GridTrack> into 1-indexed grid line numbers. See [`GridItem::row_indexes`] or [`GridItem::column_indexes`]
#[inline(always)]
fn to_one_indexed_grid_line(grid_track_index: u16) -> u16 {
grid_track_index / 2 + 1
}
DetailedGridItemsInfo {
row_start: to_one_indexed_grid_line(grid_item.row_indexes.start),
row_end: to_one_indexed_grid_line(grid_item.row_indexes.end),
column_start: to_one_indexed_grid_line(grid_item.column_indexes.start),
column_end: to_one_indexed_grid_line(grid_item.column_indexes.end),
}
}
}
+764
View File
@@ -0,0 +1,764 @@
//! Implements placing items in the grid and resolving the implicit grid.
//! <https://www.w3.org/TR/css-grid-1/#placement>
use super::types::{CellOccupancyMatrix, CellOccupancyState, GridItem};
use super::{NamedLineResolver, OriginZeroLine};
use crate::geometry::Line;
use crate::geometry::{AbsoluteAxis, InBothAbsAxis};
use crate::style::{AlignItems, GridAutoFlow, OriginZeroGridPlacement};
use crate::tree::NodeId;
use crate::util::sys::Vec;
use crate::{CoreStyle, Direction, GridItemStyle};
#[inline]
/// Returns whether placement/search should run in reverse for this axis.
fn axis_is_reversed(direction: Direction, axis: AbsoluteAxis) -> bool {
direction.is_rtl() && axis == AbsoluteAxis::Horizontal
}
#[inline]
/// Advances the cursor by one track in the active search direction.
fn advance_position(position: OriginZeroLine, axis_is_reversed: bool) -> OriginZeroLine {
if axis_is_reversed {
OriginZeroLine(position.0 - 1)
} else {
OriginZeroLine(position.0 + 1)
}
}
#[inline]
/// Returns the initial search line for sparse/dense placement in the given axis direction.
fn search_start_line(
grid_start_line: OriginZeroLine,
grid_end_line: OriginZeroLine,
axis_is_reversed: bool,
) -> OriginZeroLine {
if axis_is_reversed {
grid_end_line - 1
} else {
grid_start_line
}
}
#[inline]
/// Resolves an indefinite span at `position`, respecting the active axis direction.
fn resolve_indefinite_grid_span(position: OriginZeroLine, span: u16, axis_is_reversed: bool) -> Line<OriginZeroLine> {
if axis_is_reversed {
Line { start: (position - span) + 1, end: position + 1 }
} else {
Line { start: position, end: position + span }
}
}
#[inline]
/// Mirrors a horizontal span around the explicit grid width.
fn mirror_horizontal_span(span: Line<OriginZeroLine>, explicit_col_count: u16) -> Line<OriginZeroLine> {
let explicit_col_end_line = explicit_col_count as i16;
Line {
start: OriginZeroLine(explicit_col_end_line - span.end.0),
end: OriginZeroLine(explicit_col_end_line - span.start.0),
}
}
#[inline]
/// Mirrors horizontal spans for RTL while leaving all other spans unchanged.
fn maybe_mirror_span(
span: Line<OriginZeroLine>,
axis: AbsoluteAxis,
direction: Direction,
explicit_col_count: u16,
) -> Line<OriginZeroLine> {
if axis == AbsoluteAxis::Horizontal && direction.is_rtl() {
mirror_horizontal_span(span, explicit_col_count)
} else {
span
}
}
/// 8.5. Grid Item Placement Algorithm
/// Place items into the grid, generating new rows/column into the implicit grid as required
///
/// [Specification](https://www.w3.org/TR/css-grid-2/#auto-placement-algo)
#[allow(clippy::too_many_arguments)]
pub(super) fn place_grid_items<'a, S, ChildIter>(
cell_occupancy_matrix: &mut CellOccupancyMatrix,
items: &mut Vec<GridItem>,
children_iter: impl Fn() -> ChildIter,
direction: Direction,
grid_auto_flow: GridAutoFlow,
align_items: AlignItems,
justify_items: AlignItems,
named_line_resolver: &NamedLineResolver<<S as CoreStyle>::CustomIdent>,
) where
S: GridItemStyle + 'a,
ChildIter: Iterator<Item = (usize, NodeId, S)>,
{
let primary_axis = grid_auto_flow.primary_axis();
let secondary_axis = primary_axis.other_axis();
let explicit_col_count = cell_occupancy_matrix.track_counts(AbsoluteAxis::Horizontal).explicit;
let map_child_style_to_origin_zero_placement = {
let explicit_row_count = cell_occupancy_matrix.track_counts(AbsoluteAxis::Vertical).explicit;
move |(index, node, style): (usize, NodeId, S)| -> (_, _, _, S) {
let origin_zero_placement = InBothAbsAxis {
horizontal: named_line_resolver
.resolve_column_names(&style.grid_column())
.map(|placement| placement.into_origin_zero_placement(explicit_col_count)),
vertical: named_line_resolver
.resolve_row_names(&style.grid_row())
.map(|placement| placement.into_origin_zero_placement(explicit_row_count)),
};
(index, node, origin_zero_placement, style)
}
};
// 1. Place children with definite positions
let mut idx = 0;
children_iter()
.map(map_child_style_to_origin_zero_placement)
.filter(|(_, _, placement, _)| placement.horizontal.is_definite() && placement.vertical.is_definite())
.for_each(|(index, child_node, child_placement, style)| {
idx += 1;
#[cfg(test)]
println!("Definite Item {idx}\n==============");
let (row_span, col_span) =
place_definite_grid_item(child_placement, primary_axis, direction, explicit_col_count);
record_grid_placement(
cell_occupancy_matrix,
items,
child_node,
index,
style,
align_items,
justify_items,
primary_axis,
row_span,
col_span,
CellOccupancyState::DefinitelyPlaced,
);
});
// 2. Place remaining children with definite secondary axis positions
let mut idx = 0;
children_iter()
.map(map_child_style_to_origin_zero_placement)
.filter(|(_, _, placement, _)| {
placement.get(secondary_axis).is_definite() && !placement.get(primary_axis).is_definite()
})
.for_each(|(index, child_node, child_placement, style)| {
idx += 1;
#[cfg(test)]
println!("Definite Secondary Item {idx}\n==============");
let (primary_span, secondary_span) = place_definite_secondary_axis_item(
&*cell_occupancy_matrix,
child_placement,
grid_auto_flow,
direction,
explicit_col_count,
);
record_grid_placement(
cell_occupancy_matrix,
items,
child_node,
index,
style,
align_items,
justify_items,
primary_axis,
primary_span,
secondary_span,
CellOccupancyState::AutoPlaced,
);
});
// 3. Determine the number of columns in the implicit grid
// By the time we get to this point in the execution, this is actually already accounted for:
//
// 3.1 Start with the columns from the explicit grid
// => Handled by grid size estimate which is used to pre-size the GridOccupancyMatrix
//
// 3.2 Among all the items with a definite column position (explicitly positioned items, items positioned in the previous step,
// and items not yet positioned but with a definite column) add columns to the beginning and end of the implicit grid as necessary
// to accommodate those items.
// => Handled by expand_to_fit_range which expands the GridOccupancyMatrix as necessary
// -> Called by mark_area_as
// -> Called by record_grid_placement
//
// 3.3 If the largest column span among all the items without a definite column position is larger than the width of
// the implicit grid, add columns to the end of the implicit grid to accommodate that column span.
// => Handled by grid size estimate which is used to pre-size the GridOccupancyMatrix
// 4. Position the remaining grid items
// (which either have definite position only in the secondary axis or indefinite positions in both axis)
let primary_axis = grid_auto_flow.primary_axis();
let secondary_axis = primary_axis.other_axis();
let primary_axis_grid_start_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_start_line();
let primary_axis_grid_end_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_end_line();
let secondary_axis_grid_start_line = cell_occupancy_matrix.track_counts(secondary_axis).implicit_start_line();
let secondary_axis_grid_end_line = cell_occupancy_matrix.track_counts(secondary_axis).implicit_end_line();
let primary_axis_is_reversed = axis_is_reversed(direction, primary_axis);
let grid_start_position = (
search_start_line(primary_axis_grid_start_line, primary_axis_grid_end_line, primary_axis_is_reversed),
search_start_line(
secondary_axis_grid_start_line,
secondary_axis_grid_end_line,
axis_is_reversed(direction, secondary_axis),
),
);
let mut grid_position = grid_start_position;
let mut idx = 0;
children_iter()
.map(map_child_style_to_origin_zero_placement)
.filter(|(_, _, placement, _)| !placement.get(secondary_axis).is_definite())
.for_each(|(index, child_node, child_placement, style)| {
idx += 1;
#[cfg(test)]
println!("\nAuto Item {idx}\n==============");
// Compute placement
let (primary_span, secondary_span) = place_indefinitely_positioned_item(
&*cell_occupancy_matrix,
child_placement,
grid_auto_flow,
grid_position,
direction,
explicit_col_count,
);
// Record item
record_grid_placement(
cell_occupancy_matrix,
items,
child_node,
index,
style,
align_items,
justify_items,
primary_axis,
primary_span,
secondary_span,
CellOccupancyState::AutoPlaced,
);
// If using the "dense" placement algorithm then reset the grid position back to grid_start_position ready for the next item
// Otherwise set it to the position of the current item so that the next item it placed after it.
grid_position = match (grid_auto_flow.is_dense(), primary_axis_is_reversed) {
(true, _) => grid_start_position,
(false, false) => (primary_span.end, secondary_span.start),
(false, true) => (primary_span.start, secondary_span.start),
};
});
}
/// 8.5. Grid Item Placement Algorithm
/// Place a single definitely placed item into the grid
fn place_definite_grid_item(
placement: InBothAbsAxis<Line<OriginZeroGridPlacement>>,
primary_axis: AbsoluteAxis,
direction: Direction,
explicit_col_count: u16,
) -> (Line<OriginZeroLine>, Line<OriginZeroLine>) {
// Resolve spans to tracks
let primary_span = maybe_mirror_span(
placement.get(primary_axis).resolve_definite_grid_lines(),
primary_axis,
direction,
explicit_col_count,
);
let secondary_span = maybe_mirror_span(
placement.get(primary_axis.other_axis()).resolve_definite_grid_lines(),
primary_axis.other_axis(),
direction,
explicit_col_count,
);
(primary_span, secondary_span)
}
/// 8.5. Grid Item Placement Algorithm
/// Step 2. Place remaining children with definite secondary axis positions
fn place_definite_secondary_axis_item(
cell_occupancy_matrix: &CellOccupancyMatrix,
placement: InBothAbsAxis<Line<OriginZeroGridPlacement>>,
auto_flow: GridAutoFlow,
direction: Direction,
explicit_col_count: u16,
) -> (Line<OriginZeroLine>, Line<OriginZeroLine>) {
let primary_axis = auto_flow.primary_axis();
let secondary_axis = primary_axis.other_axis();
let primary_axis_is_reversed = axis_is_reversed(direction, primary_axis);
let primary_axis_grid_start_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_start_line();
let primary_axis_grid_end_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_end_line();
let secondary_axis_placement = maybe_mirror_span(
placement.get(secondary_axis).resolve_definite_grid_lines(),
secondary_axis,
direction,
explicit_col_count,
);
let starting_position = match auto_flow.is_dense() {
true => search_start_line(primary_axis_grid_start_line, primary_axis_grid_end_line, primary_axis_is_reversed),
false => {
let lookup_result = if primary_axis_is_reversed {
cell_occupancy_matrix.first_of_type(
primary_axis,
secondary_axis_placement.start,
CellOccupancyState::AutoPlaced,
)
} else {
cell_occupancy_matrix.last_of_type(
primary_axis,
secondary_axis_placement.start,
CellOccupancyState::AutoPlaced,
)
};
lookup_result.unwrap_or(search_start_line(
primary_axis_grid_start_line,
primary_axis_grid_end_line,
primary_axis_is_reversed,
))
}
};
let primary_axis_span = placement.get(primary_axis).indefinite_span();
let mut position: OriginZeroLine = starting_position;
loop {
let primary_axis_placement =
resolve_indefinite_grid_span(position, primary_axis_span, primary_axis_is_reversed);
let does_fit = cell_occupancy_matrix.line_area_is_unoccupied(
primary_axis,
primary_axis_placement,
secondary_axis_placement,
);
if does_fit {
return (primary_axis_placement, secondary_axis_placement);
} else {
position = advance_position(position, primary_axis_is_reversed);
}
}
}
/// 8.5. Grid Item Placement Algorithm
/// Step 4. Position the remaining grid items.
fn place_indefinitely_positioned_item(
cell_occupancy_matrix: &CellOccupancyMatrix,
placement: InBothAbsAxis<Line<OriginZeroGridPlacement>>,
auto_flow: GridAutoFlow,
grid_position: (OriginZeroLine, OriginZeroLine),
direction: Direction,
explicit_col_count: u16,
) -> (Line<OriginZeroLine>, Line<OriginZeroLine>) {
let primary_axis = auto_flow.primary_axis();
let secondary_axis = primary_axis.other_axis();
let primary_axis_is_reversed = axis_is_reversed(direction, primary_axis);
let secondary_axis_is_reversed = axis_is_reversed(direction, secondary_axis);
let primary_placement_style = placement.get(primary_axis);
let secondary_placement_style = placement.get(secondary_axis);
let secondary_span = secondary_placement_style.indefinite_span();
let has_definite_primary_axis_position = primary_placement_style.is_definite();
let primary_axis_grid_start_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_start_line();
let primary_axis_grid_end_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_end_line();
let secondary_axis_grid_start_line = cell_occupancy_matrix.track_counts(secondary_axis).implicit_start_line();
let secondary_axis_grid_end_line = cell_occupancy_matrix.track_counts(secondary_axis).implicit_end_line();
let primary_start_position =
search_start_line(primary_axis_grid_start_line, primary_axis_grid_end_line, primary_axis_is_reversed);
let secondary_start_position =
search_start_line(secondary_axis_grid_start_line, secondary_axis_grid_end_line, secondary_axis_is_reversed);
let line_area_is_occupied = |primary_span, secondary_span| {
!cell_occupancy_matrix.line_area_is_unoccupied(primary_axis, primary_span, secondary_span)
};
let (mut primary_idx, mut secondary_idx) = grid_position;
if has_definite_primary_axis_position {
let primary_span = maybe_mirror_span(
primary_placement_style.resolve_definite_grid_lines(),
primary_axis,
direction,
explicit_col_count,
);
// Compute secondary axis starting position for search
secondary_idx = match auto_flow.is_dense() {
// If auto-flow is dense then we always search from the first track
true => secondary_start_position,
false => {
let should_advance_secondary = if primary_axis_is_reversed {
primary_span.start > primary_idx
} else {
primary_span.start < primary_idx
};
if should_advance_secondary {
advance_position(secondary_idx, secondary_axis_is_reversed)
} else {
secondary_idx
}
}
};
// Item has fixed primary axis position: so we simply increment the secondary axis position
// until we find a space that the item fits in
loop {
let secondary_span =
resolve_indefinite_grid_span(secondary_idx, secondary_span, secondary_axis_is_reversed);
// If area is occupied, increment the index and try again
if line_area_is_occupied(primary_span, secondary_span) {
secondary_idx = advance_position(secondary_idx, secondary_axis_is_reversed);
continue;
}
// Once we find a free space, return that position
return (primary_span, secondary_span);
}
} else {
let primary_span = primary_placement_style.indefinite_span();
// Item does not have any fixed axis, so we search along the primary axis until we hit the end of the already
// existent tracks, and then we reset the primary axis back to zero and increment the secondary axis index.
// We continue in this vein until we find a space that the item fits in.
loop {
let primary_span = resolve_indefinite_grid_span(primary_idx, primary_span, primary_axis_is_reversed);
let secondary_span =
resolve_indefinite_grid_span(secondary_idx, secondary_span, secondary_axis_is_reversed);
// If the primary index is out of bounds, then increment the secondary index and reset the primary
// index back to the start of the grid
let primary_out_of_bounds = if primary_axis_is_reversed {
primary_span.start < primary_axis_grid_start_line
} else {
primary_span.end > primary_axis_grid_end_line
};
if primary_out_of_bounds {
secondary_idx = advance_position(secondary_idx, secondary_axis_is_reversed);
primary_idx = primary_start_position;
continue;
}
// If area is occupied, increment the primary index and try again
if line_area_is_occupied(primary_span, secondary_span) {
primary_idx = advance_position(primary_idx, primary_axis_is_reversed);
continue;
}
// Once we find a free space that's in bounds, return that position
return (primary_span, secondary_span);
}
}
}
/// Record the grid item in both CellOccupancyMatric and the GridItems list
/// once a definite placement has been determined
#[allow(clippy::too_many_arguments)]
fn record_grid_placement<S: GridItemStyle>(
cell_occupancy_matrix: &mut CellOccupancyMatrix,
items: &mut Vec<GridItem>,
node: NodeId,
index: usize,
style: S,
parent_align_items: AlignItems,
parent_justify_items: AlignItems,
primary_axis: AbsoluteAxis,
primary_span: Line<OriginZeroLine>,
secondary_span: Line<OriginZeroLine>,
placement_type: CellOccupancyState,
) {
#[cfg(test)]
println!("BEFORE placement:");
#[cfg(test)]
println!("{cell_occupancy_matrix:?}");
// Mark area of grid as occupied
cell_occupancy_matrix.mark_area_as(primary_axis, primary_span, secondary_span, placement_type);
// Create grid item
let (col_span, row_span) = match primary_axis {
AbsoluteAxis::Horizontal => (primary_span, secondary_span),
AbsoluteAxis::Vertical => (secondary_span, primary_span),
};
items.push(GridItem::new_with_placement_style_and_order(
node,
col_span,
row_span,
style,
parent_align_items,
parent_justify_items,
index as u16,
));
#[cfg(test)]
println!("AFTER placement:");
#[cfg(test)]
println!("{cell_occupancy_matrix:?}");
#[cfg(test)]
println!("\n");
}
#[cfg(test)]
mod tests {
mod test_placement_algorithm {
use crate::compute::grid::implicit_grid::compute_grid_size_estimate;
use crate::compute::grid::types::TrackCounts;
use crate::compute::grid::util::*;
use crate::compute::grid::CellOccupancyMatrix;
use crate::compute::grid::NamedLineResolver;
use crate::prelude::*;
use crate::style::GridAutoFlow;
use crate::Direction;
use super::super::place_grid_items;
type ExpectedPlacement = (i16, i16, i16, i16);
fn placement_test_runner(
explicit_col_count: u16,
explicit_row_count: u16,
children: Vec<(usize, Style, ExpectedPlacement)>,
expected_col_counts: TrackCounts,
expected_row_counts: TrackCounts,
flow: GridAutoFlow,
) {
// Setup test
let children_iter = || children.iter().map(|(index, style, _)| (*index, NodeId::from(*index), style));
let child_styles_iter = children.iter().map(|(_, style, _)| style);
let estimated_sizes =
compute_grid_size_estimate(explicit_col_count, explicit_row_count, Direction::Ltr, child_styles_iter);
let mut items = Vec::new();
let mut cell_occupancy_matrix =
CellOccupancyMatrix::with_track_counts(estimated_sizes.0, estimated_sizes.1);
let mut name_resolver = NamedLineResolver::new(&Style::DEFAULT, 0, 0);
name_resolver.set_explicit_column_count(explicit_col_count);
name_resolver.set_explicit_row_count(explicit_row_count);
// Run placement algorithm
place_grid_items(
&mut cell_occupancy_matrix,
&mut items,
children_iter,
Direction::Ltr,
flow,
AlignSelf::START,
AlignSelf::START,
// TODO: actually test named line resolution
&name_resolver,
);
// Assert that each item has been placed in the right location
let mut sorted_children = children.clone();
sorted_children.sort_by_key(|child| child.0);
for (idx, ((id, _style, expected_placement), item)) in sorted_children.iter().zip(items.iter()).enumerate()
{
assert_eq!(item.node, NodeId::from(*id));
let actual_placement = (item.column.start, item.column.end, item.row.start, item.row.end);
assert_eq!(actual_placement, (*expected_placement).into_oz(), "Item {idx} (0-indexed)");
}
// Assert that the correct number of implicit rows have been generated
let actual_row_counts = *cell_occupancy_matrix.track_counts(crate::compute::grid::AbsoluteAxis::Vertical);
assert_eq!(actual_row_counts, expected_row_counts, "row track counts");
let actual_col_counts = *cell_occupancy_matrix.track_counts(crate::compute::grid::AbsoluteAxis::Horizontal);
assert_eq!(actual_col_counts, expected_col_counts, "column track counts");
}
#[test]
fn test_only_fixed_placement() {
let flow = GridAutoFlow::Row;
let explicit_col_count = 2;
let explicit_row_count = 2;
let children = {
vec![
// node, style (grid coords), expected_placement (oz coords)
(1, (line(1), auto(), line(1), auto()).into_grid_child(), (0, 1, 0, 1)),
(2, (line(-4), auto(), line(-3), auto()).into_grid_child(), (-1, 0, 0, 1)),
(3, (line(-3), auto(), line(-4), auto()).into_grid_child(), (0, 1, -1, 0)),
(4, (line(3), span(2), line(5), auto()).into_grid_child(), (2, 4, 4, 5)),
]
};
let expected_cols = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 2 };
let expected_rows = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 3 };
placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
}
#[test]
fn test_placement_spanning_origin() {
let flow = GridAutoFlow::Row;
let explicit_col_count = 2;
let explicit_row_count = 2;
let children = {
vec![
// node, style (grid coords), expected_placement (oz coords)
(1, (line(-1), line(-1), line(-1), line(-1)).into_grid_child(), (2, 3, 2, 3)),
(2, (line(-1), span(2), line(-1), span(2)).into_grid_child(), (2, 4, 2, 4)),
(3, (line(-4), line(-4), line(-4), line(-4)).into_grid_child(), (-1, 0, -1, 0)),
(4, (line(-4), span(2), line(-4), span(2)).into_grid_child(), (-1, 1, -1, 1)),
]
};
let expected_cols = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 2 };
let expected_rows = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 2 };
placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
}
#[test]
fn test_only_auto_placement_row_flow() {
let flow = GridAutoFlow::Row;
let explicit_col_count = 2;
let explicit_row_count = 2;
let children = {
let auto_child = (auto(), auto(), auto(), auto()).into_grid_child();
vec![
// output order, node, style (grid coords), expected_placement (oz coords)
(1, auto_child.clone(), (0, 1, 0, 1)),
(2, auto_child.clone(), (1, 2, 0, 1)),
(3, auto_child.clone(), (0, 1, 1, 2)),
(4, auto_child.clone(), (1, 2, 1, 2)),
(5, auto_child.clone(), (0, 1, 2, 3)),
(6, auto_child.clone(), (1, 2, 2, 3)),
(7, auto_child.clone(), (0, 1, 3, 4)),
(8, auto_child.clone(), (1, 2, 3, 4)),
]
};
let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 2 };
placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
}
#[test]
fn test_only_auto_placement_column_flow() {
let flow = GridAutoFlow::Column;
let explicit_col_count = 2;
let explicit_row_count = 2;
let children = {
let auto_child = (auto(), auto(), auto(), auto()).into_grid_child();
vec![
// output order, node, style (grid coords), expected_placement (oz coords)
(1, auto_child.clone(), (0, 1, 0, 1)),
(2, auto_child.clone(), (0, 1, 1, 2)),
(3, auto_child.clone(), (1, 2, 0, 1)),
(4, auto_child.clone(), (1, 2, 1, 2)),
(5, auto_child.clone(), (2, 3, 0, 1)),
(6, auto_child.clone(), (2, 3, 1, 2)),
(7, auto_child.clone(), (3, 4, 0, 1)),
(8, auto_child.clone(), (3, 4, 1, 2)),
]
};
let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 2 };
let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
}
#[test]
fn test_oversized_item() {
let flow = GridAutoFlow::Row;
let explicit_col_count = 2;
let explicit_row_count = 2;
let children = {
vec![
// output order, node, style (grid coords), expected_placement (oz coords)
(1, (span(5), auto(), auto(), auto()).into_grid_child(), (0, 5, 0, 1)),
]
};
let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 3 };
let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
}
#[test]
fn test_fixed_in_secondary_axis() {
let flow = GridAutoFlow::Row;
let explicit_col_count = 2;
let explicit_row_count = 2;
let children = {
vec![
// output order, node, style (grid coords), expected_placement (oz coords)
(1, (span(2), auto(), line(1), auto()).into_grid_child(), (0, 2, 0, 1)),
(2, (auto(), auto(), line(2), auto()).into_grid_child(), (0, 1, 1, 2)),
(3, (auto(), auto(), line(1), auto()).into_grid_child(), (2, 3, 0, 1)),
(4, (auto(), auto(), line(4), auto()).into_grid_child(), (0, 1, 3, 4)),
]
};
let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 1 };
let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 2 };
placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
}
#[test]
fn test_definite_in_secondary_axis_with_fully_definite_negative() {
let flow = GridAutoFlow::Row;
let explicit_col_count = 2;
let explicit_row_count = 2;
let children = {
vec![
// output order, node, style (grid coords), expected_placement (oz coords)
(2, (auto(), auto(), line(2), auto()).into_grid_child(), (0, 1, 1, 2)),
(1, (line(-4), auto(), line(2), auto()).into_grid_child(), (-1, 0, 1, 2)),
(3, (auto(), auto(), line(1), auto()).into_grid_child(), (-1, 0, 0, 1)),
]
};
let expected_cols = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 0 };
let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
}
#[test]
fn test_dense_packing_algorithm() {
let flow = GridAutoFlow::RowDense;
let explicit_col_count = 4;
let explicit_row_count = 4;
let children = {
vec![
// output order, node, style (grid coords), expected_placement (oz coords)
(1, (line(2), auto(), line(1), auto()).into_grid_child(), (1, 2, 0, 1)), // Definitely positioned in column 2
(2, (span(2), auto(), auto(), auto()).into_grid_child(), (2, 4, 0, 1)), // Spans 2 columns, so positioned after item 1
(3, (auto(), auto(), auto(), auto()).into_grid_child(), (0, 1, 0, 1)), // Spans 1 column, so should be positioned before item 1
]
};
let expected_cols = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
let expected_rows = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
}
#[test]
fn test_sparse_packing_algorithm() {
let flow = GridAutoFlow::Row;
let explicit_col_count = 4;
let explicit_row_count = 4;
let children = {
vec![
// output order, node, style (grid coords), expected_placement (oz coords)
(1, (auto(), span(3), auto(), auto()).into_grid_child(), (0, 3, 0, 1)), // Width 3
(2, (auto(), span(3), auto(), auto()).into_grid_child(), (0, 3, 1, 2)), // Width 3 (wraps to next row)
(3, (auto(), span(1), auto(), auto()).into_grid_child(), (3, 4, 1, 2)), // Width 1 (uses second row as we're already on it)
]
};
let expected_cols = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
let expected_rows = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
}
#[test]
fn test_auto_placement_in_negative_tracks() {
let flow = GridAutoFlow::RowDense;
let explicit_col_count = 2;
let explicit_row_count = 2;
let children = {
vec![
// output order, node, style (grid coords), expected_placement (oz coords)
(1, (line(-5), auto(), line(1), auto()).into_grid_child(), (-2, -1, 0, 1)), // Row 1. Definitely positioned in column -2
(2, (auto(), auto(), line(2), auto()).into_grid_child(), (-2, -1, 1, 2)), // Row 2. Auto positioned in column -2
(3, (auto(), auto(), auto(), auto()).into_grid_child(), (-1, 0, 0, 1)), // Row 1. Auto positioned in column -1
]
};
let expected_cols = TrackCounts { negative_implicit: 2, explicit: 2, positive_implicit: 0 };
let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
}
}
}
File diff suppressed because it is too large. Load diff
+301
View File
@@ -0,0 +1,301 @@
//! Contains CellOccupancyMatrix used to track occupied cells during grid placement
use super::TrackCounts;
use crate::compute::grid::OriginZeroLine;
use crate::geometry::AbsoluteAxis;
use crate::geometry::Line;
use crate::util::sys::Vec;
use core::cmp::max;
use core::fmt::Debug;
use core::ops::Range;
use grid::Grid;
/// The occupancy state of a single grid cell
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub(crate) enum CellOccupancyState {
#[default]
/// Indicates that a grid cell is unoccupied
Unoccupied,
/// Indicates that a grid cell is occupied by a definitely placed item
DefinitelyPlaced,
/// Indicates that a grid cell is occupied by an item that was placed by the auto placement algorithm
AutoPlaced,
}
/// A dynamically sized matrix (2d grid) which tracks the occupancy of each grid cell during auto-placement
/// It also keeps tabs on how many tracks there are and which tracks are implicit and which are explicit.
pub(crate) struct CellOccupancyMatrix {
/// The grid of occupancy states
inner: Grid<CellOccupancyState>,
/// The counts of implicit and explicit columns
columns: TrackCounts,
/// The counts of implicit and explicit rows
rows: TrackCounts,
}
/// Debug impl that represents the matrix in a compact 2d text format
impl Debug for CellOccupancyMatrix {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
writeln!(
f,
"Rows: neg_implicit={} explicit={} pos_implicit={}",
self.rows.negative_implicit, self.rows.explicit, self.rows.positive_implicit
)?;
writeln!(
f,
"Cols: neg_implicit={} explicit={} pos_implicit={}",
self.columns.negative_implicit, self.columns.explicit, self.columns.positive_implicit
)?;
writeln!(f, "State:")?;
for row_idx in 0..self.inner.rows() {
for cell in self.inner.iter_row(row_idx) {
let letter = match *cell {
CellOccupancyState::Unoccupied => '_',
CellOccupancyState::DefinitelyPlaced => 'D',
CellOccupancyState::AutoPlaced => 'A',
};
write!(f, "{letter}")?;
}
writeln!(f)?;
}
Ok(())
}
}
impl CellOccupancyMatrix {
/// Create a CellOccupancyMatrix given a set of provisional track counts. The grid can expand as needed to fit more tracks,
/// the provisional track counts represent a best effort attempt to avoid the extra allocations this requires.
pub fn with_track_counts(columns: TrackCounts, rows: TrackCounts) -> Self {
Self { inner: Grid::new(rows.len(), columns.len()), rows, columns }
}
/// Determines whether the specified area fits within the tracks currently represented by the matrix
pub fn is_area_in_range(
&self,
primary_axis: AbsoluteAxis,
primary_range: Range<i16>,
secondary_range: Range<i16>,
) -> bool {
if primary_range.start < 0 || primary_range.end > self.track_counts(primary_axis).len() as i16 {
return false;
}
if secondary_range.start < 0 || secondary_range.end > self.track_counts(primary_axis.other_axis()).len() as i16
{
return false;
}
true
}
/// Expands the grid (potentially in all 4 directions) in order to ensure that the specified range fits within the allocated space
fn expand_to_fit_range(&mut self, row_range: Range<i16>, col_range: Range<i16>) {
// Calculate number of rows and columns missing to accommodate ranges (if any)
let req_negative_rows = max(-row_range.start, 0);
let req_positive_rows = max(row_range.end - self.rows.len() as i16, 0);
let req_negative_cols = max(-col_range.start, 0);
let req_positive_cols = max(col_range.end - self.columns.len() as i16, 0);
let old_row_count = self.rows.len();
let old_col_count = self.columns.len();
let new_row_count = old_row_count + (req_negative_rows + req_positive_rows) as usize;
let new_col_count = old_col_count + (req_negative_cols + req_positive_cols) as usize;
let mut data = Vec::with_capacity(new_row_count * new_col_count);
// Push new negative rows
for _ in 0..(req_negative_rows as usize * new_col_count) {
data.push(CellOccupancyState::Unoccupied);
}
// Push existing rows
for row in 0..old_row_count {
// Push new negative columns
for _ in 0..req_negative_cols {
data.push(CellOccupancyState::Unoccupied);
}
// Push existing columns
for col in 0..old_col_count {
data.push(*self.inner.get(row, col).unwrap());
}
// Push new positive columns
for _ in 0..req_positive_cols {
data.push(CellOccupancyState::Unoccupied);
}
}
// Push new negative rows
for _ in 0..(req_positive_rows as usize * new_col_count) {
data.push(CellOccupancyState::Unoccupied);
}
// Update self with new data
self.inner = Grid::from_vec(data, new_col_count);
self.rows.negative_implicit += req_negative_rows as u16;
self.rows.positive_implicit += req_positive_rows as u16;
self.columns.negative_implicit += req_negative_cols as u16;
self.columns.positive_implicit += req_positive_cols as u16;
}
/// Mark an area of the matrix as occupied, expanding the allocated space as necessary to accommodate the passed area.
pub fn mark_area_as(
&mut self,
primary_axis: AbsoluteAxis,
primary_span: Line<OriginZeroLine>,
secondary_span: Line<OriginZeroLine>,
value: CellOccupancyState,
) {
let (row_span, column_span) = match primary_axis {
AbsoluteAxis::Horizontal => (secondary_span, primary_span),
AbsoluteAxis::Vertical => (primary_span, secondary_span),
};
let mut col_range = self.columns.oz_line_range_to_track_range(column_span);
let mut row_range = self.rows.oz_line_range_to_track_range(row_span);
// Check that if the resolved ranges fit within the allocated grid. And if they don't then expand the grid to fit
// and then re-resolve the ranges once the grid has been expanded as the resolved indexes may have changed
let is_in_range = self.is_area_in_range(AbsoluteAxis::Horizontal, col_range.clone(), row_range.clone());
if !is_in_range {
self.expand_to_fit_range(row_range.clone(), col_range.clone());
col_range = self.columns.oz_line_range_to_track_range(column_span);
row_range = self.rows.oz_line_range_to_track_range(row_span);
}
for x in row_range {
for y in col_range.clone() {
*self.inner.get_mut(x as usize, y as usize).unwrap() = value;
}
}
}
/// Determines whether a grid area specified by the bounding grid lines in OriginZero coordinates
/// is entirely unnocupied. Returns true if all grid cells within the grid area are unnocupied, else false.
pub fn line_area_is_unoccupied(
&self,
primary_axis: AbsoluteAxis,
primary_span: Line<OriginZeroLine>,
secondary_span: Line<OriginZeroLine>,
) -> bool {
let primary_range = self.track_counts(primary_axis).oz_line_range_to_track_range(primary_span);
let secondary_range = self.track_counts(primary_axis.other_axis()).oz_line_range_to_track_range(secondary_span);
self.track_area_is_unoccupied(primary_axis, primary_range, secondary_range)
}
/// Determines whether a grid area specified by a range of indexes into this CellOccupancyMatrix
/// is entirely unnocupied. Returns true if all grid cells within the grid area are unnocupied, else false.
pub fn track_area_is_unoccupied(
&self,
primary_axis: AbsoluteAxis,
primary_range: Range<i16>,
secondary_range: Range<i16>,
) -> bool {
let (row_range, col_range) = match primary_axis {
AbsoluteAxis::Horizontal => (secondary_range, primary_range),
AbsoluteAxis::Vertical => (primary_range, secondary_range),
};
// Search for occupied cells in the specified area. Out of bounds cells are considered unoccupied.
for x in row_range {
for y in col_range.clone() {
match self.inner.get(x as usize, y as usize) {
None | Some(CellOccupancyState::Unoccupied) => continue,
_ => return false,
}
}
}
true
}
/// Determines whether the specified row contains any items
pub fn row_is_occupied(&self, row_index: usize) -> bool {
if row_index >= self.inner.rows() {
return false;
}
self.inner.iter_row(row_index).any(|cell| !matches!(cell, CellOccupancyState::Unoccupied))
}
/// Determines whether the specified column contains any items
pub fn column_is_occupied(&self, column_index: usize) -> bool {
if column_index >= self.inner.cols() {
return false;
}
self.inner.iter_col(column_index).any(|cell| !matches!(cell, CellOccupancyState::Unoccupied))
}
/// Returns the track counts of this CellOccunpancyMatrix in the relevant axis
pub fn track_counts(&self, track_type: AbsoluteAxis) -> &TrackCounts {
match track_type {
AbsoluteAxis::Horizontal => &self.columns,
AbsoluteAxis::Vertical => &self.rows,
}
}
/// Given an axis and a track index
/// Search backwards from the end of the track and find the last grid cell matching the specified state (if any)
/// Return the index of that cell or None.
pub fn last_of_type(
&self,
track_type: AbsoluteAxis,
start_at: OriginZeroLine,
kind: CellOccupancyState,
) -> Option<OriginZeroLine> {
let track_counts = self.track_counts(track_type.other_axis());
let track_computed_index = track_counts.oz_line_to_next_track(start_at);
let maybe_index = match track_type {
AbsoluteAxis::Horizontal => {
if track_computed_index < 0 || track_computed_index >= self.inner.rows() as i16 {
// Index out of bounds: no tracks to search
None
} else {
self.inner.iter_row(track_computed_index as usize).rposition(|item| *item == kind)
}
}
AbsoluteAxis::Vertical => {
if track_computed_index < 0 || track_computed_index >= self.inner.cols() as i16 {
// Index out of bounds: no tracks to search
None
} else {
self.inner.iter_col(track_computed_index as usize).rposition(|item| *item == kind)
}
}
};
maybe_index.map(|idx| track_counts.track_to_prev_oz_line(idx as u16))
}
/// Given an axis and a track index
/// Search forwards from the start of the track and find the first grid cell matching the specified state (if any)
/// Return the index of that cell or None.
pub fn first_of_type(
&self,
track_type: AbsoluteAxis,
start_at: OriginZeroLine,
kind: CellOccupancyState,
) -> Option<OriginZeroLine> {
let track_counts = self.track_counts(track_type.other_axis());
let track_computed_index = track_counts.oz_line_to_next_track(start_at);
let maybe_index = match track_type {
AbsoluteAxis::Horizontal => {
if track_computed_index < 0 || track_computed_index >= self.inner.rows() as i16 {
// Index out of bounds: no tracks to search
None
} else {
self.inner.iter_row(track_computed_index as usize).position(|item| *item == kind)
}
}
AbsoluteAxis::Vertical => {
if track_computed_index < 0 || track_computed_index >= self.inner.cols() as i16 {
// Index out of bounds: no tracks to search
None
} else {
self.inner.iter_col(track_computed_index as usize).position(|item| *item == kind)
}
}
};
maybe_index.map(|idx| track_counts.track_to_prev_oz_line(idx as u16))
}
}
+150
View File
@@ -0,0 +1,150 @@
//! Taffy uses two coordinate systems to refer to grid lines (the gaps/gutters between rows/columns):
use super::super::types::TrackCounts;
use crate::geometry::Line;
use core::cmp::{max, Ordering};
use core::ops::{Add, AddAssign, Sub};
/// Represents a grid line position in "CSS Grid Line" coordinates
///
/// "CSS Grid Line" coordinates are those used in grid-row/grid-column in the CSS grid spec:
/// - The line at left hand (or top) edge of the explicit grid is line 1
/// (and counts up from there)
/// - The line at the right hand (or bottom) edge of the explicit grid is -1
/// (and counts down from there)
/// - 0 is not a valid index
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(transparent)]
pub struct GridLine(i16);
impl From<i16> for GridLine {
fn from(value: i16) -> Self {
Self(value)
}
}
impl GridLine {
/// Returns the underlying i16
pub fn as_i16(self) -> i16 {
self.0
}
/// Convert into OriginZero coordinates using the specified explicit track count
pub(crate) fn into_origin_zero_line(self, explicit_track_count: u16) -> OriginZeroLine {
let explicit_line_count = explicit_track_count + 1;
let oz_line = match self.0.cmp(&0) {
Ordering::Greater => self.0 - 1,
Ordering::Less => self.0 + explicit_line_count as i16,
Ordering::Equal => panic!("Grid line of zero is invalid"),
};
OriginZeroLine(oz_line)
}
}
/// Represents a grid line position in "OriginZero" coordinates
///
/// "OriginZero" coordinates are a normalized form:
/// - The line at left hand (or top) edge of the explicit grid is line 0
/// - The next line to the right (or down) is 1, and so on
/// - The next line to the left (or up) is -1, and so on
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct OriginZeroLine(pub i16);
// Add and Sub with Self
impl Add<OriginZeroLine> for OriginZeroLine {
type Output = Self;
fn add(self, rhs: OriginZeroLine) -> Self::Output {
OriginZeroLine(self.0 + rhs.0)
}
}
impl Sub<OriginZeroLine> for OriginZeroLine {
type Output = Self;
fn sub(self, rhs: OriginZeroLine) -> Self::Output {
OriginZeroLine(self.0 - rhs.0)
}
}
// Add and Sub with u16
impl Add<u16> for OriginZeroLine {
type Output = Self;
fn add(self, rhs: u16) -> Self::Output {
OriginZeroLine(self.0 + rhs as i16)
}
}
impl AddAssign<u16> for OriginZeroLine {
fn add_assign(&mut self, rhs: u16) {
self.0 += rhs as i16;
}
}
impl Sub<u16> for OriginZeroLine {
type Output = Self;
fn sub(self, rhs: u16) -> Self::Output {
OriginZeroLine(self.0 - rhs as i16)
}
}
impl OriginZeroLine {
/// Converts a grid line in OriginZero coordinates into the index of that same grid line in the GridTrackVec.
pub(crate) fn into_track_vec_index(self, track_counts: TrackCounts) -> usize {
self.try_into_track_vec_index(track_counts).unwrap_or_else(|| {
if self.0 > 0 {
panic!("OriginZero grid line cannot be more than the number of positive grid lines");
} else {
panic!("OriginZero grid line cannot be less than the number of negative grid lines");
}
})
}
/// Converts a grid line in OriginZero coordinates into the index of that same grid line in the GridTrackVec.
///
/// This fallible version is used for the placement of absolutely positioned grid items:
///
/// If a grid-placement property refers to a non-existent line either by explicitly specifying such a line or by
/// spanning outside of the existing implicit grid, it is instead treated as specifying auto (instead of creating
/// new implicit grid lines).
///
/// The infallible version above if used when placing regular in-flow grid items.
pub(crate) fn try_into_track_vec_index(self, track_counts: TrackCounts) -> Option<usize> {
// OriginZero grid line cannot be less than the number of negative grid lines
if self.0 < -(track_counts.negative_implicit as i16) {
return None;
};
// OriginZero grid line cannot be more than the number of positive grid lines
if self.0 > (track_counts.explicit + track_counts.positive_implicit) as i16 {
return None;
};
Some(2 * ((self.0 + track_counts.negative_implicit as i16) as usize))
}
/// The minimum number of negative implicit track there must be if a grid item starts at this line.
pub(crate) fn implied_negative_implicit_tracks(self) -> u16 {
if self.0 < 0 {
self.0.unsigned_abs()
} else {
0
}
}
/// The minimum number of positive implicit track there must be if a grid item end at this line.
pub(crate) fn implied_positive_implicit_tracks(self, explicit_track_count: u16) -> u16 {
if self.0 > explicit_track_count as i16 {
self.0 as u16 - explicit_track_count
} else {
0
}
}
}
impl Line<OriginZeroLine> {
/// The number of tracks between the start and end lines
pub(crate) fn span(self) -> u16 {
max(self.end.0 - self.start.0, 0) as u16
}
}
/// A trait for the different coordinates used to define grid lines.
pub trait GridCoordinate: Copy {}
impl GridCoordinate for GridLine {}
impl GridCoordinate for OriginZeroLine {}
+609
View File
@@ -0,0 +1,609 @@
//! Contains GridItem used to represent a single grid item during layout
use super::GridTrack;
use crate::compute::grid::OriginZeroLine;
use crate::geometry::AbstractAxis;
use crate::geometry::{Line, Point, Rect, Size};
use crate::style::{AlignItems, AlignSelf, AvailableSpace, Dimension, LengthPercentageAuto, Overflow};
use crate::tree::{LayoutPartialTree, LayoutPartialTreeExt, NodeId, SizingMode};
use crate::util::{MaybeMath, MaybeResolve, ResolveOrZero};
use crate::{BoxSizing, GridItemStyle, LengthPercentage};
use core::ops::Range;
/// Represents a single grid item
#[derive(Debug)]
pub(in super::super) struct GridItem {
/// The id of the node that this item represents
pub node: NodeId,
/// The order of the item in the children array
///
/// We sort the list of grid items during track sizing. This field allows us to sort back the original order
/// for final positioning
pub source_order: u16,
/// The item's definite row-start and row-end, as resolved by the placement algorithm
/// (in origin-zero coordinates)
pub row: Line<OriginZeroLine>,
/// The items definite column-start and column-end, as resolved by the placement algorithm
/// (in origin-zero coordinates)
pub column: Line<OriginZeroLine>,
/// Is it a compressible replaced element?
/// https://drafts.csswg.org/css-sizing-3/#min-content-zero
pub is_compressible_replaced: bool,
/// The item's overflow style
pub overflow: Point<Overflow>,
/// The item's box_sizing style
pub box_sizing: BoxSizing,
/// The item's size style
pub size: Size<Dimension>,
/// The item's min_size style
pub min_size: Size<Dimension>,
/// The item's max_size style
pub max_size: Size<Dimension>,
/// The item's aspect_ratio style
pub aspect_ratio: Option<f32>,
/// The item's padding style
pub padding: Rect<LengthPercentage>,
/// The item's border style
pub border: Rect<LengthPercentage>,
/// The item's margin style
pub margin: Rect<LengthPercentageAuto>,
/// The item's align_self property, or the parent's align_items property is not set
pub align_self: AlignSelf,
/// The item's justify_self property, or the parent's justify_items property is not set
pub justify_self: AlignSelf,
/// The items first baseline (horizontal)
pub baseline: Option<f32>,
/// Shim for baseline alignment that acts like an extra top margin
/// TODO: Support last baseline and vertical text baselines
pub baseline_shim: f32,
/// The item's definite row-start and row-end (same as `row` field, except in a different coordinate system)
/// (as indexes into the Vec<GridTrack> stored in a grid's AbstractAxisTracks)
pub row_indexes: Line<u16>,
/// The items definite column-start and column-end (same as `column` field, except in a different coordinate system)
/// (as indexes into the Vec<GridTrack> stored in a grid's AbstractAxisTracks)
pub column_indexes: Line<u16>,
/// Whether the item crosses a flexible row
pub crosses_flexible_row: bool,
/// Whether the item crosses a flexible column
pub crosses_flexible_column: bool,
/// Whether the item crosses a intrinsic row
pub crosses_intrinsic_row: bool,
/// Whether the item crosses a intrinsic column
pub crosses_intrinsic_column: bool,
// Caches for intrinsic size computation. These caches are only valid for a single run of the track-sizing algorithm.
/// Cache for the known_dimensions input to intrinsic sizing computation
pub grid_area_size_cache: Option<Size<Option<f32>>>,
/// Cache for the min-content size
pub min_content_contribution_cache: Size<Option<f32>>,
/// Cache for the minimum contribution
pub minimum_contribution_cache: Size<Option<f32>>,
/// Cache for the max-content size
pub max_content_contribution_cache: Size<Option<f32>>,
/// Final y position. Used to compute baseline alignment for the container.
pub y_position: f32,
/// Final height. Used to compute baseline alignment for the container.
pub height: f32,
}
impl GridItem {
/// Create a new item given a concrete placement in both axes
pub fn new_with_placement_style_and_order<S: GridItemStyle>(
node: NodeId,
col_span: Line<OriginZeroLine>,
row_span: Line<OriginZeroLine>,
style: S,
parent_align_items: AlignItems,
parent_justify_items: AlignItems,
source_order: u16,
) -> Self {
GridItem {
node,
source_order,
row: row_span,
column: col_span,
is_compressible_replaced: style.is_compressible_replaced(),
overflow: style.overflow(),
box_sizing: style.box_sizing(),
size: style.size(),
min_size: style.min_size(),
max_size: style.max_size(),
aspect_ratio: style.aspect_ratio(),
padding: style.padding(),
border: style.border(),
margin: style.margin(),
align_self: style.align_self().unwrap_or(parent_align_items),
justify_self: style.justify_self().unwrap_or(parent_justify_items),
baseline: None,
baseline_shim: 0.0,
row_indexes: Line { start: 0, end: 0 }, // Properly initialised later
column_indexes: Line { start: 0, end: 0 }, // Properly initialised later
crosses_flexible_row: false, // Properly initialised later
crosses_flexible_column: false, // Properly initialised later
crosses_intrinsic_row: false, // Properly initialised later
crosses_intrinsic_column: false, // Properly initialised later
grid_area_size_cache: None,
min_content_contribution_cache: Size::NONE,
max_content_contribution_cache: Size::NONE,
minimum_contribution_cache: Size::NONE,
y_position: 0.0,
height: 0.0,
}
}
/// This item's placement in the specified axis in OriginZero coordinates
pub fn placement(&self, axis: AbstractAxis) -> Line<OriginZeroLine> {
match axis {
AbstractAxis::Block => self.row,
AbstractAxis::Inline => self.column,
}
}
/// This item's placement in the specified axis as GridTrackVec indices
pub fn placement_indexes(&self, axis: AbstractAxis) -> Line<u16> {
match axis {
AbstractAxis::Block => self.row_indexes,
AbstractAxis::Inline => self.column_indexes,
}
}
/// Returns a range which can be used as an index into the GridTrackVec in the specified axis
/// which will produce a sub-slice of covering all the tracks and lines that this item spans
/// excluding the lines that bound it.
pub fn track_range_excluding_lines(&self, axis: AbstractAxis) -> Range<usize> {
let indexes = self.placement_indexes(axis);
(indexes.start as usize + 1)..(indexes.end as usize)
}
/// Returns the number of tracks that this item spans in the specified axis
pub fn span(&self, axis: AbstractAxis) -> u16 {
match axis {
AbstractAxis::Block => self.row.span(),
AbstractAxis::Inline => self.column.span(),
}
}
/// Returns the pre-computed value indicating whether the grid item crosses a flexible track in
/// the specified axis
pub fn crosses_flexible_track(&self, axis: AbstractAxis) -> bool {
match axis {
AbstractAxis::Inline => self.crosses_flexible_column,
AbstractAxis::Block => self.crosses_flexible_row,
}
}
/// Returns the pre-computed value indicating whether the grid item crosses an intrinsic track in
/// the specified axis
pub fn crosses_intrinsic_track(&self, axis: AbstractAxis) -> bool {
match axis {
AbstractAxis::Inline => self.crosses_intrinsic_column,
AbstractAxis::Block => self.crosses_intrinsic_row,
}
}
/// For an item spanning multiple tracks, the upper limit used to calculate its limited min-/max-content contribution is the
/// sum of the fixed max track sizing functions of any tracks it spans, and is applied if it only spans such tracks.
pub fn spanned_track_limit(
&mut self,
axis: AbstractAxis,
axis_tracks: &[GridTrack],
axis_parent_size: Option<f32>,
resolve_calc_value: &dyn Fn(*const (), f32) -> f32,
) -> Option<f32> {
let spanned_tracks = &axis_tracks[self.track_range_excluding_lines(axis)];
let tracks_all_fixed = spanned_tracks.iter().all(|track| {
track.max_track_sizing_function.definite_limit(axis_parent_size, resolve_calc_value).is_some()
});
if tracks_all_fixed {
let limit: f32 = spanned_tracks
.iter()
.map(|track| {
track.max_track_sizing_function.definite_limit(axis_parent_size, resolve_calc_value).unwrap()
})
.sum();
Some(limit)
} else {
None
}
}
/// Similar to the spanned_track_limit, but excludes FitContent arguments from the limit.
/// Used to clamp the automatic minimum contributions of an item
pub fn spanned_fixed_track_limit(
&mut self,
axis: AbstractAxis,
axis_tracks: &[GridTrack],
axis_parent_size: Option<f32>,
resolve_calc_value: &dyn Fn(*const (), f32) -> f32,
) -> Option<f32> {
let spanned_tracks = &axis_tracks[self.track_range_excluding_lines(axis)];
let tracks_all_fixed = spanned_tracks.iter().all(|track| {
track.max_track_sizing_function.definite_value(axis_parent_size, resolve_calc_value).is_some()
});
if tracks_all_fixed {
let limit: f32 = spanned_tracks
.iter()
.map(|track| {
track.max_track_sizing_function.definite_value(axis_parent_size, resolve_calc_value).unwrap()
})
.sum();
Some(limit)
} else {
None
}
}
/// Compute the known_dimensions to be passed to the child sizing functions
/// The key thing that is being done here is applying stretch alignment, which is necessary to
/// allow percentage sizes further down the tree to resolve properly in some cases
fn known_dimensions(
&self,
tree: &mut impl LayoutPartialTree,
grid_area_size: Size<Option<f32>>,
) -> Size<Option<f32>> {
let margins = self.margins_axis_sums_with_baseline_shims(grid_area_size.width, tree);
let aspect_ratio = self.aspect_ratio;
// CSS resolves percentage padding and border against the inline size of the containing
// block. For a grid item under intrinsic measurement, that inline-size basis is the grid
// area's width when it is definite.
// Spec:
// https://www.w3.org/TR/css-grid-1/#item-margins
// https://www.w3.org/TR/CSS22/box.html#padding-properties
let padding = self.padding.resolve_or_zero(grid_area_size.width, |val, basis| tree.calc(val, basis));
let border = self.border.resolve_or_zero(grid_area_size.width, |val, basis| tree.calc(val, basis));
let padding_border_size = (padding + border).sum_axes();
let box_sizing_adjustment =
if self.box_sizing == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
let inherent_size = self
.size
.maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
let min_size = self
.min_size
.maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
let max_size = self
.max_size
.maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
let grid_area_minus_item_margins_size = grid_area_size.maybe_sub(margins);
// If node is absolutely positioned and width is not set explicitly, then deduce it
// from left, right and container_content_box if both are set.
let width = inherent_size.width.or_else(|| {
// Apply width based on stretch alignment if:
// - Alignment style is "stretch"
// - The node is not absolutely positioned
// - The node does not have auto margins in this axis.
if !self.margin.left.is_auto() && !self.margin.right.is_auto() && self.justify_self == AlignSelf::STRETCH {
return grid_area_minus_item_margins_size.width;
}
None
});
// Reapply aspect ratio after stretch and absolute position width adjustments
let Size { width, height } =
Size { width, height: inherent_size.height }.maybe_apply_aspect_ratio(aspect_ratio);
let height = height.or_else(|| {
// Apply height based on stretch alignment if:
// - Alignment style is "stretch"
// - The node is not absolutely positioned
// - The node does not have auto margins in this axis.
if !self.margin.top.is_auto() && !self.margin.bottom.is_auto() && self.align_self == AlignSelf::STRETCH {
return grid_area_minus_item_margins_size.height;
}
None
});
// Reapply aspect ratio after stretch and absolute position height adjustments
let Size { width, height } = Size { width, height }.maybe_apply_aspect_ratio(aspect_ratio);
// Clamp size by min and max width/height
let Size { width, height } = Size { width, height }.maybe_clamp(min_size, max_size);
Size { width, height }
}
/// Returns the grid area's size in the specified axis when every spanned track has a definite fixed size.
///
/// During intrinsic sizing, percentages on grid items resolve against the size of the grid area,
/// not the grid container. If the spanned tracks in an axis are not all definite yet, the grid
/// area is still indefinite in that axis and percentage-dependent values must stay unresolved here.
///
/// Spec:
/// https://www.w3.org/TR/css-grid-1/#grid-item-sizing
/// https://www.w3.org/TR/css-grid-1/#algo-overview
///
/// Compute the available_space to be passed to the child sizing functions
/// These are estimates based on either the max track sizing function or the provisional base size in the opposite
/// axis to the one currently being sized.
/// https://www.w3.org/TR/css-grid-1/#algo-overview
pub fn grid_area_size(
&self,
axis: AbstractAxis,
axis_tracks: &[GridTrack],
other_axis_tracks: &[GridTrack],
available_space: Size<Option<f32>>,
get_track_size_estimate: impl Fn(&GridTrack, Option<f32>) -> Option<f32>,
resolve_calc_value: &impl Fn(*const (), f32) -> f32,
) -> Size<Option<f32>> {
let mut size = Size::NONE;
size.set(
axis,
axis_tracks[self.track_range_excluding_lines(axis)]
.iter()
.map(|track| {
let min_size = track
.min_track_sizing_function
.definite_value(available_space.get(axis), resolve_calc_value)?;
let max_size = track
.max_track_sizing_function
.definite_value(available_space.get(axis), resolve_calc_value)?;
if min_size == max_size {
Some(track.base_size)
} else {
None
}
})
.sum::<Option<f32>>(),
);
size.set(
axis.other(),
other_axis_tracks[self.track_range_excluding_lines(axis.other())]
.iter()
.map(|track| {
get_track_size_estimate(track, available_space.get(axis.other()))
.map(|size| size + track.content_alignment_adjustment)
})
.sum::<Option<f32>>(),
);
size
}
/// Retrieve the available_space from the cache or compute them using the passed parameters
pub fn grid_area_size_cached(
&mut self,
axis: AbstractAxis,
axis_tracks: &[GridTrack],
other_axis_tracks: &[GridTrack],
available_space: Size<Option<f32>>,
get_track_size_estimate: impl Fn(&GridTrack, Option<f32>) -> Option<f32>,
resolve_calc_value: &impl Fn(*const (), f32) -> f32,
) -> Size<Option<f32>> {
self.grid_area_size_cache.unwrap_or_else(|| {
let grid_area_size = self.grid_area_size(
axis,
axis_tracks,
other_axis_tracks,
available_space,
get_track_size_estimate,
resolve_calc_value,
);
self.grid_area_size_cache = Some(grid_area_size);
grid_area_size
})
}
/// Compute the item's resolved margins for size contributions. Horizontal percentage margins always resolve
/// to zero if the container size is indefinite as otherwise this would introduce a cyclic dependency.
#[inline(always)]
pub fn margins_axis_sums_with_baseline_shims(
&self,
inner_node_width: Option<f32>,
tree: &impl LayoutPartialTree,
) -> Size<f32> {
Rect {
left: self.margin.left.resolve_or_zero(Some(0.0), |val, basis| tree.calc(val, basis)),
right: self.margin.right.resolve_or_zero(Some(0.0), |val, basis| tree.calc(val, basis)),
top: self.margin.top.resolve_or_zero(inner_node_width, |val, basis| tree.calc(val, basis))
+ self.baseline_shim,
bottom: self.margin.bottom.resolve_or_zero(inner_node_width, |val, basis| tree.calc(val, basis)),
}
.sum_axes()
}
/// Compute the item's min content contribution from the provided parameters
pub fn min_content_contribution(
&self,
axis: AbstractAxis,
tree: &mut impl LayoutPartialTree,
grid_area_size: Size<Option<f32>>,
available_space: Size<Option<f32>>,
) -> f32 {
let known_dimensions = self.known_dimensions(tree, grid_area_size);
// The child sees the grid area as its containing block during intrinsic measurement, so
// percentage box properties resolve against the grid area when that size is definite.
// Spec:
// https://www.w3.org/TR/css-grid-1/#grid-item-sizing
// https://www.w3.org/TR/css-grid-1/#algo-overview
tree.measure_child_size(
self.node,
known_dimensions,
grid_area_size,
available_space.map(|opt| match opt {
Some(size) => AvailableSpace::Definite(size),
None => AvailableSpace::MinContent,
}),
SizingMode::InherentSize,
axis.as_abs_naive(),
Line::FALSE,
)
}
/// Retrieve the item's min content contribution from the cache or compute it using the provided parameters
#[inline(always)]
pub fn min_content_contribution_cached(
&mut self,
axis: AbstractAxis,
tree: &mut impl LayoutPartialTree,
grid_area_size: Size<Option<f32>>,
available_space: Size<Option<f32>>,
) -> f32 {
self.min_content_contribution_cache.get(axis).unwrap_or_else(|| {
let size = self.min_content_contribution(axis, tree, grid_area_size, available_space);
self.min_content_contribution_cache.set(axis, Some(size));
size
})
}
/// Compute the item's max content contribution from the provided parameters
pub fn max_content_contribution(
&self,
axis: AbstractAxis,
tree: &mut impl LayoutPartialTree,
grid_area_size: Size<Option<f32>>,
available_space: Size<Option<f32>>,
) -> f32 {
let known_dimensions = self.known_dimensions(tree, grid_area_size);
// See the min-content path above. Max-content measurement uses the same containing-block
// basis so percentage-dependent item geometry is measured from the grid area rather than
// from the container.
tree.measure_child_size(
self.node,
known_dimensions,
grid_area_size,
available_space.map(|opt| match opt {
Some(size) => AvailableSpace::Definite(size),
None => AvailableSpace::MaxContent,
}),
SizingMode::InherentSize,
axis.as_abs_naive(),
Line::FALSE,
)
}
/// Retrieve the item's max content contribution from the cache or compute it using the provided parameters
#[inline(always)]
pub fn max_content_contribution_cached(
&mut self,
axis: AbstractAxis,
tree: &mut impl LayoutPartialTree,
grid_area_size: Size<Option<f32>>,
available_space: Size<Option<f32>>,
) -> f32 {
self.max_content_contribution_cache.get(axis).unwrap_or_else(|| {
let size = self.max_content_contribution(axis, tree, grid_area_size, available_space);
self.max_content_contribution_cache.set(axis, Some(size));
size
})
}
/// The minimum contribution of an item is the smallest outer size it can have.
/// Specifically:
/// - If the item’s computed preferred size behaves as auto or depends on the size of its containing block in the relevant axis:
/// Its minimum contribution is the outer size that would result from assuming the item’s used minimum size as its preferred size;
/// - Else the item’s minimum contribution is its min-content contribution.
///
/// Because the minimum contribution often depends on the size of the item’s content, it is considered a type of intrinsic size contribution.
/// See: https://www.w3.org/TR/css-grid-1/#min-size-auto
pub fn minimum_contribution(
&mut self,
tree: &mut impl LayoutPartialTree,
axis: AbstractAxis,
axis_tracks: &[GridTrack],
grid_area_size: Size<Option<f32>>,
inner_node_size: Size<Option<f32>>,
) -> f32 {
let padding = self.padding.resolve_or_zero(grid_area_size.width, |val, basis| tree.calc(val, basis));
let border = self.border.resolve_or_zero(grid_area_size.width, |val, basis| tree.calc(val, basis));
let padding_border_size = (padding + border).sum_axes();
let box_sizing_adjustment =
if self.box_sizing == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
let size = self
.size
.maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(self.aspect_ratio)
.maybe_add(box_sizing_adjustment)
.get(axis)
.or_else(|| {
self.min_size
.maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(self.aspect_ratio)
.maybe_add(box_sizing_adjustment)
.get(axis)
})
.or_else(|| self.overflow.get(axis).maybe_into_automatic_min_size())
.unwrap_or_else(|| {
// Automatic minimum size. See https://www.w3.org/TR/css-grid-1/#min-size-auto
// To provide a more reasonable default minimum size for grid items, the used value of its automatic minimum size
// in a given axis is the content-based minimum size if all of the following are true:
let item_axis_tracks = &axis_tracks[self.track_range_excluding_lines(axis)];
// it is not a scroll container
// TODO: support overflow property
// it spans at least one track in that axis whose min track sizing function is auto
let spans_auto_min_track = axis_tracks
.iter()
// TODO: should this be 'behaves as auto' rather than just literal auto?
.any(|track| track.min_track_sizing_function.is_auto());
// if it spans more than one track in that axis, none of those tracks are flexible
let only_span_one_track = item_axis_tracks.len() == 1;
let spans_a_flexible_track = axis_tracks.iter().any(|track| track.max_track_sizing_function.is_fr());
let use_content_based_minimum =
spans_auto_min_track && (only_span_one_track || !spans_a_flexible_track);
// Otherwise, the automatic minimum size is zero, as usual.
if use_content_based_minimum {
let mut minimum_contribution =
self.min_content_contribution_cached(axis, tree, grid_area_size, grid_area_size);
// If the item is a compressible replaced element, and has a definite preferred size or maximum size in the
// relevant axis, the size suggestion is capped by those sizes; for this purpose, any indefinite percentages
// in these sizes are resolved against zero (and considered definite).
if self.is_compressible_replaced {
let size = self.size.get(axis).maybe_resolve(Some(0.0), |val, basis| tree.calc(val, basis));
let max_size =
self.max_size.get(axis).maybe_resolve(Some(0.0), |val, basis| tree.calc(val, basis));
minimum_contribution = minimum_contribution.maybe_min(size).maybe_min(max_size);
}
minimum_contribution
} else {
0.0
}
});
// In all cases, the size suggestion is additionally clamped by the maximum size in the affected axis, if it’s definite.
// Note: The argument to fit-content() does not clamp the content-based minimum size in the same way as a fixed max track
// sizing function.
let limit = self.spanned_fixed_track_limit(axis, axis_tracks, inner_node_size.get(axis), &|val, basis| {
tree.resolve_calc_value(val, basis)
});
size.maybe_min(limit)
}
/// Retrieve the item's minimum contribution from the cache or compute it using the provided parameters
#[inline(always)]
pub fn minimum_contribution_cached(
&mut self,
tree: &mut impl LayoutPartialTree,
axis: AbstractAxis,
axis_tracks: &[GridTrack],
grid_area_size: Size<Option<f32>>,
inner_node_size: Size<Option<f32>>,
) -> f32 {
self.minimum_contribution_cache.get(axis).unwrap_or_else(|| {
let size = self.minimum_contribution(tree, axis, axis_tracks, grid_area_size, inner_node_size);
self.minimum_contribution_cache.set(axis, Some(size));
size
})
}
}
+156
View File
@@ -0,0 +1,156 @@
//! Contains GridTrack used to represent a single grid track (row/column) during layout
use crate::{
prelude::TaffyZero,
style::{LengthPercentage, MaxTrackSizingFunction, MinTrackSizingFunction},
util::sys::f32_min,
CompactLength,
};
/// Whether a GridTrack represents an actual track or a gutter.
#[derive(Copy, Clone, Debug, PartialEq)]
pub(in super::super) enum GridTrackKind {
/// Track is an actual track
Track,
/// Track is a gutter (aka grid line) (aka gap)
Gutter, // { name: Option<u16> },
}
/// Internal sizing information for a single grid track (row/column)
/// Gutters between tracks are sized similarly to actual tracks, so they
/// are also represented by this struct
#[derive(Debug, Clone)]
pub(in super::super) struct GridTrack {
#[allow(dead_code)] // Used in tests + may be useful in future
/// Whether the track is a full track, a gutter, or a placeholder that has not yet been initialised
pub kind: GridTrackKind,
/// Whether the track is a collapsed track/gutter. Collapsed tracks are effectively treated as if
/// they don't exist for the purposes of grid sizing. Gutters between collapsed tracks are also collapsed.
pub is_collapsed: bool,
/// The minimum track sizing function of the track
pub min_track_sizing_function: MinTrackSizingFunction,
/// The maximum track sizing function of the track
pub max_track_sizing_function: MaxTrackSizingFunction,
/// The distance of the start of the track from the start of the grid container
pub offset: f32,
/// The size (width/height as applicable) of the track
pub base_size: f32,
/// A temporary scratch value when sizing tracks
/// Note: can be infinity
pub growth_limit: f32,
/// A temporary scratch value when sizing tracks. Is used as an additional amount to add to the
/// estimate for the available space in the opposite axis when content sizing items
pub content_alignment_adjustment: f32,
/// A temporary scratch value when "distributing space" to avoid clobbering planned increase variable
pub item_incurred_increase: f32,
/// A temporary scratch value when "distributing space" to avoid clobbering the main variable
pub base_size_planned_increase: f32,
/// A temporary scratch value when "distributing space" to avoid clobbering the main variable
pub growth_limit_planned_increase: f32,
/// A temporary scratch value when "distributing space"
/// See: https://www.w3.org/TR/css3-grid-layout/#infinitely-growable
pub infinitely_growable: bool,
}
impl GridTrack {
/// GridTrack constructor with all configuration parameters for the other constructors exposed
const fn new_with_kind(
kind: GridTrackKind,
min_track_sizing_function: MinTrackSizingFunction,
max_track_sizing_function: MaxTrackSizingFunction,
) -> GridTrack {
GridTrack {
kind,
is_collapsed: false,
min_track_sizing_function,
max_track_sizing_function,
offset: 0.0,
base_size: 0.0,
growth_limit: 0.0,
content_alignment_adjustment: 0.0,
item_incurred_increase: 0.0,
base_size_planned_increase: 0.0,
growth_limit_planned_increase: 0.0,
infinitely_growable: false,
}
}
/// Create new GridTrack representing an actual track (not a gutter)
pub const fn new(
min_track_sizing_function: MinTrackSizingFunction,
max_track_sizing_function: MaxTrackSizingFunction,
) -> GridTrack {
Self::new_with_kind(GridTrackKind::Track, min_track_sizing_function, max_track_sizing_function)
}
/// Create a new GridTrack representing a gutter
pub fn gutter(size: LengthPercentage) -> GridTrack {
Self::new_with_kind(
GridTrackKind::Gutter,
MinTrackSizingFunction::from(size),
MaxTrackSizingFunction::from(size),
)
}
/// Mark a GridTrack as collapsed. Also sets both of the track's sizing functions
/// to fixed zero-sized sizing functions.
pub fn collapse(&mut self) {
self.is_collapsed = true;
self.min_track_sizing_function = MinTrackSizingFunction::ZERO;
self.max_track_sizing_function = MaxTrackSizingFunction::ZERO;
}
#[inline(always)]
/// Returns true if the track is flexible (has a Flex MaxTrackSizingFunction), else false.
pub fn is_flexible(&self) -> bool {
self.max_track_sizing_function.is_fr()
}
#[inline(always)]
/// Returns true if the track is flexible (has a Flex MaxTrackSizingFunction), else false.
pub fn uses_percentage(&self) -> bool {
self.min_track_sizing_function.uses_percentage() || self.max_track_sizing_function.uses_percentage()
}
#[inline(always)]
/// Returns true if the track has an intrinsic min and or max sizing function
pub fn has_intrinsic_sizing_function(&self) -> bool {
self.min_track_sizing_function.is_intrinsic() || self.max_track_sizing_function.is_intrinsic()
}
#[inline]
/// Returns true if the track is flexible (has a Flex MaxTrackSizingFunction), else false.
pub fn fit_content_limit(&self, axis_available_grid_space: Option<f32>) -> f32 {
match self.max_track_sizing_function.0.tag() {
CompactLength::FIT_CONTENT_PX_TAG => self.max_track_sizing_function.0.value(),
CompactLength::FIT_CONTENT_PERCENT_TAG => match axis_available_grid_space {
Some(space) => space * self.max_track_sizing_function.0.value(),
None => f32::INFINITY,
},
_ => f32::INFINITY,
}
}
#[inline]
/// Returns true if the track is flexible (has a Flex MaxTrackSizingFunction), else false.
pub fn fit_content_limited_growth_limit(&self, axis_available_grid_space: Option<f32>) -> f32 {
f32_min(self.growth_limit, self.fit_content_limit(axis_available_grid_space))
}
#[inline]
/// Returns the track's flex factor if it is a flex track, else 0.
pub fn flex_factor(&self) -> f32 {
if self.max_track_sizing_function.is_fr() {
self.max_track_sizing_function.0.value()
} else {
0.0
}
}
}
+102
View File
@@ -0,0 +1,102 @@
//! Contains TrackCounts used to keep track of the number of tracks in the explicit and implicit grids.
//! Also contains coordinate conversion functions which depend on those counts
//!
//! Taffy uses two coordinate systems to refer to grid lines (the gaps/gutters between rows/columns):
//!
//! "CSS Grid Line" coordinates are those used in grid-row/grid-column in the CSS grid spec:
//! - 0 is not a valid index
//! - The line at left hand (or top) edge of the explicit grid is line 1
//! (and counts up from there)
//! - The line at the right hand (or bottom) edge of the explicit grid in -1
//! (and counts down from there)
//!
//! "OriginZero" coordinates are a normalized form:
//! - The line at left hand (or top) edge of the explicit grid is line 0
//! - The next line to the right (or down) is 1, and so on
//! - The next line to the left (or up) is -1, and so on
//!
//! Taffy also uses two coordinate systems to refer to grid tracks (rows/columns):
//!
//! Both of these systems represent the entire implicit grid, not just the explicit grid.
//!
//! "CellOccupancyMatrix track indices":
//! - These are indexes into the CellOccupancyMatrix
//! - The CellOccupancyMatrix stores only tracks
//! - 0 is the leftmost track of the implicit grid, and indexes count up there
//!
//! "GridTrackVec track indices":
//! - The GridTrackVecs store both lines and tracks, so:
//! - even indices (0, 2, 4, etc) represent lines
//! - odd indices (1, 3, 5, etc) represent tracks
//! - These is always an odd number of
//! - Index 1 is the leftmost track of the implicit grid. Index 3 is the second leftmost track, etc.
//! - Index 0 is the leftmost grid line. Index 2 is the second leftmost line, etc.
//!
use crate::{compute::grid::OriginZeroLine, geometry::Line};
use core::ops::Range;
/// Stores the number of tracks in a given dimension.
/// Stores separately the number of tracks in the implicit and explicit grids
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub(crate) struct TrackCounts {
/// The number of track in the implicit grid before the explicit grid
pub negative_implicit: u16,
/// The number of tracks in the explicit grid
pub explicit: u16,
/// The number of tracks in the implicit grid after the explicit grid
pub positive_implicit: u16,
}
impl TrackCounts {
/// Create a TrackCounts instance from raw track count numbers
pub const fn from_raw(negative_implicit: u16, explicit: u16, positive_implicit: u16) -> Self {
Self { negative_implicit, explicit, positive_implicit }
}
/// Count the total number of tracks in the axis
pub const fn len(&self) -> usize {
(self.negative_implicit + self.explicit + self.positive_implicit) as usize
}
/// The OriginZeroLine representing the start of the implicit grid
pub const fn implicit_start_line(&self) -> OriginZeroLine {
OriginZeroLine(-(self.negative_implicit as i16))
}
/// The OriginZeroLine representing the end of the implicit grid
pub const fn implicit_end_line(&self) -> OriginZeroLine {
OriginZeroLine((self.explicit + self.positive_implicit) as i16)
}
}
/// Conversion functions between OriginZero coordinates and CellOccupancyMatrix track indexes
#[allow(dead_code)]
impl TrackCounts {
/// Converts a grid line in OriginZero coordinates into the track immediately
/// following that grid line as an index into the CellOccupancyMatrix.
pub const fn oz_line_to_next_track(&self, index: OriginZeroLine) -> i16 {
index.0 + (self.negative_implicit as i16)
}
/// Converts start and end grid lines in OriginZero coordinates into a range of tracks
/// as indexes into the CellOccupancyMatrix
pub const fn oz_line_range_to_track_range(&self, input: Line<OriginZeroLine>) -> Range<i16> {
let start = self.oz_line_to_next_track(input.start);
let end = self.oz_line_to_next_track(input.end); // Don't subtract 1 as output range is exclusive
start..end
}
/// Converts a track as an index into the CellOccupancyMatrix into the grid line immediately
/// preceding that track in OriginZero coordinates.
pub const fn track_to_prev_oz_line(&self, index: u16) -> OriginZeroLine {
OriginZeroLine((index as i16) - (self.negative_implicit as i16))
}
/// Converts a range of tracks as indexes into the CellOccupancyMatrix into
/// start and end grid lines in OriginZero coordinates
pub const fn track_range_to_oz_line_range(&self, input: Range<i16>) -> Line<OriginZeroLine> {
let start = self.track_to_prev_oz_line(input.start as u16);
let end = self.track_to_prev_oz_line(input.end as u16); // Don't add 1 as input range is exclusive
Line { start, end }
}
}
+18
View File
@@ -0,0 +1,18 @@
//! Structs and enums that are used within the grid module
mod cell_occupancy;
mod coordinates;
mod grid_item;
mod grid_track;
mod grid_track_counts;
mod named;
// Publish only locally in the grid module
pub(super) use cell_occupancy::{CellOccupancyMatrix, CellOccupancyState};
pub(crate) use coordinates::{GridCoordinate, GridLine, OriginZeroLine};
pub(super) use grid_item::GridItem;
pub(super) use grid_track::GridTrack;
pub(super) use grid_track_counts::TrackCounts;
pub(super) use named::NamedLineResolver;
#[allow(unused_imports)]
pub(super) use grid_track::GridTrackKind;
+428
View File
@@ -0,0 +1,428 @@
//! Code for resolving name grid lines and areas
use crate::{
CheapCloneStr, GenericGridTemplateComponent, GenericRepetition as _, GridAreaAxis, GridAreaEnd, GridContainerStyle,
GridPlacement, GridTemplateArea, Line, NonNamedGridPlacement, RepetitionCount,
};
use core::{borrow::Borrow, cmp::Ordering, fmt::Debug};
use super::GridLine;
// use alloc::fmt::format;
use crate::sys::{format, single_value_vec, Map, Vec};
/// Wrap an `AsRef<str>` type with a type which implements Hash by first
/// deferring to the underlying `&str`'s implementation of Hash.
#[derive(Debug, Clone)]
pub(crate) struct StrHasher<T: CheapCloneStr>(pub T);
impl<T: CheapCloneStr> PartialOrd for StrHasher<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<T: CheapCloneStr> Ord for StrHasher<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.0.as_ref().cmp(other.0.as_ref())
}
}
impl<T: CheapCloneStr> PartialEq for StrHasher<T> {
fn eq(&self, other: &Self) -> bool {
other.0.as_ref() == self.0.as_ref()
}
}
impl<T: CheapCloneStr> Eq for StrHasher<T> {}
#[cfg(feature = "std")]
impl<T: CheapCloneStr> std::hash::Hash for StrHasher<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.as_ref().hash(state)
}
}
impl<T: CheapCloneStr> Borrow<str> for StrHasher<T> {
fn borrow(&self) -> &str {
self.0.as_ref()
}
}
/// Resolver that takes grid lines names and area names as input and can then be used to
/// resolve line names of grid placement properties into line numbers.
pub(crate) struct NamedLineResolver<S: CheapCloneStr> {
/// Map of row line names to line numbers. Each line name may correspond to multiple lines
/// so we store a `Vec`
row_lines: Map<StrHasher<S>, Vec<u16>>,
/// Map of column line names to line numbers. Each line name may correspond to multiple lines
/// so we store a `Vec`
column_lines: Map<StrHasher<S>, Vec<u16>>,
/// Map of area names to area definitions (start and end lines numbers in each axis)
areas: Map<StrHasher<S>, GridTemplateArea<S>>,
/// Number of columns implied by grid area definitions
area_column_count: u16,
/// Number of rows implied by grid area definitions
area_row_count: u16,
/// The number of explicit columns in the grid. This is an *input* to the `NamedLineResolver` and is
/// used when computing the fallback line when a non-existent named line is specified.
explicit_column_count: u16,
/// The number of explicit rows in the grid. This is an *input* to the `NamedLineResolver` and is
/// used when computing the fallback line when a non-existent named line is specified.
explicit_row_count: u16,
}
/// Utility function to create or update an entry in a line name map
fn upsert_line_name_map<S: CheapCloneStr>(map: &mut Map<StrHasher<S>, Vec<u16>>, key: S, value: u16) {
map.entry(StrHasher(key)).and_modify(|lines| lines.push(value)).or_insert_with(|| single_value_vec(value));
}
impl<S: CheapCloneStr> NamedLineResolver<S> {
/// Create and initialise a new `NamedLineResolver`
pub(crate) fn new(
style: &impl GridContainerStyle<CustomIdent = S>,
column_auto_repetitions: u16,
row_auto_repetitions: u16,
) -> Self {
let mut areas: Map<StrHasher<S>, GridTemplateArea<_>> = Map::new();
let mut column_lines: Map<StrHasher<S>, Vec<u16>> = Map::new();
let mut row_lines: Map<StrHasher<S>, Vec<u16>> = Map::new();
let mut area_column_count = 0;
let mut area_row_count = 0;
if let Some(area_iter) = style.grid_template_areas() {
for area in area_iter.into_iter() {
// TODO: Investigate eliminating clones
areas.insert(StrHasher(area.name.clone()), area.clone());
area_column_count = area_column_count.max(area.column_end.max(1) - 1);
area_row_count = area_row_count.max(area.row_end.max(1) - 1);
let col_start_name = S::from(format!("{}-start", area.name.as_ref()));
upsert_line_name_map(&mut column_lines, col_start_name, area.column_start);
let col_end_name = S::from(format!("{}-end", area.name.as_ref()));
upsert_line_name_map(&mut column_lines, col_end_name, area.column_end);
let row_start_name = S::from(format!("{}-start", area.name.as_ref()));
upsert_line_name_map(&mut row_lines, row_start_name, area.row_start);
let row_end_name = S::from(format!("{}-end", area.name.as_ref()));
upsert_line_name_map(&mut row_lines, row_end_name, area.row_end);
}
}
// ---
let mut current_line = 0;
if let Some(mut column_tracks) = style.grid_template_columns() {
if let Some(column_line_names_iter) = style.grid_template_column_names() {
for line_names in column_line_names_iter {
current_line += 1;
for line_name in line_names.into_iter() {
column_lines
.entry(StrHasher(line_name.clone()))
.and_modify(|lines: &mut Vec<u16>| lines.push(current_line))
.or_insert_with(|| single_value_vec(current_line));
}
if let Some(GenericGridTemplateComponent::Repeat(repeat)) = column_tracks.next() {
let repeat_count = match repeat.count() {
RepetitionCount::Count(count) => count,
RepetitionCount::AutoFill | RepetitionCount::AutoFit => column_auto_repetitions,
};
for _ in 0..repeat_count {
for line_name_set in repeat.lines_names() {
for line_name in line_name_set {
upsert_line_name_map(&mut column_lines, line_name.clone(), current_line);
}
current_line += 1;
}
// Last line name set collapses with following line name set
current_line -= 1;
}
// Last line name set collapses with following line name set
current_line -= 1;
}
}
}
}
// Sort and dedup lines for each column name
for lines in column_lines.values_mut() {
lines.sort_unstable();
lines.dedup();
}
let mut current_line = 0;
if let Some(mut row_tracks) = style.grid_template_rows() {
if let Some(row_line_names_iter) = style.grid_template_row_names() {
for line_names in row_line_names_iter {
current_line += 1;
for line_name in line_names.into_iter() {
row_lines
.entry(StrHasher(line_name.clone()))
.and_modify(|lines: &mut Vec<u16>| lines.push(current_line))
.or_insert_with(|| single_value_vec(current_line));
}
if let Some(GenericGridTemplateComponent::Repeat(repeat)) = row_tracks.next() {
let repeat_count = match repeat.count() {
RepetitionCount::Count(count) => count,
RepetitionCount::AutoFill | RepetitionCount::AutoFit => row_auto_repetitions,
};
for _ in 0..repeat_count {
for line_name_set in repeat.lines_names() {
for line_name in line_name_set {
upsert_line_name_map(&mut row_lines, line_name.clone(), current_line);
}
current_line += 1;
}
// Last line name set collapses with following line name set
current_line -= 1;
}
// Last line name set collapses with following line name set
current_line -= 1;
}
}
}
}
// Sort and dedup lines for each row name
for lines in row_lines.values_mut() {
lines.sort_unstable();
lines.dedup();
}
Self {
area_column_count,
area_row_count,
explicit_column_count: 0, // Overwritten later
explicit_row_count: 0, // Overwritten later
areas,
row_lines,
column_lines,
}
}
/// Resolve named lines for both the `start` and `end` of a row-axis grid placement
#[inline(always)]
pub(crate) fn resolve_row_names(&self, line: &Line<GridPlacement<S>>) -> Line<NonNamedGridPlacement> {
self.resolve_line_names(line, GridAreaAxis::Row)
}
/// Resolve named lines for both the `start` and `end` of a column-axis grid placement
#[inline(always)]
pub(crate) fn resolve_column_names(&self, line: &Line<GridPlacement<S>>) -> Line<NonNamedGridPlacement> {
self.resolve_line_names(line, GridAreaAxis::Column)
}
/// Resolve named lines for both the `start` and `end` of a grid placement
#[inline(always)]
pub(crate) fn resolve_line_names(
&self,
line: &Line<GridPlacement<S>>,
axis: GridAreaAxis,
) -> Line<NonNamedGridPlacement> {
let start_holder;
let start_line_resolved = if let GridPlacement::NamedLine(name, idx) = &line.start {
start_holder =
GridPlacement::Line(self.find_line_index(name, *idx, axis, GridAreaEnd::Start, &|lines| lines));
&start_holder
} else {
&line.start
};
let end_holder;
let end_line_resolved = if let GridPlacement::NamedLine(name, idx) = &line.end {
end_holder = GridPlacement::Line(self.find_line_index(name, *idx, axis, GridAreaEnd::End, &|lines| lines));
&end_holder
} else {
&line.end
};
// If both the *-start and *-end values of its grid-placement properties specify a line, its grid span is implicit.
// If it has an explicit span value, its grid span is explicit.
// Otherwise, its grid span is automatic:
// - if it is subgridded in that axis, its grid span is determined from its <line-name-list>;
// - otherwise its grid span is 1.
//
// <https://drafts.csswg.org/css-grid-2/#grid-span>
match (&start_line_resolved, &end_line_resolved) {
(GridPlacement::Line(start_line), GridPlacement::NamedSpan(name, idx)) => {
let explicit_track_count = match axis {
GridAreaAxis::Row => self.explicit_row_count as i16,
GridAreaAxis::Column => self.explicit_column_count as i16,
};
let normalized_start_line = if start_line.as_i16() > 0 {
start_line.as_i16() as u16
} else {
(explicit_track_count + 1 + start_line.as_i16()).max(0) as u16
};
let end_line = self.find_line_index(name, *idx as i16, axis, GridAreaEnd::End, &|lines| {
let point = lines.partition_point(|line| *line <= normalized_start_line);
&lines[point..]
});
Line { start: NonNamedGridPlacement::Line(*start_line), end: NonNamedGridPlacement::Line(end_line) }
}
(GridPlacement::NamedSpan(name, idx), GridPlacement::Line(end_line)) => {
let explicit_track_count = match axis {
GridAreaAxis::Row => self.explicit_row_count as i16,
GridAreaAxis::Column => self.explicit_column_count as i16,
};
let normalized_end_line = if end_line.as_i16() > 0 {
end_line.as_i16() as u16
} else {
(explicit_track_count + 1 + end_line.as_i16()).max(0) as u16
};
let start_line = self.find_line_index(name, *idx as i16, axis, GridAreaEnd::Start, &|lines| {
let point = lines.partition_point(|line| *line < normalized_end_line);
&lines[..point]
});
Line { start: NonNamedGridPlacement::Line(start_line), end: NonNamedGridPlacement::Line(*end_line) }
}
(start, end) => Line {
start: match start {
GridPlacement::Auto => NonNamedGridPlacement::Auto,
GridPlacement::Line(grid_line) => NonNamedGridPlacement::Line(*grid_line),
GridPlacement::Span(span) => NonNamedGridPlacement::Span(*span),
GridPlacement::NamedSpan(_, _) => NonNamedGridPlacement::Span(1),
_ => unreachable!(),
},
end: match end {
GridPlacement::Auto => NonNamedGridPlacement::Auto,
GridPlacement::Line(grid_line) => NonNamedGridPlacement::Line(*grid_line),
GridPlacement::Span(span) => NonNamedGridPlacement::Span(*span),
GridPlacement::NamedSpan(_, _) => NonNamedGridPlacement::Span(1),
_ => unreachable!(),
},
},
}
}
/// Resolve the grid line for a named grid line or span
fn find_line_index(
&self,
name: &S,
idx: i16,
axis: GridAreaAxis,
end: GridAreaEnd,
filter_lines: &dyn Fn(&[u16]) -> &[u16],
) -> GridLine {
let name = name.as_ref();
let mut idx = idx;
let explicit_track_count = match axis {
GridAreaAxis::Row => self.explicit_row_count as i16,
GridAreaAxis::Column => self.explicit_column_count as i16,
};
// An index of 0 is used to represent "no index specified".
if idx == 0 {
idx = 1;
}
fn get_line(lines: &[u16], explicit_track_count: i16, idx: i16) -> i16 {
let abs_idx = idx.abs();
let enough_lines = abs_idx <= lines.len() as i16;
if enough_lines {
if idx > 0 {
lines[(abs_idx - 1) as usize] as i16
} else {
lines[lines.len() - (abs_idx) as usize] as i16
}
} else {
let remaining_lines = (abs_idx - lines.len() as i16) * idx.signum();
if idx > 0 {
(explicit_track_count + 1) + remaining_lines
} else {
-((explicit_track_count + 1) + remaining_lines)
}
}
}
// Lookup lines
let line_lookup = match axis {
GridAreaAxis::Row => &self.row_lines,
GridAreaAxis::Column => &self.column_lines,
};
if let Some(lines) = line_lookup.get(name) {
return GridLine::from(get_line(filter_lines(lines), explicit_track_count, idx));
} else {
// TODO: eliminate string allocations
match end {
GridAreaEnd::Start => {
let implicit_name = format!("{name}-start");
if let Some(lines) = line_lookup.get(&*implicit_name) {
// println!("IMPLICIT COL {implicit_name}");
return GridLine::from(get_line(filter_lines(lines), explicit_track_count, idx));
}
}
GridAreaEnd::End => {
let implicit_name = format!("{name}-end");
if let Some(lines) = line_lookup.get(&*implicit_name) {
// println!("IMPLICIT ROW {implicit_name}");
return GridLine::from(get_line(filter_lines(lines), explicit_track_count, idx));
}
}
}
}
// The CSS Grid specification has a weird quirk where it matches non-existent line names
// to the first (positive) implicit line in the grid
//
// We add/subtract 2 to the explicit track count because (in each axis) a grid has one more explicit
// grid line than it has tracks. And the fallback line is the line *after* that.
//
// See: <https://github.com/w3c/csswg-drafts/issues/966#issuecomment-277042153>
let line = if idx > 0 { (explicit_track_count + 1) + idx } else { -((explicit_track_count + 1) + idx) };
GridLine::from(line)
}
/// Get the number of columns defined by the grid areas
pub(crate) fn area_column_count(&self) -> u16 {
self.area_column_count
}
/// Get the number of rows defined by the grid areas
pub(crate) fn area_row_count(&self) -> u16 {
self.area_row_count
}
/// Set the number of columns in the explicit grid
pub(crate) fn set_explicit_column_count(&mut self, count: u16) {
self.explicit_column_count = count;
}
/// Set the number of rows in the explicit grid
pub(crate) fn set_explicit_row_count(&mut self, count: u16) {
self.explicit_row_count = count;
}
}
impl<S: CheapCloneStr> Debug for NamedLineResolver<S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
writeln!(f, "Grid Areas:")?;
for area in self.areas.values() {
writeln!(
f,
"{}: row:{}/{} col: {}/{}",
area.name.as_ref(),
area.row_start,
area.row_end,
area.column_start,
area.column_end
)?;
}
writeln!(f, "Grid Rows:")?;
for (name, lines) in self.row_lines.iter() {
write!(f, "{}: ", name.0.as_ref())?;
for line in lines {
write!(f, "{line} ")?;
}
writeln!(f)?;
}
writeln!(f, "Grid Columns:")?;
for (name, lines) in self.column_lines.iter() {
write!(f, "{}: ", name.0.as_ref())?;
for line in lines {
write!(f, "{line} ")?;
}
writeln!(f)?;
}
Ok(())
}
}
+6
View File
@@ -0,0 +1,6 @@
//! Utility functions used within the grid module
#[cfg(test)]
pub(super) mod test_helpers;
#[cfg(test)]
pub(super) use test_helpers::{CreateChildTestNode, CreateExpectedPlacement, CreateParentTestNode};
+43
View File
@@ -0,0 +1,43 @@
//! Helpers for use in unit tests within the grid module
use super::super::OriginZeroLine;
use crate::prelude::*;
use crate::style::{Dimension, GridPlacement, Style};
pub(crate) trait CreateParentTestNode {
fn into_grid(self) -> Style;
}
impl CreateParentTestNode for (f32, f32, i32, i32) {
fn into_grid(self) -> Style {
Style {
display: Display::Grid,
size: Size { width: Dimension::from_length(self.0), height: Dimension::from_length(self.1) },
grid_template_columns: vec![fr(1f32); self.2 as usize],
grid_template_rows: vec![fr(1f32); self.3 as usize],
..Default::default()
}
}
}
pub(crate) trait CreateChildTestNode {
fn into_grid_child(self) -> Style;
}
impl CreateChildTestNode
for (GridPlacement<String>, GridPlacement<String>, GridPlacement<String>, GridPlacement<String>)
{
fn into_grid_child(self) -> Style<String> {
Style {
display: Display::Grid,
grid_column: Line { start: self.0, end: self.1 },
grid_row: Line { start: self.2, end: self.3 },
..Default::default()
}
}
}
pub(crate) trait CreateExpectedPlacement {
fn into_oz(self) -> (OriginZeroLine, OriginZeroLine, OriginZeroLine, OriginZeroLine);
}
impl CreateExpectedPlacement for (i16, i16, i16, i16) {
fn into_oz(self) -> (OriginZeroLine, OriginZeroLine, OriginZeroLine, OriginZeroLine) {
(OriginZeroLine(self.0), OriginZeroLine(self.1), OriginZeroLine(self.2), OriginZeroLine(self.3))
}
}
+164
View File
@@ -0,0 +1,164 @@
//! Computes size using styles and measure functions
use crate::geometry::{Point, Size};
use crate::style::{AvailableSpace, Overflow, Position};
use crate::tree::{CollapsibleMarginSet, RunMode};
use crate::tree::{LayoutInput, LayoutOutput, 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};
use core::unreachable;
/// Compute the size of a leaf node (node with no children)
pub fn compute_leaf_layout<MeasureFunction>(
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;
let has_styles_preventing_being_collapsed_through = !style.is_block()
|| 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!("LEAF");
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 run_mode == RunMode::ComputeSize && has_styles_preventing_being_collapsed_through {
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()
}),
};
// Measure node
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,
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,
}
}
+337
View File
@@ -0,0 +1,337 @@
//! Low-level access to the layout algorithms themselves. For a higher-level API, see the [`TaffyTree`](crate::TaffyTree) struct.
//!
//! ### Layout functions
//!
//! The layout functions all take an [`&mut impl LayoutPartialTree`](crate::LayoutPartialTree) parameter, which represents a single container node and it's direct children.
//!
//! | Function | Purpose |
//! | --- | --- |
//! | [`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_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`) |
//! | [`compute_cached_layout`] | Attempts to find a cached layout for the specified node and layout inputs. Uses the provided closure to compute the layout (and then stores the result in the cache) if no cached layout is found. |
//!
//! ### Other functions
//!
//! | Function | Requires | Purpose |
//! | --- | --- | --- |
//! | [`round_layout`] | [`RoundTree`] | Round a tree of float-valued layouts to integer pixels |
//! | [`print_tree`](crate::print_tree) | [`PrintTree`](crate::PrintTree) | Print a debug representation of a node tree and it's computed layout |
//!
pub(crate) mod common;
pub(crate) mod leaf;
#[cfg(feature = "block_layout")]
pub(crate) mod block;
#[cfg(feature = "float_layout")]
pub(crate) mod float;
#[cfg(feature = "flexbox")]
pub(crate) mod flexbox;
#[cfg(feature = "grid")]
pub(crate) mod grid;
pub use leaf::compute_leaf_layout;
#[cfg(feature = "block_layout")]
pub use self::block::{compute_block_layout, BlockContext, BlockFormattingContext};
#[cfg(feature = "flexbox")]
pub use self::flexbox::compute_flexbox_layout;
#[cfg(feature = "grid")]
pub use self::grid::compute_grid_layout;
#[cfg(feature = "float_layout")]
pub use self::float::{ContentSlot, FloatContext, FloatIntrinsicWidthCalculator};
use crate::geometry::{Line, Point, Size};
use crate::style::{AvailableSpace, CoreStyle, Overflow};
use crate::tree::{
Layout, LayoutInput, LayoutOutput, LayoutPartialTree, LayoutPartialTreeExt, NodeId, RoundTree, SizingMode,
};
use crate::util::debug::{debug_log, debug_log_node, debug_pop_node, debug_push_node};
use crate::util::sys::round;
use crate::util::ResolveOrZero;
use crate::{CacheTree, MaybeMath, MaybeResolve};
/// Compute layout for the root node in the tree
pub fn compute_root_layout(tree: &mut impl LayoutPartialTree, root: NodeId, available_space: Size<AvailableSpace>) {
let mut known_dimensions = Size::NONE;
#[cfg(feature = "block_layout")]
{
use crate::BoxSizing;
let parent_size = available_space.into_options();
let style = tree.get_core_container_style(root);
if style.is_block() {
// Pull these out earlier to avoid borrowing issues
let aspect_ratio = style.aspect_ratio();
let margin = style.margin().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
let padding = style.padding().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
let border = style.border().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
let padding_border_size = (padding + border).sum_axes();
let box_sizing_adjustment =
if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
let min_size = style
.min_size()
.maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
let max_size = style
.max_size()
.maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);
let clamped_style_size = style
.size()
.maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment)
.maybe_clamp(min_size, max_size);
// If both min and max in a given axis are set and max <= min then this determines the size in that axis
let min_max_definite_size = min_size.zip_map(max_size, |min, max| match (min, max) {
(Some(min), Some(max)) if max <= min => Some(min),
_ => None,
});
// Block nodes automatically stretch fit their width to fit available space if available space is definite
let available_space_based_size = Size {
width: available_space.width.into_option().maybe_sub(margin.horizontal_axis_sum()),
height: None,
};
let styled_based_known_dimensions = known_dimensions
.or(min_max_definite_size)
.or(clamped_style_size)
.or(available_space_based_size)
.maybe_max(padding_border_size);
known_dimensions = styled_based_known_dimensions;
}
}
// Recursively compute node layout
let output = tree.perform_child_layout(
root,
known_dimensions,
available_space.into_options(),
available_space,
SizingMode::InherentSize,
Line::FALSE,
);
let style = tree.get_core_container_style(root);
let padding =
style.padding().resolve_or_zero(available_space.width.into_option(), |val, basis| tree.calc(val, basis));
let border =
style.border().resolve_or_zero(available_space.width.into_option(), |val, basis| tree.calc(val, basis));
let margin =
style.margin().resolve_or_zero(available_space.width.into_option(), |val, basis| tree.calc(val, basis));
let scrollbar_size = Size {
width: if style.overflow().y == Overflow::Scroll { style.scrollbar_width() } else { 0.0 },
height: if style.overflow().x == Overflow::Scroll { style.scrollbar_width() } else { 0.0 },
};
let location = Point {
x: if style.direction().is_rtl() {
available_space.width.into_option().map_or(0.0, |available_width| available_width - output.size.width)
} else {
0.0
},
y: 0.0,
};
drop(style);
tree.set_unrounded_layout(
root,
&Layout {
order: 0,
location,
size: output.size,
#[cfg(feature = "content_size")]
content_size: output.content_size,
scrollbar_size,
padding,
border,
// TODO: support auto margins for root node?
margin,
},
);
}
/// Attempts to find a cached layout for the specified node and layout inputs.
///
/// Uses the provided closure to compute the layout (and then stores the result in the cache) if no cached layout is found.
#[inline(always)]
pub fn compute_cached_layout<Tree: CacheTree + ?Sized, ComputeFunction>(
tree: &mut Tree,
node: NodeId,
inputs: LayoutInput,
compute_uncached: ComputeFunction,
) -> LayoutOutput
where
ComputeFunction: FnOnce(&mut Tree, NodeId, LayoutInput) -> LayoutOutput,
{
debug_push_node!(node);
// First we check if we have a cached result for the given input
let cache_entry = tree.cache_get(node, &inputs);
if let Some(cached_size_and_baselines) = cache_entry {
debug_log_node!(inputs);
debug_log!("RESULT (CACHED)", dbg:cached_size_and_baselines.size);
debug_pop_node!();
return cached_size_and_baselines;
}
debug_log_node!(inputs);
let computed_size_and_baselines = compute_uncached(tree, node, inputs);
// Cache result
tree.cache_store(node, &inputs, computed_size_and_baselines);
debug_log!("RESULT", dbg:computed_size_and_baselines.size);
debug_pop_node!();
computed_size_and_baselines
}
/// Rounds the calculated layout to exact pixel values
///
/// In order to ensure that no gaps in the layout are introduced we:
/// - Always round based on the cumulative x/y coordinates (relative to the viewport) rather than
/// parent-relative coordinates
/// - Compute width/height by first rounding the top/bottom/left/right and then computing the difference
/// rather than rounding the width/height directly
///
/// See <https://github.com/facebook/yoga/commit/aa5b296ac78f7a22e1aeaf4891243c6bb76488e2> for more context
///
/// In order to prevent innacuracies caused by rounding already-rounded values, we read from `unrounded_layout`
/// and write to `final_layout`.
pub fn round_layout(tree: &mut impl RoundTree, node_id: NodeId) {
return round_layout_inner(tree, node_id, 0.0, 0.0);
/// Recursive function to apply rounding to all descendents
fn round_layout_inner(tree: &mut impl RoundTree, node_id: NodeId, cumulative_x: f32, cumulative_y: f32) {
let unrounded_layout = tree.get_unrounded_layout(node_id);
let mut layout = unrounded_layout;
let cumulative_x = cumulative_x + unrounded_layout.location.x;
let cumulative_y = cumulative_y + unrounded_layout.location.y;
layout.location.x = round(unrounded_layout.location.x);
layout.location.y = round(unrounded_layout.location.y);
layout.size.width = round(cumulative_x + unrounded_layout.size.width) - round(cumulative_x);
layout.size.height = round(cumulative_y + unrounded_layout.size.height) - round(cumulative_y);
layout.scrollbar_size.width = round(unrounded_layout.scrollbar_size.width);
layout.scrollbar_size.height = round(unrounded_layout.scrollbar_size.height);
layout.border.left = round(cumulative_x + unrounded_layout.border.left) - round(cumulative_x);
layout.border.right = round(cumulative_x + unrounded_layout.size.width)
- round(cumulative_x + unrounded_layout.size.width - unrounded_layout.border.right);
layout.border.top = round(cumulative_y + unrounded_layout.border.top) - round(cumulative_y);
layout.border.bottom = round(cumulative_y + unrounded_layout.size.height)
- round(cumulative_y + unrounded_layout.size.height - unrounded_layout.border.bottom);
layout.padding.left = round(cumulative_x + unrounded_layout.padding.left) - round(cumulative_x);
layout.padding.right = round(cumulative_x + unrounded_layout.size.width)
- round(cumulative_x + unrounded_layout.size.width - unrounded_layout.padding.right);
layout.padding.top = round(cumulative_y + unrounded_layout.padding.top) - round(cumulative_y);
layout.padding.bottom = round(cumulative_y + unrounded_layout.size.height)
- round(cumulative_y + unrounded_layout.size.height - unrounded_layout.padding.bottom);
#[cfg(feature = "content_size")]
round_content_size(&mut layout, unrounded_layout.content_size, cumulative_x, cumulative_y);
tree.set_final_layout(node_id, &layout);
let child_count = tree.child_count(node_id);
for index in 0..child_count {
let child = tree.get_child_id(node_id, index);
round_layout_inner(tree, child, cumulative_x, cumulative_y);
}
}
#[cfg(feature = "content_size")]
#[inline(always)]
/// Round content size variables.
/// This is split into a separate function to make it easier to feature flag.
fn round_content_size(
layout: &mut Layout,
unrounded_content_size: Size<f32>,
cumulative_x: f32,
cumulative_y: f32,
) {
layout.content_size.width = round(cumulative_x + unrounded_content_size.width) - round(cumulative_x);
layout.content_size.height = round(cumulative_y + unrounded_content_size.height) - round(cumulative_y);
}
}
/// Creates a layout for this node and its children, recursively.
/// Each hidden node has zero size and is placed at the origin
pub fn compute_hidden_layout(tree: &mut (impl LayoutPartialTree + CacheTree), node: NodeId) -> LayoutOutput {
// Clear cache and set zeroed-out layout for the node
tree.cache_clear(node);
tree.set_unrounded_layout(node, &Layout::with_order(0));
// Perform hidden layout on all children
for index in 0..tree.child_count(node) {
let child_id = tree.get_child_id(node, index);
tree.compute_child_layout(child_id, LayoutInput::HIDDEN);
}
LayoutOutput::HIDDEN
}
/// A module for unified re-exports of detailed layout info structs, used by low level API
#[cfg(feature = "detailed_layout_info")]
pub mod detailed_info {
#[cfg(feature = "grid")]
pub use super::grid::{DetailedGridInfo, DetailedGridItemsInfo, DetailedGridTracksInfo};
}
#[cfg(test)]
mod tests {
use super::compute_hidden_layout;
use crate::geometry::{Point, Size};
use crate::style::{Display, Style};
use crate::TaffyTree;
#[test]
fn hidden_layout_should_hide_recursively() {
let mut taffy: TaffyTree<()> = TaffyTree::new();
let style: Style = Style { display: Display::Flex, size: Size::from_lengths(50.0, 50.0), ..Default::default() };
let grandchild_00 = taffy.new_leaf(style.clone()).unwrap();
let grandchild_01 = taffy.new_leaf(style.clone()).unwrap();
let child_00 = taffy.new_with_children(style.clone(), &[grandchild_00, grandchild_01]).unwrap();
let grandchild_02 = taffy.new_leaf(style.clone()).unwrap();
let child_01 = taffy.new_with_children(style.clone(), &[grandchild_02]).unwrap();
let root = taffy
.new_with_children(
Style { display: Display::None, size: Size::from_lengths(50.0, 50.0), ..Default::default() },
&[child_00, child_01],
)
.unwrap();
compute_hidden_layout(&mut taffy.as_layout_tree(), root);
// Whatever size and display-mode the nodes had previously,
// all layouts should resolve to ZERO due to the root's DISPLAY::NONE
for node in [root, child_00, child_01, grandchild_00, grandchild_01, grandchild_02] {
let layout = taffy.layout(node).unwrap();
assert_eq!(layout.size, Size::zero());
assert_eq!(layout.location, Point::zero());
}
}
}
+741
View File
@@ -0,0 +1,741 @@
//! Geometric primitives useful for layout
use crate::util::sys::f32_max;
use crate::CompactLength;
use crate::{style::Dimension, util::sys::f32_min};
use core::ops::{Add, Sub};
#[cfg(feature = "flexbox")]
use crate::style::FlexDirection;
/// The simple absolute horizontal and vertical axis
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AbsoluteAxis {
/// The horizontal axis
Horizontal,
/// The vertical axis
Vertical,
}
impl AbsoluteAxis {
/// Returns the other variant of the enum
#[inline]
pub const fn other_axis(&self) -> Self {
match *self {
AbsoluteAxis::Horizontal => AbsoluteAxis::Vertical,
AbsoluteAxis::Vertical => AbsoluteAxis::Horizontal,
}
}
}
impl<T> Size<T> {
#[inline(always)]
/// Get either the width or height depending on the AbsoluteAxis passed in
pub fn get_abs(self, axis: AbsoluteAxis) -> T {
match axis {
AbsoluteAxis::Horizontal => self.width,
AbsoluteAxis::Vertical => self.height,
}
}
}
impl<T: Add> Rect<T> {
#[inline(always)]
/// Get either the width or height depending on the AbsoluteAxis passed in
pub fn grid_axis_sum(self, axis: AbsoluteAxis) -> <T as Add>::Output {
match axis {
AbsoluteAxis::Horizontal => self.left + self.right,
AbsoluteAxis::Vertical => self.top + self.bottom,
}
}
}
/// The CSS abstract axis
/// <https://www.w3.org/TR/css-writing-modes-3/#abstract-axes>
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AbstractAxis {
/// The axis in the inline dimension, i.e. the horizontal axis in horizontal writing modes and the vertical axis in vertical writing modes.
Inline,
/// The axis in the block dimension, i.e. the vertical axis in horizontal writing modes and the horizontal axis in vertical writing modes.
Block,
}
impl AbstractAxis {
/// Returns the other variant of the enum
#[inline]
pub const fn other(&self) -> AbstractAxis {
match *self {
AbstractAxis::Inline => AbstractAxis::Block,
AbstractAxis::Block => AbstractAxis::Inline,
}
}
/// Convert an `AbstractAxis` into an `AbsoluteAxis` naively assuming that the Inline axis is Horizontal
/// This is currently always true, but will change if Taffy ever implements the `writing_mode` property
#[inline]
pub const fn as_abs_naive(&self) -> AbsoluteAxis {
match self {
AbstractAxis::Inline => AbsoluteAxis::Horizontal,
AbstractAxis::Block => AbsoluteAxis::Vertical,
}
}
}
/// Container that holds an item in each absolute axis without specifying
/// what kind of item it is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct InBothAbsAxis<T> {
/// The item in the horizontal axis
pub horizontal: T,
/// The item in the vertical axis
pub vertical: T,
}
impl<T: Copy> InBothAbsAxis<T> {
#[cfg(feature = "grid")]
/// Get the contained item based on the AbsoluteAxis passed
pub const fn get(&self, axis: AbsoluteAxis) -> T {
match axis {
AbsoluteAxis::Horizontal => self.horizontal,
AbsoluteAxis::Vertical => self.vertical,
}
}
}
/// An axis-aligned UI rectangle
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Rect<T> {
/// This can represent either the x-coordinate of the starting edge,
/// or the amount of padding on the starting side.
///
/// The starting edge is the left edge when working with LTR text,
/// and the right edge when working with RTL text.
pub left: T,
/// This can represent either the x-coordinate of the ending edge,
/// or the amount of padding on the ending side.
///
/// The ending edge is the right edge when working with LTR text,
/// and the left edge when working with RTL text.
pub right: T,
/// This can represent either the y-coordinate of the top edge,
/// or the amount of padding on the top side.
pub top: T,
/// This can represent either the y-coordinate of the bottom edge,
/// or the amount of padding on the bottom side.
pub bottom: T,
}
impl<U, T: Add<U>> Add<Rect<U>> for Rect<T> {
type Output = Rect<T::Output>;
fn add(self, rhs: Rect<U>) -> Self::Output {
Rect {
left: self.left + rhs.left,
right: self.right + rhs.right,
top: self.top + rhs.top,
bottom: self.bottom + rhs.bottom,
}
}
}
impl<T> Rect<T> {
/// Applies the function `f` to all four sides of the rect
///
/// When applied to the left and right sides, the width is used
/// as the second parameter of `f`.
/// When applied to the top or bottom sides, the height is used instead.
#[cfg(any(feature = "flexbox", feature = "block_layout"))]
pub(crate) fn zip_size<R, F, U>(self, size: Size<U>, f: F) -> Rect<R>
where
F: Fn(T, U) -> R,
U: Copy,
{
Rect {
left: f(self.left, size.width),
right: f(self.right, size.width),
top: f(self.top, size.height),
bottom: f(self.bottom, size.height),
}
}
/// Applies the function `f` to the left, right, top, and bottom properties
///
/// This is used to transform a `Rect<T>` into a `Rect<R>`.
pub fn map<R, F>(self, f: F) -> Rect<R>
where
F: Fn(T) -> R,
{
Rect { left: f(self.left), right: f(self.right), top: f(self.top), bottom: f(self.bottom) }
}
/// Returns a `Line<T>` representing the left and right properties of the Rect
pub fn horizontal_components(self) -> Line<T> {
Line { start: self.left, end: self.right }
}
/// Returns a `Line<T>` containing the top and bottom properties of the Rect
pub fn vertical_components(self) -> Line<T> {
Line { start: self.top, end: self.bottom }
}
}
impl<T, U> Rect<T>
where
T: Add<Output = U> + Copy + Clone,
{
/// The sum of [`Rect.start`](Rect) and [`Rect.end`](Rect)
///
/// This is typically used when computing total padding.
///
/// **NOTE:** this is *not* the width of the rectangle.
#[inline(always)]
pub fn horizontal_axis_sum(&self) -> U {
self.left + self.right
}
/// The sum of [`Rect.top`](Rect) and [`Rect.bottom`](Rect)
///
/// This is typically used when computing total padding.
///
/// **NOTE:** this is *not* the height of the rectangle.
#[inline(always)]
pub fn vertical_axis_sum(&self) -> U {
self.top + self.bottom
}
/// Both horizontal_axis_sum and vertical_axis_sum as a `Size<T>`
///
/// **NOTE:** this is *not* the width/height of the rectangle.
#[inline(always)]
#[allow(dead_code)] // Fixes spurious clippy warning: this function is used!
pub fn sum_axes(&self) -> Size<U> {
Size { width: self.horizontal_axis_sum(), height: self.vertical_axis_sum() }
}
/// The sum of the two fields of the [`Rect`] representing the main axis.
///
/// This is typically used when computing total padding.
///
/// If the [`FlexDirection`] is [`FlexDirection::Row`] or [`FlexDirection::RowReverse`], this is [`Rect::horizontal`].
/// Otherwise, this is [`Rect::vertical`].
#[cfg(feature = "flexbox")]
pub(crate) fn main_axis_sum(&self, direction: FlexDirection) -> U {
if direction.is_row() {
self.horizontal_axis_sum()
} else {
self.vertical_axis_sum()
}
}
/// The sum of the two fields of the [`Rect`] representing the cross axis.
///
/// If the [`FlexDirection`] is [`FlexDirection::Row`] or [`FlexDirection::RowReverse`], this is [`Rect::vertical`].
/// Otherwise, this is [`Rect::horizontal`].
#[cfg(feature = "flexbox")]
pub(crate) fn cross_axis_sum(&self, direction: FlexDirection) -> U {
if direction.is_row() {
self.vertical_axis_sum()
} else {
self.horizontal_axis_sum()
}
}
}
impl<T> Rect<T>
where
T: Copy + Clone,
{
/// The `start` or `top` value of the [`Rect`], from the perspective of the main layout axis
#[cfg(feature = "flexbox")]
pub(crate) const fn main_start(&self, direction: FlexDirection) -> T {
if direction.is_row() {
self.left
} else {
self.top
}
}
/// The `end` or `bottom` value of the [`Rect`], from the perspective of the main layout axis
#[cfg(feature = "flexbox")]
pub(crate) const fn main_end(&self, direction: FlexDirection) -> T {
if direction.is_row() {
self.right
} else {
self.bottom
}
}
/// The `start` or `top` value of the [`Rect`], from the perspective of the cross layout axis
#[cfg(feature = "flexbox")]
pub(crate) const fn cross_start(&self, direction: FlexDirection) -> T {
if direction.is_row() {
self.top
} else {
self.left
}
}
/// The `end` or `bottom` value of the [`Rect`], from the perspective of the main layout axis
#[cfg(feature = "flexbox")]
pub(crate) const fn cross_end(&self, direction: FlexDirection) -> T {
if direction.is_row() {
self.bottom
} else {
self.right
}
}
}
impl Rect<f32> {
/// Creates a new Rect with `0.0` as all parameters
pub const ZERO: Rect<f32> = Self { left: 0.0, right: 0.0, top: 0.0, bottom: 0.0 };
/// Creates a new Rect
#[must_use]
pub const fn new(start: f32, end: f32, top: f32, bottom: f32) -> Self {
Self { left: start, right: end, top, bottom }
}
}
/// An abstract "line". Represents any type that has a start and an end
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Line<T> {
/// The start position of a line
pub start: T,
/// The end position of a line
pub end: T,
}
impl<T> Line<T> {
/// Applies the function `f` to both the width and height
///
/// This is used to transform a `Line<T>` into a `Line<R>`.
pub fn map<R, F>(self, f: F) -> Line<R>
where
F: Fn(T) -> R,
{
Line { start: f(self.start), end: f(self.end) }
}
}
impl Line<bool> {
/// A `Line<bool>` with both start and end set to `true`
pub const TRUE: Self = Line { start: true, end: true };
/// A `Line<bool>` with both start and end set to `false`
pub const FALSE: Self = Line { start: false, end: false };
}
impl<T: Add + Copy> Line<T> {
/// Adds the start and end values together and returns the result
pub fn sum(&self) -> <T as Add>::Output {
self.start + self.end
}
}
/// The width and height of a [`Rect`]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Size<T> {
/// The x extent of the rectangle
pub width: T,
/// The y extent of the rectangle
pub height: T,
}
// Generic Add impl for Size<T> + Size<U> where T + U has an Add impl
impl<U, T: Add<U>> Add<Size<U>> for Size<T> {
type Output = Size<<T as Add<U>>::Output>;
fn add(self, rhs: Size<U>) -> Self::Output {
Size { width: self.width + rhs.width, height: self.height + rhs.height }
}
}
// Generic Sub impl for Size<T> + Size<U> where T + U has an Sub impl
impl<U, T: Sub<U>> Sub<Size<U>> for Size<T> {
type Output = Size<<T as Sub<U>>::Output>;
fn sub(self, rhs: Size<U>) -> Self::Output {
Size { width: self.width - rhs.width, height: self.height - rhs.height }
}
}
// Note: we allow dead_code here as we want to provide a complete API of helpers that is symmetrical in all axes,
// but sometimes we only currently have a use for the helper in a single axis
#[allow(dead_code)]
impl<T> Size<T> {
/// Applies the function `f` to both the width and height
///
/// This is used to transform a `Size<T>` into a `Size<R>`.
pub fn map<R, F>(self, f: F) -> Size<R>
where
F: Fn(T) -> R,
{
Size { width: f(self.width), height: f(self.height) }
}
/// Applies the function `f` to the width
pub fn map_width<F>(self, f: F) -> Size<T>
where
F: Fn(T) -> T,
{
Size { width: f(self.width), height: self.height }
}
/// Applies the function `f` to the height
pub fn map_height<F>(self, f: F) -> Size<T>
where
F: Fn(T) -> T,
{
Size { width: self.width, height: f(self.height) }
}
/// Applies the function `f` to both the width and height
/// of this value and another passed value
pub fn zip_map<Other, Ret, Func>(self, other: Size<Other>, f: Func) -> Size<Ret>
where
Func: Fn(T, Other) -> Ret,
{
Size { width: f(self.width, other.width), height: f(self.height, other.height) }
}
/// Sets the extent of the main layout axis
///
/// Whether this is the width or height depends on the `direction` provided
#[cfg(feature = "flexbox")]
pub(crate) fn set_main(&mut self, direction: FlexDirection, value: T) {
if direction.is_row() {
self.width = value
} else {
self.height = value
}
}
/// Sets the extent of the cross layout axis
///
/// Whether this is the width or height depends on the `direction` provided
#[cfg(feature = "flexbox")]
pub(crate) fn set_cross(&mut self, direction: FlexDirection, value: T) {
if direction.is_row() {
self.height = value
} else {
self.width = value
}
}
/// Creates a new value of type Self with the main axis set to value provided
///
/// Whether this is the width or height depends on the `direction` provided
#[cfg(feature = "flexbox")]
pub(crate) fn with_main(self, direction: FlexDirection, value: T) -> Self {
let mut new = self;
if direction.is_row() {
new.width = value
} else {
new.height = value
}
new
}
/// Creates a new value of type Self with the cross axis set to value provided
///
/// Whether this is the width or height depends on the `direction` provided
#[cfg(feature = "flexbox")]
pub(crate) fn with_cross(self, direction: FlexDirection, value: T) -> Self {
let mut new = self;
if direction.is_row() {
new.height = value
} else {
new.width = value
}
new
}
/// Creates a new value of type Self with the main axis modified by the callback provided
///
/// Whether this is the width or height depends on the `direction` provided
#[cfg(feature = "flexbox")]
pub(crate) fn map_main(self, direction: FlexDirection, mapper: impl FnOnce(T) -> T) -> Self {
let mut new = self;
if direction.is_row() {
new.width = mapper(new.width);
} else {
new.height = mapper(new.height);
}
new
}
/// Creates a new value of type Self with the cross axis modified by the callback provided
///
/// Whether this is the width or height depends on the `direction` provided
#[cfg(feature = "flexbox")]
pub(crate) fn map_cross(self, direction: FlexDirection, mapper: impl FnOnce(T) -> T) -> Self {
let mut new = self;
if direction.is_row() {
new.height = mapper(new.height);
} else {
new.width = mapper(new.width);
}
new
}
/// Gets the extent of the main layout axis
///
/// Whether this is the width or height depends on the `direction` provided
#[cfg(feature = "flexbox")]
pub(crate) fn main(self, direction: FlexDirection) -> T {
if direction.is_row() {
self.width
} else {
self.height
}
}
/// Gets the extent of the cross layout axis
///
/// Whether this is the width or height depends on the `direction` provided
#[cfg(feature = "flexbox")]
pub(crate) fn cross(self, direction: FlexDirection) -> T {
if direction.is_row() {
self.height
} else {
self.width
}
}
/// Gets the extent of the specified layout axis
/// Whether this is the width or height depends on the `GridAxis` provided
#[cfg(feature = "grid")]
pub(crate) fn get(self, axis: AbstractAxis) -> T {
match axis {
AbstractAxis::Inline => self.width,
AbstractAxis::Block => self.height,
}
}
/// Sets the extent of the specified layout axis
/// Whether this is the width or height depends on the `GridAxis` provided
#[cfg(feature = "grid")]
pub(crate) fn set(&mut self, axis: AbstractAxis, value: T) {
match axis {
AbstractAxis::Inline => self.width = value,
AbstractAxis::Block => self.height = value,
}
}
/// Sets the extent of the specified layout axis
/// Whether this is the width or height depends on the `GridAxis` provided
#[cfg(feature = "grid")]
pub(crate) fn with(mut self, axis: AbstractAxis, value: T) -> Self {
match axis {
AbstractAxis::Inline => self.width = value,
AbstractAxis::Block => self.height = value,
}
self
}
}
impl Size<f32> {
/// A [`Size`] with zero width and height
pub const ZERO: Size<f32> = Self { width: 0.0, height: 0.0 };
/// Applies f32_max to each component separately
#[inline(always)]
pub fn f32_max(self, rhs: Size<f32>) -> Size<f32> {
Size { width: f32_max(self.width, rhs.width), height: f32_max(self.height, rhs.height) }
}
/// Applies f32_min to each component separately
#[inline(always)]
pub fn f32_min(self, rhs: Size<f32>) -> Size<f32> {
Size { width: f32_min(self.width, rhs.width), height: f32_min(self.height, rhs.height) }
}
/// Return true if both width and height are greater than 0 else false
#[inline(always)]
pub fn has_non_zero_area(self) -> bool {
self.width > 0.0 && self.height > 0.0
}
}
impl Size<Option<f32>> {
/// A [`Size`] with `None` width and height
pub const NONE: Size<Option<f32>> = Self { width: None, height: None };
/// A [`Size<Option<f32>>`] with `Some(width)` and `Some(height)` as parameters
#[must_use]
pub const fn new(width: f32, height: f32) -> Self {
Size { width: Some(width), height: Some(height) }
}
/// Creates a new [`Size<Option<f32>>`] with either the width or height set based on the provided `direction`
#[cfg(feature = "flexbox")]
pub const fn from_cross(direction: FlexDirection, value: Option<f32>) -> Self {
let mut new = Self::NONE;
if direction.is_row() {
new.height = value
} else {
new.width = value
}
new
}
/// Applies aspect_ratio (if one is supplied) to the Size:
/// - If width is `Some` but height is `None`, then height is computed from width and aspect_ratio
/// - If height is `Some` but width is `None`, then width is computed from height and aspect_ratio
///
/// If aspect_ratio is `None` then this function simply returns self.
pub fn maybe_apply_aspect_ratio(self, aspect_ratio: Option<f32>) -> Size<Option<f32>> {
match aspect_ratio {
Some(ratio) => match (self.width, self.height) {
(Some(width), None) => Size { width: Some(width), height: Some(width / ratio) },
(None, Some(height)) => Size { width: Some(height * ratio), height: Some(height) },
_ => self,
},
None => self,
}
}
}
impl<T> Size<Option<T>> {
/// Performs Option::unwrap_or on each component separately
pub fn unwrap_or(self, alt: Size<T>) -> Size<T> {
Size { width: self.width.unwrap_or(alt.width), height: self.height.unwrap_or(alt.height) }
}
/// Performs Option::or on each component separately
pub fn or(self, alt: Size<Option<T>>) -> Size<Option<T>> {
Size { width: self.width.or(alt.width), height: self.height.or(alt.height) }
}
/// Return true if both components are Some, else false.
#[inline(always)]
pub fn both_axis_defined(&self) -> bool {
self.width.is_some() && self.height.is_some()
}
}
impl Size<Dimension> {
/// Generates a [`Size<Dimension>`] using length values
#[must_use]
pub const fn from_lengths(width: f32, height: f32) -> Self {
Size { width: Dimension(CompactLength::length(width)), height: Dimension(CompactLength::length(height)) }
}
/// Generates a [`Size<Dimension>`] using percentage values
#[must_use]
pub const fn from_percent(width: f32, height: f32) -> Self {
Size { width: Dimension(CompactLength::percent(width)), height: Dimension(CompactLength::percent(height)) }
}
}
/// A 2-dimensional coordinate.
///
/// When used in association with a [`Rect`], represents the top-left corner.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Point<T> {
/// The x-coordinate
pub x: T,
/// The y-coordinate
pub y: T,
}
impl Point<f32> {
/// A [`Point`] with values (0,0), representing the origin
pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
}
impl Point<Option<f32>> {
/// A [`Point`] with values (None, None)
pub const NONE: Self = Self { x: None, y: None };
}
// Generic Add impl for Point<T> + Point<U> where T + U has an Add impl
impl<U, T: Add<U>> Add<Point<U>> for Point<T> {
type Output = Point<<T as Add<U>>::Output>;
fn add(self, rhs: Point<U>) -> Self::Output {
Point { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
impl<T> Point<T> {
/// Applies the function `f` to both the x and y
///
/// This is used to transform a `Point<T>` into a `Point<R>`.
pub fn map<R, F>(self, f: F) -> Point<R>
where
F: Fn(T) -> R,
{
Point { x: f(self.x), y: f(self.y) }
}
/// Gets the extent of the specified layout axis
/// Whether this is the width or height depends on the `GridAxis` provided
#[cfg(feature = "grid")]
pub fn get(self, axis: AbstractAxis) -> T {
match axis {
AbstractAxis::Inline => self.x,
AbstractAxis::Block => self.y,
}
}
/// Swap x and y components
pub fn transpose(self) -> Point<T> {
Point { x: self.y, y: self.x }
}
/// Sets the extent of the specified layout axis
/// Whether this is the width or height depends on the `GridAxis` provided
#[cfg(feature = "grid")]
pub fn set(&mut self, axis: AbstractAxis, value: T) {
match axis {
AbstractAxis::Inline => self.x = value,
AbstractAxis::Block => self.y = value,
}
}
/// Gets the component in the main layout axis
///
/// Whether this is the x or y depends on the `direction` provided
#[cfg(feature = "flexbox")]
pub(crate) fn main(self, direction: FlexDirection) -> T {
if direction.is_row() {
self.x
} else {
self.y
}
}
/// Gets the component in the cross layout axis
///
/// Whether this is the x or y depends on the `direction` provided
#[cfg(feature = "flexbox")]
pub(crate) fn cross(self, direction: FlexDirection) -> T {
if direction.is_row() {
self.y
} else {
self.x
}
}
}
impl<T> From<Point<T>> for Size<T> {
fn from(value: Point<T>) -> Self {
Size { width: value.x, height: value.y }
}
}
/// Generic struct which holds a "min" value and a "max" value
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MinMax<Min, Max> {
/// The value representing the minimum
pub min: Min,
/// The value representing the maximum
pub max: Max,
}
+133
View File
@@ -0,0 +1,133 @@
//! # Taffy
//!
//! Taffy is a flexible, high-performance library for **UI layout**.
//! It currently implements the Flexbox, Grid and Block layout algorithms from the CSS specification. Support for other paradigms is planned.
//! For more information on this and other future development plans see the [roadmap issue](https://github.com/DioxusLabs/taffy/issues/345).
//!
//! ## Architecture
//!
//! Taffy is based on a tree of "UI nodes" similar to the tree of DOM nodes that one finds in web-based UI. Each node has:
//! - A [`Style`] struct which holds a set of CSS styles which function as the primary input to the layout computations.
//! - A [`Layout`] struct containing a position (x/y) and a size (width/height) which function as the output of the layout computations.
//! - Optionally:
//! - A `Vec` set of child nodes
//! - "Context": arbitrary user-defined data (which you can access when using a "measure function" to integrate Taffy with other kinds of layout such as text layout)
//!
//! Usage of Taffy consists of constructing a tree of UI nodes (with associated styles, children and context), then calling function(s)
//! from Taffy to translate those styles, parent-child relationships and measure functions into a size and position in 2d space for each node
//! in the tree.
//!
//! ## High-level API vs. Low-level API
//!
//! Taffy has two APIs: a high-level API that is simpler and easier to get started with, and a low-level API that is more flexible gives greater control.
//! We would generally recommend the high-level API for users using Taffy standalone and the low-level API for users wanting to embed Taffy as part of
//! a wider layout system or as part of a UI framework that already has it's own node/widget tree representation.
//!
//! ### High-level API
//!
//! The high-level API consists of the [`TaffyTree`] struct which contains a tree implementation and provides methods that allow you to construct
//! a tree of UI nodes. Once constructed, you can call the [`compute_layout_with_measure`](crate::TaffyTree::compute_layout_with_measure) method to compute the layout (passing in a "measure function" closure which is used to compute the size of leaf nodes), and then access
//! the layout of each node using the [`layout`](crate::TaffyTree::layout) method.
//!
//! When using the high-level API, Taffy will take care of node storage, caching and dispatching to the correct layout algorithm for a given node for you.
//! See the [`TaffyTree`] struct for more details on this API.
//!
//! Examples which show usage of the high-level API include:
//!
//! - [basic](https://github.com/DioxusLabs/taffy/blob/main/examples/basic.rs)
//! - [flexbox_gap](https://github.com/DioxusLabs/taffy/blob/main/examples/flexbox_gap.rs)
//! - [grid_holy_grail](https://github.com/DioxusLabs/taffy/blob/main/examples/grid_holy_grail.rs)
//! - [measure](https://github.com/DioxusLabs/taffy/blob/main/examples/measure.rs)
//! - [cosmic_text](https://github.com/DioxusLabs/taffy/blob/main/examples/cosmic_text.rs)
//!
//! In particular, the "measure" example shows how to integrate Taffy layout with other layout modalities such as text or image layout when using the high level API.
//!
//! ### Low-level API
//!
//! The low-level API consists of a [set of traits](crate::tree::traits) (notably the [`LayoutPartialTree`] trait) which define an interface behind which you must implement your own
//! tree implementation, and a [set of functions](crate::compute) such as [`compute_flexbox_layout`] and [`compute_grid_layout`] which implement the layout algorithms (for a single node at a time), and are designed to be flexible
//! and easy to integrate into a wider layout or UI system.
//!
//! When using this API, you must handle node storage, caching, and dispatching to the correct layout algorithm for a given node yourself.
//! See the [`crate::tree::traits`] module for more details on this API.
//!
//! Examples which show usage of the low-level API are:
//!
//! - [custom_tree_vec](https://github.com/DioxusLabs/taffy/blob/main/examples/custom_tree_vec.rs) which implements a custom Taffy tree using a `Vec` as an arena with NodeId's being index's into the Vec.
//! - [custom_tree_owned_partial](https://github.com/DioxusLabs/taffy/blob/main/examples/custom_tree_owned_partial.rs) which implements a custom Taffy tree using directly owned children with NodeId's being index's into vec on parent node.
//! - [custom_tree_owned_unsafe](https://github.com/DioxusLabs/taffy/blob/main/examples/custom_tree_owned_unsafe.rs) which implements a custom Taffy tree using directly owned children with NodeId's being pointers.
// document the feature flags for the crate by extracting the comments from Cargo.toml
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
// annotate items with their required features (gated by docsrs flag as this requires the nightly toolchain)
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
#![deny(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::missing_docs_in_private_items)]
// Disable "unused_x" warnings when default features aren't enabled.
#![cfg_attr(not(feature = "default"), allow(dead_code))]
#![cfg_attr(not(feature = "default"), allow(unused_imports))]
#![cfg_attr(not(feature = "default"), allow(unused_variables))]
#![cfg_attr(not(feature = "default"), allow(unused_mut))]
// We always need std for the tests
// See <https://github.com/la10736/rstest/issues/149#issuecomment-1156402989>
#[cfg(all(test, not(feature = "std")))]
#[macro_use]
extern crate std;
#[cfg(all(not(feature = "std"), feature = "alloc"))]
extern crate alloc;
#[cfg_attr(feature = "serde", macro_use)]
#[cfg(feature = "serde")]
extern crate serde;
pub mod compute;
pub mod geometry;
pub mod prelude;
pub mod style;
pub mod style_helpers;
pub mod tree;
#[macro_use]
pub mod util;
mod readme_doctest {
#![doc = include_str!("../README.md")]
}
#[cfg(feature = "block_layout")]
#[doc(inline)]
pub use crate::compute::compute_block_layout;
#[cfg(feature = "flexbox")]
#[doc(inline)]
pub use crate::compute::compute_flexbox_layout;
#[cfg(feature = "grid")]
#[doc(inline)]
pub use crate::compute::compute_grid_layout;
#[cfg(feature = "detailed_layout_info")]
pub use crate::compute::detailed_info::*;
#[doc(inline)]
pub use crate::compute::{
compute_cached_layout, compute_hidden_layout, compute_leaf_layout, compute_root_layout, round_layout,
};
#[doc(inline)]
pub use crate::style::Style;
#[doc(inline)]
pub use crate::tree::traits::*;
#[cfg(feature = "taffy_tree")]
#[doc(inline)]
pub use crate::tree::TaffyTree;
#[cfg(feature = "std")]
#[doc(inline)]
pub use crate::util::print_tree;
#[cfg(feature = "parse")]
pub use parse::{ParseError, ParseResult};
pub use crate::compute::*;
pub use crate::geometry::*;
pub use crate::style::*;
pub use crate::tree::*;
pub use crate::util::*;
+31
View File
@@ -0,0 +1,31 @@
//! Commonly used types
pub use crate::{
geometry::{Line, Rect, Size},
style::{
AlignContent, AlignContentKeyword, AlignItems, AlignItemsKeyword, AlignSelf, AlignmentSafety, AvailableSpace,
BoxSizing, CompactLength, Dimension, Display, JustifyContent, JustifyItems, JustifySelf, LengthPercentage,
LengthPercentageAuto, Position, Style,
},
style_helpers::{
auto, fit_content, length, max_content, min_content, percent, zero, FromFr, FromLength, FromPercent, TaffyAuto,
TaffyFitContent, TaffyMaxContent, TaffyMinContent, TaffyZero,
},
tree::{Layout, LayoutPartialTree, NodeId, PrintTree, RoundTree, TraversePartialTree, TraverseTree},
};
#[cfg(feature = "flexbox")]
pub use crate::style::{FlexDirection, FlexWrap};
#[cfg(feature = "grid")]
pub use crate::style::{
GridAutoFlow, GridPlacement, GridTemplateComponent, MaxTrackSizingFunction, MinTrackSizingFunction,
RepetitionCount, TrackSizingFunction,
};
#[cfg(feature = "grid")]
pub use crate::style_helpers::{
evenly_sized_tracks, flex, fr, line, minmax, repeat, span, TaffyGridLine, TaffyGridSpan,
};
#[cfg(feature = "taffy_tree")]
pub use crate::TaffyTree;
+726
View File
@@ -0,0 +1,726 @@
//! Style types for controlling alignment.
//!
//! The public alignment types ([`AlignItems`], [`AlignContent`], and their aliases) are
//! structs with two orthogonal fields: a *position* keyword
//! ([`AlignItemsKeyword`] / [`AlignContentKeyword`]) and an *overflow-position*
//! modifier ([`AlignmentSafety`]). The pre-existing CSS spellings — `Start`, `End`,
//! `FlexStart`, `FlexEnd`, `Center`, `Stretch`, `SpaceBetween`, …, `SafeStart`,
//! `SafeEnd`, `SafeFlexStart`, `SafeFlexEnd`, `SafeCenter` — are exposed as associated
//! constants on the structs, so call sites read identically to the previous enum form.
#[cfg(feature = "parse")]
use crate::util::parse::{CssParseResult, FromCss, Parser, Token};
/// The position-keyword half of [`AlignItems`] (and its aliases `AlignSelf`,
/// `JustifyItems`, `JustifySelf`).
///
/// Compute paths match on this enum directly so every match is exhaustive and
/// requires no `Safe*` siblings.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u8)]
pub enum AlignItemsKeyword {
/// Items are packed toward the start of the axis.
Start,
/// Items are packed toward the end of the axis.
End,
/// Items are packed towards the flex-relative start of the axis.
///
/// For flex containers with flex_direction RowReverse or ColumnReverse this is
/// equivalent to End. In all other cases it is equivalent to Start.
FlexStart,
/// Items are packed towards the flex-relative end of the axis.
///
/// For flex containers with flex_direction RowReverse or ColumnReverse this is
/// equivalent to Start. In all other cases it is equivalent to End.
FlexEnd,
/// Items are packed along the center of the cross axis.
Center,
/// Items are aligned such as their baselines align.
Baseline,
/// Stretch to fill the container.
Stretch,
}
/// The position-keyword half of [`AlignContent`] (and its alias `JustifyContent`).
///
/// Compute paths match on this enum directly so every match is exhaustive and
/// requires no `Safe*` siblings.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u8)]
pub enum AlignContentKeyword {
/// Items are packed toward the start of the axis.
Start,
/// Items are packed toward the end of the axis.
End,
/// Items are packed towards the flex-relative start of the axis.
FlexStart,
/// Items are packed towards the flex-relative end of the axis.
FlexEnd,
/// Items are centered around the middle of the axis.
Center,
/// Items are stretched to fill the container.
Stretch,
/// The first and last items are aligned flush with the edges of the container
/// (no gap). The gap between items is distributed evenly.
SpaceBetween,
/// The gap between the first and last items is exactly THE SAME as the gap
/// between items. The gaps are distributed evenly.
SpaceEvenly,
/// The gap between the first and last items is exactly HALF the gap between
/// items. The gaps are distributed evenly in proportion to these ratios.
SpaceAround,
}
impl AlignContentKeyword {
/// Returns the reversed keyword for RTL (right-to-left) contexts: `Start`↔`End`,
/// `FlexStart`↔`FlexEnd`. `Stretch` maps to `End` to preserve the layout
/// algorithms' historical handling. Center and the distribution keywords
/// (`SpaceBetween`, `SpaceEvenly`, `SpaceAround`) are unaffected because their
/// visual placement is direction-symmetric.
pub(crate) fn reversed(self) -> Self {
match self {
Self::Start => Self::End,
Self::End => Self::Start,
Self::FlexStart => Self::FlexEnd,
Self::FlexEnd => Self::FlexStart,
Self::Stretch => Self::End,
Self::Center | Self::SpaceBetween | Self::SpaceEvenly | Self::SpaceAround => self,
}
}
}
/// The overflow-position modifier per [CSS Box Alignment §4.3][css-align-overflow].
///
/// `Safe` falls back to start-edge alignment when the alignment subject would
/// overflow the alignment container, so the start of the content stays visible.
/// `Unsafe` (the default) keeps the requested alignment even when that causes
/// overflow at the start edge.
///
/// CSS only defines `safe` / `unsafe` against the position values `start`, `end`,
/// `flex-start`, `flex-end`, `center`. The struct shape does not enforce that
/// constraint at the type level — the parser rejects invalid combinations, and
/// the compute pass treats `Safe` paired with a non-position keyword (`Stretch`,
/// `Baseline`, `Space*`) the same as `Unsafe`.
///
/// [css-align-overflow]: https://www.w3.org/TR/css-align-3/#overflow-values
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u8)]
pub enum AlignmentSafety {
/// Default — keeps the requested alignment even when the subject overflows the
/// alignment container at the start edge.
Unsafe,
/// Falls back to the start edge when the subject would overflow, to avoid data
/// loss.
Safe,
}
/// Used to control how child nodes are aligned.
/// For Flexbox it controls alignment in the cross axis.
/// For Grid it controls alignment in the block axis.
///
/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items)
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct AlignItems {
/// Position keyword.
pub keyword: AlignItemsKeyword,
/// Overflow-position modifier (`safe` / `unsafe`).
pub safety: AlignmentSafety,
}
impl AlignItems {
/// Items are packed toward the start of the axis.
pub const START: Self = Self { keyword: AlignItemsKeyword::Start, safety: AlignmentSafety::Unsafe };
/// Items are packed toward the end of the axis.
pub const END: Self = Self { keyword: AlignItemsKeyword::End, safety: AlignmentSafety::Unsafe };
/// Items are packed towards the flex-relative start of the axis.
pub const FLEX_START: Self = Self { keyword: AlignItemsKeyword::FlexStart, safety: AlignmentSafety::Unsafe };
/// Items are packed towards the flex-relative end of the axis.
pub const FLEX_END: Self = Self { keyword: AlignItemsKeyword::FlexEnd, safety: AlignmentSafety::Unsafe };
/// Items are packed along the center of the cross axis.
pub const CENTER: Self = Self { keyword: AlignItemsKeyword::Center, safety: AlignmentSafety::Unsafe };
/// Items are aligned such as their baselines align.
pub const BASELINE: Self = Self { keyword: AlignItemsKeyword::Baseline, safety: AlignmentSafety::Unsafe };
/// Stretch to fill the container.
pub const STRETCH: Self = Self { keyword: AlignItemsKeyword::Stretch, safety: AlignmentSafety::Unsafe };
/// Like [`AlignItems::START`], but falls back to [`AlignItems::START`] when the
/// alignment subject overflows the alignment container, to avoid data loss.
pub const SAFE_START: Self = Self { keyword: AlignItemsKeyword::Start, safety: AlignmentSafety::Safe };
/// Like [`AlignItems::END`], but falls back to [`AlignItems::START`] when the
/// alignment subject overflows the alignment container, to avoid data loss.
pub const SAFE_END: Self = Self { keyword: AlignItemsKeyword::End, safety: AlignmentSafety::Safe };
/// Like [`AlignItems::FLEX_START`], but falls back to [`AlignItems::START`] when the
/// alignment subject overflows the alignment container, to avoid data loss.
pub const SAFE_FLEX_START: Self = Self { keyword: AlignItemsKeyword::FlexStart, safety: AlignmentSafety::Safe };
/// Like [`AlignItems::FLEX_END`], but falls back to [`AlignItems::START`] when the
/// alignment subject overflows the alignment container, to avoid data loss.
pub const SAFE_FLEX_END: Self = Self { keyword: AlignItemsKeyword::FlexEnd, safety: AlignmentSafety::Safe };
/// Like [`AlignItems::CENTER`], but falls back to [`AlignItems::START`] when the
/// alignment subject overflows the alignment container, to avoid data loss.
pub const SAFE_CENTER: Self = Self { keyword: AlignItemsKeyword::Center, safety: AlignmentSafety::Safe };
/// Returns `true` iff this carries the `safe` overflow-position modifier.
#[inline]
pub const fn is_safe(self) -> bool {
matches!(self.safety, AlignmentSafety::Safe)
}
/// Returns the underlying position keyword, discarding the safety modifier.
#[inline]
pub const fn keyword(self) -> AlignItemsKeyword {
self.keyword
}
}
#[cfg(feature = "parse")]
impl FromCss for AlignItems {
fn from_css<'i>(input: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
let first = input.expect_ident()?.clone();
cssparser::match_ignore_ascii_case! { &*first,
"safe" => {
let pos = input.expect_ident()?.clone();
cssparser::match_ignore_ascii_case! { &*pos,
"start" => Ok(Self::SAFE_START),
"end" => Ok(Self::SAFE_END),
"flex-start" => Ok(Self::SAFE_FLEX_START),
"flex-end" => Ok(Self::SAFE_FLEX_END),
"center" => Ok(Self::SAFE_CENTER),
_ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
}
},
"unsafe" => {
let pos = input.expect_ident()?.clone();
cssparser::match_ignore_ascii_case! { &*pos,
"start" => Ok(Self::START),
"end" => Ok(Self::END),
"flex-start" => Ok(Self::FLEX_START),
"flex-end" => Ok(Self::FLEX_END),
"center" => Ok(Self::CENTER),
_ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
}
},
"start" => Ok(Self::START),
"end" => Ok(Self::END),
"flex-start" => Ok(Self::FLEX_START),
"flex-end" => Ok(Self::FLEX_END),
"center" => Ok(Self::CENTER),
"baseline" => Ok(Self::BASELINE),
"stretch" => Ok(Self::STRETCH),
_ => Err(input.new_unexpected_token_error(Token::Ident(first))),
}
}
}
#[cfg(feature = "parse")]
crate::util::parse::from_str_from_css!(AlignItems);
/// Used to control how child nodes are aligned.
/// Does not apply to Flexbox, and will be ignored if specified on a flex container.
/// For Grid it controls alignment in the inline axis.
///
/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items)
pub type JustifyItems = AlignItems;
/// Controls alignment of an individual node.
///
/// Overrides the parent Node's `AlignItems` property.
/// For Flexbox it controls alignment in the cross axis.
/// For Grid it controls alignment in the block axis.
///
/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self)
pub type AlignSelf = AlignItems;
/// Controls alignment of an individual node.
///
/// Overrides the parent Node's `JustifyItems` property.
/// Does not apply to Flexbox, and will be ignored if specified on a flex child.
/// For Grid it controls alignment in the inline axis.
///
/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self)
pub type JustifySelf = AlignItems;
/// Sets the distribution of space between and around content items.
/// For Flexbox it controls alignment in the cross axis.
/// For Grid it controls alignment in the block axis.
///
/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content)
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct AlignContent {
/// Position keyword.
pub keyword: AlignContentKeyword,
/// Overflow-position modifier (`safe` / `unsafe`).
pub safety: AlignmentSafety,
}
impl AlignContent {
/// Items are packed toward the start of the axis.
pub const START: Self = Self { keyword: AlignContentKeyword::Start, safety: AlignmentSafety::Unsafe };
/// Items are packed toward the end of the axis.
pub const END: Self = Self { keyword: AlignContentKeyword::End, safety: AlignmentSafety::Unsafe };
/// Items are packed towards the flex-relative start of the axis.
pub const FLEX_START: Self = Self { keyword: AlignContentKeyword::FlexStart, safety: AlignmentSafety::Unsafe };
/// Items are packed towards the flex-relative end of the axis.
pub const FLEX_END: Self = Self { keyword: AlignContentKeyword::FlexEnd, safety: AlignmentSafety::Unsafe };
/// Items are centered around the middle of the axis.
pub const CENTER: Self = Self { keyword: AlignContentKeyword::Center, safety: AlignmentSafety::Unsafe };
/// Items are stretched to fill the container.
pub const STRETCH: Self = Self { keyword: AlignContentKeyword::Stretch, safety: AlignmentSafety::Unsafe };
/// The first and last items are aligned flush with the edges of the container.
pub const SPACE_BETWEEN: Self =
Self { keyword: AlignContentKeyword::SpaceBetween, safety: AlignmentSafety::Unsafe };
/// The gap between the first and last items equals the gap between items.
pub const SPACE_EVENLY: Self = Self { keyword: AlignContentKeyword::SpaceEvenly, safety: AlignmentSafety::Unsafe };
/// The gap between the first and last items is half the gap between items.
pub const SPACE_AROUND: Self = Self { keyword: AlignContentKeyword::SpaceAround, safety: AlignmentSafety::Unsafe };
/// Like [`AlignContent::START`], but falls back to [`AlignContent::START`] when the
/// content overflows the alignment container, to avoid data loss.
pub const SAFE_START: Self = Self { keyword: AlignContentKeyword::Start, safety: AlignmentSafety::Safe };
/// Like [`AlignContent::END`], but falls back to [`AlignContent::START`] when the
/// content overflows the alignment container, to avoid data loss.
pub const SAFE_END: Self = Self { keyword: AlignContentKeyword::End, safety: AlignmentSafety::Safe };
/// Like [`AlignContent::FLEX_START`], but falls back to [`AlignContent::START`] when
/// the content overflows the alignment container, to avoid data loss.
pub const SAFE_FLEX_START: Self = Self { keyword: AlignContentKeyword::FlexStart, safety: AlignmentSafety::Safe };
/// Like [`AlignContent::FLEX_END`], but falls back to [`AlignContent::START`] when the
/// content overflows the alignment container, to avoid data loss.
pub const SAFE_FLEX_END: Self = Self { keyword: AlignContentKeyword::FlexEnd, safety: AlignmentSafety::Safe };
/// Like [`AlignContent::CENTER`], but falls back to [`AlignContent::START`] when the
/// content overflows the alignment container, to avoid data loss.
pub const SAFE_CENTER: Self = Self { keyword: AlignContentKeyword::Center, safety: AlignmentSafety::Safe };
/// Returns `true` iff this carries the `safe` overflow-position modifier.
#[inline]
pub const fn is_safe(self) -> bool {
matches!(self.safety, AlignmentSafety::Safe)
}
/// Returns the underlying position keyword, discarding the safety modifier.
#[inline]
pub const fn keyword(self) -> AlignContentKeyword {
self.keyword
}
}
#[cfg(feature = "parse")]
impl FromCss for AlignContent {
fn from_css<'i>(input: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
let first = input.expect_ident()?.clone();
cssparser::match_ignore_ascii_case! { &*first,
"safe" => {
let pos = input.expect_ident()?.clone();
cssparser::match_ignore_ascii_case! { &*pos,
"start" => Ok(Self::SAFE_START),
"end" => Ok(Self::SAFE_END),
"flex-start" => Ok(Self::SAFE_FLEX_START),
"flex-end" => Ok(Self::SAFE_FLEX_END),
"center" => Ok(Self::SAFE_CENTER),
_ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
}
},
"unsafe" => {
let pos = input.expect_ident()?.clone();
cssparser::match_ignore_ascii_case! { &*pos,
"start" => Ok(Self::START),
"end" => Ok(Self::END),
"flex-start" => Ok(Self::FLEX_START),
"flex-end" => Ok(Self::FLEX_END),
"center" => Ok(Self::CENTER),
_ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
}
},
"start" => Ok(Self::START),
"end" => Ok(Self::END),
"flex-start" => Ok(Self::FLEX_START),
"flex-end" => Ok(Self::FLEX_END),
"center" => Ok(Self::CENTER),
"stretch" => Ok(Self::STRETCH),
"space-between" => Ok(Self::SPACE_BETWEEN),
"space-evenly" => Ok(Self::SPACE_EVENLY),
"space-around" => Ok(Self::SPACE_AROUND),
_ => Err(input.new_unexpected_token_error(Token::Ident(first))),
}
}
}
#[cfg(feature = "parse")]
crate::util::parse::from_str_from_css!(AlignContent);
/// Sets the distribution of space between and around content items.
/// For Flexbox it controls alignment in the main axis.
/// For Grid it controls alignment in the inline axis.
///
/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)
pub type JustifyContent = AlignContent;
// ---------------------------------------------------------------------------
// Serde — custom impls preserve the pre-struct wire format (single tag string
// per public spelling) so consumers reading data serialized before the refactor
// continue to deserialize correctly.
// ---------------------------------------------------------------------------
/// Canonical tag-string set accepted by [`AlignItems`] serde deserialization, used in
/// `unknown_variant` errors. Mirrors the spellings produced by `Serialize`.
#[cfg(feature = "serde")]
const ALIGN_ITEMS_NAMES: &[&str] = &[
"Start",
"End",
"FlexStart",
"FlexEnd",
"Center",
"Baseline",
"Stretch",
"SafeStart",
"SafeEnd",
"SafeFlexStart",
"SafeFlexEnd",
"SafeCenter",
];
#[cfg(feature = "serde")]
impl serde::Serialize for AlignItems {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let name = match (self.keyword, self.safety) {
(AlignItemsKeyword::Start, AlignmentSafety::Unsafe) => "Start",
(AlignItemsKeyword::End, AlignmentSafety::Unsafe) => "End",
(AlignItemsKeyword::FlexStart, AlignmentSafety::Unsafe) => "FlexStart",
(AlignItemsKeyword::FlexEnd, AlignmentSafety::Unsafe) => "FlexEnd",
(AlignItemsKeyword::Center, AlignmentSafety::Unsafe) => "Center",
(AlignItemsKeyword::Baseline, _) => "Baseline",
(AlignItemsKeyword::Stretch, _) => "Stretch",
(AlignItemsKeyword::Start, AlignmentSafety::Safe) => "SafeStart",
(AlignItemsKeyword::End, AlignmentSafety::Safe) => "SafeEnd",
(AlignItemsKeyword::FlexStart, AlignmentSafety::Safe) => "SafeFlexStart",
(AlignItemsKeyword::FlexEnd, AlignmentSafety::Safe) => "SafeFlexEnd",
(AlignItemsKeyword::Center, AlignmentSafety::Safe) => "SafeCenter",
};
serializer.serialize_str(name)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for AlignItems {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct AlignItemsVisitor;
impl<'de> serde::de::Visitor<'de> for AlignItemsVisitor {
type Value = AlignItems;
fn expecting(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
fmt.write_str("an AlignItems variant tag string")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(match v {
"Start" => AlignItems::START,
"End" => AlignItems::END,
"FlexStart" => AlignItems::FLEX_START,
"FlexEnd" => AlignItems::FLEX_END,
"Center" => AlignItems::CENTER,
"Baseline" => AlignItems::BASELINE,
"Stretch" => AlignItems::STRETCH,
"SafeStart" => AlignItems::SAFE_START,
"SafeEnd" => AlignItems::SAFE_END,
"SafeFlexStart" => AlignItems::SAFE_FLEX_START,
"SafeFlexEnd" => AlignItems::SAFE_FLEX_END,
"SafeCenter" => AlignItems::SAFE_CENTER,
other => return Err(E::unknown_variant(other, ALIGN_ITEMS_NAMES)),
})
}
}
deserializer.deserialize_str(AlignItemsVisitor)
}
}
/// Canonical tag-string set accepted by [`AlignContent`] serde deserialization, used in
/// `unknown_variant` errors. Mirrors the spellings produced by `Serialize`.
#[cfg(feature = "serde")]
const ALIGN_CONTENT_NAMES: &[&str] = &[
"Start",
"End",
"FlexStart",
"FlexEnd",
"Center",
"Stretch",
"SpaceBetween",
"SpaceEvenly",
"SpaceAround",
"SafeStart",
"SafeEnd",
"SafeFlexStart",
"SafeFlexEnd",
"SafeCenter",
];
#[cfg(feature = "serde")]
impl serde::Serialize for AlignContent {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let name = match (self.keyword, self.safety) {
(AlignContentKeyword::Start, AlignmentSafety::Unsafe) => "Start",
(AlignContentKeyword::End, AlignmentSafety::Unsafe) => "End",
(AlignContentKeyword::FlexStart, AlignmentSafety::Unsafe) => "FlexStart",
(AlignContentKeyword::FlexEnd, AlignmentSafety::Unsafe) => "FlexEnd",
(AlignContentKeyword::Center, AlignmentSafety::Unsafe) => "Center",
(AlignContentKeyword::Stretch, _) => "Stretch",
(AlignContentKeyword::SpaceBetween, _) => "SpaceBetween",
(AlignContentKeyword::SpaceEvenly, _) => "SpaceEvenly",
(AlignContentKeyword::SpaceAround, _) => "SpaceAround",
(AlignContentKeyword::Start, AlignmentSafety::Safe) => "SafeStart",
(AlignContentKeyword::End, AlignmentSafety::Safe) => "SafeEnd",
(AlignContentKeyword::FlexStart, AlignmentSafety::Safe) => "SafeFlexStart",
(AlignContentKeyword::FlexEnd, AlignmentSafety::Safe) => "SafeFlexEnd",
(AlignContentKeyword::Center, AlignmentSafety::Safe) => "SafeCenter",
};
serializer.serialize_str(name)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for AlignContent {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct AlignContentVisitor;
impl<'de> serde::de::Visitor<'de> for AlignContentVisitor {
type Value = AlignContent;
fn expecting(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
fmt.write_str("an AlignContent variant tag string")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(match v {
"Start" => AlignContent::START,
"End" => AlignContent::END,
"FlexStart" => AlignContent::FLEX_START,
"FlexEnd" => AlignContent::FLEX_END,
"Center" => AlignContent::CENTER,
"Stretch" => AlignContent::STRETCH,
"SpaceBetween" => AlignContent::SPACE_BETWEEN,
"SpaceEvenly" => AlignContent::SPACE_EVENLY,
"SpaceAround" => AlignContent::SPACE_AROUND,
"SafeStart" => AlignContent::SAFE_START,
"SafeEnd" => AlignContent::SAFE_END,
"SafeFlexStart" => AlignContent::SAFE_FLEX_START,
"SafeFlexEnd" => AlignContent::SAFE_FLEX_END,
"SafeCenter" => AlignContent::SAFE_CENTER,
other => return Err(E::unknown_variant(other, ALIGN_CONTENT_NAMES)),
})
}
}
deserializer.deserialize_str(AlignContentVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::mem::size_of;
// Size budget — struct = 1B keyword + 1B safety, no niche packing.
// Pre-refactor each was a single-byte enum; spec §V13 caps regression at +2B.
#[test]
fn align_types_within_size_budget() {
assert!(size_of::<AlignItems>() <= 2, "AlignItems grew to {}", size_of::<AlignItems>());
assert!(size_of::<AlignContent>() <= 2, "AlignContent grew to {}", size_of::<AlignContent>());
assert!(size_of::<Option<AlignItems>>() <= 3);
assert!(size_of::<Option<AlignContent>>() <= 3);
}
#[test]
fn align_items_is_safe() {
assert!(AlignItems::SAFE_START.is_safe());
assert!(AlignItems::SAFE_END.is_safe());
assert!(AlignItems::SAFE_FLEX_START.is_safe());
assert!(AlignItems::SAFE_FLEX_END.is_safe());
assert!(AlignItems::SAFE_CENTER.is_safe());
assert!(!AlignItems::START.is_safe());
assert!(!AlignItems::END.is_safe());
assert!(!AlignItems::FLEX_START.is_safe());
assert!(!AlignItems::FLEX_END.is_safe());
assert!(!AlignItems::CENTER.is_safe());
assert!(!AlignItems::BASELINE.is_safe());
assert!(!AlignItems::STRETCH.is_safe());
}
#[test]
fn align_items_keyword_strips_safe() {
assert_eq!(AlignItems::SAFE_START.keyword(), AlignItemsKeyword::Start);
assert_eq!(AlignItems::SAFE_END.keyword(), AlignItemsKeyword::End);
assert_eq!(AlignItems::SAFE_FLEX_START.keyword(), AlignItemsKeyword::FlexStart);
assert_eq!(AlignItems::SAFE_FLEX_END.keyword(), AlignItemsKeyword::FlexEnd);
assert_eq!(AlignItems::SAFE_CENTER.keyword(), AlignItemsKeyword::Center);
}
#[test]
fn align_items_keyword_passthrough() {
assert_eq!(AlignItems::START.keyword(), AlignItemsKeyword::Start);
assert_eq!(AlignItems::STRETCH.keyword(), AlignItemsKeyword::Stretch);
assert_eq!(AlignItems::BASELINE.keyword(), AlignItemsKeyword::Baseline);
assert_eq!(AlignItems::FLEX_START.keyword(), AlignItemsKeyword::FlexStart);
}
#[test]
fn align_content_is_safe() {
assert!(AlignContent::SAFE_START.is_safe());
assert!(AlignContent::SAFE_CENTER.is_safe());
assert!(!AlignContent::SPACE_BETWEEN.is_safe());
assert!(!AlignContent::STRETCH.is_safe());
}
#[test]
fn align_content_keyword_strips_safe() {
assert_eq!(AlignContent::SAFE_START.keyword(), AlignContentKeyword::Start);
assert_eq!(AlignContent::SAFE_FLEX_END.keyword(), AlignContentKeyword::FlexEnd);
assert_eq!(AlignContent::SAFE_CENTER.keyword(), AlignContentKeyword::Center);
assert_eq!(AlignContent::SPACE_BETWEEN.keyword(), AlignContentKeyword::SpaceBetween);
}
#[test]
fn align_content_keyword_reversed_swaps_start_end() {
assert_eq!(AlignContentKeyword::Start.reversed(), AlignContentKeyword::End);
assert_eq!(AlignContentKeyword::End.reversed(), AlignContentKeyword::Start);
assert_eq!(AlignContentKeyword::FlexStart.reversed(), AlignContentKeyword::FlexEnd);
assert_eq!(AlignContentKeyword::FlexEnd.reversed(), AlignContentKeyword::FlexStart);
// Stretch reverses to End — preserves pre-refactor behaviour.
assert_eq!(AlignContentKeyword::Stretch.reversed(), AlignContentKeyword::End);
assert_eq!(AlignContentKeyword::Center.reversed(), AlignContentKeyword::Center);
assert_eq!(AlignContentKeyword::SpaceBetween.reversed(), AlignContentKeyword::SpaceBetween);
assert_eq!(AlignContentKeyword::SpaceEvenly.reversed(), AlignContentKeyword::SpaceEvenly);
assert_eq!(AlignContentKeyword::SpaceAround.reversed(), AlignContentKeyword::SpaceAround);
}
#[cfg(feature = "parse")]
#[test]
fn parse_align_items_plain() {
assert_eq!("start".parse::<AlignItems>().unwrap(), AlignItems::START);
assert_eq!("end".parse::<AlignItems>().unwrap(), AlignItems::END);
assert_eq!("flex-start".parse::<AlignItems>().unwrap(), AlignItems::FLEX_START);
assert_eq!("flex-end".parse::<AlignItems>().unwrap(), AlignItems::FLEX_END);
assert_eq!("center".parse::<AlignItems>().unwrap(), AlignItems::CENTER);
assert_eq!("baseline".parse::<AlignItems>().unwrap(), AlignItems::BASELINE);
assert_eq!("stretch".parse::<AlignItems>().unwrap(), AlignItems::STRETCH);
}
#[cfg(feature = "parse")]
#[test]
fn parse_align_items_safe() {
assert_eq!("safe start".parse::<AlignItems>().unwrap(), AlignItems::SAFE_START);
assert_eq!("safe end".parse::<AlignItems>().unwrap(), AlignItems::SAFE_END);
assert_eq!("safe flex-start".parse::<AlignItems>().unwrap(), AlignItems::SAFE_FLEX_START);
assert_eq!("safe flex-end".parse::<AlignItems>().unwrap(), AlignItems::SAFE_FLEX_END);
assert_eq!("safe center".parse::<AlignItems>().unwrap(), AlignItems::SAFE_CENTER);
}
#[cfg(feature = "parse")]
#[test]
fn parse_align_items_safe_case_insensitive() {
assert_eq!("SAFE Start".parse::<AlignItems>().unwrap(), AlignItems::SAFE_START);
assert_eq!("Safe FLEX-end".parse::<AlignItems>().unwrap(), AlignItems::SAFE_FLEX_END);
}
#[cfg(feature = "parse")]
#[test]
fn parse_align_items_unsafe_drops_modifier() {
assert_eq!("unsafe start".parse::<AlignItems>().unwrap(), AlignItems::START);
assert_eq!("unsafe end".parse::<AlignItems>().unwrap(), AlignItems::END);
assert_eq!("unsafe center".parse::<AlignItems>().unwrap(), AlignItems::CENTER);
}
#[cfg(feature = "parse")]
#[test]
fn parse_align_items_rejects_invalid_safe_combos() {
assert!("safe stretch".parse::<AlignItems>().is_err());
assert!("safe baseline".parse::<AlignItems>().is_err());
assert!("safe space-between".parse::<AlignItems>().is_err());
assert!("safe".parse::<AlignItems>().is_err());
assert!("safe garbage".parse::<AlignItems>().is_err());
assert!("unsafe stretch".parse::<AlignItems>().is_err());
assert!("unsafe baseline".parse::<AlignItems>().is_err());
}
#[cfg(feature = "parse")]
#[test]
fn parse_align_content_plain() {
assert_eq!("start".parse::<AlignContent>().unwrap(), AlignContent::START);
assert_eq!("space-between".parse::<AlignContent>().unwrap(), AlignContent::SPACE_BETWEEN);
assert_eq!("space-evenly".parse::<AlignContent>().unwrap(), AlignContent::SPACE_EVENLY);
assert_eq!("space-around".parse::<AlignContent>().unwrap(), AlignContent::SPACE_AROUND);
assert_eq!("stretch".parse::<AlignContent>().unwrap(), AlignContent::STRETCH);
}
#[cfg(feature = "parse")]
#[test]
fn parse_align_content_safe() {
assert_eq!("safe start".parse::<AlignContent>().unwrap(), AlignContent::SAFE_START);
assert_eq!("safe end".parse::<AlignContent>().unwrap(), AlignContent::SAFE_END);
assert_eq!("safe flex-start".parse::<AlignContent>().unwrap(), AlignContent::SAFE_FLEX_START);
assert_eq!("safe flex-end".parse::<AlignContent>().unwrap(), AlignContent::SAFE_FLEX_END);
assert_eq!("safe center".parse::<AlignContent>().unwrap(), AlignContent::SAFE_CENTER);
}
#[cfg(feature = "parse")]
#[test]
fn parse_align_content_unsafe_drops_modifier() {
assert_eq!("unsafe start".parse::<AlignContent>().unwrap(), AlignContent::START);
assert_eq!("unsafe flex-end".parse::<AlignContent>().unwrap(), AlignContent::FLEX_END);
}
#[cfg(feature = "parse")]
#[test]
fn parse_align_content_rejects_invalid_safe_combos() {
assert!("safe stretch".parse::<AlignContent>().is_err());
assert!("safe space-between".parse::<AlignContent>().is_err());
assert!("safe space-evenly".parse::<AlignContent>().is_err());
assert!("safe space-around".parse::<AlignContent>().is_err());
assert!("safe".parse::<AlignContent>().is_err());
assert!("unsafe stretch".parse::<AlignContent>().is_err());
assert!("unsafe space-between".parse::<AlignContent>().is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serde_align_items_round_trip() {
let cases = [
(AlignItems::START, "\"Start\""),
(AlignItems::END, "\"End\""),
(AlignItems::FLEX_START, "\"FlexStart\""),
(AlignItems::FLEX_END, "\"FlexEnd\""),
(AlignItems::CENTER, "\"Center\""),
(AlignItems::BASELINE, "\"Baseline\""),
(AlignItems::STRETCH, "\"Stretch\""),
(AlignItems::SAFE_START, "\"SafeStart\""),
(AlignItems::SAFE_END, "\"SafeEnd\""),
(AlignItems::SAFE_FLEX_START, "\"SafeFlexStart\""),
(AlignItems::SAFE_FLEX_END, "\"SafeFlexEnd\""),
(AlignItems::SAFE_CENTER, "\"SafeCenter\""),
];
for (value, expected) in cases {
let serialized = serde_json::to_string(&value).unwrap();
assert_eq!(serialized, expected, "serialize {:?}", value);
let deserialized: AlignItems = serde_json::from_str(expected).unwrap();
assert_eq!(deserialized, value, "round-trip {:?}", value);
}
assert!(serde_json::from_str::<AlignItems>("\"NotAVariant\"").is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serde_align_content_round_trip() {
let cases = [
(AlignContent::START, "\"Start\""),
(AlignContent::END, "\"End\""),
(AlignContent::FLEX_START, "\"FlexStart\""),
(AlignContent::FLEX_END, "\"FlexEnd\""),
(AlignContent::CENTER, "\"Center\""),
(AlignContent::STRETCH, "\"Stretch\""),
(AlignContent::SPACE_BETWEEN, "\"SpaceBetween\""),
(AlignContent::SPACE_EVENLY, "\"SpaceEvenly\""),
(AlignContent::SPACE_AROUND, "\"SpaceAround\""),
(AlignContent::SAFE_START, "\"SafeStart\""),
(AlignContent::SAFE_END, "\"SafeEnd\""),
(AlignContent::SAFE_FLEX_START, "\"SafeFlexStart\""),
(AlignContent::SAFE_FLEX_END, "\"SafeFlexEnd\""),
(AlignContent::SAFE_CENTER, "\"SafeCenter\""),
];
for (value, expected) in cases {
let serialized = serde_json::to_string(&value).unwrap();
assert_eq!(serialized, expected, "serialize {:?}", value);
let deserialized: AlignContent = serde_json::from_str(expected).unwrap();
assert_eq!(deserialized, value, "round-trip {:?}", value);
}
assert!(serde_json::from_str::<AlignContent>("\"NotAVariant\"").is_err());
}
}
+163
View File
@@ -0,0 +1,163 @@
//! Style type for representing available space as a sizing constraint
use crate::{
prelude::{FromLength, TaffyMaxContent, TaffyMinContent, TaffyZero},
sys::abs,
Size,
};
#[cfg(feature = "parse")]
use crate::util::parse::{from_str_from_css, CssParseResult, FromCss, Parser, Token};
/// The amount of space available to a node in a given axis
/// <https://www.w3.org/TR/css-sizing-3/#available>
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum AvailableSpace {
/// The amount of space available is the specified number of pixels
Definite(f32),
/// The amount of space available is indefinite and the node should be laid out under a min-content constraint
MinContent,
/// The amount of space available is indefinite and the node should be laid out under a max-content constraint
MaxContent,
}
impl TaffyZero for AvailableSpace {
const ZERO: Self = Self::Definite(0.0);
}
impl TaffyMaxContent for AvailableSpace {
const MAX_CONTENT: Self = Self::MaxContent;
}
impl TaffyMinContent for AvailableSpace {
const MIN_CONTENT: Self = Self::MinContent;
}
impl FromLength for AvailableSpace {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
Self::Definite(value.into())
}
}
#[cfg(feature = "parse")]
impl FromCss for AvailableSpace {
fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
match parser.next()?.clone() {
Token::Number { value, .. } if value >= 0.0 => Ok(Self::Definite(value)),
Token::Dimension { value, .. } if value >= 0.0 => Ok(Self::Definite(value)),
Token::Ident(ident) if ident == "max-content" => Ok(Self::MaxContent),
Token::Ident(ident) if ident == "min-content" => Ok(Self::MinContent),
token => Err(parser.new_unexpected_token_error(token))?,
}
}
}
#[cfg(feature = "parse")]
from_str_from_css!(AvailableSpace);
impl AvailableSpace {
/// Returns true for definite values, else false
pub const fn is_definite(self) -> bool {
matches!(self, AvailableSpace::Definite(_))
}
/// Convert to Option
/// Definite values become Some(value). Constraints become None.
pub const fn into_option(self) -> Option<f32> {
match self {
AvailableSpace::Definite(value) => Some(value),
_ => None,
}
}
/// Return the definite value or a default value
pub fn unwrap_or(self, default: f32) -> f32 {
self.into_option().unwrap_or(default)
}
/// Return the definite value. Panic is the value is not definite.
#[track_caller]
pub fn unwrap(self) -> f32 {
self.into_option().unwrap()
}
/// Return self if definite or a default value
pub fn or(self, default: AvailableSpace) -> AvailableSpace {
match self {
AvailableSpace::Definite(_) => self,
_ => default,
}
}
/// Return self if definite or a the result of the default value callback
pub fn or_else(self, default_cb: impl FnOnce() -> AvailableSpace) -> AvailableSpace {
match self {
AvailableSpace::Definite(_) => self,
_ => default_cb(),
}
}
/// Return the definite value or the result of the default value callback
pub fn unwrap_or_else(self, default_cb: impl FnOnce() -> f32) -> f32 {
self.into_option().unwrap_or_else(default_cb)
}
/// If passed value is Some then return AvailableSpace::Definite containing that value, else return self
pub fn maybe_set(self, value: Option<f32>) -> AvailableSpace {
match value {
Some(value) => AvailableSpace::Definite(value),
None => self,
}
}
/// If passed value is Some then return AvailableSpace::Definite containing that value, else return self
pub fn map_definite_value(self, map_function: impl FnOnce(f32) -> f32) -> AvailableSpace {
match self {
AvailableSpace::Definite(value) => AvailableSpace::Definite(map_function(value)),
_ => self,
}
}
/// Compute free_space given the passed used_space
pub fn compute_free_space(&self, used_space: f32) -> f32 {
match self {
AvailableSpace::MaxContent => f32::INFINITY,
AvailableSpace::MinContent => 0.0,
AvailableSpace::Definite(available_space) => available_space - used_space,
}
}
/// Compare equality with another AvailableSpace, treating definite values
/// that are within f32::EPSILON of each other as equal
pub fn is_roughly_equal(self, other: AvailableSpace) -> bool {
use AvailableSpace::*;
match (self, other) {
(Definite(a), Definite(b)) => abs(a - b) < f32::EPSILON,
(MinContent, MinContent) => true,
(MaxContent, MaxContent) => true,
_ => false,
}
}
}
impl From<f32> for AvailableSpace {
fn from(value: f32) -> Self {
Self::Definite(value)
}
}
impl From<Option<f32>> for AvailableSpace {
fn from(option: Option<f32>) -> Self {
match option {
Some(value) => Self::Definite(value),
None => Self::MaxContent,
}
}
}
impl Size<AvailableSpace> {
/// Convert `Size<AvailableSpace>` into `Size<Option<f32>>`
pub fn into_options(self) -> Size<Option<f32>> {
Size { width: self.width.into_option(), height: self.height.into_option() }
}
/// If passed value is Some then return AvailableSpace::Definite containing that value, else return self
pub fn maybe_set(self, value: Size<Option<f32>>) -> Size<AvailableSpace> {
Size { width: self.width.maybe_set(value.width), height: self.height.maybe_set(value.height) }
}
}
+64
View File
@@ -0,0 +1,64 @@
//! Style types for Block layout
use crate::style::AlignContent;
use crate::{CoreStyle, Style};
/// The set of styles required for a Block layout container
pub trait BlockContainerStyle: CoreStyle {
/// Defines which row in the grid the item should start and end at
#[inline(always)]
fn text_align(&self) -> TextAlign {
Style::<Self::CustomIdent>::DEFAULT.text_align
}
/// How children of this block container are aligned in the block (cross) axis
#[inline(always)]
fn align_content(&self) -> Option<AlignContent> {
Style::<Self::CustomIdent>::DEFAULT.align_content
}
}
/// The set of styles required for a Block layout item (child of a Block container)
pub trait BlockItemStyle: CoreStyle {
/// Whether the item is a table. Table children are handled specially in block layout.
#[inline(always)]
fn is_table(&self) -> bool {
false
}
/// Whether the item is a floated
#[cfg(feature = "float_layout")]
#[inline(always)]
fn float(&self) -> super::Float {
super::Float::None
}
/// Whether the item is a floated
#[cfg(feature = "float_layout")]
#[inline(always)]
fn clear(&self) -> super::Clear {
super::Clear::None
}
}
/// Used by block layout to implement the legacy behaviour of `<center>` and `<div align="left | right | center">`
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TextAlign {
/// No special legacy text align behaviour.
#[default]
Auto,
/// Corresponds to `-webkit-left` or `-moz-left` in browsers
LegacyLeft,
/// Corresponds to `-webkit-right` or `-moz-right` in browsers
LegacyRight,
/// Corresponds to `-webkit-center` or `-moz-center` in browsers
LegacyCenter,
}
#[cfg(feature = "parse")]
crate::util::parse::impl_parse_for_keyword_enum!(TextAlign,
"auto" => Auto,
"-webkit-left" => LegacyLeft,
"-webkit-right" => LegacyRight,
"-webkit-center" => LegacyCenter,
);
+540
View File
@@ -0,0 +1,540 @@
//! A tagged-pointer abstraction that allows size styles in Taffy to be represented
//! in just 64 bits. Wrapped by types in the `super::dimension` and `super::grid` modules.
use super::LengthPercentage;
use crate::style_helpers::{
FromFr, FromLength, FromPercent, TaffyAuto, TaffyFitContent, TaffyMaxContent, TaffyMinContent, TaffyZero,
};
/// Note: these two functions are copied directly from the std (core) library. But by duplicating them
/// here we can reduce MSRV from 1.84 all the way down to 1.65 while retaining const constructors and
/// strict pointer provenance
mod compat {
#![allow(unsafe_code)]
#![allow(unknown_lints)]
#![allow(unnecessary_transmutes)]
/// Raw transmutation from `f32` to `u32`.
pub const fn f32_to_bits(val: f32) -> u32 {
// SAFETY: `u32` is a plain old datatype so we can always transmute to it.
unsafe { core::mem::transmute(val) }
}
/// Raw transmutation from `u32` to `f32`.
pub const fn f32_from_bits(v: u32) -> f32 {
// SAFETY: `u32` is a plain old datatype so we can always transmute from it.
unsafe { core::mem::transmute(v) }
}
/// Tag a pointer preserving provenance (requires Rust 1.84)
#[inline(always)]
#[cfg(all(target_pointer_width = "64", feature = "strict_provenance"))]
pub fn tag_ptr(ptr: *const (), tag: usize) -> *const () {
ptr.map_addr(|a| a | tag)
}
/// Tag a pointer exposing provenance (works back to Rust 1.0)
#[inline(always)]
#[cfg(all(target_pointer_width = "64", not(feature = "strict_provenance")))]
pub fn tag_ptr(ptr: *const (), tag: usize) -> *const () {
(ptr as usize | tag) as *const ()
}
}
#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))]
std::compile_error!("Taffy only supports targets with a pointer width of 32 or 64 bits");
/// CompactLengthInner implementation for 64 bit platforms
#[cfg(target_pointer_width = "64")]
mod inner {
use super::compat::{f32_from_bits, f32_to_bits, tag_ptr};
/// The low byte (8 bits)
const TAG_MASK: usize = 0b11111111;
/// The low 3 bits
const CALC_TAG_MASK: usize = 0b111;
// The high 63 bits
// const CALC_PTR_MASK: usize = usize::MAX ^ 0b111;
/// On 64 bit platforms the tag, value and pointer are packed into a single 64 bit pointer
///
/// The tagged pointer always has a tag and may contain an f32 value or a pointer
/// (or neither) depending on the variant indicated by the tag.
#[derive(Copy, Clone, Debug, PartialEq)]
pub(super) struct CompactLengthInner {
/// The tagged pointer
tagged_ptr: *const (),
}
impl CompactLengthInner {
/// Construct a `CompactLengthInner` from a tag and pointer
#[inline(always)]
pub(super) fn from_ptr(ptr: *const (), tag: usize) -> Self {
let tagged_ptr = tag_ptr(ptr, tag);
Self { tagged_ptr }
}
/// Construct a `CompactLengthInner` from a tag and numeric value
#[inline(always)]
pub(super) const fn from_val(val: f32, tag: usize) -> Self {
let tagged_ptr = (((f32_to_bits(val) as usize) << 32) | tag) as *const ();
Self { tagged_ptr }
}
/// Construct a `CompactLengthInner` from only a tag
#[inline(always)]
pub(super) const fn from_tag(tag: usize) -> Self {
let tagged_ptr = tag as *const ();
Self { tagged_ptr }
}
/// Get the calc tag (low 3 bits)
#[inline(always)]
#[cfg(feature = "calc")]
pub(super) fn calc_tag(self) -> usize {
(self.tagged_ptr as usize) & CALC_TAG_MASK
}
/// Get the general tag (low 8 bits)
#[inline(always)]
pub(super) fn tag(self) -> usize {
(self.tagged_ptr as usize) & TAG_MASK
}
/// Get the pointer value
#[inline(always)]
pub(super) fn ptr(self) -> *const () {
self.tagged_ptr
}
/// Get the numeric value
#[inline(always)]
pub(super) fn value(self) -> f32 {
f32_from_bits((self.tagged_ptr as usize >> 32) as u32)
}
/// Get the serialized value.
#[inline(always)]
#[cfg(feature = "serde")]
pub(super) fn serialized(self) -> u64 {
(self.tagged_ptr as usize as u64).rotate_left(32)
}
/// Derialized from a value.
#[inline(always)]
#[cfg(feature = "serde")]
pub(super) fn from_serialized(value: u64) -> Self {
Self { tagged_ptr: value.rotate_right(32) as usize as *const () }
}
}
}
/// CompactLengthInner implementation for 32 bit platforms
#[cfg(target_pointer_width = "32")]
mod inner {
use super::compat::{f32_from_bits, f32_to_bits};
/// On 32 bit platforms the tag is stored separately.
/// Either an f32 value or a pointer (or neither) are packed into the ptr field
/// depending on the variant indicated by the tag
#[derive(Copy, Clone, Debug, PartialEq)]
pub(super) struct CompactLengthInner {
/// The tag indicating what kind of value we are storing
tag: usize,
/// The pointer of numeric value
ptr: *const (),
}
impl CompactLengthInner {
/// Construct a `CompactLengthInner` from a tag and pointer
#[inline(always)]
pub(super) fn from_ptr(ptr: *const (), tag: usize) -> Self {
Self { ptr, tag }
}
/// Construct a `CompactLengthInner` from a tag and numeric value
#[inline(always)]
pub(super) const fn from_val(val: f32, tag: usize) -> Self {
Self { ptr: f32_to_bits(val) as usize as *const (), tag }
}
/// Construct a `CompactLengthInner` from only a tag
#[inline(always)]
pub(super) const fn from_tag(tag: usize) -> Self {
Self { ptr: 0 as *const (), tag }
}
/// Get the calc tag (low 3 bits)
#[inline(always)]
#[cfg(feature = "calc")]
pub(super) fn calc_tag(self) -> usize {
self.tag
}
/// Get the general tag (low 8 bits)
#[inline(always)]
pub(super) fn tag(self) -> usize {
self.tag
}
/// Get the pointer value
#[inline(always)]
pub(super) fn ptr(self) -> *const () {
self.ptr
}
/// Get the numeric value
#[inline(always)]
pub(super) fn value(self) -> f32 {
f32_from_bits(self.ptr as u32)
}
/// Get the serialized value.
#[inline(always)]
#[cfg(feature = "serde")]
pub(super) fn serialized(self) -> u64 {
(self.tag as u64) << 32 | (self.ptr as u64)
}
/// Derialized from a value.
#[inline(always)]
#[cfg(feature = "serde")]
pub(super) fn from_serialized(value: u64) -> Self {
Self { tag: (value >> 32) as usize, ptr: (value & 0xFFFFFFFF) as usize as *const () }
}
}
}
use inner::CompactLengthInner;
/// A representation of a length as a compact 64-bit tagged pointer
#[derive(Copy, Clone, PartialEq, Debug)]
#[repr(transparent)]
pub struct CompactLength(CompactLengthInner);
impl CompactLength {
/// The tag indicating a calc() value
#[cfg(feature = "calc")]
pub const CALC_TAG: usize = 0b000;
/// The tag indicating a length value
pub const LENGTH_TAG: usize = 0b0000_0001;
/// The tag indicating a percentage value
pub const PERCENT_TAG: usize = 0b0000_0010;
/// The tag indicating an auto value
pub const AUTO_TAG: usize = 0b0000_0011;
/// The tag indicating an fr value
pub const FR_TAG: usize = 0b0000_0100;
/// The tag indicating a min-content value
pub const MIN_CONTENT_TAG: usize = 0b00000111;
/// The tag indicating a max-content value
pub const MAX_CONTENT_TAG: usize = 0b00001111;
/// The tag indicating a fit-content value with px limit
pub const FIT_CONTENT_PX_TAG: usize = 0b00010111;
/// The tag indicating a fit-content value with percent limit
pub const FIT_CONTENT_PERCENT_TAG: usize = 0b00011111;
}
impl CompactLength {
/// An absolute length in some abstract units. Users of Taffy may define what they correspond
/// to in their application (pixels, logical pixels, mm, etc) as they see fit.
#[inline(always)]
pub const fn length(val: f32) -> Self {
Self(CompactLengthInner::from_val(val, Self::LENGTH_TAG))
}
/// A percentage length relative to the size of the containing block.
///
/// **NOTE: percentages are represented as a f32 value in the range [0.0, 1.0] NOT the range [0.0, 100.0]**
#[inline(always)]
pub const fn percent(val: f32) -> Self {
Self(CompactLengthInner::from_val(val, Self::PERCENT_TAG))
}
/// A `calc()` value. The value passed here is treated as an opaque handle to
/// the actual calc representation and may be a pointer, index, etc.
///
/// The low 3 bits are used as a tag value and will be returned as 0.
#[inline]
#[cfg(feature = "calc")]
pub fn calc(ptr: *const ()) -> Self {
assert_ne!(ptr as u64, 0);
assert_eq!(ptr as u64 & 0b111, 0);
Self(CompactLengthInner::from_ptr(ptr, Self::CALC_TAG))
}
/// The dimension should be automatically computed according to algorithm-specific rules
/// regarding the default size of boxes.
#[inline(always)]
pub const fn auto() -> Self {
Self(CompactLengthInner::from_tag(Self::AUTO_TAG))
}
/// The dimension as a fraction of the total available grid space (`fr` units in CSS)
/// Specified value is the numerator of the fraction. Denominator is the sum of all fraction specified in that grid dimension
/// Spec: <https://www.w3.org/TR/css3-grid-layout/#fr-unit>
#[inline(always)]
pub const fn fr(val: f32) -> Self {
Self(CompactLengthInner::from_val(val, Self::FR_TAG))
}
/// The size should be the "min-content" size.
/// This is the smallest size that can fit the item's contents with ALL soft line-wrapping opportunities taken
#[inline(always)]
pub const fn min_content() -> Self {
Self(CompactLengthInner::from_tag(Self::MIN_CONTENT_TAG))
}
/// The size should be the "max-content" size.
/// This is the smallest size that can fit the item's contents with NO soft line-wrapping opportunities taken
#[inline(always)]
pub const fn max_content() -> Self {
Self(CompactLengthInner::from_tag(Self::MAX_CONTENT_TAG))
}
/// The size should be computed according to the "fit content" formula:
/// `max(min_content, min(max_content, limit))`
/// where:
/// - `min_content` is the [min-content](Self::min_content) size
/// - `max_content` is the [max-content](Self::max_content) size
/// - `limit` is a LENGTH value passed to this function
///
/// The effect of this is that the item takes the size of `limit` clamped
/// by the min-content and max-content sizes.
#[inline(always)]
pub const fn fit_content_px(limit: f32) -> Self {
Self(CompactLengthInner::from_val(limit, Self::FIT_CONTENT_PX_TAG))
}
/// The size should be computed according to the "fit content" formula:
/// `max(min_content, min(max_content, limit))`
/// where:
/// - `min_content` is the [min-content](Self::min_content) size
/// - `max_content` is the [max-content](Self::max_content) size
/// - `limit` is a PERCENTAGE value passed to this function
///
/// The effect of this is that the item takes the size of `limit` clamped
/// by the min-content and max-content sizes.
#[inline(always)]
pub const fn fit_content_percent(limit: f32) -> Self {
Self(CompactLengthInner::from_val(limit, Self::FIT_CONTENT_PERCENT_TAG))
}
/// Get the primary tag
#[inline(always)]
pub fn tag(self) -> usize {
self.0.tag()
}
/// Get the numeric value associated with the `CompactLength`
/// (e.g. the pixel value for a LENGTH variant)
#[inline(always)]
pub fn value(self) -> f32 {
self.0.value()
}
/// Get the calc pointer of the `CompactLength`
#[inline(always)]
#[cfg(feature = "calc")]
pub fn calc_value(self) -> *const () {
self.0.ptr()
}
/// Returns true if the value is 0 px
#[inline(always)]
#[cfg(feature = "calc")]
pub fn is_calc(self) -> bool {
self.0.calc_tag() == 0
}
/// Returns true if the value is 0 px
#[inline(always)]
pub fn is_zero(self) -> bool {
self.0 == Self::ZERO.0
}
/// Returns true if the value is a length or percentage value
#[inline(always)]
pub fn is_length_or_percentage(self) -> bool {
matches!(self.tag(), Self::LENGTH_TAG | Self::PERCENT_TAG)
}
/// Returns true if the value is auto
#[inline(always)]
pub fn is_auto(self) -> bool {
self.tag() == Self::AUTO_TAG
}
/// Returns true if the value is min-content
#[inline(always)]
pub fn is_min_content(self) -> bool {
matches!(self.tag(), Self::MIN_CONTENT_TAG)
}
/// Returns true if the value is max-content
#[inline(always)]
pub fn is_max_content(self) -> bool {
matches!(self.tag(), Self::MAX_CONTENT_TAG)
}
/// Returns true if the value is a fit-content(...) value
#[inline(always)]
pub fn is_fit_content(self) -> bool {
matches!(self.tag(), Self::FIT_CONTENT_PX_TAG | Self::FIT_CONTENT_PERCENT_TAG)
}
/// Returns true if the value is max-content or a fit-content(...) value
#[inline(always)]
pub fn is_max_or_fit_content(self) -> bool {
matches!(self.tag(), Self::MAX_CONTENT_TAG | Self::FIT_CONTENT_PX_TAG | Self::FIT_CONTENT_PERCENT_TAG)
}
/// Returns true if the max track sizing function is `MaxContent`, `FitContent` or `Auto` else false.
/// "In all cases, treat auto and fit-content() as max-content, except where specified otherwise for fit-content()."
/// See: <https://www.w3.org/TR/css-grid-1/#algo-terms>
#[inline(always)]
pub fn is_max_content_alike(&self) -> bool {
matches!(
self.tag(),
CompactLength::AUTO_TAG
| CompactLength::MAX_CONTENT_TAG
| CompactLength::FIT_CONTENT_PX_TAG
| CompactLength::FIT_CONTENT_PERCENT_TAG
)
}
/// Returns true if the min track sizing function is `MinContent` or `MaxContent`, else false.
#[inline(always)]
pub fn is_min_or_max_content(&self) -> bool {
matches!(self.tag(), Self::MIN_CONTENT_TAG | Self::MAX_CONTENT_TAG)
}
/// Returns true if the value is auto, min-content, max-content, or fit-content(...)
#[inline(always)]
pub fn is_intrinsic(self) -> bool {
matches!(
self.tag(),
Self::AUTO_TAG
| Self::MIN_CONTENT_TAG
| Self::MAX_CONTENT_TAG
| Self::FIT_CONTENT_PX_TAG
| Self::FIT_CONTENT_PERCENT_TAG
)
}
/// Returns true if the value is and fr value
#[inline(always)]
pub fn is_fr(self) -> bool {
self.tag() == Self::FR_TAG
}
/// Whether the track sizing functions depends on the size of the parent node
#[inline(always)]
pub fn uses_percentage(self) -> bool {
#[cfg(feature = "calc")]
{
matches!(self.tag(), CompactLength::PERCENT_TAG | CompactLength::FIT_CONTENT_PERCENT_TAG) || self.is_calc()
}
#[cfg(not(feature = "calc"))]
{
matches!(self.tag(), CompactLength::PERCENT_TAG | CompactLength::FIT_CONTENT_PERCENT_TAG)
}
}
/// Resolve percentage values against the passed parent_size, returning Some(value)
/// Non-percentage values always return None.
#[inline(always)]
pub fn resolved_percentage_size(
self,
parent_size: f32,
calc_resolver: impl Fn(*const (), f32) -> f32,
) -> Option<f32> {
match self.tag() {
CompactLength::PERCENT_TAG => Some(self.value() * parent_size),
#[cfg(feature = "calc")]
_ if self.is_calc() => Some(calc_resolver(self.0.ptr(), parent_size)),
_ => None,
}
}
}
impl TaffyZero for CompactLength {
const ZERO: Self = Self::length(0.0);
}
impl TaffyAuto for CompactLength {
const AUTO: Self = Self::auto();
}
impl TaffyMinContent for CompactLength {
const MIN_CONTENT: Self = Self::min_content();
}
impl TaffyMaxContent for CompactLength {
const MAX_CONTENT: Self = Self::max_content();
}
impl FromLength for CompactLength {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
Self::length(value.into())
}
}
impl FromPercent for CompactLength {
fn from_percent<Input: Into<f32> + Copy>(value: Input) -> Self {
Self::percent(value.into())
}
}
impl FromFr for CompactLength {
fn from_fr<Input: Into<f32> + Copy>(value: Input) -> Self {
Self::fr(value.into())
}
}
impl TaffyFitContent for CompactLength {
fn fit_content(lp: LengthPercentage) -> Self {
let value = lp.0.value();
match lp.0.tag() {
Self::LENGTH_TAG => Self::fit_content_px(value),
Self::PERCENT_TAG => Self::fit_content_percent(value),
_ => unreachable!(),
}
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for CompactLength {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[cfg(feature = "calc")]
{
if self.tag() == Self::CALC_TAG {
Err(serde::ser::Error::custom("Cannot serialize Calc value"))
} else {
serializer.serialize_u64(self.0.serialized())
}
}
#[cfg(not(feature = "calc"))]
serializer.serialize_u64(self.0.serialized())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for CompactLength {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bits: u64 = u64::deserialize(deserializer)?;
let value = Self(CompactLengthInner::from_serialized(bits));
// Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported
if matches!(
value.tag(),
CompactLength::LENGTH_TAG
| CompactLength::PERCENT_TAG
| CompactLength::AUTO_TAG
| CompactLength::MIN_CONTENT_TAG
| CompactLength::MAX_CONTENT_TAG
| CompactLength::FIT_CONTENT_PX_TAG
| CompactLength::FIT_CONTENT_PERCENT_TAG
| CompactLength::FR_TAG
) {
Ok(value)
} else {
Err(serde::de::Error::custom("Cannot deserialize Calc value"))
}
}
}
+380
View File
@@ -0,0 +1,380 @@
//! Style types for representing lengths / sizes
use super::CompactLength;
use crate::geometry::Rect;
use crate::style_helpers::{FromLength, FromPercent, TaffyAuto, TaffyZero};
#[cfg(feature = "parse")]
use crate::util::parse::{from_str_from_css, CssParseResult, FromCss, Parser, Token};
/// A unit of linear measurement
///
/// This is commonly combined with [`Rect`], [`Point`](crate::geometry::Point) and [`Size<T>`](crate::geometry::Size).
#[derive(Copy, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct LengthPercentage(pub(crate) CompactLength);
impl TaffyZero for LengthPercentage {
const ZERO: Self = Self(CompactLength::ZERO);
}
impl FromLength for LengthPercentage {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
Self::length(value.into())
}
}
impl FromPercent for LengthPercentage {
fn from_percent<Input: Into<f32> + Copy>(value: Input) -> Self {
Self::percent(value.into())
}
}
#[cfg(feature = "parse")]
impl FromCss for LengthPercentage {
fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
match parser.next()?.clone() {
Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
token => Err(parser.new_unexpected_token_error(token))?,
}
}
}
#[cfg(feature = "parse")]
from_str_from_css!(LengthPercentage);
impl LengthPercentage {
/// An absolute length in some abstract units. Users of Taffy may define what they correspond
/// to in their application (pixels, logical pixels, mm, etc) as they see fit.
#[inline(always)]
pub const fn length(val: f32) -> Self {
Self(CompactLength::length(val))
}
/// A percentage length relative to the size of the containing block.
///
/// **NOTE: percentages are represented as a f32 value in the range [0.0, 1.0] NOT the range [0.0, 100.0]**
#[inline(always)]
pub const fn percent(val: f32) -> Self {
Self(CompactLength::percent(val))
}
/// A `calc()` value. The value passed here is treated as an opaque handle to
/// the actual calc representation and may be a pointer, index, etc.
///
/// The low 3 bits are used as a tag value and will be returned as 0.
#[inline(always)]
#[cfg(feature = "calc")]
pub fn calc(ptr: *const ()) -> Self {
Self(CompactLength::calc(ptr))
}
/// Create a LengthPercentage from a raw `CompactLength`.
/// # Safety
/// CompactLength must represent a valid variant for LengthPercentage
#[allow(unsafe_code)]
pub const unsafe fn from_raw(val: CompactLength) -> Self {
Self(val)
}
/// Get the underlying `CompactLength` representation of the value
pub const fn into_raw(self) -> CompactLength {
self.0
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for LengthPercentage {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let inner = CompactLength::deserialize(deserializer)?;
// Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported
if matches!(inner.tag(), CompactLength::LENGTH_TAG | CompactLength::PERCENT_TAG) {
Ok(Self(inner))
} else {
Err(serde::de::Error::custom("Invalid tag"))
}
}
}
/// A unit of linear measurement
///
/// This is commonly combined with [`Rect`], [`Point`](crate::geometry::Point) and [`Size<T>`](crate::geometry::Size).
#[derive(Copy, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct LengthPercentageAuto(pub(crate) CompactLength);
impl TaffyZero for LengthPercentageAuto {
const ZERO: Self = Self(CompactLength::ZERO);
}
impl TaffyAuto for LengthPercentageAuto {
const AUTO: Self = Self(CompactLength::AUTO);
}
impl FromLength for LengthPercentageAuto {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
Self::length(value.into())
}
}
impl FromPercent for LengthPercentageAuto {
fn from_percent<Input: Into<f32> + Copy>(value: Input) -> Self {
Self::percent(value.into())
}
}
impl From<LengthPercentage> for LengthPercentageAuto {
fn from(input: LengthPercentage) -> Self {
Self(input.0)
}
}
#[cfg(feature = "parse")]
impl FromCss for LengthPercentageAuto {
fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
match parser.next()?.clone() {
Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
Token::Ident(ident) if ident == "auto" => Ok(Self::auto()),
token => Err(parser.new_unexpected_token_error(token))?,
}
}
}
#[cfg(feature = "parse")]
from_str_from_css!(LengthPercentageAuto);
impl LengthPercentageAuto {
/// An absolute length in some abstract units. Users of Taffy may define what they correspond
/// to in their application (pixels, logical pixels, mm, etc) as they see fit.
#[inline(always)]
pub const fn length(val: f32) -> Self {
Self(CompactLength::length(val))
}
/// A percentage length relative to the size of the containing block.
///
/// **NOTE: percentages are represented as a f32 value in the range [0.0, 1.0] NOT the range [0.0, 100.0]**
#[inline(always)]
pub const fn percent(val: f32) -> Self {
Self(CompactLength::percent(val))
}
/// The dimension should be automatically computed according to algorithm-specific rules
/// regarding the default size of boxes.
#[inline(always)]
pub const fn auto() -> Self {
Self(CompactLength::auto())
}
/// A `calc()` value. The value passed here is treated as an opaque handle to
/// the actual calc representation and may be a pointer, index, etc.
///
/// The low 3 bits are used as a tag value and will be returned as 0.
#[inline]
#[cfg(feature = "calc")]
pub fn calc(ptr: *const ()) -> Self {
Self(CompactLength::calc(ptr))
}
/// Create a LengthPercentageAuto from a raw `CompactLength`.
/// # Safety
/// CompactLength must represent a valid variant for LengthPercentageAuto
#[allow(unsafe_code)]
pub const unsafe fn from_raw(val: CompactLength) -> Self {
Self(val)
}
/// Get the underlying `CompactLength` representation of the value
pub const fn into_raw(self) -> CompactLength {
self.0
}
/// Returns:
/// - Some(length) for Length variants
/// - Some(resolved) using the provided context for Percent variants
/// - None for Auto variants
#[inline(always)]
pub fn resolve_to_option(self, context: f32, calc_resolver: impl Fn(*const (), f32) -> f32) -> Option<f32> {
match self.0.tag() {
CompactLength::LENGTH_TAG => Some(self.0.value()),
CompactLength::PERCENT_TAG => Some(context * self.0.value()),
CompactLength::AUTO_TAG => None,
#[cfg(feature = "calc")]
_ if self.0.is_calc() => Some(calc_resolver(self.0.calc_value(), context)),
_ => unreachable!("LengthPercentageAuto values cannot be constructed with other tags"),
}
}
/// Returns true if value is LengthPercentageAuto::Auto
#[inline(always)]
pub fn is_auto(self) -> bool {
self.0.is_auto()
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for LengthPercentageAuto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let inner = CompactLength::deserialize(deserializer)?;
// Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported
if matches!(inner.tag(), CompactLength::LENGTH_TAG | CompactLength::PERCENT_TAG | CompactLength::AUTO_TAG) {
Ok(Self(inner))
} else {
Err(serde::de::Error::custom("Invalid tag"))
}
}
}
/// A unit of linear measurement
///
/// This is commonly combined with [`Rect`], [`Point`](crate::geometry::Point) and [`Size<T>`](crate::geometry::Size).
#[derive(Copy, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Dimension(pub(crate) CompactLength);
impl TaffyZero for Dimension {
const ZERO: Self = Self(CompactLength::ZERO);
}
impl TaffyAuto for Dimension {
const AUTO: Self = Self(CompactLength::AUTO);
}
impl FromLength for Dimension {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
Self::length(value.into())
}
}
impl FromPercent for Dimension {
fn from_percent<Input: Into<f32> + Copy>(value: Input) -> Self {
Self::percent(value.into())
}
}
impl From<LengthPercentage> for Dimension {
fn from(input: LengthPercentage) -> Self {
Self(input.0)
}
}
impl From<LengthPercentageAuto> for Dimension {
fn from(input: LengthPercentageAuto) -> Self {
Self(input.0)
}
}
#[cfg(feature = "parse")]
impl FromCss for Dimension {
fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
match parser.next()?.clone() {
Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
Token::Ident(ident) if ident == "auto" => Ok(Self::auto()),
token => Err(parser.new_unexpected_token_error(token))?,
}
}
}
#[cfg(feature = "parse")]
from_str_from_css!(Dimension);
impl Dimension {
/// An absolute length in some abstract units. Users of Taffy may define what they correspond
/// to in their application (pixels, logical pixels, mm, etc) as they see fit.
#[inline(always)]
pub const fn length(val: f32) -> Self {
Self(CompactLength::length(val))
}
/// A percentage length relative to the size of the containing block.
///
/// **NOTE: percentages are represented as a f32 value in the range [0.0, 1.0] NOT the range [0.0, 100.0]**
#[inline(always)]
pub const fn percent(val: f32) -> Self {
Self(CompactLength::percent(val))
}
/// The dimension should be automatically computed according to algorithm-specific rules
/// regarding the default size of boxes.
#[inline(always)]
pub const fn auto() -> Self {
Self(CompactLength::auto())
}
/// A `calc()` value. The value passed here is treated as an opaque handle to
/// the actual calc representation and may be a pointer, index, etc.
///
/// The low 3 bits are used as a tag value and will be returned as 0.
#[inline]
#[cfg(feature = "calc")]
pub fn calc(ptr: *const ()) -> Self {
Self(CompactLength::calc(ptr))
}
/// Create a LengthPercentageAuto from a raw `CompactLength`.
/// # Safety
/// CompactLength must represent a valid variant for LengthPercentageAuto
#[allow(unsafe_code)]
pub const unsafe fn from_raw(val: CompactLength) -> Self {
Self(val)
}
/// Get the underlying `CompactLength` representation of the value
pub const fn into_raw(self) -> CompactLength {
self.0
}
/// Get Length value if value is Length variant
#[cfg(feature = "grid")]
pub fn into_option(self) -> Option<f32> {
match self.0.tag() {
CompactLength::LENGTH_TAG => Some(self.0.value()),
_ => None,
}
}
/// Returns true if value is Auto
#[inline(always)]
pub fn is_auto(self) -> bool {
self.0.is_auto()
}
/// Get the raw `CompactLength` tag
pub fn tag(self) -> usize {
self.0.tag()
}
/// Get the raw `CompactLength` value for non-calc variants that have a numeric parameter
pub fn value(self) -> f32 {
self.0.value()
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Dimension {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let inner = CompactLength::deserialize(deserializer)?;
// Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported
if matches!(inner.tag(), CompactLength::LENGTH_TAG | CompactLength::PERCENT_TAG | CompactLength::AUTO_TAG) {
Ok(Self(inner))
} else {
Err(serde::de::Error::custom("Invalid tag"))
}
}
}
impl Rect<Dimension> {
/// Create a new Rect with length values
#[must_use]
pub const fn from_length(start: f32, end: f32, top: f32, bottom: f32) -> Self {
Rect {
left: Dimension(CompactLength::length(start)),
right: Dimension(CompactLength::length(end)),
top: Dimension(CompactLength::length(top)),
bottom: Dimension(CompactLength::length(bottom)),
}
}
/// Create a new Rect with percentage values
#[must_use]
pub const fn from_percent(start: f32, end: f32, top: f32, bottom: f32) -> Self {
Rect {
left: Dimension(CompactLength::percent(start)),
right: Dimension(CompactLength::percent(end)),
top: Dimension(CompactLength::percent(top)),
bottom: Dimension(CompactLength::percent(bottom)),
}
}
}
+203
View File
@@ -0,0 +1,203 @@
//! Style types for Flexbox layout
use super::{AlignContent, AlignItems, AlignSelf, CoreStyle, Dimension, JustifyContent, LengthPercentage, Style};
use crate::geometry::Size;
/// The set of styles required for a Flexbox container
pub trait FlexboxContainerStyle: CoreStyle {
/// Which direction does the main axis flow in?
#[inline(always)]
fn flex_direction(&self) -> FlexDirection {
Style::<Self::CustomIdent>::DEFAULT.flex_direction
}
/// Should elements wrap, or stay in a single line?
#[inline(always)]
fn flex_wrap(&self) -> FlexWrap {
Style::<Self::CustomIdent>::DEFAULT.flex_wrap
}
/// How large should the gaps between items in a grid or flex container be?
#[inline(always)]
fn gap(&self) -> Size<LengthPercentage> {
Style::<Self::CustomIdent>::DEFAULT.gap
}
// Alignment properties
/// How should content contained within this item be aligned in the cross/block axis
#[inline(always)]
fn align_content(&self) -> Option<AlignContent> {
Style::<Self::CustomIdent>::DEFAULT.align_content
}
/// How this node's children aligned in the cross/block axis?
#[inline(always)]
fn align_items(&self) -> Option<AlignItems> {
Style::<Self::CustomIdent>::DEFAULT.align_items
}
/// How this node's children should be aligned in the inline axis
#[inline(always)]
fn justify_content(&self) -> Option<JustifyContent> {
Style::<Self::CustomIdent>::DEFAULT.justify_content
}
}
/// The set of styles required for a Flexbox item (child of a Flexbox container)
pub trait FlexboxItemStyle: CoreStyle {
/// Sets the initial main axis size of the item
#[inline(always)]
fn flex_basis(&self) -> Dimension {
Style::<Self::CustomIdent>::DEFAULT.flex_basis
}
/// The relative rate at which this item grows when it is expanding to fill space
#[inline(always)]
fn flex_grow(&self) -> f32 {
Style::<Self::CustomIdent>::DEFAULT.flex_grow
}
/// The relative rate at which this item shrinks when it is contracting to fit into space
#[inline(always)]
fn flex_shrink(&self) -> f32 {
Style::<Self::CustomIdent>::DEFAULT.flex_shrink
}
/// How this node should be aligned in the cross/block axis
/// Falls back to the parents [`AlignItems`] if not set
#[inline(always)]
fn align_self(&self) -> Option<AlignSelf> {
Style::<Self::CustomIdent>::DEFAULT.align_self
}
}
use crate::geometry::AbsoluteAxis;
/// Controls whether flex items are forced onto one line or can wrap onto multiple lines.
///
/// Defaults to [`FlexWrap::NoWrap`]
///
/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property)
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum FlexWrap {
/// Items will not wrap and stay on a single line
#[default]
NoWrap,
/// Items will wrap according to this item's [`FlexDirection`]
Wrap,
/// Items will wrap in the opposite direction to this item's [`FlexDirection`]
WrapReverse,
}
#[cfg(feature = "parse")]
crate::util::parse::impl_parse_for_keyword_enum!(FlexWrap,
"nowrap" => NoWrap,
"wrap" => Wrap,
"wrap-reverse" => WrapReverse,
);
/// The direction of the flexbox layout main axis.
///
/// There are always two perpendicular layout axes: main (or primary) and cross (or secondary).
/// Adding items will cause them to be positioned adjacent to each other along the main axis.
/// By varying this value throughout your tree, you can create complex axis-aligned layouts.
///
/// Items are always aligned relative to the cross axis, and justified relative to the main axis.
///
/// The default behavior is [`FlexDirection::Row`].
///
/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-direction-property)
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum FlexDirection {
/// Defines +x as the main axis
///
/// Items will be added from left to right in a row.
#[default]
Row,
/// Defines +y as the main axis
///
/// Items will be added from top to bottom in a column.
Column,
/// Defines -x as the main axis
///
/// Items will be added from right to left in a row.
RowReverse,
/// Defines -y as the main axis
///
/// Items will be added from bottom to top in a column.
ColumnReverse,
}
#[cfg(feature = "parse")]
crate::util::parse::impl_parse_for_keyword_enum!(FlexDirection,
"row" => Row,
"column" => Column,
"row-reverse" => RowReverse,
"column-reverse" => ColumnReverse,
);
impl FlexDirection {
#[inline]
/// Is the direction [`FlexDirection::Row`] or [`FlexDirection::RowReverse`]?
pub(crate) const fn is_row(self) -> bool {
matches!(self, Self::Row | Self::RowReverse)
}
#[inline]
/// Is the direction [`FlexDirection::Column`] or [`FlexDirection::ColumnReverse`]?
pub(crate) const fn is_column(self) -> bool {
matches!(self, Self::Column | Self::ColumnReverse)
}
#[inline]
/// Is the direction [`FlexDirection::RowReverse`] or [`FlexDirection::ColumnReverse`]?
pub(crate) const fn is_reverse(self) -> bool {
matches!(self, Self::RowReverse | Self::ColumnReverse)
}
#[inline]
/// The `AbsoluteAxis` that corresponds to the main axis
pub(crate) const fn main_axis(self) -> AbsoluteAxis {
match self {
Self::Row | Self::RowReverse => AbsoluteAxis::Horizontal,
Self::Column | Self::ColumnReverse => AbsoluteAxis::Vertical,
}
}
#[inline]
/// The `AbsoluteAxis` that corresponds to the cross axis
pub(crate) const fn cross_axis(self) -> AbsoluteAxis {
match self {
Self::Row | Self::RowReverse => AbsoluteAxis::Vertical,
Self::Column | Self::ColumnReverse => AbsoluteAxis::Horizontal,
}
}
}
#[cfg(test)]
mod tests {
mod test_flex_direction {
use crate::style::*;
#[test]
fn flex_direction_is_row() {
assert!(FlexDirection::Row.is_row());
assert!(FlexDirection::RowReverse.is_row());
assert!(!FlexDirection::Column.is_row());
assert!(!FlexDirection::ColumnReverse.is_row());
}
#[test]
fn flex_direction_is_column() {
assert!(!FlexDirection::Row.is_column());
assert!(!FlexDirection::RowReverse.is_column());
assert!(FlexDirection::Column.is_column());
assert!(FlexDirection::ColumnReverse.is_column());
}
#[test]
fn flex_direction_is_reverse() {
assert!(!FlexDirection::Row.is_reverse());
assert!(FlexDirection::RowReverse.is_reverse());
assert!(!FlexDirection::Column.is_reverse());
assert!(FlexDirection::ColumnReverse.is_reverse());
}
}
}
+80
View File
@@ -0,0 +1,80 @@
//! Style types for float layout
/// Floats a box to the left or right.
/// This property only applies to children of a block layout
///
/// See <https://developer.mozilla.org/en-US/docs/Web/CSS/float>
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Float {
/// The box is floated to the left
Left,
/// The box is floated to the right
Right,
/// The box is not floated
#[default]
None,
}
#[cfg(feature = "parse")]
crate::util::parse::impl_parse_for_keyword_enum!(Float,
"left" => Left,
"right" => Right,
"none" => None,
);
/// Whether a box that is definitely floated is floated to the left
/// of to the right.
///
/// This type is only used in the low-level parts of the
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u8)]
pub enum FloatDirection {
/// The box is floated to the left
Left = 0,
/// The box is floated to the right
Right = 1,
}
impl Float {
/// Whether the box is floated
pub fn is_floated(self) -> bool {
matches!(self, Self::Left | Self::Right)
}
/// Converts [`Float`] into `Option<FloatDirection>`ca
pub fn float_direction(&self) -> Option<FloatDirection> {
match self {
Float::Left => Some(FloatDirection::Left),
Float::Right => Some(FloatDirection::Right),
Float::None => None,
}
}
}
/// Gives a box "clearance", which moves it below floated boxes which precede
/// it in the tree.
///
/// See <https://developer.mozilla.org/en-US/docs/Web/CSS/clear>
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Clear {
/// The box clears left-floated boxes
Left,
/// The box clears right-floated boxes
Right,
/// The box clears boxes floated in either direction
Both,
/// The box does not clear floated boxes
#[default]
None,
}
#[cfg(feature = "parse")]
crate::util::parse::impl_parse_for_keyword_enum!(Clear,
"left" => Left,
"right" => Right,
"both" => Both,
"none" => None,
);
+1607
View File
File diff suppressed because it is too large. Load diff
+1357
View File
File diff suppressed because it is too large. Load diff
+577
View File
@@ -0,0 +1,577 @@
//! Helper functions which it make it easier to create instances of types in the `style` and `geometry` modules.
use crate::{
geometry::{Line, Point, Rect, Size},
style::LengthPercentage,
};
#[cfg(feature = "grid")]
use crate::{
geometry::MinMax,
style::{
GridTemplateComponent, GridTemplateRepetition, MaxTrackSizingFunction, MinTrackSizingFunction, RepetitionCount,
TrackSizingFunction,
},
util::sys::Vec,
CheapCloneStr,
};
#[cfg(feature = "grid")]
use core::fmt::Debug;
/// Returns an auto-repeated track definition
#[cfg(feature = "grid")]
pub fn repeat<Input, S>(repetition_kind: Input, tracks: Vec<TrackSizingFunction>) -> GridTemplateComponent<S>
where
Input: TryInto<RepetitionCount>,
<Input as TryInto<RepetitionCount>>::Error: Debug,
S: CheapCloneStr,
{
GridTemplateComponent::Repeat(GridTemplateRepetition {
count: repetition_kind.try_into().unwrap(),
tracks,
line_names: Vec::new(),
})
}
#[cfg(feature = "grid")]
/// Returns a grid template containing `count` evenly sized tracks
pub fn evenly_sized_tracks<S: CheapCloneStr>(count: u16) -> Vec<GridTemplateComponent<S>> {
use crate::util::sys::new_vec_with_capacity;
let mut repeated_tracks = new_vec_with_capacity(1);
repeated_tracks.push(flex(1.0f32));
let mut tracks = new_vec_with_capacity(1);
tracks.push(repeat(count, repeated_tracks));
tracks
}
/// Specifies a grid line to place a grid item between in CSS Grid Line coordinates:
/// - Positive indices count upwards from the start (top or left) of the explicit grid
/// - Negative indices count downwards from the end (bottom or right) of the explicit grid
/// - ZERO IS INVALID index, and will be treated as a GridPlacement::Auto.
pub fn line<T: TaffyGridLine>(index: i16) -> T {
T::from_line_index(index)
}
/// Trait to abstract over grid line values
pub trait TaffyGridLine {
/// Converts an i16 into Self
fn from_line_index(index: i16) -> Self;
}
/// Returns a GridPlacement::Span
pub fn span<T: TaffyGridSpan>(span: u16) -> T {
T::from_span(span)
}
/// Trait to abstract over grid span values
pub trait TaffyGridSpan {
/// Converts an u16 into Self
fn from_span(span: u16) -> Self;
}
/// Returns a MinMax with min value of min and max value of max
#[cfg(feature = "grid")]
pub fn minmax<Output>(min: MinTrackSizingFunction, max: MaxTrackSizingFunction) -> Output
where
Output: From<MinMax<MinTrackSizingFunction, MaxTrackSizingFunction>>,
{
MinMax { min, max }.into()
}
/// Shorthand for minmax(0, Nfr). Probably what you want if you want exactly evenly sized tracks.
#[cfg(feature = "grid")]
pub fn flex<Input, Output>(flex_fraction: Input) -> Output
where
Input: Into<f32> + Copy,
Output: From<MinMax<MinTrackSizingFunction, MaxTrackSizingFunction>>,
{
MinMax { min: zero(), max: fr(flex_fraction.into()) }.into()
}
/// Returns the zero value for that type
pub const fn zero<T: TaffyZero>() -> T {
T::ZERO
}
/// Trait to abstract over zero values
pub trait TaffyZero {
/// The zero value for type implementing TaffyZero
const ZERO: Self;
}
impl TaffyZero for f32 {
const ZERO: f32 = 0.0;
}
impl<T: TaffyZero> TaffyZero for Option<T> {
const ZERO: Option<T> = Some(T::ZERO);
}
impl<T: TaffyZero> TaffyZero for Point<T> {
const ZERO: Point<T> = Point { x: T::ZERO, y: T::ZERO };
}
impl<T: TaffyZero> Point<T> {
/// Returns a Point where both the x and y values are the zero value of the contained type
/// (e.g. 0.0, Some(0.0), or Dimension::Length(0.0))
pub const fn zero() -> Self {
zero::<Self>()
}
}
impl<T: TaffyZero> TaffyZero for Line<T> {
const ZERO: Line<T> = Line { start: T::ZERO, end: T::ZERO };
}
impl<T: TaffyZero> Line<T> {
/// Returns a Line where both the start and end values are the zero value of the contained type
/// (e.g. 0.0, Some(0.0), or Dimension::Length(0.0))
pub const fn zero() -> Self {
zero::<Self>()
}
}
impl<T: TaffyZero> TaffyZero for Size<T> {
const ZERO: Size<T> = Size { width: T::ZERO, height: T::ZERO };
}
impl<T: TaffyZero> Size<T> {
/// Returns a Size where both the width and height values are the zero value of the contained type
/// (e.g. 0.0, Some(0.0), or Dimension::Length(0.0))
pub const fn zero() -> Self {
zero::<Self>()
}
}
impl<T: TaffyZero> TaffyZero for Rect<T> {
const ZERO: Rect<T> = Rect { left: T::ZERO, right: T::ZERO, top: T::ZERO, bottom: T::ZERO };
}
impl<T: TaffyZero> Rect<T> {
/// Returns a Rect where the left, right, top, and bottom values are all the zero value of the contained type
/// (e.g. 0.0, Some(0.0), or Dimension::Length(0.0))
pub const fn zero() -> Self {
zero::<Self>()
}
}
/// Returns the auto value for that type
pub const fn auto<T: TaffyAuto>() -> T {
T::AUTO
}
/// Trait to abstract over auto values
pub trait TaffyAuto {
/// The auto value for type implementing TaffyAuto
const AUTO: Self;
}
impl<T: TaffyAuto> TaffyAuto for Option<T> {
const AUTO: Option<T> = Some(T::AUTO);
}
impl<T: TaffyAuto> TaffyAuto for Point<T> {
const AUTO: Point<T> = Point { x: T::AUTO, y: T::AUTO };
}
impl<T: TaffyAuto> Point<T> {
/// Returns a Point where both the x and y values are the auto value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn auto() -> Self {
auto::<Self>()
}
}
impl<T: TaffyAuto> TaffyAuto for Line<T> {
const AUTO: Line<T> = Line { start: T::AUTO, end: T::AUTO };
}
impl<T: TaffyAuto> Line<T> {
/// Returns a Line where both the start and end values are the auto value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn auto() -> Self {
auto::<Self>()
}
}
impl<T: TaffyAuto> TaffyAuto for Size<T> {
const AUTO: Size<T> = Size { width: T::AUTO, height: T::AUTO };
}
impl<T: TaffyAuto> Size<T> {
/// Returns a Size where both the width and height values are the auto value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn auto() -> Self {
auto::<Self>()
}
}
impl<T: TaffyAuto> TaffyAuto for Rect<T> {
const AUTO: Rect<T> = Rect { left: T::AUTO, right: T::AUTO, top: T::AUTO, bottom: T::AUTO };
}
impl<T: TaffyAuto> Rect<T> {
/// Returns a Rect where the left, right, top, and bottom values are all the auto value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn auto() -> Self {
auto::<Self>()
}
}
/// Returns the auto value for that type
pub const fn min_content<T: TaffyMinContent>() -> T {
T::MIN_CONTENT
}
/// Trait to abstract over min_content values
pub trait TaffyMinContent {
/// The min_content value for type implementing TaffyZero
const MIN_CONTENT: Self;
}
impl<T: TaffyMinContent> TaffyMinContent for Option<T> {
const MIN_CONTENT: Option<T> = Some(T::MIN_CONTENT);
}
impl<T: TaffyMinContent> TaffyMinContent for Point<T> {
const MIN_CONTENT: Point<T> = Point { x: T::MIN_CONTENT, y: T::MIN_CONTENT };
}
impl<T: TaffyMinContent> Point<T> {
/// Returns a Point where both the x and y values are the min_content value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn min_content() -> Self {
min_content::<Self>()
}
}
impl<T: TaffyMinContent> TaffyMinContent for Line<T> {
const MIN_CONTENT: Line<T> = Line { start: T::MIN_CONTENT, end: T::MIN_CONTENT };
}
impl<T: TaffyMinContent> Line<T> {
/// Returns a Line where both the start and end values are the min_content value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn min_content() -> Self {
min_content::<Self>()
}
}
impl<T: TaffyMinContent> TaffyMinContent for Size<T> {
const MIN_CONTENT: Size<T> = Size { width: T::MIN_CONTENT, height: T::MIN_CONTENT };
}
impl<T: TaffyMinContent> Size<T> {
/// Returns a Size where both the width and height values are the min_content value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn min_content() -> Self {
min_content::<Self>()
}
}
impl<T: TaffyMinContent> TaffyMinContent for Rect<T> {
const MIN_CONTENT: Rect<T> =
Rect { left: T::MIN_CONTENT, right: T::MIN_CONTENT, top: T::MIN_CONTENT, bottom: T::MIN_CONTENT };
}
impl<T: TaffyMinContent> Rect<T> {
/// Returns a Rect where the left, right, top, and bottom values are all the min_content value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn min_content() -> Self {
min_content::<Self>()
}
}
/// Returns the auto value for that type
pub const fn max_content<T: TaffyMaxContent>() -> T {
T::MAX_CONTENT
}
/// Trait to abstract over max_content values
pub trait TaffyMaxContent {
/// The max_content value for type implementing TaffyZero
const MAX_CONTENT: Self;
}
impl<T: TaffyMaxContent> TaffyMaxContent for Option<T> {
const MAX_CONTENT: Option<T> = Some(T::MAX_CONTENT);
}
impl<T: TaffyMaxContent> TaffyMaxContent for Point<T> {
const MAX_CONTENT: Point<T> = Point { x: T::MAX_CONTENT, y: T::MAX_CONTENT };
}
impl<T: TaffyMaxContent> Point<T> {
/// Returns a Point where both the x and y values are the max_content value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn max_content() -> Self {
max_content::<Self>()
}
}
impl<T: TaffyMaxContent> TaffyMaxContent for Line<T> {
const MAX_CONTENT: Line<T> = Line { start: T::MAX_CONTENT, end: T::MAX_CONTENT };
}
impl<T: TaffyMaxContent> Line<T> {
/// Returns a Line where both the start and end values are the max_content value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn max_content() -> Self {
max_content::<Self>()
}
}
impl<T: TaffyMaxContent> TaffyMaxContent for Size<T> {
const MAX_CONTENT: Size<T> = Size { width: T::MAX_CONTENT, height: T::MAX_CONTENT };
}
impl<T: TaffyMaxContent> Size<T> {
/// Returns a Size where both the width and height values are the max_content value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn max_content() -> Self {
max_content::<Self>()
}
}
impl<T: TaffyMaxContent> TaffyMaxContent for Rect<T> {
const MAX_CONTENT: Rect<T> =
Rect { left: T::MAX_CONTENT, right: T::MAX_CONTENT, top: T::MAX_CONTENT, bottom: T::MAX_CONTENT };
}
impl<T: TaffyMaxContent> Rect<T> {
/// Returns a Rect where the left, right, top, and bottom values are all the max_content value of the contained type
/// (e.g. Dimension::Auto or LengthPercentageAuto::Auto)
pub const fn max_content() -> Self {
max_content::<Self>()
}
}
/// Returns a value of the inferred type which represent a `fit-content(…)` value
/// with the given argument.
pub fn fit_content<T: TaffyFitContent>(argument: LengthPercentage) -> T {
T::fit_content(argument)
}
/// Trait to create `fit-content(…)` values from plain numbers
pub trait TaffyFitContent {
/// Converts a LengthPercentage into Self
fn fit_content(argument: LengthPercentage) -> Self;
}
impl<T: TaffyFitContent> TaffyFitContent for Point<T> {
fn fit_content(argument: LengthPercentage) -> Self {
Point { x: T::fit_content(argument), y: T::fit_content(argument) }
}
}
impl<T: TaffyFitContent> Point<T> {
/// Returns a Point with x and y set to the same `fit-content(…)` value
/// with the given argument.
pub fn fit_content(argument: LengthPercentage) -> Self {
fit_content(argument)
}
}
impl<T: TaffyFitContent> TaffyFitContent for Line<T> {
fn fit_content(argument: LengthPercentage) -> Self {
Line { start: T::fit_content(argument), end: T::fit_content(argument) }
}
}
impl<T: TaffyFitContent> Line<T> {
/// Returns a Line with start and end set to the same `fit-content(…)` value
/// with the given argument.
pub fn fit_content(argument: LengthPercentage) -> Self {
fit_content(argument)
}
}
impl<T: TaffyFitContent> TaffyFitContent for Size<T> {
fn fit_content(argument: LengthPercentage) -> Self {
Size { width: T::fit_content(argument), height: T::fit_content(argument) }
}
}
impl<T: TaffyFitContent> Size<T> {
/// Returns a Size where with width and height set to the same `fit-content(…)` value
/// with the given argument.
pub fn fit_content(argument: LengthPercentage) -> Self {
fit_content(argument)
}
}
impl<T: TaffyFitContent> TaffyFitContent for Rect<T> {
fn fit_content(argument: LengthPercentage) -> Self {
Rect {
left: T::fit_content(argument),
right: T::fit_content(argument),
top: T::fit_content(argument),
bottom: T::fit_content(argument),
}
}
}
impl<T: TaffyFitContent> Rect<T> {
/// Returns a Rect where the left, right, top and bottom values are all constant fit_content value of the contained type
/// (e.g. 2.1, Some(2.1), or Dimension::Length(2.1))
pub fn fit_content(argument: LengthPercentage) -> Self {
fit_content(argument)
}
}
/// Returns a value of the inferred type which represent an absolute length
pub fn length<Input: Into<f32> + Copy, T: FromLength>(value: Input) -> T {
T::from_length(value)
}
/// Trait to create absolute length values from plain numbers
pub trait FromLength {
/// Converts into an `Into<f32>` into Self
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self;
}
impl FromLength for f32 {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
value.into()
}
}
impl FromLength for Option<f32> {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
Some(value.into())
}
}
impl<T: FromLength> FromLength for Point<T> {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
Point { x: T::from_length(value.into()), y: T::from_length(value.into()) }
}
}
impl<T: FromLength> Point<T> {
/// Returns a Point where x and y values are the same given absolute length
pub fn length<Input: Into<f32> + Copy>(value: Input) -> Self {
length::<Input, Self>(value)
}
}
impl<T: FromLength> FromLength for Line<T> {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
Line { start: T::from_length(value.into()), end: T::from_length(value.into()) }
}
}
impl<T: FromLength> Line<T> {
/// Returns a Line where both the start and end values are the same given absolute length
pub fn length<Input: Into<f32> + Copy>(value: Input) -> Self {
length::<Input, Self>(value)
}
}
impl<T: FromLength> FromLength for Size<T> {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
Size { width: T::from_length(value.into()), height: T::from_length(value.into()) }
}
}
impl<T: FromLength> Size<T> {
/// Returns a Size where both the width and height values the same given absolute length
pub fn length<Input: Into<f32> + Copy>(value: Input) -> Self {
length::<Input, Self>(value)
}
}
impl<T: FromLength> FromLength for Rect<T> {
fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
Rect {
left: T::from_length(value.into()),
right: T::from_length(value.into()),
top: T::from_length(value.into()),
bottom: T::from_length(value.into()),
}
}
}
impl<T: FromLength> Rect<T> {
/// Returns a Rect where the left, right, top and bottom values are all the same given absolute length
pub fn length<Input: Into<f32> + Copy>(value: Input) -> Self {
length::<Input, Self>(value)
}
}
/// Returns a value of the inferred type which represent a percentage
pub fn percent<Input: Into<f32> + Copy, T: FromPercent>(percent: Input) -> T {
T::from_percent(percent)
}
/// Trait to create constant percent values from plain numbers
pub trait FromPercent {
/// Converts into an `Into<f32>` into Self
fn from_percent<Input: Into<f32> + Copy>(percent: Input) -> Self;
}
impl FromPercent for f32 {
fn from_percent<Input: Into<f32> + Copy>(percent: Input) -> Self {
percent.into()
}
}
impl FromPercent for Option<f32> {
fn from_percent<Input: Into<f32> + Copy>(percent: Input) -> Self {
Some(percent.into())
}
}
impl<T: FromPercent> FromPercent for Point<T> {
fn from_percent<Input: Into<f32> + Copy>(percent: Input) -> Self {
Point { x: T::from_percent(percent.into()), y: T::from_percent(percent.into()) }
}
}
impl<T: FromPercent> Point<T> {
/// Returns a Point where both the x and y values are the constant percent value of the contained type
/// (e.g. 2.1, Some(2.1), or Dimension::Length(2.1))
pub fn percent<Input: Into<f32> + Copy>(percent_value: Input) -> Self {
percent::<Input, Self>(percent_value)
}
}
impl<T: FromPercent> FromPercent for Line<T> {
fn from_percent<Input: Into<f32> + Copy>(percent: Input) -> Self {
Line { start: T::from_percent(percent.into()), end: T::from_percent(percent.into()) }
}
}
impl<T: FromPercent> Line<T> {
/// Returns a Line where both the start and end values are the constant percent value of the contained type
/// (e.g. 2.1, Some(2.1), or Dimension::Length(2.1))
pub fn percent<Input: Into<f32> + Copy>(percent_value: Input) -> Self {
percent::<Input, Self>(percent_value)
}
}
impl<T: FromPercent> FromPercent for Size<T> {
fn from_percent<Input: Into<f32> + Copy>(percent: Input) -> Self {
Size { width: T::from_percent(percent.into()), height: T::from_percent(percent.into()) }
}
}
impl<T: FromPercent> Size<T> {
/// Returns a Size where both the width and height values are the constant percent value of the contained type
/// (e.g. 2.1, Some(2.1), or Dimension::Length(2.1))
pub fn percent<Input: Into<f32> + Copy>(percent_value: Input) -> Self {
percent::<Input, Self>(percent_value)
}
}
impl<T: FromPercent> FromPercent for Rect<T> {
fn from_percent<Input: Into<f32> + Copy>(percent: Input) -> Self {
Rect {
left: T::from_percent(percent.into()),
right: T::from_percent(percent.into()),
top: T::from_percent(percent.into()),
bottom: T::from_percent(percent.into()),
}
}
}
impl<T: FromPercent> Rect<T> {
/// Returns a Rect where the left, right, top and bottom values are all constant percent value of the contained type
/// (e.g. 2.1, Some(2.1), or Dimension::Length(2.1))
pub fn percent<Input: Into<f32> + Copy>(percent_value: Input) -> Self {
percent::<Input, Self>(percent_value)
}
}
/// Create a `Fraction` track sizing function (`fr` in CSS)
#[cfg(feature = "grid")]
pub fn fr<Input: Into<f32> + Copy, T: FromFr>(flex: Input) -> T {
T::from_fr(flex)
}
/// Trait to create constant percent values from plain numbers
pub trait FromFr {
/// Converts into an `Into<f32>` into Self
fn from_fr<Input: Into<f32> + Copy>(flex: Input) -> Self;
}
#[cfg(feature = "grid")]
#[cfg(test)]
mod repeat_fn_tests {
type S = crate::sys::DefaultCheapStr;
use super::repeat;
use crate::{
style::{GridTemplateComponent, RepetitionCount, TrackSizingFunction},
GridTemplateRepetition,
};
const TEST_VEC: Vec<TrackSizingFunction> = Vec::new();
#[test]
fn test_repeat_u16() {
assert_eq!(
repeat::<_, S>(123, TEST_VEC),
GridTemplateComponent::Repeat(GridTemplateRepetition {
count: RepetitionCount::Count(123),
tracks: TEST_VEC,
line_names: Vec::new(),
})
);
}
#[test]
fn test_repeat_auto_fit_str() {
assert_eq!(
repeat::<_, S>("auto-fit", TEST_VEC),
GridTemplateComponent::Repeat(GridTemplateRepetition {
count: RepetitionCount::AutoFit,
tracks: TEST_VEC,
line_names: Vec::new(),
})
);
}
#[test]
fn test_repeat_auto_fill_str() {
assert_eq!(
repeat::<_, S>("auto-fill", TEST_VEC),
GridTemplateComponent::Repeat(GridTemplateRepetition {
count: RepetitionCount::AutoFill,
tracks: TEST_VEC,
line_names: Vec::new(),
})
);
}
}
+174
View File
@@ -0,0 +1,174 @@
#![allow(dead_code)]
use taffy::{AvailableSpace, NodeId, Size, Style};
/// A shared measure function for tests which means that tests compiled with separate crates
/// and using different styles of measure function. This saves on compile time when running tests.
#[derive(Debug, Copy, Clone)]
pub struct TestNodeContext {
/// How many times the measure function has been called
pub count: usize,
/// The measurement data to compute the intrinsic size from
pub measure_data: TestMeasureData,
}
impl TestNodeContext {
/// Create a new `TestNodeContext` from `TestMeasureData`
pub const fn new(measure_data: TestMeasureData) -> Self {
Self { count: 0, measure_data }
}
/// Create a `TestNodeContext` for a zero-sized node
pub const fn zero() -> Self {
Self::new(TestMeasureData::Zero)
}
/// Create a `TestNodeContext` for a fixed-sized node
pub const fn fixed(size: Size<f32>) -> Self {
Self::new(TestMeasureData::Fixed(size))
}
/// Create a `TestNodeContext` for a node with a width and aspect-ratio
pub const fn aspect_ratio(width: f32, height_ratio: f32) -> Self {
let data = AspectRatioMeasureData { width, height_ratio };
Self::new(TestMeasureData::AspectRatio(data))
}
/// Create a `TestNodeContext` for a node with text using the Ahem font
pub const fn ahem_text(text_content: &'static str, writing_mode: WritingMode) -> Self {
let data = AhemTextMeasureData { text_content, writing_mode };
Self::new(TestMeasureData::AhemText(data))
}
}
/// The measurement data for the node
#[derive(Debug, Copy, Clone)]
pub enum TestMeasureData {
/// A zero-sized node
Zero,
/// A node with a fixed size
Fixed(Size<f32>),
/// A node with a fixed size
AspectRatio(AspectRatioMeasureData),
/// A node with text using the Ahem font
AhemText(AhemTextMeasureData),
}
/// A measure function for tests that works with `TestNodeContext`
pub fn test_measure_function(
known_dimensions: Size<Option<f32>>,
available_space: Size<AvailableSpace>,
_node_id: NodeId,
context: Option<&mut TestNodeContext>,
_style: &Style,
) -> Size<f32> {
if let Size { width: Some(width), height: Some(height) } = known_dimensions {
return Size { width, height };
}
let Some(context) = context else { return known_dimensions.map(|d| d.unwrap_or(0.0)) };
// Increment count
context.count += 1;
let compute_size = match &context.measure_data {
TestMeasureData::Zero => Size::ZERO,
TestMeasureData::Fixed(size) => *size,
TestMeasureData::AspectRatio(data) => data.measure(known_dimensions),
TestMeasureData::AhemText(data) => data.measure(known_dimensions, available_space),
};
Size {
width: known_dimensions.width.unwrap_or(compute_size.width),
height: known_dimensions.height.unwrap_or(compute_size.height),
}
}
/// Measure data for nodes that returns results based on an intrinsic aspect ratio
#[derive(Debug, Copy, Clone)]
pub struct AspectRatioMeasureData {
width: f32,
height_ratio: f32,
}
impl AspectRatioMeasureData {
fn measure(&self, known_dimensions: Size<Option<f32>>) -> Size<f32> {
let width = known_dimensions.width.unwrap_or(self.width);
let height = known_dimensions.height.unwrap_or(width * self.height_ratio);
Size { width, height }
}
}
/// Whether text is horizontal or vertical
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WritingMode {
/// Horizontal text
Horizontal,
/// Vertical text
Vertical,
}
/// Measure data for nodes contain text using the Ahem testing font
#[derive(Debug, Clone, Copy)]
pub struct AhemTextMeasureData {
/// The text string
pub text_content: &'static str,
/// The writing mode
pub writing_mode: WritingMode,
}
impl AhemTextMeasureData {
fn measure(
&self,
known_dimensions: taffy::Size<Option<f32>>,
available_space: taffy::Size<taffy::AvailableSpace>,
) -> taffy::Size<f32> {
use taffy::prelude::*;
use taffy::AbsoluteAxis;
const ZWS: char = '\u{200B}';
const H_WIDTH: f32 = 10.0;
const H_HEIGHT: f32 = 10.0;
let inline_axis = match self.writing_mode {
WritingMode::Horizontal => AbsoluteAxis::Horizontal,
WritingMode::Vertical => AbsoluteAxis::Vertical,
};
let block_axis = inline_axis.other_axis();
let lines: Vec<&str> = self.text_content.split(ZWS).collect();
if lines.is_empty() {
return Size::ZERO;
}
let min_line_length: usize = lines.iter().map(|line| line.len()).max().unwrap_or(0);
let max_line_length: usize = lines.iter().map(|line| line.len()).sum();
let inline_size = known_dimensions
.get_abs(inline_axis)
.unwrap_or_else(|| match available_space.get_abs(inline_axis) {
AvailableSpace::MinContent => min_line_length as f32 * H_WIDTH,
AvailableSpace::MaxContent => max_line_length as f32 * H_WIDTH,
AvailableSpace::Definite(inline_size) => inline_size.min(max_line_length as f32 * H_WIDTH),
})
.max(min_line_length as f32 * H_WIDTH);
let block_size = known_dimensions.get_abs(block_axis).unwrap_or_else(|| {
let inline_line_length = (inline_size / H_WIDTH).floor() as usize;
let mut line_count = 1;
let mut current_line_length = 0;
for line in &lines {
if current_line_length + line.len() > inline_line_length {
if current_line_length > 0 {
line_count += 1
};
current_line_length = line.len();
} else {
current_line_length += line.len();
};
}
(line_count as f32) * H_HEIGHT
});
match self.writing_mode {
WritingMode::Horizontal => Size { width: inline_size, height: block_size },
WritingMode::Vertical => Size { width: block_size, height: inline_size },
}
}
}
+284
View File
@@ -0,0 +1,284 @@
//! A cache for storing the results of layout computation
#![allow(clippy::unusual_byte_groupings)]
use crate::geometry::Size;
use crate::style::AvailableSpace;
use crate::tree::{LayoutInput, LayoutOutput, RunMode};
use crate::RequestedAxis;
/// The number of cache entries for each node in the tree
const CACHE_SIZE: usize = 9;
// Manually written-out results of float to u32 bit casts because
// `f32::to_bits` is not yet const at our MSRV.
/// `f32::INFINITY` as a u32
const INFINITY_BITS: u32 = 0b_0_11111111_00000000000000000000000_u32;
/// `f32::NEG_INFINITY` as a u32
const NEG_INFINITY_BITS: u32 = 0b_1_11111111_00000000000000000000000_u32;
// The `CacheKey` encodes two f32s as a u64. We know that the f32s will always be
// non-negative, so we pack two extra bits encoding the `RequestedAxis` into the
// sign bits of the f32s. These constants help to encode and decode those bits.
/// The sign bit of the first f32
const SIGN_BIT_1: u64 = 1u64 << 63;
/// The sign bit of the second f32
const SIGN_BIT_2: u64 = 1u64 << 31;
/// Mask of both sign bits (used to compute NON_SIGN_BITS_MASK)
const BOTH_SIGN_BITS_MASK: u64 = SIGN_BIT_1 | SIGN_BIT_2;
/// Mask of excluding the sign bits (used when setting/getting the size excluding the packed bits)
const NON_SIGN_BITS_MASK: u64 = !BOTH_SIGN_BITS_MASK;
/// Mask which includes only the bits which encode the x-axis value that we can use to ignore the
/// y-axis value when comparing a cache key.
const X_AXIS_VALUE_MASK: u64 = (u32::MAX as u64) << 32;
/// Pack `Option<f32>` into `u32`
#[inline(always)]
fn option_cache_key(input: Option<f32>) -> u32 {
match input {
Some(value) => value.to_bits(),
None => INFINITY_BITS,
}
}
/// Pack `Size<Option<f32>>` into `u64`
#[inline(always)]
fn size_option_cache_key(input: Size<Option<f32>>) -> u64 {
(option_cache_key(input.width) as u64) << 32 | option_cache_key(input.height) as u64
}
/// Pack `AvailableSpace` into `u32`
#[inline(always)]
fn available_space_cache_key(input: AvailableSpace) -> u32 {
match input {
AvailableSpace::Definite(value) => (-value).to_bits(),
AvailableSpace::MinContent => NEG_INFINITY_BITS,
AvailableSpace::MaxContent => INFINITY_BITS,
}
}
/// Pack `Size<AvailableSpace>` into `u64`
#[inline(always)]
#[allow(dead_code)]
fn size_available_space_cache_key(input: Size<AvailableSpace>) -> u64 {
(available_space_cache_key(input.width) as u64) << 32 | available_space_cache_key(input.height) as u64
}
/// Encodes combination of a `known_dimension` (Option<f32>) and `AvailableSpace` in
/// a single dimension into a cache key in a single dimension.
#[inline(always)]
fn mixed_cache_key(kd: Option<f32>, avs: AvailableSpace) -> u32 {
kd.map(|kd| kd.to_bits()).unwrap_or_else(|| available_space_cache_key(avs))
}
/// Encodes combination of a `known_dimension` (Option<f32>) and `AvailableSpace` in
/// two dimensions into a cache key in a single dimension.
#[inline(always)]
fn size_mixed_cache_key(kd: Size<Option<f32>>, avs: Size<AvailableSpace>) -> u64 {
(mixed_cache_key(kd.width, avs.width) as u64) << 32 | mixed_cache_key(kd.height, avs.height) as u64
}
/// Space-optimised cache key that packs bits into as small a size as possible
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
struct CacheKey {
/// The initial cached size of the node itself
kd_available_space: u64,
/// The initial cached size of the parent's node
parent_size: u64,
}
impl CacheKey {
#[inline(always)]
#[allow(dead_code)]
/// Return the parent size with the extra bits that encode the requested axis masked out
fn parent_size(&self) -> u64 {
self.parent_size & NON_SIGN_BITS_MASK
}
/// Return the parent size with the extra bits that encode the requested axis masked out
/// And the y-axis value masked out
fn x_axis_parent_size(&self) -> u64 {
self.parent_size & (X_AXIS_VALUE_MASK & NON_SIGN_BITS_MASK)
}
}
impl From<&LayoutInput> for CacheKey {
fn from(input: &LayoutInput) -> Self {
// Pack axis enum into spare bits in the known_dimensions and available_space values
let extra_bits = match input.axis {
RequestedAxis::Horizontal => SIGN_BIT_1,
RequestedAxis::Vertical => SIGN_BIT_2,
RequestedAxis::Both => SIGN_BIT_1 | SIGN_BIT_2,
};
Self {
kd_available_space: size_mixed_cache_key(input.known_dimensions, input.available_space),
parent_size: (size_option_cache_key(input.parent_size) & NON_SIGN_BITS_MASK) | extra_bits,
}
}
}
/// Cached intermediate layout results
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub(crate) struct CacheEntry<T> {
/// The key for the cache entry
key: CacheKey,
/// The cached size and baselines of the item
content: T,
}
/// A cache for caching the results of a sizing a Grid Item or Flexbox Item
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Cache {
/// The cache entry for the node's final layout
final_layout_entry: Option<CacheEntry<LayoutOutput>>,
/// The cache entries for the node's preliminary size measurements
measure_entries: [Option<CacheEntry<Size<f32>>>; CACHE_SIZE],
/// Tracks if all cache entries are empty
is_empty: bool,
}
impl Default for Cache {
fn default() -> Self {
Self::new()
}
}
impl Cache {
/// Create a new empty cache
pub const fn new() -> Self {
Self { final_layout_entry: None, measure_entries: [None; CACHE_SIZE], is_empty: true }
}
/// Return the cache slot to cache the current computed result in
///
/// ## Caching Strategy
///
/// We need multiple cache slots, because a node's size is often queried by it's parent multiple times in the course of the layout
/// process, and we don't want later results to clobber earlier ones.
///
/// The two variables that we care about when determining cache slot are:
///
/// - How many "known_dimensions" are set. In the worst case, a node may be called first with neither dimension known, then with one
/// dimension known (either width of height - which doesn't matter for our purposes here), and then with both dimensions known.
/// - Whether unknown dimensions are being sized under a min-content or a max-content available space constraint (definite available space
/// shares a cache slot with max-content because a node will generally be sized under one or the other but not both).
///
/// ## Cache slots:
///
/// - Slot 0: Both known_dimensions were set
/// - Slots 1-4: 1 of 2 known_dimensions were set and:
/// - Slot 1: width but not height known_dimension was set and the other dimension was either a MaxContent or Definite available space constraintraint
/// - Slot 2: width but not height known_dimension was set and the other dimension was a MinContent constraint
/// - Slot 3: height but not width known_dimension was set and the other dimension was either a MaxContent or Definite available space constraintable space constraint
/// - Slot 4: height but not width known_dimension was set and the other dimension was a MinContent constraint
/// - Slots 5-8: Neither known_dimensions were set and:
/// - Slot 5: x-axis available space is MaxContent or Definite and y-axis available space is MaxContent or Definite
/// - Slot 6: x-axis available space is MaxContent or Definite and y-axis available space is MinContent
/// - Slot 7: x-axis available space is MinContent and y-axis available space is MaxContent or Definite
/// - Slot 8: x-axis available space is MinContent and y-axis available space is MinContent
#[inline]
fn compute_cache_slot(known_dimensions: Size<Option<f32>>, available_space: Size<AvailableSpace>) -> usize {
use AvailableSpace::{Definite, MaxContent, MinContent};
let has_known_width = known_dimensions.width.is_some();
let has_known_height = known_dimensions.height.is_some();
// Slot 0: Both known_dimensions were set
if has_known_width && has_known_height {
return 0;
}
// Slot 1: width but not height known_dimension was set and the other dimension was either a MaxContent or Definite available space constraint
// Slot 2: width but not height known_dimension was set and the other dimension was a MinContent constraint
if has_known_width && !has_known_height {
return 1 + (available_space.height == MinContent) as usize;
}
// Slot 3: height but not width known_dimension was set and the other dimension was either a MaxContent or Definite available space constraint
// Slot 4: height but not width known_dimension was set and the other dimension was a MinContent constraint
if has_known_height && !has_known_width {
return 3 + (available_space.width == MinContent) as usize;
}
// Slots 5-8: Neither known_dimensions were set and:
match (available_space.width, available_space.height) {
// Slot 5: x-axis available space is MaxContent or Definite and y-axis available space is MaxContent or Definite
(MaxContent | Definite(_), MaxContent | Definite(_)) => 5,
// Slot 6: x-axis available space is MaxContent or Definite and y-axis available space is MinContent
(MaxContent | Definite(_), MinContent) => 6,
// Slot 7: x-axis available space is MinContent and y-axis available space is MaxContent or Definite
(MinContent, MaxContent | Definite(_)) => 7,
// Slot 8: x-axis available space is MinContent and y-axis available space is MinContent
(MinContent, MinContent) => 8,
}
}
/// Try to retrieve a cached result from the cache
#[inline]
pub fn get(&self, input: &LayoutInput) -> Option<LayoutOutput> {
let key = CacheKey::from(input);
match input.run_mode {
RunMode::PerformLayout => self.final_layout_entry.filter(|entry| entry.key == key).map(|e| e.content),
RunMode::ComputeSize => {
for entry in self.measure_entries.iter().flatten() {
if entry.key.kd_available_space == key.kd_available_space
&& (entry.key.x_axis_parent_size() == key.x_axis_parent_size())
{
return Some(LayoutOutput::from_outer_size(entry.content));
}
}
None
}
RunMode::PerformHiddenLayout => None,
}
}
/// Store a computed size in the cache
pub fn store(&mut self, input: &LayoutInput, layout_output: LayoutOutput) {
let key = CacheKey::from(input);
match input.run_mode {
RunMode::PerformLayout => {
self.is_empty = false;
self.final_layout_entry = Some(CacheEntry { key, content: layout_output })
}
RunMode::ComputeSize => {
self.is_empty = false;
let cache_slot = Self::compute_cache_slot(input.known_dimensions, input.available_space);
self.measure_entries[cache_slot] = Some(CacheEntry { key, content: layout_output.size });
}
RunMode::PerformHiddenLayout => {}
}
}
/// Clear all cache entries and reports clear operation outcome ([`ClearState`])
pub fn clear(&mut self) -> ClearState {
if self.is_empty {
return ClearState::AlreadyEmpty;
}
self.is_empty = true;
self.final_layout_entry = None;
self.measure_entries = [None; CACHE_SIZE];
ClearState::Cleared
}
/// Returns true if all cache entries are None, else false
pub fn is_empty(&self) -> bool {
self.final_layout_entry.is_none() && !self.measure_entries.iter().any(|entry| entry.is_some())
}
}
/// Clear operation outcome. See [`Cache::clear`]
pub enum ClearState {
/// Cleared some values
Cleared,
/// Everything was already cleared
AlreadyEmpty,
}
+357
View File
@@ -0,0 +1,357 @@
//! Final data structures that represent the high-level UI layout
use crate::geometry::{AbsoluteAxis, Line, Point, Rect, Size};
use crate::style::AvailableSpace;
use crate::style_helpers::TaffyMaxContent;
use crate::util::sys::{f32_max, f32_min};
/// Whether we are performing a full layout, or we merely need to size the node
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum RunMode {
/// A full layout for this node and all children should be computed
PerformLayout,
/// The layout algorithm should be executed such that an accurate container size for the node can be determined.
/// Layout steps that aren't necessary for determining the container size of the current node can be skipped.
ComputeSize,
/// This node should have a null layout set as it has been hidden (i.e. using `Display::None`)
PerformHiddenLayout,
}
/// Whether styles should be taken into account when computing size
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum SizingMode {
/// Only content contributions should be taken into account
ContentSize,
/// Inherent size styles should be taken into account in addition to content contributions
InherentSize,
}
/// A set of margins that are available for collapsing with for block layout's margin collapsing
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct CollapsibleMarginSet {
/// The largest positive margin
positive: f32,
/// The smallest negative margin (with largest absolute value)
negative: f32,
}
impl CollapsibleMarginSet {
/// A default margin set with no collapsible margins
pub const ZERO: Self = Self { positive: 0.0, negative: 0.0 };
/// Create a set from a single margin
pub fn from_margin(margin: f32) -> Self {
if margin >= 0.0 {
Self { positive: margin, negative: 0.0 }
} else {
Self { positive: 0.0, negative: margin }
}
}
/// Collapse a single margin with this set
pub fn collapse_with_margin(mut self, margin: f32) -> Self {
if margin >= 0.0 {
self.positive = f32_max(self.positive, margin);
} else {
self.negative = f32_min(self.negative, margin);
}
self
}
/// Collapse another margin set with this set
pub fn collapse_with_set(mut self, other: CollapsibleMarginSet) -> Self {
self.positive = f32_max(self.positive, other.positive);
self.negative = f32_min(self.negative, other.negative);
self
}
/// Resolve the resultant margin from this set once all collapsible margins
/// have been collapsed into it
pub fn resolve(&self) -> f32 {
self.positive + self.negative
}
}
/// An axis that layout algorithms can be requested to compute a size for
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum RequestedAxis {
/// The horizontal axis
Horizontal,
/// The vertical axis
Vertical,
/// Both axes
Both,
}
impl From<AbsoluteAxis> for RequestedAxis {
fn from(value: AbsoluteAxis) -> Self {
match value {
AbsoluteAxis::Horizontal => RequestedAxis::Horizontal,
AbsoluteAxis::Vertical => RequestedAxis::Vertical,
}
}
}
impl TryFrom<RequestedAxis> for AbsoluteAxis {
type Error = ();
fn try_from(value: RequestedAxis) -> Result<Self, Self::Error> {
match value {
RequestedAxis::Horizontal => Ok(AbsoluteAxis::Horizontal),
RequestedAxis::Vertical => Ok(AbsoluteAxis::Vertical),
RequestedAxis::Both => Err(()),
}
}
}
/// A struct containing the inputs constraints/hints for laying out a node, which are passed in by the parent
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct LayoutInput {
/// Whether we only need to know the Node's size, or whether we need to perform a full layout
pub run_mode: RunMode,
/// Whether a Node's style sizes should be taken into account or ignored
pub sizing_mode: SizingMode,
/// Which axis we need the size of
pub axis: RequestedAxis,
/// Known dimensions represent dimensions (width/height) which should be taken as fixed when performing layout.
/// For example, if known_dimensions.width is set to Some(WIDTH) then this means something like:
///
/// "What would the height of this node be, assuming the width is WIDTH"
///
/// Layout functions will be called with both known_dimensions set for final layout. Where the meaning is:
///
/// "The exact size of this node is WIDTHxHEIGHT. Please lay out your children"
///
pub known_dimensions: Size<Option<f32>>,
/// Parent size dimensions are intended to be used for percentage resolution.
pub parent_size: Size<Option<f32>>,
/// Available space represents an amount of space to layout into, and is used as a soft constraint
/// for the purpose of wrapping.
pub available_space: Size<AvailableSpace>,
/// Specific to CSS Block layout. Used for correctly computing margin collapsing. You probably want to set this to `Line::FALSE`.
pub vertical_margins_are_collapsible: Line<bool>,
}
impl LayoutInput {
/// A LayoutInput that can be used to request hidden layout
pub const HIDDEN: LayoutInput = LayoutInput {
// The important property for hidden layout
run_mode: RunMode::PerformHiddenLayout,
// The rest will be ignored
known_dimensions: Size::NONE,
parent_size: Size::NONE,
available_space: Size::MAX_CONTENT,
sizing_mode: SizingMode::InherentSize,
axis: RequestedAxis::Both,
vertical_margins_are_collapsible: Line::FALSE,
};
}
/// A struct containing the result of laying a single node, which is returned up to the parent node
///
/// A baseline is the line on which text sits. Your node likely has a baseline if it is a text node, or contains
/// children that may be text nodes. See <https://www.w3.org/TR/css-writing-modes-3/#intro-baselines> for details.
/// If your node does not have a baseline (or you are unsure how to compute it), then simply return `Point::NONE`
/// for the first_baselines field
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct LayoutOutput {
/// The size of the node
pub size: Size<f32>,
#[cfg(feature = "content_size")]
/// The size of the content within the node
pub content_size: Size<f32>,
/// The first baseline of the node in each dimension, if any
pub first_baselines: Point<Option<f32>>,
/// Top margin that can be collapsed with. This is used for CSS block layout and can be set to
/// `CollapsibleMarginSet::ZERO` for other layout modes that don't support margin collapsing
pub top_margin: CollapsibleMarginSet,
/// Bottom margin that can be collapsed with. This is used for CSS block layout and can be set to
/// `CollapsibleMarginSet::ZERO` for other layout modes that don't support margin collapsing
pub bottom_margin: CollapsibleMarginSet,
/// Whether margins can be collapsed through this node. This is used for CSS block layout and can
/// be set to `false` for other layout modes that don't support margin collapsing
pub margins_can_collapse_through: bool,
}
impl LayoutOutput {
/// An all-zero `LayoutOutput` for hidden nodes
pub const HIDDEN: Self = Self {
size: Size::ZERO,
#[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,
};
/// A blank layout output
pub const DEFAULT: Self = Self::HIDDEN;
/// Constructor to create a `LayoutOutput` from just the size and baselines
pub fn from_sizes_and_baselines(
size: Size<f32>,
#[cfg_attr(not(feature = "content_size"), allow(unused_variables))] content_size: Size<f32>,
first_baselines: Point<Option<f32>>,
) -> Self {
Self {
size,
#[cfg(feature = "content_size")]
content_size,
first_baselines,
top_margin: CollapsibleMarginSet::ZERO,
bottom_margin: CollapsibleMarginSet::ZERO,
margins_can_collapse_through: false,
}
}
/// Construct a `LayoutOutput` from just the container and content sizes
pub fn from_sizes(size: Size<f32>, content_size: Size<f32>) -> Self {
Self::from_sizes_and_baselines(size, content_size, Point::NONE)
}
/// Construct a `LayoutOutput` from just the container's size.
pub fn from_outer_size(size: Size<f32>) -> Self {
Self::from_sizes(size, Size::zero())
}
}
/// The final result of a layout algorithm for a single node.
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Layout {
/// The relative ordering of the node
///
/// Nodes with a higher order should be rendered on top of those with a lower order.
/// This is effectively a topological sort of each tree.
pub order: u32,
/// The top-left corner of the node
pub location: Point<f32>,
/// The width and height of the node
pub size: Size<f32>,
#[cfg(feature = "content_size")]
/// The width and height of the content inside the node. This may be larger than the size of the node in the case of
/// overflowing content and is useful for computing a "scroll width/height" for scrollable nodes
pub content_size: Size<f32>,
/// The size of the scrollbars in each dimension. If there is no scrollbar then the size will be zero.
pub scrollbar_size: Size<f32>,
/// The size of the borders of the node
pub border: Rect<f32>,
/// The size of the padding of the node
pub padding: Rect<f32>,
/// The size of the margin of the node
pub margin: Rect<f32>,
}
impl Default for Layout {
fn default() -> Self {
Self::new()
}
}
impl Layout {
/// Creates a new zero-[`Layout`].
///
/// The Zero-layout has size and location set to ZERO.
/// The `order` value of this layout is set to the minimum value of 0.
/// This means it should be rendered below all other [`Layout`]s.
#[must_use]
pub const fn new() -> Self {
Self {
order: 0,
location: Point::ZERO,
size: Size::zero(),
#[cfg(feature = "content_size")]
content_size: Size::zero(),
scrollbar_size: Size::zero(),
border: Rect::zero(),
padding: Rect::zero(),
margin: Rect::zero(),
}
}
/// Creates a new zero-[`Layout`] with the supplied `order` value.
///
/// Nodes with a higher order should be rendered on top of those with a lower order.
/// The Zero-layout has size and location set to ZERO.
#[must_use]
pub const fn with_order(order: u32) -> Self {
Self {
order,
size: Size::zero(),
location: Point::ZERO,
#[cfg(feature = "content_size")]
content_size: Size::zero(),
scrollbar_size: Size::zero(),
border: Rect::zero(),
padding: Rect::zero(),
margin: Rect::zero(),
}
}
/// Get the width of the node's content box
#[inline]
pub fn content_box_width(&self) -> f32 {
self.size.width - self.padding.left - self.padding.right - self.border.left - self.border.right
}
/// Get the height of the node's content box
#[inline]
pub fn content_box_height(&self) -> f32 {
self.size.height - self.padding.top - self.padding.bottom - self.border.top - self.border.bottom
}
/// Get the size of the node's content box
#[inline]
pub fn content_box_size(&self) -> Size<f32> {
Size { width: self.content_box_width(), height: self.content_box_height() }
}
/// Get x offset of the node's content box relative to it's parent's border box
pub fn content_box_x(&self) -> f32 {
self.location.x + self.border.left + self.padding.left
}
/// Get x offset of the node's content box relative to it's parent's border box
pub fn content_box_y(&self) -> f32 {
self.location.y + self.border.top + self.padding.top
}
}
#[cfg(feature = "content_size")]
impl Layout {
/// Return the scroll width of the node.
/// The scroll width is the difference between the width and the content width, floored at zero
pub fn scroll_width(&self) -> f32 {
f32_max(
0.0,
self.content_size.width + f32_min(self.scrollbar_size.width, self.size.width) - self.size.width
+ self.border.right,
)
}
/// Return the scroll height of the node.
/// The scroll height is the difference between the height and the content height, floored at zero
pub fn scroll_height(&self) -> f32 {
f32_max(
0.0,
self.content_size.height + f32_min(self.scrollbar_size.height, self.size.height) - self.size.height
+ self.border.bottom,
)
}
}
/// The additional information from layout algorithm
#[cfg(feature = "detailed_layout_info")]
#[derive(Debug, Clone, PartialEq)]
pub enum DetailedLayoutInfo {
/// Enum variant for [`DetailedGridInfo`](crate::compute::grid::DetailedGridInfo)
#[cfg(feature = "grid")]
Grid(Box<crate::compute::grid::DetailedGridInfo>),
/// For node that hasn't had any detailed information yet
None,
}
+33
View File
@@ -0,0 +1,33 @@
//! Contains both a high-level interface to Taffy using a ready-made node tree, and a set of traits for defining custom node trees.
//!
//! - For documentation on the high-level API, see the [`TaffyTree`] struct.
//! - For documentation on the low-level trait-based API, see the [`traits`] module.
// Submodules
mod cache;
mod layout;
mod node;
pub mod traits;
pub use cache::{Cache, ClearState};
pub use layout::{CollapsibleMarginSet, Layout, LayoutInput, LayoutOutput, RequestedAxis, RunMode, SizingMode};
pub use node::NodeId;
pub(crate) use traits::LayoutPartialTreeExt;
pub use traits::{LayoutPartialTree, PrintTree, RoundTree, TraversePartialTree, TraverseTree};
#[cfg(feature = "flexbox")]
pub use traits::LayoutFlexboxContainer;
#[cfg(feature = "grid")]
pub use traits::LayoutGridContainer;
#[cfg(feature = "block_layout")]
pub use traits::LayoutBlockContainer;
#[cfg(feature = "taffy_tree")]
mod taffy_tree;
#[cfg(feature = "taffy_tree")]
pub use taffy_tree::{TaffyError, TaffyResult, TaffyTree};
#[cfg(feature = "detailed_layout_info")]
pub use layout::DetailedLayoutInfo;
+60
View File
@@ -0,0 +1,60 @@
//! UI node types and related data structures.
//!
//! Layouts are composed of multiple nodes, which live in a tree-like data structure.
#[cfg(feature = "taffy_tree")]
use slotmap::{DefaultKey, Key, KeyData};
/// A type representing the id of a single node in a tree of nodes
///
/// Internally it is a wrapper around a u64 and a `NodeId` can be converted to and from
/// and u64 if needed.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct NodeId(u64);
impl NodeId {
/// Create a new NodeId from a u64 value
pub const fn new(val: u64) -> Self {
Self(val)
}
}
impl From<u64> for NodeId {
#[inline]
fn from(raw: u64) -> Self {
Self(raw)
}
}
impl From<NodeId> for u64 {
#[inline]
fn from(id: NodeId) -> Self {
id.0
}
}
impl From<usize> for NodeId {
#[inline]
fn from(raw: usize) -> Self {
Self(raw as u64)
}
}
impl From<NodeId> for usize {
#[inline]
fn from(id: NodeId) -> Self {
id.0 as usize
}
}
#[cfg(feature = "taffy_tree")]
impl From<DefaultKey> for NodeId {
#[inline]
fn from(key: DefaultKey) -> Self {
Self(key.data().as_ffi())
}
}
#[cfg(feature = "taffy_tree")]
impl From<NodeId> for DefaultKey {
#[inline]
fn from(key: NodeId) -> Self {
KeyData::from_ffi(key.0).into()
}
}
File diff suppressed because it is too large. Load diff
+418
View File
@@ -0,0 +1,418 @@
//! The abstractions that make up the core of Taffy's low-level API
//!
//! ## Examples
//!
//! The following examples demonstrate end-to-end implementation of Taffy's traits and usage of the low-level compute APIs:
//!
//! - [custom_tree_vec](https://github.com/DioxusLabs/taffy/blob/main/examples/custom_tree_vec.rs) which implements a custom Taffy tree using a `Vec` as an arena with NodeId's being index's into the Vec.
//! - [custom_tree_owned_partial](https://github.com/DioxusLabs/taffy/blob/main/examples/custom_tree_owned_partial.rs) which implements a custom Taffy tree using directly owned children with NodeId's being index's into vec on parent node.
//! - [custom_tree_owned_unsafe](https://github.com/DioxusLabs/taffy/blob/main/examples/custom_tree_owned_unsafe.rs) which implements a custom Taffy tree using directly owned children with NodeId's being pointers.
//!
//! ## Overview
//!
//! ### Trait dependency tree
//!
//! The tree below illustrates which traits depend on which other traits.
//!
//! ```text
//! TraversePartialTree - Access a node's children
//! ├── LayoutPartialTree - Run layout algorithms on a node and it's direct children
//! └── TraverseTree - Recursively access a node's descendants
//! ├── RoundTree - Round a float-valued` layout to integer pixels
//! └── PrintTree - Print a debug representation of a node tree
//! ```
//!
//! ### A table of traits
//!
//! | Trait | Requires | Enables |
//! | --- | --- | --- |
//! | [`LayoutPartialTree`] | [`TraversePartialTree`] | [`compute_flexbox_layout`](crate::compute_flexbox_layout)<br />[`compute_grid_layout`](crate::compute_grid_layout)<br />[`compute_block_layout`](crate::compute_block_layout)<br />[`compute_root_layout`](crate::compute_root_layout)<br />[`compute_leaf_layout`](crate::compute_leaf_layout)<br />[`compute_hidden_layout`](crate::compute_hidden_layout)<br />[`compute_cached_layout`](crate::compute_cached_layout) |
//! | [`RoundTree`] | [`TraverseTree`] | [`round_layout`](crate::round_layout) |
//! | [`PrintTree`] | [`TraverseTree`] | [`print_tree`](crate::print_tree) |
//!
//! ## All of the traits on one page
//!
//! ### TraversePartialTree and TraverseTree
//! These traits are Taffy's abstraction for downward tree traversal:
//! - [`TraversePartialTree`] allows access to a single container node, and it's immediate children. This is the only "traverse" trait that is required
//! for use of Taffy's core layout algorithms (flexbox, grid, etc).
//! - [`TraverseTree`] is a marker trait which uses the same API signature as `TraversePartialTree`, but extends it with a guarantee that the child/children methods can be used to recurse
//! infinitely down the tree. It is required by the `RoundTree` and
//! the `PrintTree` traits.
//! ```rust
//! # use taffy::*;
//! pub trait TraversePartialTree {
//! /// Type representing an iterator of the children of a node
//! type ChildIter<'a>: Iterator<Item = NodeId>
//! where
//! Self: 'a;
//!
//! /// Get the list of children IDs for the given node
//! fn child_ids(&self, parent_node_id: NodeId) -> Self::ChildIter<'_>;
//!
//! /// Get the number of children for the given node
//! fn child_count(&self, parent_node_id: NodeId) -> usize;
//!
//! /// Get a specific child of a node, where the index represents the nth child
//! fn get_child_id(&self, parent_node_id: NodeId, child_index: usize) -> NodeId;
//! }
//!
//! pub trait TraverseTree: TraversePartialTree {}
//! ```
//!
//! You must implement [`TraversePartialTree`] to access any of Taffy's low-level API. If your tree implementation allows you to implement [`TraverseTree`] with
//! the correct semantics (full recursive traversal is available) then you should.
//!
//! ### LayoutPartialTree
//!
//! **Requires:** `TraversePartialTree`<br />
//! **Enables:** Flexbox, Grid, Block and Leaf layout algorithms from the [`crate::compute`] module
//!
//! Any type that implements [`LayoutPartialTree`] can be laid out using [Taffy's algorithms](crate::compute)
//!
//! Note that this trait extends [`TraversePartialTree`] (not [`TraverseTree`]). Taffy's algorithm implementations have been designed such that they can be used for a laying out a single
//! node that only has access to it's immediate children.
//!
//! ```rust
//! # use taffy::*;
//! pub trait LayoutPartialTree: TraversePartialTree {
//! /// Get a reference to the [`Style`] for this node.
//! fn get_style(&self, node_id: NodeId) -> &Style;
//!
//! /// Set the node's unrounded layout
//! fn set_unrounded_layout(&mut self, node_id: NodeId, layout: &Layout);
//!
//! /// Get a mutable reference to the [`Cache`] for this node.
//! fn get_cache_mut(&mut self, node_id: NodeId) -> &mut Cache;
//!
//! /// Compute the specified node's size or full layout given the specified constraints
//! fn compute_child_layout(&mut self, node_id: NodeId, inputs: LayoutInput) -> LayoutOutput;
//! }
//! ```
//!
//! ### RoundTree
//!
//! **Requires:** `TraverseTree`
//!
//! Trait used by the `round_layout` method which takes a tree of unrounded float-valued layouts and performs
//! rounding to snap the values to the pixel grid.
//!
//! As indicated by it's dependence on `TraverseTree`, it required full recursive access to the tree.
//!
//! ```rust
//! # use taffy::*;
//! pub trait RoundTree: TraverseTree {
//! /// Get the node's unrounded layout
//! fn get_unrounded_layout(&self, node_id: NodeId) -> Layout;
//! /// Get a reference to the node's final layout
//! fn set_final_layout(&mut self, node_id: NodeId, layout: &Layout);
//! }
//! ```
//!
//! ### PrintTree
//!
//! **Requires:** `TraverseTree`
//!
//! ```rust
//! /// Trait used by the `print_tree` method which prints a debug representation
//! ///
//! /// As indicated by it's dependence on `TraverseTree`, it required full recursive access to the tree.
//! # use taffy::*;
//! pub trait PrintTree: TraverseTree {
//! /// Get a debug label for the node (typically the type of node: flexbox, grid, text, image, etc)
//! fn get_debug_label(&self, node_id: NodeId) -> &'static str;
//! /// Get a reference to the node's final layout
//! fn get_final_layout(&self, node_id: NodeId) -> Layout;
//! }
//! ```
//!
use super::{Layout, LayoutInput, LayoutOutput, NodeId, RequestedAxis, RunMode, SizingMode};
#[cfg(feature = "detailed_layout_info")]
use crate::debug::debug_log;
use crate::geometry::{AbsoluteAxis, Line, Size};
use crate::style::{AvailableSpace, CoreStyle};
#[cfg(feature = "flexbox")]
use crate::style::{FlexboxContainerStyle, FlexboxItemStyle};
#[cfg(feature = "grid")]
use crate::style::{GridContainerStyle, GridItemStyle};
use crate::CheapCloneStr;
#[cfg(feature = "block_layout")]
use crate::{BlockContainerStyle, BlockContext, BlockItemStyle};
#[cfg(all(feature = "grid", feature = "detailed_layout_info"))]
use crate::compute::grid::DetailedGridInfo;
/// Taffy's abstraction for downward tree traversal.
///
/// However, this trait does *not* require access to any node's other than a single container node's immediate children unless you also intend to implement `TraverseTree`.
pub trait TraversePartialTree {
/// Type representing an iterator of the children of a node
type ChildIter<'a>: Iterator<Item = NodeId>
where
Self: 'a;
/// Get the list of children IDs for the given node
fn child_ids(&self, parent_node_id: NodeId) -> Self::ChildIter<'_>;
/// Get the number of children for the given node
fn child_count(&self, parent_node_id: NodeId) -> usize;
/// Get a specific child of a node, where the index represents the nth child
fn get_child_id(&self, parent_node_id: NodeId, child_index: usize) -> NodeId;
}
/// A marker trait which extends `TraversePartialTree`
///
/// Implementing this trait implies the additional guarantee that the child/children methods can be used to recurse
/// infinitely down the tree. Is required by the `RoundTree` and the `PrintTree` traits.
pub trait TraverseTree: TraversePartialTree {}
/// Any type that implements [`LayoutPartialTree`] can be laid out using [Taffy's algorithms](crate::compute)
///
/// Note that this trait extends [`TraversePartialTree`] (not [`TraverseTree`]). Taffy's algorithm implementations have been designed such that they can be used for a laying out a single
/// node that only has access to it's immediate children.
pub trait LayoutPartialTree: TraversePartialTree {
/// The style type representing the core container styles that all containers should have
/// Used when laying out the root node of a tree
type CoreContainerStyle<'a>: CoreStyle<CustomIdent = Self::CustomIdent>
where
Self: 'a;
/// String type for representing "custom identifiers" (for example, named grid lines or areas)
/// If you are unsure what to use here then consider `Arc<str>`.
type CustomIdent: CheapCloneStr;
/// Get core style
fn get_core_container_style(&self, node_id: NodeId) -> Self::CoreContainerStyle<'_>;
/// Resolve calc value
#[inline(always)]
fn resolve_calc_value(&self, val: *const (), basis: f32) -> f32 {
let _ = val;
let _ = basis;
0.0
}
/// Set the node's unrounded layout
fn set_unrounded_layout(&mut self, node_id: NodeId, layout: &Layout);
/// Compute the specified node's size or full layout given the specified constraints
fn compute_child_layout(&mut self, node_id: NodeId, inputs: LayoutInput) -> LayoutOutput;
}
/// Trait used by the `compute_cached_layout` method which allows cached layout results to be stored and retrieved.
///
/// The `Cache` struct implements a per-node cache that is compatible with this trait.
pub trait CacheTree {
/// Try to retrieve a cached result from the cache
fn cache_get(&self, node_id: NodeId, input: &LayoutInput) -> Option<LayoutOutput>;
/// Store a computed size in the cache
fn cache_store(&mut self, node_id: NodeId, input: &LayoutInput, layout_output: LayoutOutput);
/// Clear all cache entries for the node
fn cache_clear(&mut self, node_id: NodeId);
}
/// Trait used by the `round_layout` method which takes a tree of unrounded float-valued layouts and performs
/// rounding to snap the values to the pixel grid.
///
/// As indicated by it's dependence on `TraverseTree`, it required full recursive access to the tree.
pub trait RoundTree: TraverseTree {
/// Get the node's unrounded layout
fn get_unrounded_layout(&self, node_id: NodeId) -> Layout;
/// Get a reference to the node's final layout
fn set_final_layout(&mut self, node_id: NodeId, layout: &Layout);
}
/// Trait used by the `print_tree` method which prints a debug representation
///
/// As indicated by it's dependence on `TraverseTree`, it required full recursive access to the tree.
pub trait PrintTree: TraverseTree {
/// Get a debug label for the node (typically the type of node: flexbox, grid, text, image, etc)
fn get_debug_label(&self, node_id: NodeId) -> &'static str;
/// Get a reference to the node's final layout
fn get_final_layout(&self, node_id: NodeId) -> Layout;
}
#[cfg(feature = "flexbox")]
/// Extends [`LayoutPartialTree`] with getters for the styles required for Flexbox layout
pub trait LayoutFlexboxContainer: LayoutPartialTree {
/// The style type representing the Flexbox container's styles
type FlexboxContainerStyle<'a>: FlexboxContainerStyle
where
Self: 'a;
/// The style type representing each Flexbox item's styles
type FlexboxItemStyle<'a>: FlexboxItemStyle
where
Self: 'a;
/// Get the container's styles
fn get_flexbox_container_style(&self, node_id: NodeId) -> Self::FlexboxContainerStyle<'_>;
/// Get the child's styles
fn get_flexbox_child_style(&self, child_node_id: NodeId) -> Self::FlexboxItemStyle<'_>;
}
#[cfg(feature = "grid")]
/// Extends [`LayoutPartialTree`] with getters for the styles required for CSS Grid layout
pub trait LayoutGridContainer: LayoutPartialTree {
/// The style type representing the CSS Grid container's styles
type GridContainerStyle<'a>: GridContainerStyle<CustomIdent = Self::CustomIdent>
where
Self: 'a;
/// The style type representing each CSS Grid item's styles
type GridItemStyle<'a>: GridItemStyle<CustomIdent = Self::CustomIdent>
where
Self: 'a;
/// Get the container's styles
fn get_grid_container_style(&self, node_id: NodeId) -> Self::GridContainerStyle<'_>;
/// Get the child's styles
fn get_grid_child_style(&self, child_node_id: NodeId) -> Self::GridItemStyle<'_>;
/// Set the node's detailed grid information
///
/// Implementing this method is optional. Doing so allows you to access details about the the grid such as
/// the computed size of each grid track and the computed placement of each grid item.
#[cfg(feature = "detailed_layout_info")]
fn set_detailed_grid_info(&mut self, _node_id: NodeId, _detailed_grid_info: DetailedGridInfo) {
debug_log!("LayoutGridContainer::set_detailed_grid_info called");
}
}
#[cfg(feature = "block_layout")]
/// Extends [`LayoutPartialTree`] with getters for the styles required for CSS Block layout
pub trait LayoutBlockContainer: LayoutPartialTree {
/// The style type representing the CSS Block container's styles
type BlockContainerStyle<'a>: BlockContainerStyle
where
Self: 'a;
/// The style type representing each CSS Block item's styles
type BlockItemStyle<'a>: BlockItemStyle
where
Self: 'a;
/// Get the container's styles
fn get_block_container_style(&self, node_id: NodeId) -> Self::BlockContainerStyle<'_>;
/// Get the child's styles
fn get_block_child_style(&self, child_node_id: NodeId) -> Self::BlockItemStyle<'_>;
/// Compute the specified node's size or full layout given the specified constraints
#[cfg(feature = "block_layout")]
fn compute_block_child_layout(
&mut self,
node_id: NodeId,
inputs: LayoutInput,
block_ctx: Option<&mut BlockContext<'_>>,
) -> LayoutOutput {
let _ = block_ctx;
self.compute_child_layout(node_id, inputs)
}
}
// --- PRIVATE TRAITS
/// A private trait which allows us to add extra convenience methods to types which implement
/// LayoutTree without making those methods public.
pub(crate) trait LayoutPartialTreeExt: LayoutPartialTree {
/// Compute the size of the node given the specified constraints
#[inline(always)]
#[allow(clippy::too_many_arguments)]
fn measure_child_size(
&mut self,
node_id: NodeId,
known_dimensions: Size<Option<f32>>,
parent_size: Size<Option<f32>>,
available_space: Size<AvailableSpace>,
sizing_mode: SizingMode,
axis: AbsoluteAxis,
vertical_margins_are_collapsible: Line<bool>,
) -> f32 {
self.compute_child_layout(
node_id,
LayoutInput {
known_dimensions,
parent_size,
available_space,
sizing_mode,
axis: axis.into(),
run_mode: RunMode::ComputeSize,
vertical_margins_are_collapsible,
},
)
.size
.get_abs(axis)
}
/// Compute the size of the node given the specified constraints
#[inline(always)]
#[allow(clippy::too_many_arguments)]
fn measure_child_size_both(
&mut self,
node_id: NodeId,
known_dimensions: Size<Option<f32>>,
parent_size: Size<Option<f32>>,
available_space: Size<AvailableSpace>,
sizing_mode: SizingMode,
vertical_margins_are_collapsible: Line<bool>,
) -> Size<f32> {
self.compute_child_layout(
node_id,
LayoutInput {
known_dimensions,
parent_size,
available_space,
sizing_mode,
axis: RequestedAxis::Both,
run_mode: RunMode::ComputeSize,
vertical_margins_are_collapsible,
},
)
.size
}
/// Perform a full layout on the node given the specified constraints
#[inline(always)]
fn perform_child_layout(
&mut self,
node_id: NodeId,
known_dimensions: Size<Option<f32>>,
parent_size: Size<Option<f32>>,
available_space: Size<AvailableSpace>,
sizing_mode: SizingMode,
vertical_margins_are_collapsible: Line<bool>,
) -> LayoutOutput {
self.compute_child_layout(
node_id,
LayoutInput {
known_dimensions,
parent_size,
available_space,
sizing_mode,
axis: RequestedAxis::Both,
run_mode: RunMode::PerformLayout,
vertical_margins_are_collapsible,
},
)
}
/// Alias to `resolve_calc_value` with a shorter function name
#[inline(always)]
#[cfg(feature = "calc")]
fn calc(&self, val: *const (), basis: f32) -> f32 {
self.resolve_calc_value(val, basis)
}
/// Alias to `resolve_calc_value` with a shorter function name
#[inline(always)]
#[cfg(not(feature = "calc"))]
fn calc(&self, _val: *const (), _basis: f32) -> f32 {
0.0
}
}
impl<T: LayoutPartialTree> LayoutPartialTreeExt for T {}
+143
View File
@@ -0,0 +1,143 @@
#![allow(dead_code)]
#[cfg(any(feature = "debug", feature = "profile"))]
use core::fmt::{Debug, Display, Write};
#[cfg(any(feature = "debug", feature = "profile"))]
use std::sync::Mutex;
#[doc(hidden)]
#[cfg(any(feature = "debug", feature = "profile"))]
pub struct DebugLogger {
stack: Mutex<Vec<String>>,
}
#[cfg(any(feature = "debug", feature = "profile"))]
static EMPTY_STRING: String = String::new();
#[cfg(any(feature = "debug", feature = "profile"))]
impl DebugLogger {
pub const fn new() -> Self {
Self { stack: Mutex::new(Vec::new()) }
}
pub fn push_node(&self, new_key: crate::NodeId) {
let mut stack = self.stack.lock().unwrap();
let mut key_string = String::new();
write!(&mut key_string, "{:?}", new_key).unwrap();
stack.push(key_string);
}
pub fn pop_node(&self) {
let mut stack = self.stack.lock().unwrap();
stack.pop();
}
pub fn log(&self, message: impl Display) {
let stack = self.stack.lock().unwrap();
let key = stack.last().unwrap_or(&EMPTY_STRING);
let level = stack.len() * 4;
let space = " ";
println!("{space:level$}{key}: {message}");
}
pub fn labelled_log(&self, label: &str, message: impl Display) {
let stack = self.stack.lock().unwrap();
let key = stack.last().unwrap_or(&EMPTY_STRING);
let level = stack.len() * 4;
let space = " ";
println!("{space:level$}{key}: {label} {message}");
}
pub fn debug_log(&self, message: impl Debug) {
let stack = self.stack.lock().unwrap();
let key = stack.last().unwrap_or(&EMPTY_STRING);
let level = stack.len() * 4;
let space = " ";
println!("{space:level$}{key}: {message:?}");
}
pub fn labelled_debug_log(&self, label: &str, message: impl Debug) {
let stack = self.stack.lock().unwrap();
let key = stack.last().unwrap_or(&EMPTY_STRING);
let level = stack.len() * 4;
let space = " ";
println!("{space:level$}{key}: {label} {message:?}");
}
}
#[cfg(any(feature = "debug", feature = "profile"))]
pub(crate) static NODE_LOGGER: DebugLogger = DebugLogger::new();
macro_rules! debug_log {
// String literal label with debug printing
($label:literal, dbg:$item:expr) => {{
#[cfg(feature = "debug")]
$crate::util::debug::NODE_LOGGER.labelled_debug_log($label, $item);
}};
// String literal label with display printing
($label:literal, $item:expr) => {{
#[cfg(feature = "debug")]
$crate::util::debug::NODE_LOGGER.labelled_log($label, $item);
}};
// Debug printing
(dbg:$item:expr) => {{
#[cfg(feature = "debug")]
$crate::util::debug::NODE_LOGGER.debug_log($item);
}};
// Display printing
($item:expr) => {{
#[cfg(feature = "debug")]
$crate::util::debug::NODE_LOGGER.log($item);
}};
// Blank newline
() => {{
#[cfg(feature = "debug")]
println!();
}};
}
macro_rules! debug_log_node {
($inputs: expr) => {
debug_log!(dbg:$inputs.run_mode);
debug_log!("sizing_mode", dbg:$inputs.sizing_mode);
debug_log!("known_dimensions", dbg:$inputs.known_dimensions);
debug_log!("parent_size", dbg:$inputs.parent_size);
debug_log!("available_space", dbg:$inputs.available_space);
};
}
macro_rules! debug_push_node {
($node_id:expr) => {
#[cfg(any(feature = "debug", feature = "profile"))]
$crate::util::debug::NODE_LOGGER.push_node($node_id);
debug_log!("");
};
}
macro_rules! debug_pop_node {
() => {
#[cfg(any(feature = "debug", feature = "profile"))]
$crate::util::debug::NODE_LOGGER.pop_node();
};
}
#[cfg(feature = "profile")]
#[allow(unused_macros)]
macro_rules! time {
($label:expr, $($code:tt)*) => {
let start = ::std::time::Instant::now();
$($code)*
let duration = ::std::time::Instant::now().duration_since(start);
crate::util::debug::NODE_LOGGER.log(format_args!("Performed {} in {}ms", $label, duration.as_millis()));
};
}
#[cfg(not(feature = "profile"))]
#[allow(unused_macros)]
macro_rules! time {
($label:expr, $($code:tt)*) => {
$($code)*
};
}
#[allow(unused_imports)]
pub(crate) use {debug_log, debug_log_node, debug_pop_node, debug_push_node, time};
+356
View File
@@ -0,0 +1,356 @@
//! Contains numerical helper traits and functions
#![allow(clippy::manual_clamp)]
use crate::geometry::Size;
use crate::style::AvailableSpace;
/// A trait to conveniently calculate minimums and maximums when some data may not be defined
///
/// If the left-hand value is [`None`], these operations return [`None`].
/// If the right-hand value is [`None`], it is treated as zero.
pub trait MaybeMath<In, Out> {
/// Returns the minimum of `self` and `rhs`
fn maybe_min(self, rhs: In) -> Out;
/// Returns the maximum of `self` and `rhs`
fn maybe_max(self, rhs: In) -> Out;
/// Returns `self` clamped between `min` and `max`
fn maybe_clamp(self, min: In, max: In) -> Out;
/// Adds `self` and `rhs`.
fn maybe_add(self, rhs: In) -> Out;
/// Subtracts rhs from `self`, treating [`None`] values as default
fn maybe_sub(self, rhs: In) -> Out;
}
impl MaybeMath<Option<f32>, Option<f32>> for Option<f32> {
fn maybe_min(self, rhs: Option<f32>) -> Option<f32> {
match (self, rhs) {
(Some(l), Some(r)) => Some(l.min(r)),
(Some(_l), None) => self,
(None, Some(_r)) => None,
(None, None) => None,
}
}
fn maybe_max(self, rhs: Option<f32>) -> Option<f32> {
match (self, rhs) {
(Some(l), Some(r)) => Some(l.max(r)),
(Some(_l), None) => self,
(None, Some(_r)) => None,
(None, None) => None,
}
}
fn maybe_clamp(self, min: Option<f32>, max: Option<f32>) -> Option<f32> {
match (self, min, max) {
(Some(base), Some(min), Some(max)) => Some(base.min(max).max(min)),
(Some(base), None, Some(max)) => Some(base.min(max)),
(Some(base), Some(min), None) => Some(base.max(min)),
(Some(_), None, None) => self,
(None, _, _) => None,
}
}
fn maybe_add(self, rhs: Option<f32>) -> Option<f32> {
match (self, rhs) {
(Some(l), Some(r)) => Some(l + r),
(Some(_l), None) => self,
(None, Some(_r)) => None,
(None, None) => None,
}
}
fn maybe_sub(self, rhs: Option<f32>) -> Option<f32> {
match (self, rhs) {
(Some(l), Some(r)) => Some(l - r),
(Some(_l), None) => self,
(None, Some(_r)) => None,
(None, None) => None,
}
}
}
impl MaybeMath<f32, Option<f32>> for Option<f32> {
fn maybe_min(self, rhs: f32) -> Option<f32> {
self.map(|val| val.min(rhs))
}
fn maybe_max(self, rhs: f32) -> Option<f32> {
self.map(|val| val.max(rhs))
}
fn maybe_clamp(self, min: f32, max: f32) -> Option<f32> {
self.map(|val| val.min(max).max(min))
}
fn maybe_add(self, rhs: f32) -> Option<f32> {
self.map(|val| val + rhs)
}
fn maybe_sub(self, rhs: f32) -> Option<f32> {
self.map(|val| val - rhs)
}
}
impl MaybeMath<Option<f32>, f32> for f32 {
fn maybe_min(self, rhs: Option<f32>) -> f32 {
match rhs {
Some(val) => self.min(val),
None => self,
}
}
fn maybe_max(self, rhs: Option<f32>) -> f32 {
match rhs {
Some(val) => self.max(val),
None => self,
}
}
fn maybe_clamp(self, min: Option<f32>, max: Option<f32>) -> f32 {
match (min, max) {
(Some(min), Some(max)) => self.min(max).max(min),
(None, Some(max)) => self.min(max),
(Some(min), None) => self.max(min),
(None, None) => self,
}
}
fn maybe_add(self, rhs: Option<f32>) -> f32 {
match rhs {
Some(val) => self + val,
None => self,
}
}
fn maybe_sub(self, rhs: Option<f32>) -> f32 {
match rhs {
Some(val) => self - val,
None => self,
}
}
}
impl MaybeMath<f32, AvailableSpace> for AvailableSpace {
fn maybe_min(self, rhs: f32) -> AvailableSpace {
match self {
AvailableSpace::Definite(val) => AvailableSpace::Definite(val.min(rhs)),
AvailableSpace::MinContent => AvailableSpace::Definite(rhs),
AvailableSpace::MaxContent => AvailableSpace::Definite(rhs),
}
}
fn maybe_max(self, rhs: f32) -> AvailableSpace {
match self {
AvailableSpace::Definite(val) => AvailableSpace::Definite(val.max(rhs)),
AvailableSpace::MinContent => AvailableSpace::MinContent,
AvailableSpace::MaxContent => AvailableSpace::MaxContent,
}
}
fn maybe_clamp(self, min: f32, max: f32) -> AvailableSpace {
match self {
AvailableSpace::Definite(val) => AvailableSpace::Definite(val.min(max).max(min)),
AvailableSpace::MinContent => AvailableSpace::MinContent,
AvailableSpace::MaxContent => AvailableSpace::MaxContent,
}
}
fn maybe_add(self, rhs: f32) -> AvailableSpace {
match self {
AvailableSpace::Definite(val) => AvailableSpace::Definite(val + rhs),
AvailableSpace::MinContent => AvailableSpace::MinContent,
AvailableSpace::MaxContent => AvailableSpace::MaxContent,
}
}
fn maybe_sub(self, rhs: f32) -> AvailableSpace {
match self {
AvailableSpace::Definite(val) => AvailableSpace::Definite(val - rhs),
AvailableSpace::MinContent => AvailableSpace::MinContent,
AvailableSpace::MaxContent => AvailableSpace::MaxContent,
}
}
}
impl MaybeMath<Option<f32>, AvailableSpace> for AvailableSpace {
fn maybe_min(self, rhs: Option<f32>) -> AvailableSpace {
match (self, rhs) {
(AvailableSpace::Definite(val), Some(rhs)) => AvailableSpace::Definite(val.min(rhs)),
(AvailableSpace::Definite(val), None) => AvailableSpace::Definite(val),
(AvailableSpace::MinContent, Some(rhs)) => AvailableSpace::Definite(rhs),
(AvailableSpace::MinContent, None) => AvailableSpace::MinContent,
(AvailableSpace::MaxContent, Some(rhs)) => AvailableSpace::Definite(rhs),
(AvailableSpace::MaxContent, None) => AvailableSpace::MaxContent,
}
}
fn maybe_max(self, rhs: Option<f32>) -> AvailableSpace {
match (self, rhs) {
(AvailableSpace::Definite(val), Some(rhs)) => AvailableSpace::Definite(val.max(rhs)),
(AvailableSpace::Definite(val), None) => AvailableSpace::Definite(val),
(AvailableSpace::MinContent, _) => AvailableSpace::MinContent,
(AvailableSpace::MaxContent, _) => AvailableSpace::MaxContent,
}
}
fn maybe_clamp(self, min: Option<f32>, max: Option<f32>) -> AvailableSpace {
match (self, min, max) {
(AvailableSpace::Definite(val), Some(min), Some(max)) => AvailableSpace::Definite(val.min(max).max(min)),
(AvailableSpace::Definite(val), None, Some(max)) => AvailableSpace::Definite(val.min(max)),
(AvailableSpace::Definite(val), Some(min), None) => AvailableSpace::Definite(val.max(min)),
(AvailableSpace::Definite(val), None, None) => AvailableSpace::Definite(val),
(AvailableSpace::MinContent, _, _) => AvailableSpace::MinContent,
(AvailableSpace::MaxContent, _, _) => AvailableSpace::MaxContent,
}
}
fn maybe_add(self, rhs: Option<f32>) -> AvailableSpace {
match (self, rhs) {
(AvailableSpace::Definite(val), Some(rhs)) => AvailableSpace::Definite(val + rhs),
(AvailableSpace::Definite(val), None) => AvailableSpace::Definite(val),
(AvailableSpace::MinContent, _) => AvailableSpace::MinContent,
(AvailableSpace::MaxContent, _) => AvailableSpace::MaxContent,
}
}
fn maybe_sub(self, rhs: Option<f32>) -> AvailableSpace {
match (self, rhs) {
(AvailableSpace::Definite(val), Some(rhs)) => AvailableSpace::Definite(val - rhs),
(AvailableSpace::Definite(val), None) => AvailableSpace::Definite(val),
(AvailableSpace::MinContent, _) => AvailableSpace::MinContent,
(AvailableSpace::MaxContent, _) => AvailableSpace::MaxContent,
}
}
}
impl<In, Out, T: MaybeMath<In, Out>> MaybeMath<Size<In>, Size<Out>> for Size<T> {
fn maybe_min(self, rhs: Size<In>) -> Size<Out> {
Size { width: self.width.maybe_min(rhs.width), height: self.height.maybe_min(rhs.height) }
}
fn maybe_max(self, rhs: Size<In>) -> Size<Out> {
Size { width: self.width.maybe_max(rhs.width), height: self.height.maybe_max(rhs.height) }
}
fn maybe_clamp(self, min: Size<In>, max: Size<In>) -> Size<Out> {
Size {
width: self.width.maybe_clamp(min.width, max.width),
height: self.height.maybe_clamp(min.height, max.height),
}
}
fn maybe_add(self, rhs: Size<In>) -> Size<Out> {
Size { width: self.width.maybe_add(rhs.width), height: self.height.maybe_add(rhs.height) }
}
fn maybe_sub(self, rhs: Size<In>) -> Size<Out> {
Size { width: self.width.maybe_sub(rhs.width), height: self.height.maybe_sub(rhs.height) }
}
}
#[cfg(test)]
mod tests {
mod lhs_option_f32_rhs_option_f32 {
use crate::util::MaybeMath;
#[test]
fn test_maybe_min() {
assert_eq!(Some(3.0).maybe_min(Some(5.0)), Some(3.0));
assert_eq!(Some(5.0).maybe_min(Some(3.0)), Some(3.0));
assert_eq!(Some(3.0).maybe_min(None), Some(3.0));
assert_eq!(None.maybe_min(Some(3.0)), None);
assert_eq!(None.maybe_min(None), None);
}
#[test]
fn test_maybe_max() {
assert_eq!(Some(3.0).maybe_max(Some(5.0)), Some(5.0));
assert_eq!(Some(5.0).maybe_max(Some(3.0)), Some(5.0));
assert_eq!(Some(3.0).maybe_max(None), Some(3.0));
assert_eq!(None.maybe_max(Some(3.0)), None);
assert_eq!(None.maybe_max(None), None);
}
#[test]
fn test_maybe_add() {
assert_eq!(Some(3.0).maybe_add(Some(5.0)), Some(8.0));
assert_eq!(Some(5.0).maybe_add(Some(3.0)), Some(8.0));
assert_eq!(Some(3.0).maybe_add(None), Some(3.0));
assert_eq!(None.maybe_add(Some(3.0)), None);
assert_eq!(None.maybe_add(None), None);
}
#[test]
fn test_maybe_sub() {
assert_eq!(Some(3.0).maybe_sub(Some(5.0)), Some(-2.0));
assert_eq!(Some(5.0).maybe_sub(Some(3.0)), Some(2.0));
assert_eq!(Some(3.0).maybe_sub(None), Some(3.0));
assert_eq!(None.maybe_sub(Some(3.0)), None);
assert_eq!(None.maybe_sub(None), None);
}
}
mod lhs_option_f32_rhs_f32 {
use crate::util::MaybeMath;
#[test]
fn test_maybe_min() {
assert_eq!(Some(3.0).maybe_min(5.0), Some(3.0));
assert_eq!(Some(5.0).maybe_min(3.0), Some(3.0));
assert_eq!(None.maybe_min(3.0), None);
}
#[test]
fn test_maybe_max() {
assert_eq!(Some(3.0).maybe_max(5.0), Some(5.0));
assert_eq!(Some(5.0).maybe_max(3.0), Some(5.0));
assert_eq!(None.maybe_max(3.0), None);
}
#[test]
fn test_maybe_add() {
assert_eq!(Some(3.0).maybe_add(5.0), Some(8.0));
assert_eq!(Some(5.0).maybe_add(3.0), Some(8.0));
assert_eq!(None.maybe_add(3.0), None);
}
#[test]
fn test_maybe_sub() {
assert_eq!(Some(3.0).maybe_sub(5.0), Some(-2.0));
assert_eq!(Some(5.0).maybe_sub(3.0), Some(2.0));
assert_eq!(None.maybe_sub(3.0), None);
}
}
mod lhs_f32_rhs_option_f32 {
use crate::util::MaybeMath;
#[test]
fn test_maybe_min() {
assert_eq!(3.0.maybe_min(Some(5.0)), 3.0);
assert_eq!(5.0.maybe_min(Some(3.0)), 3.0);
assert_eq!(3.0.maybe_min(None), 3.0);
}
#[test]
fn test_maybe_max() {
assert_eq!(3.0.maybe_max(Some(5.0)), 5.0);
assert_eq!(5.0.maybe_max(Some(3.0)), 5.0);
assert_eq!(3.0.maybe_max(None), 3.0);
}
#[test]
fn test_maybe_add() {
assert_eq!(3.0.maybe_add(Some(5.0)), 8.0);
assert_eq!(5.0.maybe_add(Some(3.0)), 8.0);
assert_eq!(3.0.maybe_add(None), 3.0);
}
#[test]
fn test_maybe_sub() {
assert_eq!(3.0.maybe_sub(Some(5.0)), -2.0);
assert_eq!(5.0.maybe_sub(Some(3.0)), 2.0);
assert_eq!(3.0.maybe_sub(None), 3.0);
}
}
}
+36
View File
@@ -0,0 +1,36 @@
//! Helpful misc. utilities such as a function to debug print a tree
mod math;
mod resolve;
pub(crate) mod sys;
pub use math::MaybeMath;
pub use resolve::{MaybeResolve, ResolveOrZero};
#[doc(hidden)]
#[macro_use]
pub(crate) mod debug;
#[cfg(feature = "std")]
mod print;
#[cfg(feature = "std")]
pub use print::print_tree;
#[cfg(feature = "std")]
pub use print::write_tree;
#[cfg(feature = "parse")]
pub(crate) mod parse;
#[cfg(feature = "parse")]
pub use parse::{ParseError, ParseResult};
/// Deserialize a type `S` by deserializing a string, then using the `FromStr`
/// impl of `S` to create the result. The generic type `S` is not required to
/// implement `Deserialize`.
#[cfg(feature = "serde")]
pub(crate) fn deserialize_from_str<'de, S, D>(deserializer: D) -> Result<S, D::Error>
where
S: for<'a> From<&'a str>,
D: serde::Deserializer<'de>,
{
let s: String = serde::Deserialize::deserialize(deserializer)?;
Ok(S::from(&s))
}
+118
View File
@@ -0,0 +1,118 @@
//! Helpers for parsing style types from strings
use cssparser::BasicParseError;
pub(crate) use cssparser::{Parser, ParserInput, Token};
use std::borrow::Cow;
/// Error type for parsing a type from `cssparser::Parser`
pub(crate) type CssParseError<'i> = cssparser::ParseError<'i, Cow<'i, str>>;
/// Result type for parsing a type from `cssparser::Parser`
pub(crate) type CssParseResult<'i, T> = Result<T, CssParseError<'i>>;
/// Error type for parsing a type from string
#[derive(Clone, Debug)]
pub struct ParseError(String);
impl core::fmt::Display for ParseError {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(fmt, "{}", &self.0)
}
}
impl<'i> From<CssParseError<'i>> for ParseError {
fn from(value: CssParseError<'i>) -> Self {
Self(value.to_string())
}
}
impl<'i> From<BasicParseError<'i>> for ParseError {
fn from(value: BasicParseError<'i>) -> Self {
Self(CssParseError::from(value).to_string())
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseError {}
/// Result type for parsing a type from string
pub type ParseResult<T> = Result<T, ParseError>;
/// Trait for parsing a type from a `cssparser::Parser` input
pub(crate) trait FromCss: Sized {
/// Parse type from a `cssparser::Parser` input
fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self>;
}
/// Parse a string into a type that implements `FromCss`, ensuring that the
/// entire input string is consumed.
pub(crate) fn parse_css_str_entirely<T: FromCss>(input: &str) -> Result<T, ParseError> {
let mut parser_input = ParserInput::new(input);
let mut parser = Parser::new(&mut parser_input);
parser.parse_entirely(|parser| T::from_css(parser)).map_err(|err| ParseError(err.to_string()))
}
/// Automatically implement `FromStr` for a type that already implemented `FromCss`
macro_rules! from_str_from_css {
($ty:ident) => {
#[cfg(feature = "parse")]
impl core::str::FromStr for $ty {
type Err = $crate::ParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
$crate::util::parse::parse_css_str_entirely(input)
}
}
};
}
pub(crate) use from_str_from_css;
/// Implements the `FromCss` and `FromStr` traits for a simple enum that consists of just keywords
macro_rules! impl_parse_for_keyword_enum {
(
// The type name (followed by a comma)
$ty:ident,
// Repeat 1-or-more times
$(
// 0-or-more metadata attributes (e.g. #[cfg] attributes for conditional compilation)
$( #[$meta: meta] )*
// keyword => enum_variant (e.g. "center" => AlignItems::Center)
$keyword:literal => $enum_variant:ident,
)+
) => {
// Impl FromCss for the type
impl $crate::util::parse::FromCss for $ty {
fn from_css<'i>(input: &mut cssparser::Parser<'i, '_>) -> $crate::util::parse::CssParseResult<'i, Self> {
// Parse an ident (or return an error)
let ident = input.expect_ident()?;
// Match on the ident (case insensitive)
cssparser::match_ignore_ascii_case! { &*ident,
// Define each item in the macro definition as a match case
$(
// Add the metadata attributes to the match case
$( #[$meta] )*
// If the keyword matches, return the corresponding variant of the enum
$keyword => return Ok(Self::$enum_variant),
)+
// If none of the cases match the input, return an "unexpected token" error
_ => {
let ident = ident.clone();
Err(input.new_unexpected_token_error(cssparser::Token::Ident(ident)))
}
}
}
}
// Impl FromStr for the type
impl core::str::FromStr for $ty {
type Err = $crate::ParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
$crate::util::parse::parse_css_str_entirely(input)
}
}
};
}
pub(crate) use impl_parse_for_keyword_enum;
+78
View File
@@ -0,0 +1,78 @@
//! Contains functions for printing a debug representation of the tree
use crate::tree::{NodeId, PrintTree};
use std::io;
/// Prints a debug representation of the computed layout for a tree of nodes, starting with the passed root node.
pub fn print_tree(tree: &impl PrintTree, root: NodeId) {
// Buffer into a string so that output can be captured in tests
let mut buffer = Vec::<u8>::new();
write_tree(&mut buffer, tree, root).expect("Wrote tree debug representation to Stdout");
println!("{}", std::str::from_utf8(&buffer).unwrap());
}
/// Writes a debug representation of the computed layout to the writer, starting with the passed root node.
pub fn write_tree(mut writer: impl io::Write, tree: &impl PrintTree, root: NodeId) -> io::Result<()> {
writeln!(writer, "TREE")?;
write_node(&mut writer, tree, root, false, String::new())?;
/// Recursive function that writes each node in the tree
fn write_node(
writer: &mut impl io::Write,
tree: &impl PrintTree,
node_id: NodeId,
has_sibling: bool,
lines_string: String,
) -> io::Result<()> {
let layout = &tree.get_final_layout(node_id);
let display = tree.get_debug_label(node_id);
let num_children = tree.child_count(node_id);
let fork_string = if has_sibling { "├── " } else { "└── " };
#[cfg(feature = "content_size")]
writeln!(
writer,
"{lines}{fork} {display} [x: {x:<4} y: {y:<4} w: {width:<4} h: {height:<4} content_w: {content_width:<4} content_h: {content_height:<4} border: l:{bl} r:{br} t:{bt} b:{bb}, padding: l:{pl} r:{pr} t:{pt} b:{pb}] ({key:?})",
lines = lines_string,
fork = fork_string,
display = display,
x = layout.location.x,
y = layout.location.y,
width = layout.size.width,
height = layout.size.height,
content_width = layout.content_size.width,
content_height = layout.content_size.height,
bl = layout.border.left,
br = layout.border.right,
bt = layout.border.top,
bb = layout.border.bottom,
pl = layout.padding.left,
pr = layout.padding.right,
pt = layout.padding.top,
pb = layout.padding.bottom,
key = node_id,
)?;
#[cfg(not(feature = "content_size"))]
writeln!(
writer,
"{lines}{fork} {display} [x: {x:<4} y: {y:<4} width: {width:<4} height: {height:<4}] ({key:?})",
lines = lines_string,
fork = fork_string,
display = display,
x = layout.location.x,
y = layout.location.y,
width = layout.size.width,
height = layout.size.height,
key = node_id,
);
let bar = if has_sibling { "│ " } else { " " };
let new_string = lines_string + bar;
// Recurse into children
for (index, child) in tree.child_ids(node_id).enumerate() {
let has_sibling = index < num_children - 1;
write_node(writer, tree, child, has_sibling, new_string.clone())?;
}
Ok(())
}
Ok(())
}
+364
View File
@@ -0,0 +1,364 @@
//! Helper trait to calculate dimensions during layout resolution
use crate::geometry::{Rect, Size};
use crate::style::{Dimension, LengthPercentage, LengthPercentageAuto};
use crate::style_helpers::TaffyZero;
use crate::CompactLength;
/// Trait to encapsulate behaviour where we need to resolve from a
/// potentially context-dependent size or dimension into
/// a context-independent size or dimension.
///
/// Will return a `None` if it unable to resolve.
pub trait MaybeResolve<In, Out> {
/// Resolve a dimension that might be dependent on a context, with `None` as fallback value
fn maybe_resolve(self, context: In, calc: impl Fn(*const (), f32) -> f32) -> Out;
}
/// Trait to encapsulate behaviour where we need to resolve from a
/// potentially context-dependent size or dimension into
/// a context-independent size or dimension.
///
/// Will return a default value if it unable to resolve.
pub trait ResolveOrZero<TContext, TOutput: TaffyZero> {
/// Resolve a dimension that might be dependent on a context, with a default fallback value
fn resolve_or_zero(self, context: TContext, calc: impl Fn(*const (), f32) -> f32) -> TOutput;
}
impl MaybeResolve<Option<f32>, Option<f32>> for LengthPercentage {
/// Converts the given [`LengthPercentage`] into an absolute length
/// Can return `None`
fn maybe_resolve(self, context: Option<f32>, calc: impl Fn(*const (), f32) -> f32) -> Option<f32> {
match self.0.tag() {
CompactLength::LENGTH_TAG => Some(self.0.value()),
CompactLength::PERCENT_TAG => context.map(|dim| dim * self.0.value()),
#[cfg(feature = "calc")]
_ if self.0.is_calc() => context.map(|dim| calc(self.0.calc_value(), dim)),
_ => unreachable!(),
}
}
}
impl MaybeResolve<Option<f32>, Option<f32>> for LengthPercentageAuto {
/// Converts the given [`LengthPercentageAuto`] into an absolute length
/// Can return `None`
fn maybe_resolve(self, context: Option<f32>, calc: impl Fn(*const (), f32) -> f32) -> Option<f32> {
match self.0.tag() {
CompactLength::AUTO_TAG => None,
CompactLength::LENGTH_TAG => Some(self.0.value()),
CompactLength::PERCENT_TAG => context.map(|dim| dim * self.0.value()),
#[cfg(feature = "calc")]
_ if self.0.is_calc() => context.map(|dim| calc(self.0.calc_value(), dim)),
_ => unreachable!(),
}
}
}
impl MaybeResolve<Option<f32>, Option<f32>> for Dimension {
/// Converts the given [`Dimension`] into an absolute length
///
/// Can return `None`
fn maybe_resolve(self, context: Option<f32>, calc: impl Fn(*const (), f32) -> f32) -> Option<f32> {
match self.0.tag() {
CompactLength::AUTO_TAG => None,
CompactLength::LENGTH_TAG => Some(self.0.value()),
CompactLength::PERCENT_TAG => context.map(|dim| dim * self.0.value()),
#[cfg(feature = "calc")]
_ if self.0.is_calc() => context.map(|dim| calc(self.0.calc_value(), dim)),
_ => unreachable!(),
}
}
}
// Generic implementation of MaybeResolve for f32 context where MaybeResolve is implemented
// for Option<f32> context
impl<T: MaybeResolve<Option<f32>, Option<f32>>> MaybeResolve<f32, Option<f32>> for T {
/// Converts the given MaybeResolve value into an absolute length
/// Can return `None`
fn maybe_resolve(self, context: f32, calc: impl Fn(*const (), f32) -> f32) -> Option<f32> {
self.maybe_resolve(Some(context), calc)
}
}
// Generic MaybeResolve for Size
impl<In, Out, T: MaybeResolve<In, Out>> MaybeResolve<Size<In>, Size<Out>> for Size<T> {
/// Converts any `parent`-relative values for size into an absolute size
fn maybe_resolve(self, context: Size<In>, calc: impl Fn(*const (), f32) -> f32) -> Size<Out> {
Size {
width: self.width.maybe_resolve(context.width, &calc),
height: self.height.maybe_resolve(context.height, &calc),
}
}
}
impl ResolveOrZero<Option<f32>, f32> for LengthPercentage {
/// Will return a default value of result is evaluated to `None`
fn resolve_or_zero(self, context: Option<f32>, calc: impl Fn(*const (), f32) -> f32) -> f32 {
self.maybe_resolve(context, calc).unwrap_or(0.0)
}
}
impl ResolveOrZero<Option<f32>, f32> for LengthPercentageAuto {
/// Will return a default value of result is evaluated to `None`
fn resolve_or_zero(self, context: Option<f32>, calc: impl Fn(*const (), f32) -> f32) -> f32 {
self.maybe_resolve(context, calc).unwrap_or(0.0)
}
}
impl ResolveOrZero<Option<f32>, f32> for Dimension {
/// Will return a default value of result is evaluated to `None`
fn resolve_or_zero(self, context: Option<f32>, calc: impl Fn(*const (), f32) -> f32) -> f32 {
self.maybe_resolve(context, calc).unwrap_or(0.0)
}
}
// Generic ResolveOrZero for Size
impl<In, Out: TaffyZero, T: ResolveOrZero<In, Out>> ResolveOrZero<Size<In>, Size<Out>> for Size<T> {
/// Converts any `parent`-relative values for size into an absolute size
fn resolve_or_zero(self, context: Size<In>, calc: impl Fn(*const (), f32) -> f32) -> Size<Out> {
Size {
width: self.width.resolve_or_zero(context.width, &calc),
height: self.height.resolve_or_zero(context.height, &calc),
}
}
}
// Generic ResolveOrZero for resolving Rect against Size
impl<In: Copy, Out: TaffyZero, T: ResolveOrZero<In, Out>> ResolveOrZero<Size<In>, Rect<Out>> for Rect<T> {
/// Converts any `parent`-relative values for Rect into an absolute Rect
fn resolve_or_zero(self, context: Size<In>, calc: impl Fn(*const (), f32) -> f32) -> Rect<Out> {
Rect {
left: self.left.resolve_or_zero(context.width, &calc),
right: self.right.resolve_or_zero(context.width, &calc),
top: self.top.resolve_or_zero(context.height, &calc),
bottom: self.bottom.resolve_or_zero(context.height, &calc),
}
}
}
// Generic ResolveOrZero for resolving Rect against Option
impl<Out: TaffyZero, T: ResolveOrZero<Option<f32>, Out>> ResolveOrZero<Option<f32>, Rect<Out>> for Rect<T> {
/// Converts any `parent`-relative values for Rect into an absolute Rect
fn resolve_or_zero(self, context: Option<f32>, calc: impl Fn(*const (), f32) -> f32) -> Rect<Out> {
Rect {
left: self.left.resolve_or_zero(context, &calc),
right: self.right.resolve_or_zero(context, &calc),
top: self.top.resolve_or_zero(context, &calc),
bottom: self.bottom.resolve_or_zero(context, &calc),
}
}
}
// Generic ResolveOrZero for resolving Rect against f32
impl<Out: TaffyZero, T: ResolveOrZero<f32, Out>> ResolveOrZero<f32, Rect<Out>> for Rect<T> {
/// Converts any `parent`-relative values for Rect into an absolute Rect
fn resolve_or_zero(self, context: f32, calc: impl Fn(*const (), f32) -> f32) -> Rect<Out> {
Rect {
left: self.left.resolve_or_zero(context, &calc),
right: self.right.resolve_or_zero(context, &calc),
top: self.top.resolve_or_zero(context, &calc),
bottom: self.bottom.resolve_or_zero(context, &calc),
}
}
}
#[cfg(test)]
mod tests {
use super::{MaybeResolve, ResolveOrZero};
use crate::style_helpers::TaffyZero;
use core::fmt::Debug;
// MaybeResolve test runner
fn mr_case<Lhs, Rhs, Out>(input: Lhs, context: Rhs, expected: Out)
where
Lhs: MaybeResolve<Rhs, Out>,
Out: PartialEq + Debug,
{
assert_eq!(input.maybe_resolve(context, |_, _| 42.42), expected);
}
// ResolveOrZero test runner
fn roz_case<Lhs, Rhs, Out>(input: Lhs, context: Rhs, expected: Out)
where
Lhs: ResolveOrZero<Rhs, Out>,
Out: PartialEq + Debug + TaffyZero,
{
assert_eq!(input.resolve_or_zero(context, |_, _| 42.42), expected);
}
mod maybe_resolve_dimension {
use super::mr_case;
use crate::style::Dimension;
use crate::style_helpers::*;
/// `Dimension::Auto` should always return `None`
///
/// The parent / context should not affect the outcome.
#[test]
fn resolve_auto() {
mr_case(Dimension::AUTO, None, None);
mr_case(Dimension::AUTO, Some(5.0), None);
mr_case(Dimension::AUTO, Some(-5.0), None);
mr_case(Dimension::AUTO, Some(0.), None);
}
/// `Dimension::Length` should always return `Some(f32)`
/// where the f32 value is the inner absolute length.
///
/// The parent / context should not affect the outcome.
#[test]
fn resolve_length() {
mr_case(Dimension::from_length(1.0), None, Some(1.0));
mr_case(Dimension::from_length(1.0), Some(5.0), Some(1.0));
mr_case(Dimension::from_length(1.0), Some(-5.0), Some(1.0));
mr_case(Dimension::from_length(1.0), Some(0.), Some(1.0));
}
/// `Dimension::Percent` should return `None` if context is `None`.
/// Otherwise it should return `Some(f32)`
/// where the f32 value is the inner value of the percent * context value.
///
/// The parent / context __should__ affect the outcome.
#[test]
fn resolve_percent() {
mr_case(Dimension::from_percent(1.0), None, None);
mr_case(Dimension::from_percent(1.0), Some(5.0), Some(5.0));
mr_case(Dimension::from_percent(1.0), Some(-5.0), Some(-5.0));
mr_case(Dimension::from_percent(1.0), Some(50.0), Some(50.0));
}
}
mod maybe_resolve_size_dimension {
use super::mr_case;
use crate::geometry::Size;
use crate::style::Dimension;
/// Size<Dimension::Auto> should always return Size<None>
///
/// The parent / context should not affect the outcome.
#[test]
fn maybe_resolve_auto() {
mr_case(Size::<Dimension>::auto(), Size::NONE, Size::NONE);
mr_case(Size::<Dimension>::auto(), Size::new(5.0, 5.0), Size::NONE);
mr_case(Size::<Dimension>::auto(), Size::new(-5.0, -5.0), Size::NONE);
mr_case(Size::<Dimension>::auto(), Size::new(0.0, 0.0), Size::NONE);
}
/// Size<Dimension::Length> should always return a Size<Some(f32)>
/// where the f32 values are the absolute length.
///
/// The parent / context should not affect the outcome.
#[test]
fn maybe_resolve_length() {
mr_case(Size::from_lengths(5.0, 5.0), Size::NONE, Size::new(5.0, 5.0));
mr_case(Size::from_lengths(5.0, 5.0), Size::new(5.0, 5.0), Size::new(5.0, 5.0));
mr_case(Size::from_lengths(5.0, 5.0), Size::new(-5.0, -5.0), Size::new(5.0, 5.0));
mr_case(Size::from_lengths(5.0, 5.0), Size::new(0.0, 0.0), Size::new(5.0, 5.0));
}
/// `Size<Dimension::Percent>` should return `Size<None>` if context is `Size<None>`.
/// Otherwise it should return `Size<Some(f32)>`
/// where the f32 value is the inner value of the percent * context value.
///
/// The context __should__ affect the outcome.
#[test]
fn maybe_resolve_percent() {
mr_case(Size::from_percent(5.0, 5.0), Size::NONE, Size::NONE);
mr_case(Size::from_percent(5.0, 5.0), Size::new(5.0, 5.0), Size::new(25.0, 25.0));
mr_case(Size::from_percent(5.0, 5.0), Size::new(-5.0, -5.0), Size::new(-25.0, -25.0));
mr_case(Size::from_percent(5.0, 5.0), Size::new(0.0, 0.0), Size::new(0.0, 0.0));
}
}
mod resolve_or_zero_dimension_to_option_f32 {
use super::roz_case;
use crate::style::Dimension;
use crate::style_helpers::*;
#[test]
fn resolve_or_zero_auto() {
roz_case(Dimension::AUTO, None, 0.0);
roz_case(Dimension::AUTO, Some(5.0), 0.0);
roz_case(Dimension::AUTO, Some(-5.0), 0.0);
roz_case(Dimension::AUTO, Some(0.0), 0.0);
}
#[test]
fn resolve_or_zero_length() {
roz_case(Dimension::from_length(5.0), None, 5.0);
roz_case(Dimension::from_length(5.0), Some(5.0), 5.0);
roz_case(Dimension::from_length(5.0), Some(-5.0), 5.0);
roz_case(Dimension::from_length(5.0), Some(0.0), 5.0);
}
#[test]
fn resolve_or_zero_percent() {
roz_case(Dimension::from_percent(5.0), None, 0.0);
roz_case(Dimension::from_percent(5.0), Some(5.0), 25.0);
roz_case(Dimension::from_percent(5.0), Some(-5.0), -25.0);
roz_case(Dimension::from_percent(5.0), Some(0.0), 0.0);
}
}
mod resolve_or_zero_rect_dimension_to_rect {
use super::roz_case;
use crate::geometry::{Rect, Size};
use crate::style::Dimension;
#[test]
fn resolve_or_zero_auto() {
roz_case(Rect::<Dimension>::auto(), Size::NONE, Rect::zero());
roz_case(Rect::<Dimension>::auto(), Size::new(5.0, 5.0), Rect::zero());
roz_case(Rect::<Dimension>::auto(), Size::new(-5.0, -5.0), Rect::zero());
roz_case(Rect::<Dimension>::auto(), Size::new(0.0, 0.0), Rect::zero());
}
#[test]
fn resolve_or_zero_length() {
roz_case(Rect::from_length(5.0, 5.0, 5.0, 5.0), Size::NONE, Rect::new(5.0, 5.0, 5.0, 5.0));
roz_case(Rect::from_length(5.0, 5.0, 5.0, 5.0), Size::new(5.0, 5.0), Rect::new(5.0, 5.0, 5.0, 5.0));
roz_case(Rect::from_length(5.0, 5.0, 5.0, 5.0), Size::new(-5.0, -5.0), Rect::new(5.0, 5.0, 5.0, 5.0));
roz_case(Rect::from_length(5.0, 5.0, 5.0, 5.0), Size::new(0.0, 0.0), Rect::new(5.0, 5.0, 5.0, 5.0));
}
#[test]
fn resolve_or_zero_percent() {
roz_case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::NONE, Rect::zero());
roz_case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::new(5.0, 5.0), Rect::new(25.0, 25.0, 25.0, 25.0));
roz_case(
Rect::from_percent(5.0, 5.0, 5.0, 5.0),
Size::new(-5.0, -5.0),
Rect::new(-25.0, -25.0, -25.0, -25.0),
);
roz_case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::new(0.0, 0.0), Rect::zero());
}
}
mod resolve_or_zero_rect_dimension_to_rect_f32_via_option {
use super::roz_case;
use crate::geometry::Rect;
use crate::style::Dimension;
#[test]
fn resolve_or_zero_auto() {
roz_case(Rect::<Dimension>::auto(), None, Rect::zero());
roz_case(Rect::<Dimension>::auto(), Some(5.0), Rect::zero());
roz_case(Rect::<Dimension>::auto(), Some(-5.0), Rect::zero());
roz_case(Rect::<Dimension>::auto(), Some(0.0), Rect::zero());
}
#[test]
fn resolve_or_zero_length() {
roz_case(Rect::from_length(5.0, 5.0, 5.0, 5.0), None, Rect::new(5.0, 5.0, 5.0, 5.0));
roz_case(Rect::from_length(5.0, 5.0, 5.0, 5.0), Some(5.0), Rect::new(5.0, 5.0, 5.0, 5.0));
roz_case(Rect::from_length(5.0, 5.0, 5.0, 5.0), Some(-5.0), Rect::new(5.0, 5.0, 5.0, 5.0));
roz_case(Rect::from_length(5.0, 5.0, 5.0, 5.0), Some(0.0), Rect::new(5.0, 5.0, 5.0, 5.0));
}
#[test]
fn resolve_or_zero_percent() {
roz_case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), None, Rect::zero());
roz_case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Some(5.0), Rect::new(25.0, 25.0, 25.0, 25.0));
roz_case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Some(-5.0), Rect::new(-25.0, -25.0, -25.0, -25.0));
roz_case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Some(0.0), Rect::zero());
}
}
}
+301
View File
@@ -0,0 +1,301 @@
//! Allocator-flexible data types
// When std is enabled, prefer those types
#[cfg(feature = "std")]
pub(crate) use self::std::*;
// When alloc but not std is enabled, use those types
#[cfg(all(feature = "alloc", not(feature = "std")))]
pub(crate) use self::alloc::*;
// When neither alloc or std is enabled, use a heapless fallback
#[cfg(all(not(feature = "alloc"), not(feature = "std")))]
pub(crate) use self::core::*;
/// For when `std` is enabled
#[cfg(feature = "std")]
mod std {
// // Re-exporting a macro_rules macro doesn't work properly, so we wrap
// // it in a trivial new macro that just forwards it's input to the underlying
// // std/alloc macro
// macro_rules! format {
// ($($tokens:tt)*) => {
// ::std::format!($($tokens)*)
// };
// }
// pub(crate) use format;
pub(crate) use std::format;
/// A string
pub(crate) type String = std::string::String;
/// The default type for representing strings in Taffy styles
pub(crate) type DefaultCheapStr = String;
/// A map
pub(crate) type Map<K, V> = std::collections::HashMap<K, V, std::collections::hash_map::RandomState>;
/// An allocation-backend agnostic vector type
pub(crate) type Vec<A> = std::vec::Vec<A>;
/// A vector of child nodes
pub(crate) type ChildrenVec<A> = std::vec::Vec<A>;
#[cfg(feature = "grid")]
/// A vector of grid tracks
pub(crate) type GridTrackVec<A> = std::vec::Vec<A>;
/// Creates a new vector with the capacity for the specified number of items before it must be resized
#[must_use]
pub(crate) fn new_vec_with_capacity<A>(capacity: usize) -> Vec<A> {
Vec::with_capacity(capacity)
}
/// Creates a new vector with the capacity for the specified number of items before it must be resized
#[must_use]
pub(crate) fn single_value_vec<A>(value: A) -> Vec<A> {
vec![value]
}
/// Rounds to the nearest whole number
#[must_use]
#[inline(always)]
pub(crate) fn round(value: f32) -> f32 {
(value + 0.5).floor()
}
/// Rounds up to the nearest whole number
#[must_use]
#[inline(always)]
pub(crate) fn ceil(value: f32) -> f32 {
value.ceil()
}
/// Rounds down to the nearest whole number
#[must_use]
#[inline(always)]
pub(crate) fn floor(value: f32) -> f32 {
value.floor()
}
/// Computes the absolute value
#[must_use]
#[inline(always)]
pub(crate) fn abs(value: f32) -> f32 {
value.abs()
}
/// Returns the largest of two f32 values
#[inline(always)]
pub(crate) fn f32_max(a: f32, b: f32) -> f32 {
a.max(b)
}
/// Returns the smallest of two f32 values
#[inline(always)]
pub(crate) fn f32_min(a: f32, b: f32) -> f32 {
a.min(b)
}
}
/// For when `alloc` but not `std` is enabled
#[cfg(all(feature = "alloc", not(feature = "std")))]
mod alloc {
extern crate alloc;
use core::cmp::Ordering;
// // Re-exporting a macro_rules macro doesn't work properly, so we wrap
// // it in a trivial new macro that just forwards it's input to the underlying
// // std/alloc macro
// macro_rules! format {
// ($($tokens:tt)*) => {
// ::alloc::fmt::format!($($tokens)*)
// };
// }
// pub(crate) use format;
pub(crate) use alloc::format;
/// A string
pub(crate) type String = alloc::string::String;
/// The default type for representing strings in Taffy styles
pub(crate) type DefaultCheapStr = String;
/// A map
// TODO: consider using hashbrown
pub(crate) type Map<K, V> = alloc::collections::BTreeMap<K, V>;
/// An allocation-backend agnostic vector type
pub(crate) type Vec<A> = alloc::vec::Vec<A>;
/// A vector of child nodes
pub(crate) type ChildrenVec<A> = alloc::vec::Vec<A>;
#[cfg(feature = "grid")]
/// A vector of grid tracks
pub(crate) type GridTrackVec<A> = alloc::vec::Vec<A>;
/// Creates a new vector with the capacity for the specified number of items before it must be resized
#[must_use]
pub(crate) fn new_vec_with_capacity<A>(capacity: usize) -> Vec<A> {
Vec::with_capacity(capacity)
}
/// Creates a new vector with the capacity for the specified number of items before it must be resized
#[must_use]
pub(crate) fn single_value_vec<A>(value: A) -> Vec<A> {
let mut vec = Vec::with_capacity(1);
vec.push(value);
vec
}
/// Rounds to the nearest whole number
pub(crate) use super::polyfill::round;
/// Rounds up to the nearest whole number
pub(crate) use super::polyfill::ceil;
/// Rounds down to the nearest whole number
pub(crate) use super::polyfill::floor;
/// Computes the absolute value
pub(crate) use super::polyfill::abs;
/// Returns the largest of two f32 values
#[inline(always)]
pub(crate) fn f32_max(a: f32, b: f32) -> f32 {
a.max(b)
}
/// Returns the smallest of two f32 values
#[inline(always)]
pub(crate) fn f32_min(a: f32, b: f32) -> f32 {
a.min(b)
}
}
/// For when neither `alloc` nor `std` is enabled
#[cfg(all(not(feature = "alloc"), not(feature = "std")))]
mod core {
use core::cmp::Ordering;
/// The maximum number of nodes in the tree
pub const MAX_NODE_COUNT: usize = 256;
/// The maximum number of children of any given node
pub const MAX_CHILD_COUNT: usize = 16;
#[cfg(feature = "grid")]
/// The maximum number of children of any given node
pub const MAX_GRID_TRACKS: usize = 16;
/// A string
pub(crate) type String = &'static str;
/// The default type for representing strings in Taffy styles
pub(crate) type DefaultCheapStr = &'static str;
/// An allocation-backend agnostic vector type
pub(crate) type Vec<A> = arrayvec::ArrayVec<A, MAX_NODE_COUNT>;
/// A vector of child nodes, whose length cannot exceed [`MAX_CHILD_COUNT`]
pub(crate) type ChildrenVec<A> = arrayvec::ArrayVec<A, MAX_CHILD_COUNT>;
#[cfg(feature = "grid")]
/// A vector of grid tracks
pub(crate) type GridTrackVec<A> = arrayvec::ArrayVec<A, MAX_GRID_TRACKS>;
/// Creates a new map with the capacity for the specified number of items before it must be resized
///
/// This vector cannot be resized.
#[must_use]
pub(crate) fn new_vec_with_capacity<A, const CAP: usize>(_capacity: usize) -> arrayvec::ArrayVec<A, CAP> {
arrayvec::ArrayVec::new()
}
/// Creates a new vector with the capacity for the specified number of items before it must be resized
#[must_use]
pub(crate) fn single_value_vec<A, const CAP: usize>(value: A) -> arrayvec::ArrayVec<A, CAP> {
let mut vec = new_vec_with_capacity(1);
vec.push(value);
vec
}
/// Rounds to the nearest whole number
pub(crate) use super::polyfill::round;
/// Computes the absolute value
pub(crate) use super::polyfill::abs;
/// Returns the largest of two f32 values
#[inline(always)]
pub(crate) fn f32_max(a: f32, b: f32) -> f32 {
a.max(b)
}
/// Returns the smallest of two f32 values
#[inline(always)]
pub(crate) fn f32_min(a: f32, b: f32) -> f32 {
a.min(b)
}
}
/// Implementations of float functions for no_std and alloc builds
/// Copied from `num-traits` crate
#[cfg(not(feature = "std"))]
mod polyfill {
#[must_use]
#[inline(always)]
fn fract(value: f32) -> f32 {
if value == 0.0 {
0.0
} else {
value % 1.0
}
}
#[must_use]
#[inline(always)]
pub(crate) fn round(value: f32) -> f32 {
let f = fract(value);
if f.is_nan() || f == 0.0 {
value
} else if value > 0.0 {
if f < 0.5 {
value - f
} else {
value - f + 1.0
}
} else if -f < 0.5 {
value - f
} else {
value - f - 1.0
}
}
#[must_use]
#[inline(always)]
pub(crate) fn floor(value: f32) -> f32 {
let f = fract(value);
if f.is_nan() || f == 0.0 {
value
} else if value < 0.0 {
value - f - 1.0
} else {
value - f
}
}
#[must_use]
#[inline(always)]
pub(crate) fn ceil(value: f32) -> f32 {
let f = fract(value);
if f.is_nan() || f == 0.0 {
value
} else if value > 0.0 {
value - f + 1.0
} else {
value - f
}
}
/// Computes the absolute value
#[must_use]
#[inline(always)]
pub(crate) fn abs(value: f32) -> f32 {
if value.is_sign_positive() {
return value;
} else if value.is_sign_negative() {
return -value;
} else {
f32::NAN
}
}
}