From d6be828d17404972028f665478812bacadfd110b Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Sun, 26 Jul 2026 20:26:42 +0000 Subject: [PATCH] render: constrain shrink-wrapped grid items --- Cargo.lock | 2 - Cargo.toml | 4 + crates/obscura-render/tests/layout_test.rs | 74 + vendor/taffy/Cargo.toml | 171 ++ vendor/taffy/LICENSE | 22 + vendor/taffy/README.md | 132 + vendor/taffy/src/compute/block.rs | 1397 +++++++++ vendor/taffy/src/compute/common/alignment.rs | 117 + .../taffy/src/compute/common/content_size.rs | 34 + vendor/taffy/src/compute/common/mod.rs | 5 + vendor/taffy/src/compute/flexbox.rs | 2553 +++++++++++++++++ vendor/taffy/src/compute/float.rs | 552 ++++ vendor/taffy/src/compute/grid/alignment.rs | 405 +++ .../taffy/src/compute/grid/explicit_grid.rs | 732 +++++ .../taffy/src/compute/grid/implicit_grid.rs | 253 ++ vendor/taffy/src/compute/grid/mod.rs | 897 ++++++ vendor/taffy/src/compute/grid/placement.rs | 764 +++++ vendor/taffy/src/compute/grid/track_sizing.rs | 1418 +++++++++ .../src/compute/grid/types/cell_occupancy.rs | 301 ++ .../src/compute/grid/types/coordinates.rs | 150 + .../taffy/src/compute/grid/types/grid_item.rs | 609 ++++ .../src/compute/grid/types/grid_track.rs | 156 + .../compute/grid/types/grid_track_counts.rs | 102 + vendor/taffy/src/compute/grid/types/mod.rs | 18 + vendor/taffy/src/compute/grid/types/named.rs | 428 +++ vendor/taffy/src/compute/grid/util/mod.rs | 6 + .../src/compute/grid/util/test_helpers.rs | 43 + vendor/taffy/src/compute/leaf.rs | 164 ++ vendor/taffy/src/compute/mod.rs | 337 +++ vendor/taffy/src/geometry.rs | 741 +++++ vendor/taffy/src/lib.rs | 133 + vendor/taffy/src/prelude.rs | 31 + vendor/taffy/src/style/alignment.rs | 726 +++++ vendor/taffy/src/style/available_space.rs | 163 ++ vendor/taffy/src/style/block.rs | 64 + vendor/taffy/src/style/compact_length.rs | 540 ++++ vendor/taffy/src/style/dimension.rs | 380 +++ vendor/taffy/src/style/flex.rs | 203 ++ vendor/taffy/src/style/float.rs | 80 + vendor/taffy/src/style/grid.rs | 1607 +++++++++++ vendor/taffy/src/style/mod.rs | 1357 +++++++++ vendor/taffy/src/style_helpers.rs | 577 ++++ vendor/taffy/src/test.rs | 174 ++ vendor/taffy/src/tree/cache.rs | 284 ++ vendor/taffy/src/tree/layout.rs | 357 +++ vendor/taffy/src/tree/mod.rs | 33 + vendor/taffy/src/tree/node.rs | 60 + vendor/taffy/src/tree/taffy_tree.rs | 1405 +++++++++ vendor/taffy/src/tree/traits.rs | 418 +++ vendor/taffy/src/util/debug.rs | 143 + vendor/taffy/src/util/math.rs | 356 +++ vendor/taffy/src/util/mod.rs | 36 + vendor/taffy/src/util/parse.rs | 118 + vendor/taffy/src/util/print.rs | 78 + vendor/taffy/src/util/resolve.rs | 364 +++ vendor/taffy/src/util/sys.rs | 301 ++ 56 files changed, 22573 insertions(+), 2 deletions(-) create mode 100644 vendor/taffy/Cargo.toml create mode 100644 vendor/taffy/LICENSE create mode 100644 vendor/taffy/README.md create mode 100644 vendor/taffy/src/compute/block.rs create mode 100644 vendor/taffy/src/compute/common/alignment.rs create mode 100644 vendor/taffy/src/compute/common/content_size.rs create mode 100644 vendor/taffy/src/compute/common/mod.rs create mode 100644 vendor/taffy/src/compute/flexbox.rs create mode 100644 vendor/taffy/src/compute/float.rs create mode 100644 vendor/taffy/src/compute/grid/alignment.rs create mode 100644 vendor/taffy/src/compute/grid/explicit_grid.rs create mode 100644 vendor/taffy/src/compute/grid/implicit_grid.rs create mode 100644 vendor/taffy/src/compute/grid/mod.rs create mode 100644 vendor/taffy/src/compute/grid/placement.rs create mode 100644 vendor/taffy/src/compute/grid/track_sizing.rs create mode 100644 vendor/taffy/src/compute/grid/types/cell_occupancy.rs create mode 100644 vendor/taffy/src/compute/grid/types/coordinates.rs create mode 100644 vendor/taffy/src/compute/grid/types/grid_item.rs create mode 100644 vendor/taffy/src/compute/grid/types/grid_track.rs create mode 100644 vendor/taffy/src/compute/grid/types/grid_track_counts.rs create mode 100644 vendor/taffy/src/compute/grid/types/mod.rs create mode 100644 vendor/taffy/src/compute/grid/types/named.rs create mode 100644 vendor/taffy/src/compute/grid/util/mod.rs create mode 100644 vendor/taffy/src/compute/grid/util/test_helpers.rs create mode 100644 vendor/taffy/src/compute/leaf.rs create mode 100644 vendor/taffy/src/compute/mod.rs create mode 100644 vendor/taffy/src/geometry.rs create mode 100644 vendor/taffy/src/lib.rs create mode 100644 vendor/taffy/src/prelude.rs create mode 100644 vendor/taffy/src/style/alignment.rs create mode 100644 vendor/taffy/src/style/available_space.rs create mode 100644 vendor/taffy/src/style/block.rs create mode 100644 vendor/taffy/src/style/compact_length.rs create mode 100644 vendor/taffy/src/style/dimension.rs create mode 100644 vendor/taffy/src/style/flex.rs create mode 100644 vendor/taffy/src/style/float.rs create mode 100644 vendor/taffy/src/style/grid.rs create mode 100644 vendor/taffy/src/style/mod.rs create mode 100644 vendor/taffy/src/style_helpers.rs create mode 100644 vendor/taffy/src/test.rs create mode 100644 vendor/taffy/src/tree/cache.rs create mode 100644 vendor/taffy/src/tree/layout.rs create mode 100644 vendor/taffy/src/tree/mod.rs create mode 100644 vendor/taffy/src/tree/node.rs create mode 100644 vendor/taffy/src/tree/taffy_tree.rs create mode 100644 vendor/taffy/src/tree/traits.rs create mode 100644 vendor/taffy/src/util/debug.rs create mode 100644 vendor/taffy/src/util/math.rs create mode 100644 vendor/taffy/src/util/mod.rs create mode 100644 vendor/taffy/src/util/parse.rs create mode 100644 vendor/taffy/src/util/print.rs create mode 100644 vendor/taffy/src/util/resolve.rs create mode 100644 vendor/taffy/src/util/sys.rs diff --git a/Cargo.lock b/Cargo.lock index b33f1d6..2e94ec5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3227,8 +3227,6 @@ dependencies = [ [[package]] name = "taffy" version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73afc801dd6bd47529eaa7c7e90557f107527d1b7c9c7ed7d7803c7b8d0c357f" dependencies = [ "arrayvec", "grid", diff --git a/Cargo.toml b/Cargo.toml index 02be25b..3cfad81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,10 @@ thiserror = "2" anyhow = "1" 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 # 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" diff --git a/crates/obscura-render/tests/layout_test.rs b/crates/obscura-render/tests/layout_test.rs index 61b5ec7..4fe1d9c 100644 --- a/crates/obscura-render/tests/layout_test.rs +++ b/crates/obscura-render/tests/layout_test.rs @@ -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#" + +
+ +
+
+ + +
+
+ + +
+
+
+
+
+
+
+ "#, + ); + 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] fn opposing_floats_share_header_band_through_inline_wrapper() { let tree = parse_html(include_str!("../../../render-repros/opposing-header-floats.html")); diff --git a/vendor/taffy/Cargo.toml b/vendor/taffy/Cargo.toml new file mode 100644 index 0000000..3535960 --- /dev/null +++ b/vendor/taffy/Cargo.toml @@ -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 ", + "Johnathan Kelley ", + "Nico Burns ", +] +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" diff --git a/vendor/taffy/LICENSE b/vendor/taffy/LICENSE new file mode 100644 index 0000000..f6b3d8f --- /dev/null +++ b/vendor/taffy/LICENSE @@ -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. diff --git a/vendor/taffy/README.md b/vendor/taffy/README.md new file mode 100644 index 0000000..1091a7b --- /dev/null +++ b/vendor/taffy/README.md @@ -0,0 +1,132 @@ + +

+ + Taffy + +

+ +[![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. diff --git a/vendor/taffy/src/compute/block.rs b/vendor/taffy/src/compute/block.rs new file mode 100644 index 0000000..17fee8d --- /dev/null +++ b/vendor/taffy/src/compute/block.rs @@ -0,0 +1,1397 @@ +//! Computes the CSS block layout algorithm in the case that the block container being laid out contains only block-level boxes +use crate::geometry::{Line, Point, Rect, Size}; +use crate::style::{AvailableSpace, CoreStyle, LengthPercentageAuto, Overflow, Position}; +use crate::style_helpers::TaffyMaxContent; +use crate::tree::{CollapsibleMarginSet, Layout, LayoutInput, LayoutOutput, RunMode, SizingMode}; +use crate::tree::{LayoutPartialTree, LayoutPartialTreeExt, NodeId}; +use crate::util::debug::debug_log; +use crate::util::sys::f32_max; +use crate::util::sys::Vec; +use crate::util::MaybeMath; +use crate::util::{MaybeResolve, ResolveOrZero}; +use crate::{ + BlockContainerStyle, BlockItemStyle, BoxGenerationMode, BoxSizing, Direction, LayoutBlockContainer, RequestedAxis, + TextAlign, +}; + +#[cfg(feature = "float_layout")] +use super::float::{ContentSlot, FloatContext, FloatIntrinsicWidthCalculator}; +#[cfg(feature = "float_layout")] +use crate::{Clear, Float, FloatDirection}; + +/// Context for positioning Block and Float boxes within a Block Formatting Context +pub struct BlockFormattingContext { + /// The float positioning context that handles positioning floats within this Block Formatting Context + #[cfg(feature = "float_layout")] + float_context: FloatContext, +} + +impl Default for BlockFormattingContext { + fn default() -> Self { + Self { + #[cfg(feature = "float_layout")] + float_context: FloatContext::new(), + } + } +} + +impl BlockFormattingContext { + /// Create a new `BlockFormattingContext` with the specified width constraint + pub fn new() -> Self { + Default::default() + } + + /// Create an initial `BlockContext` for this `BlockFormattingContext` + pub fn root_block_context(&mut self) -> BlockContext<'_> { + BlockContext { + bfc: self, + y_offset: 0.0, + insets: [0.0, 0.0], + content_box_insets: [0.0, 0.0], + float_content_contribution: 0.0, + is_root: true, + } + } +} + +/// Context for each individual Block within a Block Formatting Context +/// +/// Contains a mutable reference to the BlockFormattingContext + block-specific data +pub struct BlockContext<'bfc> { + /// A mutable reference to the root BlockFormatttingContext that this BlockContext belongs to + bfc: &'bfc mut BlockFormattingContext, + /// The y-offset of the border-top of the block node, relative to the to the border-top of the + /// root node of the Block Formatting Context it belongs to. + y_offset: f32, + /// The x-inset of the border-box in from each side of the block node, relative to the root node of the Block Formatting Context it belongs to. + insets: [f32; 2], + /// The x-insets of the content box + content_box_insets: [f32; 2], + /// The height that floats take up in the element + float_content_contribution: f32, + /// Whether the node is the root of the Block Formatting Context is belongs to. + is_root: bool, +} + +impl BlockContext<'_> { + /// Create a sub-`BlockContext` for a child block node + pub fn sub_context(&mut self, additional_y_offset: f32, insets: [f32; 2]) -> BlockContext<'_> { + let insets = [self.insets[0] + insets[0], self.insets[1] + insets[1]]; + BlockContext { + bfc: self.bfc, + y_offset: self.y_offset + additional_y_offset, + insets, + content_box_insets: insets, + float_content_contribution: 0.0, + is_root: false, + } + } + + /// Returns whether this block is the root block of it's Block Formatting Context + pub fn is_bfc_root(&self) -> bool { + self.is_root + } +} + +#[cfg(feature = "float_layout")] +impl BlockContext<'_> { + /// Set the width of the overall Block Formatting Context. This is used to resolve positions + /// that are relative to the right of the context such as right-floated boxes. + /// + /// Sub-blocks within a Block Formatting Context should use the `Self::sub_context` method to create + /// a sub-`BlockContext` with `insets` instead. + pub fn set_width(&mut self, available_width: f32) { + self.bfc.float_context.set_width(available_width); + } + + /// Set the x-axis content-box insets of the `BlockContext`. These are the difference between the border-box + /// and the content-box of the box (padding + border + scrollbar_gutter). + pub fn apply_content_box_inset(&mut self, content_box_x_insets: [f32; 2]) { + self.content_box_insets[0] = self.insets[0] + content_box_x_insets[0]; + self.content_box_insets[1] = self.insets[1] + content_box_x_insets[1]; + } + + /// Whether the float context contains any floats + #[inline(always)] + pub fn has_floats(&self) -> bool { + self.bfc.float_context.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.bfc.float_context.has_active_floats(min_y + self.y_offset) + } + + /// Position a floated box with the context + pub fn place_floated_box( + &mut self, + floated_box: Size, + min_y: f32, + direction: FloatDirection, + clear: Clear, + ) -> Point { + let mut pos = self.bfc.float_context.place_floated_box( + floated_box, + min_y + self.y_offset, + self.content_box_insets, + direction, + clear, + ); + pos.y -= self.y_offset; + pos.x -= self.insets[0]; + + self.float_content_contribution = self.float_content_contribution.max(pos.y + floated_box.height); + + pos + } + + /// Search a space suitable for laying out non-floated content into + pub fn find_content_slot(&self, min_y: f32, clear: Clear, after: Option) -> ContentSlot { + let mut slot = + self.bfc.float_context.find_content_slot(min_y + self.y_offset, self.content_box_insets, clear, after); + slot.y -= self.y_offset; + slot.x -= self.insets[0]; + slot + } + + /// Get the bottom of lowest relevant float for the specific clear property + pub fn cleared_threshold(&self, clear: Clear) -> Option { + self.bfc.float_context.cleared_threshold(clear).map(|threshold| threshold - self.y_offset) + } + + /// Update the height that descendent floats with the height that floats consume + /// within a particular child + fn add_child_floated_content_height_contribution(&mut self, child_contribution: f32) { + self.float_content_contribution = self.float_content_contribution.max(child_contribution); + } + + /// Returns the height that descendent floats consume + pub fn floated_content_height_contribution(&self) -> f32 { + self.float_content_contribution + } +} + +#[cfg(not(feature = "float_layout"))] +impl BlockContext<'_> { + #[inline(always)] + /// Returns the height that descendent floats consume (always 0.0 when the float feature is disabled) + fn float_content_contribution(&self) -> f32 { + 0.0 + } +} + +use super::common::alignment::{apply_alignment_fallback, compute_alignment_offset}; +#[cfg(feature = "content_size")] +use super::common::content_size::compute_content_size_contribution; + +/// Per-child data that is accumulated and modified over the course of the layout algorithm +struct BlockItem { + /// The identifier for the associated node + node_id: NodeId, + + /// The "source order" of the item. This is the index of the item within the children iterator, + /// and controls the order in which the nodes are placed + order: u32, + + /// Items that are tables don't have stretch sizing applied to them + is_table: bool, + + /// Whether the child is a non-independent block or inline node + is_in_same_bfc: bool, + + #[cfg(feature = "float_layout")] + /// The `float` style of the node + float: Float, + #[cfg(feature = "float_layout")] + /// The `clear` style of the node + clear: Clear, + + /// The base size of this item + size: Size>, + /// The minimum allowable size of this item + min_size: Size>, + /// The maximum allowable size of this item + max_size: Size>, + + /// The overflow style of the item + overflow: Point, + /// The width of the item's scrollbars (if it has scrollbars) + scrollbar_width: f32, + + /// The position style of the item + position: Position, + /// The final offset of this item + inset: Rect, + /// The margin of this item + margin: Rect, + /// The margin of this item + padding: Rect, + /// The margin of this item + border: Rect, + /// The sum of padding and border for this item + padding_border_sum: Size, + + /// The computed border box size of this item + computed_size: Size, + /// The computed "static position" of this item. The static position is the position + /// taking into account padding, border, margins, and scrollbar_gutters but not inset + static_position: Point, + /// Whether margins can be collapsed through this item + can_be_collapsed_through: bool, + + /// Pending layout for in-flow non-floated items. Held back from `set_unrounded_layout` so the + /// post-loop `align-content` pass in `compute_inner` can shift `location.y` before commit. + final_layout: Option, +} + +/// Computes the layout of [`LayoutPartialTree`] according to the block layout algorithm +pub fn compute_block_layout( + tree: &mut impl LayoutBlockContainer, + node_id: NodeId, + inputs: LayoutInput, + block_ctx: Option<&mut BlockContext<'_>>, +) -> LayoutOutput { + let LayoutInput { known_dimensions, parent_size, run_mode, .. } = inputs; + let style = tree.get_block_container_style(node_id); + + // Pull these out earlier to avoid borrowing issues + let overflow = style.overflow(); + let is_scroll_container = overflow.x.is_scroll_container() || overflow.y.is_scroll_container(); + 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_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 = if inputs.sizing_mode == SizingMode::InherentSize { + 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) + } else { + Size::NONE + }; + + drop(style); + + // 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, + }); + + let styled_based_known_dimensions = + known_dimensions.or(min_max_definite_size).or(clamped_style_size).maybe_max(padding_border_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) } = styled_based_known_dimensions { + 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) = styled_based_known_dimensions.width { + return LayoutOutput::from_outer_size(Size { width, height: 0.0 }); + } + } + } + + // Unwrap the block formatting context if one was passed, or else create a new one + debug_log!("BLOCK"); + match block_ctx { + Some(inherited_bfc) if !is_scroll_container => compute_inner( + tree, + node_id, + LayoutInput { known_dimensions: styled_based_known_dimensions, ..inputs }, + inherited_bfc, + ), + _ => { + let mut root_bfc = BlockFormattingContext::new(); + let mut root_ctx = root_bfc.root_block_context(); + compute_inner( + tree, + node_id, + LayoutInput { known_dimensions: styled_based_known_dimensions, ..inputs }, + &mut root_ctx, + ) + } + } +} + +/// Computes the layout of [`LayoutBlockContainer`] according to the block layout algorithm +fn compute_inner( + tree: &mut impl LayoutBlockContainer, + node_id: NodeId, + inputs: LayoutInput, + #[allow(unused_mut)] mut block_ctx: &mut BlockContext<'_>, +) -> LayoutOutput { + let LayoutInput { + known_dimensions, parent_size, available_space, run_mode, vertical_margins_are_collapsible, .. + } = inputs; + + let style = tree.get_block_container_style(node_id); + let raw_padding = style.padding(); + let raw_border = style.border(); + let raw_margin = style.margin(); + let aspect_ratio = style.aspect_ratio(); + let padding = raw_padding.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis)); + let border = raw_border.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis)); + let direction = style.direction(); + + // 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 = { + let offsets = style.overflow().transpose().map(|overflow| match overflow { + Overflow::Scroll => style.scrollbar_width(), + _ => 0.0, + }); + match direction { + Direction::Ltr => Rect { top: 0.0, left: 0.0, right: offsets.x, bottom: offsets.y }, + Direction::Rtl => Rect { top: 0.0, left: offsets.x, right: 0.0, bottom: offsets.y }, + } + }; + let padding_border = padding + border; + let padding_border_size = padding_border.sum_axes(); + let content_box_inset = padding_border + scrollbar_gutter; + + // Apply content box inset + #[cfg(feature = "float_layout")] + block_ctx.apply_content_box_inset([content_box_inset.left, content_box_inset.right]); + + let box_sizing_adjustment = + if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO }; + let size = style + .size() + .maybe_resolve(parent_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(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); + + // css-sizing-4: a definite size in one axis transfers through `aspect-ratio` + // to make the other definite. Deriving it from `known_dimensions` self-gates + // the transfer — a block parent fills an axis only when it's a real + // constraint (e.g. the stretched width at final layout) and leaves it None + // while probing intrinsic sizes, so measure passes stay content-based. Only a + // newly-filled axis is adopted (and clamped); an incoming known size is left + // as the parent resolved it (re-clamping would undo padding/border overrides). + let known_dimensions = { + let derived = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size); + Size { width: known_dimensions.width.or(derived.width), height: known_dimensions.height.or(derived.height) } + }; + let container_content_box_size = known_dimensions.maybe_sub(content_box_inset.sum_axes()); + + let overflow = style.overflow(); + let is_scroll_container = overflow.x.is_scroll_container() || overflow.y.is_scroll_container(); + + // Determine margin collapsing behaviour + let own_margins_collapse_with_children = Line { + start: vertical_margins_are_collapsible.start + && !is_scroll_container + && style.position() == Position::Relative + && padding.top == 0.0 + && border.top == 0.0, + end: vertical_margins_are_collapsible.end + && !is_scroll_container + && style.position() == Position::Relative + && padding.bottom == 0.0 + && border.bottom == 0.0 + && size.height.is_none(), + }; + let has_styles_preventing_being_collapsed_through = !style.is_block() + || block_ctx.is_bfc_root() + || is_scroll_container + || style.position() == Position::Absolute + || padding.top > 0.0 + || padding.bottom > 0.0 + || border.top > 0.0 + || border.bottom > 0.0 + || matches!(size.height, Some(h) if h > 0.0) + || matches!(min_size.height, Some(h) if h > 0.0); + + let text_align = style.text_align(); + let align_content = style.align_content(); + drop(style); + + // 1. Generate items + let mut items = generate_item_list(tree, node_id, container_content_box_size); + + // 2. Compute container width + let container_outer_width = known_dimensions.width.unwrap_or_else(|| { + let available_width = available_space.width.maybe_sub(content_box_inset.horizontal_axis_sum()); + let intrinsic_width = determine_content_based_container_width(tree, &items, available_width) + + content_box_inset.horizontal_axis_sum(); + intrinsic_width.maybe_clamp(min_size.width, max_size.width).maybe_max(Some(padding_border_size.width)) + }); + + // Short-circuit if computing size and both dimensions known + if let (RunMode::ComputeSize, Some(container_outer_height)) = (run_mode, known_dimensions.height) { + return LayoutOutput::from_outer_size(Size { width: container_outer_width, height: container_outer_height }); + } + + // We can also short-circuit if the width is known and only the width has been requested. + if run_mode == RunMode::ComputeSize && inputs.axis == RequestedAxis::Horizontal { + return LayoutOutput::from_outer_size(Size { width: container_outer_width, height: 0.0 }); + } + + let container_percentage_resolution_height = + known_dimensions.height.or(size.height.maybe_max(min_size.height)).or(min_size.height); + + // 3. Perform final item layout and return content height + let resolved_padding = raw_padding.resolve_or_zero(Some(container_outer_width), |val, basis| tree.calc(val, basis)); + let resolved_border = raw_border.resolve_or_zero(Some(container_outer_width), |val, basis| tree.calc(val, basis)); + let resolved_content_box_inset = resolved_padding + resolved_border + scrollbar_gutter; + #[cfg_attr(not(feature = "content_size"), allow(unused_mut))] + let (mut inflow_content_size, mut intrinsic_outer_height, first_child_top_margin_set, last_child_bottom_margin_set) = + perform_final_layout_on_in_flow_children( + tree, + run_mode, + &mut items, + container_outer_width, + container_percentage_resolution_height, + content_box_inset, + resolved_content_box_inset, + text_align, + direction, + own_margins_collapse_with_children, + block_ctx, + ); + + // Root BFCs contain floats + #[cfg(feature = "float_layout")] + if block_ctx.is_bfc_root() || is_scroll_container { + intrinsic_outer_height = intrinsic_outer_height.max(block_ctx.floated_content_height_contribution()); + } + + let container_outer_height = known_dimensions + .height + .unwrap_or(intrinsic_outer_height.maybe_clamp(min_size.height, max_size.height)) + .maybe_max(Some(padding_border_size.height)); + let final_outer_size = Size { width: container_outer_width, height: container_outer_height }; + + // Apply `align-content` to in-flow non-floated items if requested. The per-item layouts were + // held back in `item.final_layout` so that this step can shift `location.y` before tree commit. + // + // For block layout the entire stack of in-flow children is treated as a single alignment + // subject. That means distribution keywords (`space-between`, `space-around`, + // `space-evenly`, `stretch`) must invoke the single-subject fallback unconditionally — + // which is what passing `num_items = 1` to `apply_alignment_fallback` does. The whole + // group then shifts by one offset, with zero inter-item gap. + if let Some(align_content) = align_content { + let container_inner_height = container_outer_height - resolved_content_box_inset.vertical_axis_sum(); + let inflow_content_height = intrinsic_outer_height - resolved_content_box_inset.vertical_axis_sum(); + let free_space = container_inner_height - inflow_content_height; + let any_in_flow = items.iter().any(|item| item.final_layout.is_some()); + if any_in_flow { + let keyword = apply_alignment_fallback(free_space, 1, align_content); + let group_offset = compute_alignment_offset(free_space, 1, 0.0, keyword, false, true); + for item in items.iter_mut() { + if let Some(layout) = item.final_layout.as_mut() { + layout.location.y += group_offset; + } + } + + #[cfg(feature = "content_size")] + { + inflow_content_size = Size::ZERO; + for item in items.iter() { + if let Some(layout) = item.final_layout.as_ref() { + inflow_content_size = inflow_content_size.f32_max(compute_content_size_contribution( + layout.location + + Point { x: -resolved_content_box_inset.left, y: -resolved_content_box_inset.top }, + layout.size, + layout.content_size, + item.overflow, + )); + } + } + } + } + } + + // Short-circuit if computing size + if run_mode == RunMode::ComputeSize { + return LayoutOutput::from_outer_size(final_outer_size); + } + + // Commit deferred in-flow layouts to the tree. Floated items already wrote their own layouts. + for item in items.iter() { + if let Some(layout) = item.final_layout.as_ref() { + tree.set_unrounded_layout(item.node_id, layout); + } + } + + // 4. Layout absolutely positioned children + let absolute_position_inset = resolved_border + scrollbar_gutter; + let absolute_position_area = final_outer_size - absolute_position_inset.sum_axes(); + let absolute_position_offset = Point { x: absolute_position_inset.left, y: absolute_position_inset.top }; + let absolute_content_size = perform_absolute_layout_on_absolute_children( + tree, + &items, + absolute_position_area, + absolute_position_offset, + direction, + ); + + // 5. Perform hidden layout on hidden children + let len = tree.child_count(node_id); + for order in 0..len { + let child = tree.get_child_id(node_id, order); + let child_style = tree.get_block_child_style(child); + if child_style.box_generation_mode() == BoxGenerationMode::None { + drop(child_style); + tree.set_unrounded_layout(child, &Layout::with_order(order as u32)); + tree.perform_child_layout( + child, + Size::NONE, + Size::NONE, + Size::MAX_CONTENT, + SizingMode::InherentSize, + Line::FALSE, + ); + } + } + + // 7. Determine whether this node can be collapsed through + let all_in_flow_children_can_be_collapsed_through = + items.iter().all(|item| item.position == Position::Absolute || item.can_be_collapsed_through); + let can_be_collapsed_through = + !has_styles_preventing_being_collapsed_through && all_in_flow_children_can_be_collapsed_through; + + #[cfg_attr(not(feature = "content_size"), allow(unused_variables))] + let content_size = inflow_content_size.f32_max(absolute_content_size); + + LayoutOutput { + size: final_outer_size, + #[cfg(feature = "content_size")] + content_size, + first_baselines: Point::NONE, + top_margin: if own_margins_collapse_with_children.start { + first_child_top_margin_set + } else { + let margin_top = raw_margin.top.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis)); + CollapsibleMarginSet::from_margin(margin_top) + }, + bottom_margin: if own_margins_collapse_with_children.end { + last_child_bottom_margin_set + } else { + let margin_bottom = + raw_margin.bottom.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis)); + CollapsibleMarginSet::from_margin(margin_bottom) + }, + margins_can_collapse_through: can_be_collapsed_through, + } +} + +/// Create a `Vec` of `BlockItem` structs where each item in the `Vec` represents a child of the current node +#[inline] +fn generate_item_list( + tree: &impl LayoutBlockContainer, + node: NodeId, + node_inner_size: Size>, +) -> Vec { + tree.child_ids(node) + .map(|child_node_id| (child_node_id, tree.get_block_child_style(child_node_id))) + .filter(|(_, style)| style.box_generation_mode() != BoxGenerationMode::None) + .enumerate() + .map(|(order, (child_node_id, child_style))| { + let aspect_ratio = child_style.aspect_ratio(); + let padding = child_style.padding().resolve_or_zero(node_inner_size, |val, basis| tree.calc(val, basis)); + let border = child_style.border().resolve_or_zero(node_inner_size, |val, basis| tree.calc(val, basis)); + let pb_sum = (padding + border).sum_axes(); + let box_sizing_adjustment = + if child_style.box_sizing() == BoxSizing::ContentBox { pb_sum } else { Size::ZERO }; + + let position = child_style.position(); + let overflow = child_style.overflow(); + + #[cfg(feature = "float_layout")] + let float = child_style.float(); + #[cfg(feature = "float_layout")] + let is_not_floated = float == Float::None; + + #[cfg(not(feature = "float_layout"))] + let is_not_floated = true; + + let is_block = child_style.is_block(); + let is_table = child_style.is_table(); + let is_scroll_container = overflow.x.is_scroll_container() || overflow.y.is_scroll_container(); + + let is_in_same_bfc: bool = + is_block && !is_table && position != Position::Absolute && is_not_floated && !is_scroll_container; + + BlockItem { + node_id: child_node_id, + order: order as u32, + is_table, + is_in_same_bfc, + #[cfg(feature = "float_layout")] + float, + #[cfg(feature = "float_layout")] + clear: child_style.clear(), + size: child_style + .size() + .maybe_resolve(node_inner_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment), + min_size: child_style + .min_size() + .maybe_resolve(node_inner_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment), + max_size: child_style + .max_size() + .maybe_resolve(node_inner_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment), + overflow, + scrollbar_width: child_style.scrollbar_width(), + position, + inset: child_style.inset(), + margin: child_style.margin(), + padding, + border, + padding_border_sum: pb_sum, + + // Fields to be computed later (for now we initialise with dummy values) + computed_size: Size::zero(), + static_position: Point::zero(), + can_be_collapsed_through: false, + final_layout: None, + } + }) + .collect() +} + +/// Compute the content-based width in the case that the width of the container is not known +#[inline] +fn determine_content_based_container_width( + tree: &mut impl LayoutPartialTree, + items: &[BlockItem], + available_width: AvailableSpace, +) -> f32 { + let available_space = Size { width: available_width, height: AvailableSpace::MinContent }; + + let mut max_child_width = 0.0; + #[cfg(feature = "float_layout")] + let mut float_contribution = FloatIntrinsicWidthCalculator::new(available_width); + for item in items.iter().filter(|item| item.position != Position::Absolute) { + let known_dimensions = item.size.maybe_clamp(item.min_size, item.max_size); + + let item_x_margin_sum = item + .margin + .resolve_or_zero(available_space.width.into_option(), |val, basis| tree.calc(val, basis)) + .horizontal_axis_sum(); + let width = known_dimensions.width.unwrap_or_else(|| { + tree.measure_child_size( + item.node_id, + known_dimensions, + Size::NONE, + available_space.map_width(|w| w.maybe_sub(item_x_margin_sum)), + SizingMode::InherentSize, + crate::AbsoluteAxis::Horizontal, + Line::TRUE, + ) + }); + + let width = f32_max(width, item.padding_border_sum.width) + item_x_margin_sum; + + #[cfg(feature = "float_layout")] + if let Some(direction) = item.float.float_direction() { + float_contribution.add_float(width, direction, item.clear); + continue; + } + + max_child_width = f32_max(max_child_width, width); + } + + #[cfg(feature = "float_layout")] + { + max_child_width = max_child_width.max(float_contribution.result()); + } + + max_child_width +} + +/// Compute each child's final size and position +#[inline] +#[allow(clippy::too_many_arguments)] +fn perform_final_layout_on_in_flow_children( + tree: &mut impl LayoutBlockContainer, + run_mode: RunMode, + items: &mut [BlockItem], + container_outer_width: f32, + container_percentage_resolution_height: Option, + content_box_inset: Rect, + resolved_content_box_inset: Rect, + text_align: TextAlign, + direction: Direction, + own_margins_collapse_with_children: Line, + block_ctx: &mut BlockContext<'_>, +) -> (Size, f32, CollapsibleMarginSet, CollapsibleMarginSet) { + // Resolve container_inner_width for sizing child nodes using initial content_box_inset + let container_inner_width = container_outer_width - resolved_content_box_inset.horizontal_axis_sum(); + let container_percentage_resolution_height = + container_percentage_resolution_height.maybe_sub(resolved_content_box_inset.vertical_axis_sum()); + let parent_size = Size { width: Some(container_inner_width), height: container_percentage_resolution_height }; + // Vertical available space in block flow is indefinite, NOT a min-content + // constraint: MaxContent is taffy's representation of "indefinite". + // Passing MinContent here made every descendant grid believe it was being + // sized under a min-content constraint, in which the maximize-tracks step + // has zero free space — so auto rows containing only scroll-container + // items (overflow != visible, automatic minimum size = 0) collapsed to + // zero height. Browsers size such rows to the item's content. + let available_space = + Size { width: AvailableSpace::Definite(container_inner_width), height: AvailableSpace::MaxContent }; + + // TODO: handle nested blocks with different widths + #[cfg(feature = "float_layout")] + if block_ctx.is_bfc_root() { + block_ctx.set_width(container_outer_width); + block_ctx.apply_content_box_inset([resolved_content_box_inset.left, resolved_content_box_inset.right]); + } + + #[cfg_attr(not(feature = "content_size"), allow(unused_mut))] + let mut inflow_content_size = Size::ZERO; + let mut committed_y_offset = resolved_content_box_inset.top; + let mut y_offset_for_absolute = resolved_content_box_inset.top; + let mut first_child_top_margin_set = CollapsibleMarginSet::ZERO; + let mut active_collapsible_margin_set = CollapsibleMarginSet::ZERO; + let mut is_collapsing_with_first_margin_set = true; + + #[cfg(feature = "float_layout")] + let mut has_active_floats = block_ctx.has_active_floats(committed_y_offset); + #[cfg(not(feature = "float_layout"))] + let has_active_floats = false; + #[cfg(feature = "float_layout")] + let mut y_offset_for_float = resolved_content_box_inset.top; + + for item in items.iter_mut() { + if item.position == Position::Absolute { + let x = match direction { + Direction::Ltr => resolved_content_box_inset.left, + Direction::Rtl => container_outer_width - resolved_content_box_inset.right, + }; + item.static_position = Point { x, y: y_offset_for_absolute } + } else { + let item_margin = item + .margin + .map(|margin| margin.resolve_to_option(container_outer_width, |val, basis| tree.calc(val, basis))); + let item_non_auto_margin = item_margin.map(|m| m.unwrap_or(0.0)); + let item_non_auto_x_margin_sum = item_non_auto_margin.horizontal_axis_sum(); + + let scrollbar_size = Size { + width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 }, + height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 }, + }; + + // Handle floated boxes + #[cfg(feature = "float_layout")] + if let Some(float_direction) = item.float.float_direction() { + has_active_floats = true; + + let item_layout = tree.perform_child_layout( + item.node_id, + Size::NONE, + parent_size, + // available_space, + Size::MAX_CONTENT, + SizingMode::InherentSize, + Line::TRUE, + ); + let margin_box = item_layout.size + item_non_auto_margin.sum_axes(); + + let mut location = + block_ctx.place_floated_box(margin_box, y_offset_for_float, float_direction, item.clear); + + // Ensure that content that appears after a float does not get positioned before/above the float + // + // FIXME: this isn't quite right, because a second float at the same location + // shouldn't cause content to push down to it's level + // committed_y_offset = committed_y_offset.max(location.y); + // y_offset_for_absolute = y_offset_for_absolute.max(location.y); + // y_offset_for_float = y_offset_for_float.max(location.y); + + // Convert the margin-box location returned by float placement into a border-box location + // for the output Layout + location.y += item_non_auto_margin.top; + location.x += item_non_auto_margin.left; + + // println!("BLOCK FLOATED BOX ({:?}) {:?}", item.node_id, float_direction); + // println!("w:{} h:{} x:{}, y:{}", margin_box.width, margin_box.height, location.x, location.y); + + tree.set_unrounded_layout( + item.node_id, + &Layout { + order: item.order, + size: item_layout.size, + #[cfg(feature = "content_size")] + content_size: item_layout.content_size, + scrollbar_size, + location, + padding: item.padding, + border: item.border, + margin: item_non_auto_margin, + }, + ); + + #[cfg(feature = "content_size")] + { + // TODO: Should content size of floated boxes count as "inflow_content_size" + // or should it be counted separately? + inflow_content_size = inflow_content_size.f32_max(compute_content_size_contribution( + location, + item_layout.size, + item_layout.content_size, + item.overflow, + )); + } + + continue; + } + + // Handle non-floated boxes + + let mut y_margin_offset: f32 = 0.0; + + let (stretch_width, float_avoiding_position, float_avoiding_width) = if item.is_in_same_bfc { + let stretch_width = container_inner_width - item_non_auto_x_margin_sum; + let position = Point { x: 0.0, y: 0.0 }; + let width = 0.0; + + (stretch_width, position, width) + } else { + 'block: { + // Set y_margin_offset (different bfc child) + if !is_collapsing_with_first_margin_set || !own_margins_collapse_with_children.start { + y_margin_offset = + active_collapsible_margin_set.collapse_with_margin(item_non_auto_margin.top).resolve(); + }; + let min_y = committed_y_offset + y_margin_offset; + + #[cfg(feature = "float_layout")] + if has_active_floats { + let slot = block_ctx.find_content_slot(min_y, item.clear, None); + has_active_floats = slot.segment_id.is_some(); + let stretch_width = slot.width - item_non_auto_x_margin_sum; + break 'block (stretch_width, Point { x: slot.x, y: slot.y }, slot.width); + } + + if !has_active_floats { + let stretch_width = container_inner_width - item_non_auto_x_margin_sum; + break 'block ( + stretch_width, + Point { x: resolved_content_box_inset.left, y: min_y }, + container_inner_width, + ); + } + + unreachable!("One of the above cases will always be hit"); + } + }; + + let known_dimensions = if item.is_table { + Size::NONE + } else { + item.size + .map_width(|width| { + // TODO: Allow stretch-sizing to be conditional, as there are exceptions. + // e.g. Table children of blocks do not stretch fit + Some(width.unwrap_or(stretch_width).maybe_clamp(item.min_size.width, item.max_size.width)) + }) + .maybe_clamp(item.min_size, item.max_size) + }; + + // + + let inputs = LayoutInput { + run_mode, + sizing_mode: SizingMode::InherentSize, + axis: RequestedAxis::Both, + known_dimensions, + parent_size, + available_space: available_space.map_width(|_| AvailableSpace::Definite(stretch_width)), + vertical_margins_are_collapsible: if item.is_in_same_bfc { Line::TRUE } else { Line::FALSE }, + }; + + #[cfg(feature = "float_layout")] + let clear_pos = block_ctx.cleared_threshold(item.clear).unwrap_or(0.0); + #[cfg(not(feature = "float_layout"))] + let clear_pos = 0.0; + + let item_layout = if item.is_in_same_bfc { + let width = known_dimensions + .width + .expect("Same-bfc child will always have defined width due to stretch sizing"); + + // TODO: account for auto margins + let inset_left = item_non_auto_margin.left + content_box_inset.left; + let inset_right = container_outer_width - width - inset_left; + let insets = [inset_left, inset_right]; + + // Compute child layout + let mut child_block_ctx = + block_ctx.sub_context((y_offset_for_absolute + item_non_auto_margin.top).max(clear_pos), insets); + let output = tree.compute_block_child_layout(item.node_id, inputs, Some(&mut child_block_ctx)); + + // Extract float contribution from child block context + #[cfg(feature = "float_layout")] + { + let child_contribution = child_block_ctx.floated_content_height_contribution(); + block_ctx.add_child_floated_content_height_contribution(y_offset_for_absolute + child_contribution); + } + + output + } else { + tree.compute_child_layout(item.node_id, inputs) + }; + let final_size = item_layout.size; + + let top_margin_set = item_layout.top_margin.collapse_with_margin(item_margin.top.unwrap_or(0.0)); + let bottom_margin_set = item_layout.bottom_margin.collapse_with_margin(item_margin.bottom.unwrap_or(0.0)); + + // Expand auto margins to fill available space + // Note: Vertical auto-margins for relatively positioned block items simply resolve to 0. + // See: https://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width + let free_x_space = f32_max(0.0, stretch_width - final_size.width); + let x_axis_auto_margin_size = { + let auto_margin_count = item_margin.left.is_none() as u8 + item_margin.right.is_none() as u8; + if auto_margin_count > 0 { + free_x_space / auto_margin_count as f32 + } else { + 0.0 + } + }; + let resolved_margin = Rect { + left: item_margin.left.unwrap_or(x_axis_auto_margin_size), + right: item_margin.right.unwrap_or(x_axis_auto_margin_size), + top: top_margin_set.resolve(), + bottom: bottom_margin_set.resolve(), + }; + + // Resolve item inset + let inset = item.inset.zip_size(Size { width: container_inner_width, height: 0.0 }, |p, s| { + p.maybe_resolve(s, |val, basis| tree.calc(val, basis)) + }); + let inset_offset = Point { + x: if direction.is_rtl() { + inset.right.map(|x| -x).or(inset.left).unwrap_or(0.0) + } else { + inset.left.or(inset.right.map(|x| -x)).unwrap_or(0.0) + }, + y: inset.top.or(inset.bottom.map(|x| -x)).unwrap_or(0.0), + }; + + // Set y_margin_offset (same bfc child) + if item.is_in_same_bfc + && (!is_collapsing_with_first_margin_set || !own_margins_collapse_with_children.start) + { + y_margin_offset = active_collapsible_margin_set.collapse_with_margin(resolved_margin.top).resolve() + }; + + #[cfg(feature = "float_layout")] + let float_or_not_clear = item.float.is_floated() || item.clear == Clear::None; + #[cfg(not(feature = "float_layout"))] + let float_or_not_clear = true; + + item.computed_size = item_layout.size; + item.can_be_collapsed_through = item_layout.margins_can_collapse_through && float_or_not_clear; + item.static_position = if item.is_in_same_bfc { + let uncleared_y = committed_y_offset + active_collapsible_margin_set.resolve(); + Point { + x: match direction { + Direction::Ltr => resolved_content_box_inset.left, + Direction::Rtl => container_outer_width - resolved_content_box_inset.right - final_size.width, + }, + y: uncleared_y.max(clear_pos), + } + } else { + // TODO: handle inset and margins + Point { + x: match direction { + Direction::Ltr => float_avoiding_position.x, + Direction::Rtl => float_avoiding_position.x + float_avoiding_width - final_size.width, + }, + y: float_avoiding_position.y, + } + }; + let mut location = if item.is_in_same_bfc { + Point { + x: match direction { + Direction::Ltr => resolved_content_box_inset.left + inset_offset.x + resolved_margin.left, + Direction::Rtl => { + container_outer_width + - resolved_content_box_inset.right + - final_size.width + - resolved_margin.right + + inset_offset.x + } + }, + y: committed_y_offset.max(clear_pos) + y_margin_offset + inset_offset.y, + } + } else { + // TODO: handle inset and margins + Point { + x: match direction { + Direction::Ltr => float_avoiding_position.x + resolved_margin.left + inset_offset.x, + Direction::Rtl => { + float_avoiding_position.x + float_avoiding_width - final_size.width - resolved_margin.right + + inset_offset.x + } + }, + y: float_avoiding_position.y + inset_offset.y, + } + }; + + // Apply alignment + let item_outer_width = item_layout.size.width + resolved_margin.horizontal_axis_sum(); + if item_outer_width < container_inner_width { + let free_x_space = container_inner_width - item_outer_width; + match (text_align, direction) { + (TextAlign::Auto, _) => { + // Do nothing + } + (TextAlign::LegacyLeft, Direction::Ltr) => { + // Do nothing. Left aligned by default. + } + (TextAlign::LegacyLeft, Direction::Rtl) => location.x -= free_x_space, + (TextAlign::LegacyRight, Direction::Ltr) => location.x += free_x_space, + (TextAlign::LegacyRight, Direction::Rtl) => { + // Do nothing. Right aligned by default. + } + (TextAlign::LegacyCenter, Direction::Ltr) => location.x += free_x_space / 2.0, + (TextAlign::LegacyCenter, Direction::Rtl) => location.x -= free_x_space / 2.0, + } + } + + // Defer `set_unrounded_layout` to the post-loop pass in `compute_inner` so that + // `align-content` can shift `location.y` before the layout is committed to the tree. + item.final_layout = Some(Layout { + order: item.order, + size: item_layout.size, + #[cfg(feature = "content_size")] + content_size: item_layout.content_size, + scrollbar_size, + location, + padding: item.padding, + border: item.border, + margin: resolved_margin, + }); + + #[cfg(feature = "content_size")] + { + inflow_content_size = inflow_content_size.f32_max(compute_content_size_contribution( + location + Point { x: -resolved_content_box_inset.left, y: -resolved_content_box_inset.top }, + final_size, + item_layout.content_size, + item.overflow, + )); + } + + // Update first_child_top_margin_set + if is_collapsing_with_first_margin_set { + if item.can_be_collapsed_through { + first_child_top_margin_set = first_child_top_margin_set + .collapse_with_set(top_margin_set) + .collapse_with_set(bottom_margin_set); + } else { + first_child_top_margin_set = first_child_top_margin_set.collapse_with_set(top_margin_set); + is_collapsing_with_first_margin_set = false; + } + } + + // Update active_collapsible_margin_set + if item.can_be_collapsed_through { + active_collapsible_margin_set = active_collapsible_margin_set + .collapse_with_set(top_margin_set) + .collapse_with_set(bottom_margin_set); + y_offset_for_absolute = committed_y_offset + item_layout.size.height + y_margin_offset; + #[cfg(feature = "float_layout")] + { + y_offset_for_float = committed_y_offset + item_layout.size.height + y_margin_offset; + } + } else { + committed_y_offset = location.y - inset_offset.y + item_layout.size.height; + active_collapsible_margin_set = bottom_margin_set; + y_offset_for_absolute = committed_y_offset + active_collapsible_margin_set.resolve(); + #[cfg(feature = "float_layout")] + { + y_offset_for_float = committed_y_offset; + } + } + } + } + + let last_child_bottom_margin_set = active_collapsible_margin_set; + let bottom_y_margin_offset = + if own_margins_collapse_with_children.end { 0.0 } else { last_child_bottom_margin_set.resolve() }; + + committed_y_offset += resolved_content_box_inset.bottom + bottom_y_margin_offset; + let content_height = f32_max(0.0, committed_y_offset); + (inflow_content_size, content_height, first_child_top_margin_set, last_child_bottom_margin_set) +} + +/// Perform absolute layout on all absolutely positioned children. +#[inline] +fn perform_absolute_layout_on_absolute_children( + tree: &mut impl LayoutBlockContainer, + items: &[BlockItem], + area_size: Size, + area_offset: Point, + direction: Direction, +) -> Size { + let area_width = area_size.width; + let area_height = area_size.height; + + #[cfg_attr(not(feature = "content_size"), allow(unused_mut))] + let mut absolute_content_size = Size::ZERO; + + for item in items.iter().filter(|item| item.position == Position::Absolute) { + let child_style = tree.get_block_child_style(item.node_id); + + // Skip items that are display:none or are not position:absolute + if child_style.box_generation_mode() == BoxGenerationMode::None || child_style.position() != Position::Absolute + { + continue; + } + + let aspect_ratio = child_style.aspect_ratio(); + let margin = + child_style.margin().map(|margin| margin.resolve_to_option(area_width, |val, basis| tree.calc(val, basis))); + let padding = child_style.padding().resolve_or_zero(Some(area_width), |val, basis| tree.calc(val, basis)); + let border = child_style.border().resolve_or_zero(Some(area_width), |val, basis| tree.calc(val, basis)); + let padding_border_sum = (padding + border).sum_axes(); + let box_sizing_adjustment = + if child_style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO }; + + // Resolve inset + let left = child_style.inset().left.maybe_resolve(area_width, |val, basis| tree.calc(val, basis)); + let right = child_style.inset().right.maybe_resolve(area_width, |val, basis| tree.calc(val, basis)); + let top = child_style.inset().top.maybe_resolve(area_height, |val, basis| tree.calc(val, basis)); + let bottom = child_style.inset().bottom.maybe_resolve(area_height, |val, basis| tree.calc(val, basis)); + + // Compute known dimensions from min/max/inherent size styles + let style_size = child_style + .size() + .maybe_resolve(area_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment); + let min_size = child_style + .min_size() + .maybe_resolve(area_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment) + .or(padding_border_sum.map(Some)) + .maybe_max(padding_border_sum); + let max_size = child_style + .max_size() + .maybe_resolve(area_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment); + let mut known_dimensions = style_size.maybe_clamp(min_size, max_size); + + drop(child_style); + + // Fill in width from left/right and reapply aspect ratio if: + // - Width is not already known + // - Item has both left and right inset properties set + if let (None, Some(left), Some(right)) = (known_dimensions.width, left, right) { + let new_width_raw = area_width.maybe_sub(margin.left).maybe_sub(margin.right) - left - right; + known_dimensions.width = Some(f32_max(new_width_raw, 0.0)); + known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size); + } + + // Fill in height from top/bottom and reapply aspect ratio if: + // - Height is not already known + // - Item has both top and bottom inset properties set + if let (None, Some(top), Some(bottom)) = (known_dimensions.height, top, bottom) { + let new_height_raw = area_height.maybe_sub(margin.top).maybe_sub(margin.bottom) - top - bottom; + known_dimensions.height = Some(f32_max(new_height_raw, 0.0)); + known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size); + } + + let measured_size = tree.measure_child_size_both( + item.node_id, + known_dimensions, + area_size.map(Some), + Size { + width: AvailableSpace::Definite(area_width.maybe_clamp(min_size.width, max_size.width)), + height: AvailableSpace::Definite(area_height.maybe_clamp(min_size.height, max_size.height)), + }, + SizingMode::ContentSize, + Line::FALSE, + ); + + let final_size = known_dimensions.unwrap_or(measured_size).maybe_clamp(min_size, max_size); + + let layout_output = tree.perform_child_layout( + item.node_id, + final_size.map(Some), + area_size.map(Some), + Size { + width: AvailableSpace::Definite(area_width.maybe_clamp(min_size.width, max_size.width)), + height: AvailableSpace::Definite(area_height.maybe_clamp(min_size.height, max_size.height)), + }, + SizingMode::ContentSize, + Line::FALSE, + ); + + let non_auto_margin = Rect { + left: if left.is_some() { margin.left.unwrap_or(0.0) } else { 0.0 }, + right: if right.is_some() { margin.right.unwrap_or(0.0) } else { 0.0 }, + top: if top.is_some() { margin.top.unwrap_or(0.0) } else { 0.0 }, + bottom: if bottom.is_some() { margin.bottom.unwrap_or(0.0) } else { 0.0 }, + }; + + // Expand auto margins to fill available space + // https://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width + let auto_margin = { + // Auto margins for absolutely positioned elements in block containers only resolve + // if inset is set. Otherwise they resolve to 0. + let absolute_auto_margin_space = Point { + x: right.map(|right| area_size.width - right - left.unwrap_or(0.0)).unwrap_or(final_size.width), + y: bottom.map(|bottom| area_size.height - bottom - top.unwrap_or(0.0)).unwrap_or(final_size.height), + }; + let free_space = Size { + width: absolute_auto_margin_space.x - final_size.width - non_auto_margin.horizontal_axis_sum(), + height: absolute_auto_margin_space.y - final_size.height - non_auto_margin.vertical_axis_sum(), + }; + + let auto_margin_size = Size { + // If all three of 'left', 'width', and 'right' are 'auto': First set any 'auto' values for 'margin-left' and 'margin-right' to 0. + // Then, if the 'direction' property of the element establishing the static-position containing block is 'ltr' set 'left' to the + // static position and apply rule number three below; otherwise, set 'right' to the static position and apply rule number one below. + // + // If none of the three is 'auto': If both 'margin-left' and 'margin-right' are 'auto', solve the equation under the extra constraint + // that the two margins get equal values, unless this would make them negative, in which case when direction of the containing block is + // 'ltr' ('rtl'), set 'margin-left' ('margin-right') to zero and solve for 'margin-right' ('margin-left'). If one of 'margin-left' or + // 'margin-right' is 'auto', solve the equation for that value. If the values are over-constrained, ignore the value for 'left' (in case + // the 'direction' property of the containing block is 'rtl') or 'right' (in case 'direction' is 'ltr') and solve for that value. + width: { + let auto_margin_count = margin.left.is_none() as u8 + margin.right.is_none() as u8; + if auto_margin_count == 2 + && (style_size.width.is_none() || style_size.width.unwrap() >= free_space.width) + { + 0.0 + } else if auto_margin_count > 0 { + free_space.width / auto_margin_count as f32 + } else { + 0.0 + } + }, + height: { + let auto_margin_count = margin.top.is_none() as u8 + margin.bottom.is_none() as u8; + if auto_margin_count == 2 + && (style_size.height.is_none() || style_size.height.unwrap() >= free_space.height) + { + 0.0 + } else if auto_margin_count > 0 { + free_space.height / auto_margin_count as f32 + } else { + 0.0 + } + }, + }; + + Rect { + left: margin.left.map(|_| 0.0).unwrap_or(auto_margin_size.width), + right: margin.right.map(|_| 0.0).unwrap_or(auto_margin_size.width), + top: margin.top.map(|_| 0.0).unwrap_or(auto_margin_size.height), + bottom: margin.bottom.map(|_| 0.0).unwrap_or(auto_margin_size.height), + } + }; + + let resolved_margin = Rect { + left: margin.left.unwrap_or(auto_margin.left), + right: margin.right.unwrap_or(auto_margin.right), + top: margin.top.unwrap_or(auto_margin.top), + bottom: margin.bottom.unwrap_or(auto_margin.bottom), + }; + + let x_offset = match (left, right) { + (Some(left), Some(right)) => { + if direction.is_rtl() { + area_size.width - final_size.width - right - resolved_margin.right + } else { + left + resolved_margin.left + } + } + (Some(left), None) => left + resolved_margin.left, + (None, Some(right)) => area_size.width - final_size.width - right - resolved_margin.right, + (None, None) => { + if direction.is_rtl() { + item.static_position.x - final_size.width - resolved_margin.right - area_offset.x + } else { + item.static_position.x + resolved_margin.left - area_offset.x + } + } + }; + let location = Point { + x: x_offset + area_offset.x, + y: top + .map(|top| top + resolved_margin.top) + .or(bottom.map(|bottom| area_size.height - final_size.height - bottom - resolved_margin.bottom)) + .maybe_add(area_offset.y) + .unwrap_or(item.static_position.y + resolved_margin.top), + }; + // Note: axis intentionally switched here as scrollbars take up space in the opposite axis + // to the axis in which scrolling is enabled. + let scrollbar_size = Size { + width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 }, + height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 }, + }; + + tree.set_unrounded_layout( + item.node_id, + &Layout { + order: item.order, + size: final_size, + #[cfg(feature = "content_size")] + content_size: layout_output.content_size, + scrollbar_size, + location, + padding, + border, + margin: resolved_margin, + }, + ); + + #[cfg(feature = "content_size")] + { + let relative_location = Point { x: location.x - area_offset.x, y: location.y - area_offset.y }; + absolute_content_size = absolute_content_size.f32_max(compute_content_size_contribution( + relative_location, + final_size, + layout_output.content_size, + item.overflow, + )); + } + } + + absolute_content_size +} diff --git a/vendor/taffy/src/compute/common/alignment.rs b/vendor/taffy/src/compute/common/alignment.rs new file mode 100644 index 0000000..39a96a4 --- /dev/null +++ b/vendor/taffy/src/compute/common/alignment.rs @@ -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 +/// . 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 this implementation follows +/// the resolution of . +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, + } + } +} diff --git a/vendor/taffy/src/compute/common/content_size.rs b/vendor/taffy/src/compute/common/content_size.rs new file mode 100644 index 0000000..48f0af8 --- /dev/null +++ b/vendor/taffy/src/compute/common/content_size.rs @@ -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, + size: Size, + content_size: Size, + overflow: Point, +) -> Size { + 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 + } +} diff --git a/vendor/taffy/src/compute/common/mod.rs b/vendor/taffy/src/compute/common/mod.rs new file mode 100644 index 0000000..d34eddb --- /dev/null +++ b/vendor/taffy/src/compute/common/mod.rs @@ -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; diff --git a/vendor/taffy/src/compute/flexbox.rs b/vendor/taffy/src/compute/flexbox.rs new file mode 100644 index 0000000..d7363e3 --- /dev/null +++ b/vendor/taffy/src/compute/flexbox.rs @@ -0,0 +1,2553 @@ +//! Computes the [flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) layout algorithm on [`TaffyTree`](crate::TaffyTree) according to the [spec](https://www.w3.org/TR/css-flexbox-1/) +use crate::compute::common::alignment::{compute_alignment_offset, resolve_self_alignment_safety}; +use crate::geometry::{Line, Point, Rect, Size}; +use crate::style::{ + AlignContent, AlignContentKeyword, AlignItems, AlignItemsKeyword, AlignSelf, AvailableSpace, FlexWrap, + JustifyContent, LengthPercentageAuto, Overflow, Position, +}; +use crate::style::{CoreStyle, FlexDirection, FlexboxContainerStyle, FlexboxItemStyle}; +use crate::style_helpers::{TaffyMaxContent, TaffyMinContent}; +use crate::tree::{Layout, LayoutInput, LayoutOutput, RunMode, SizingMode}; +use crate::tree::{LayoutFlexboxContainer, LayoutPartialTreeExt, NodeId}; +use crate::util::debug::debug_log; +use crate::util::sys::{f32_max, new_vec_with_capacity, Vec}; +use crate::util::MaybeMath; +use crate::util::{MaybeResolve, ResolveOrZero}; +use crate::{BoxGenerationMode, BoxSizing, Direction, RequestedAxis}; + +use super::common::alignment::apply_alignment_fallback; +#[cfg(feature = "content_size")] +use super::common::content_size::compute_content_size_contribution; + +/// The intermediate results of a flexbox calculation for a single item +struct FlexItem { + /// The identifier for the associated node + node: NodeId, + + /// The order of the node relative to it's siblings + order: u32, + + /// The base size of this item + size: Size>, + /// The minimum allowable size of this item + min_size: Size>, + /// The maximum allowable size of this item + max_size: Size>, + /// The cross-alignment of this item + align_self: AlignSelf, + + /// The overflow style of the item + overflow: Point, + /// The width of the scrollbars (if it has any) + scrollbar_width: f32, + /// The flex shrink style of the item + flex_shrink: f32, + /// The flex grow style of the item + flex_grow: f32, + + /// The minimum size of the item. This differs from min_size above because it also + /// takes into account content based automatic minimum sizes + resolved_minimum_main_size: f32, + + /// The final offset of this item + inset: Rect>, + /// The margin of this item + margin: Rect, + /// Whether each margin is an auto margin or not + margin_is_auto: Rect, + /// The padding of this item + padding: Rect, + /// The border of this item + border: Rect, + + /// The default size of this item + flex_basis: f32, + /// The default size of this item, minus padding and border + inner_flex_basis: f32, + /// The amount by which this item has deviated from its target size + violation: f32, + /// Is the size of this item locked + frozen: bool, + + /// Either the max- or min- content flex fraction + /// See https://www.w3.org/TR/css-flexbox-1/#intrinsic-main-sizes + content_flex_fraction: f32, + + /// The proposed inner size of this item + hypothetical_inner_size: Size, + /// The proposed outer size of this item + hypothetical_outer_size: Size, + /// The size that this item wants to be + target_size: Size, + /// The size that this item wants to be, plus any padding and border + outer_target_size: Size, + + /// The position of the bottom edge of this item + baseline: f32, + + /// A temporary value for the main offset + /// + /// Offset is the relative position from the item's natural flow position based on + /// relative position values, alignment, and justification. Does not include margin/padding/border. + offset_main: f32, + /// A temporary value for the cross offset + /// + /// Offset is the relative position from the item's natural flow position based on + /// relative position values, alignment, and justification. Does not include margin/padding/border. + offset_cross: f32, +} + +impl FlexItem { + /// Returns true if the item is a + fn is_scroll_container(&self) -> bool { + self.overflow.x.is_scroll_container() | self.overflow.y.is_scroll_container() + } +} + +/// A line of [`FlexItem`] used for intermediate computation +struct FlexLine<'a> { + /// The slice of items to iterate over during computation of this line + items: &'a mut [FlexItem], + /// The dimensions of the cross-axis + cross_size: f32, + /// The relative offset of the cross-axis + offset_cross: f32, +} + +/// Values that can be cached during the flexbox algorithm +struct AlgoConstants { + /// The direction of the current segment being laid out + dir: FlexDirection, + /// The layout direction of the current segment being laid out + layout_direction: Direction, + /// Is this segment a row + is_row: bool, + /// Is this segment a column + is_column: bool, + /// Is wrapping enabled (in either direction) + is_wrap: bool, + /// Is the wrap direction inverted + is_wrap_reverse: bool, + + /// The item's min_size style + min_size: Size>, + /// The item's max_size style + max_size: Size>, + /// The margin of this section + margin: Rect, + /// The border of this section + border: Rect, + /// The space between the content box and the border box. + /// This consists of padding + border + scrollbar_gutter. + content_box_inset: Rect, + /// The size reserved for scrollbar gutters in each axis + scrollbar_gutter: Point, + /// The gap of this section + gap: Size, + /// The align_items property of this node + align_items: AlignItems, + /// The align_content property of this node + align_content: AlignContent, + /// The justify_content property of this node + justify_content: Option, + + /// The border-box size of the node being laid out (if known) + node_outer_size: Size>, + /// The content-box size of the node being laid out (if known) + node_inner_size: Size>, + + /// The size of the virtual container containing the flex items. + container_size: Size, + /// The size of the internal container + inner_container_size: Size, +} + +/// Computes the layout of a box according to the flexbox algorithm +pub fn compute_flexbox_layout( + tree: &mut impl LayoutFlexboxContainer, + node: NodeId, + inputs: LayoutInput, +) -> LayoutOutput { + let LayoutInput { known_dimensions, parent_size, run_mode, .. } = inputs; + let style = tree.get_flexbox_container_style(node); + + // Pull these out earlier to avoid borrowing issues + 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_sum = padding.sum_axes() + border.sum_axes(); + let box_sizing_adjustment = + if style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } 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 = if inputs.sizing_mode == SizingMode::InherentSize { + 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) + } else { + Size::NONE + }; + + // 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, + }); + + // The size of the container should be floored by the padding and border + let styled_based_known_dimensions = + known_dimensions.or(min_max_definite_size.or(clamped_style_size).maybe_max(padding_border_sum)); + + // 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) } = styled_based_known_dimensions { + 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) = styled_based_known_dimensions.width { + return LayoutOutput::from_outer_size(Size { width, height: 0.0 }); + } + } + } + + // 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) } = styled_based_known_dimensions { + return LayoutOutput::from_outer_size(Size { width, height }); + } + } + + debug_log!("FLEX:", dbg:style.flex_direction()); + drop(style); + + compute_preliminary(tree, node, LayoutInput { known_dimensions: styled_based_known_dimensions, ..inputs }) +} + +/// Compute a preliminary size for an item +fn compute_preliminary(tree: &mut impl LayoutFlexboxContainer, node: NodeId, inputs: LayoutInput) -> LayoutOutput { + let LayoutInput { known_dimensions, parent_size, available_space, run_mode, .. } = inputs; + + // Define some general constants we will need for the remainder of the algorithm. + let mut constants = compute_constants(tree, tree.get_flexbox_container_style(node), known_dimensions, parent_size); + + // 9. Flex Layout Algorithm + + // 9.1. Initial Setup + + // 1. Generate anonymous flex items as described in §4 Flex Items. + debug_log!("generate_anonymous_flex_items"); + let mut flex_items = generate_anonymous_flex_items(tree, node, &constants); + + // 9.2. Line Length Determination + + // 2. Determine the available main and cross space for the flex items + debug_log!("determine_available_space"); + let available_space = determine_available_space(known_dimensions, available_space, &constants); + + // 3. Determine the flex base size and hypothetical main size of each item. + debug_log!("determine_flex_base_size"); + determine_flex_base_size(tree, &constants, available_space, &mut flex_items); + + #[cfg(feature = "debug")] + for item in flex_items.iter() { + debug_log!("item.flex_basis", item.flex_basis); + debug_log!("item.inner_flex_basis", item.inner_flex_basis); + debug_log!("item.hypothetical_outer_size", dbg:item.hypothetical_outer_size); + debug_log!("item.hypothetical_inner_size", dbg:item.hypothetical_inner_size); + debug_log!("item.resolved_minimum_main_size", dbg:item.resolved_minimum_main_size); + } + + // 4. Determine the main size of the flex container + // This has already been done as part of compute_constants. The inner size is exposed as constants.node_inner_size. + + // 9.3. Main Size Determination + + // 5. Collect flex items into flex lines. + debug_log!("collect_flex_lines"); + let mut flex_lines = collect_flex_lines(&constants, available_space, &mut flex_items); + + // If container size is undefined, determine the container's main size + // and then re-resolve gaps based on newly determined size + debug_log!("determine_container_main_size"); + if let Some(inner_main_size) = constants.node_inner_size.main(constants.dir) { + let outer_main_size = inner_main_size + constants.content_box_inset.main_axis_sum(constants.dir); + constants.inner_container_size.set_main(constants.dir, inner_main_size); + constants.container_size.set_main(constants.dir, outer_main_size); + } else { + // Sets constants.container_size and constants.outer_container_size + determine_container_main_size(tree, available_space, &mut flex_lines, &mut constants); + constants.node_inner_size.set_main(constants.dir, Some(constants.inner_container_size.main(constants.dir))); + constants.node_outer_size.set_main(constants.dir, Some(constants.container_size.main(constants.dir))); + + debug_log!("constants.node_outer_size", dbg:constants.node_outer_size); + debug_log!("constants.node_inner_size", dbg:constants.node_inner_size); + + // Re-resolve percentage gaps + let style = tree.get_flexbox_container_style(node); + let inner_container_size = constants.inner_container_size.main(constants.dir); + let new_gap = style + .gap() + .main(constants.dir) + .maybe_resolve(inner_container_size, |val, basis| tree.calc(val, basis)) + .unwrap_or(0.0); + constants.gap.set_main(constants.dir, new_gap); + } + + // 6. Resolve the flexible lengths of all the flex items to find their used main size. + debug_log!("resolve_flexible_lengths"); + for line in &mut flex_lines { + resolve_flexible_lengths(line, &constants); + } + + // 9.4. Cross Size Determination + + // 7. Determine the hypothetical cross size of each item. + debug_log!("determine_hypothetical_cross_size"); + for line in &mut flex_lines { + determine_hypothetical_cross_size(tree, line, &constants, available_space); + } + + // Calculate child baselines. This function is internally smart and only computes child baselines + // if they are necessary. + debug_log!("calculate_children_base_lines"); + calculate_children_base_lines(tree, known_dimensions, available_space, &mut flex_lines, &constants); + + // 8. Calculate the cross size of each flex line. + debug_log!("calculate_cross_size"); + calculate_cross_size(&mut flex_lines, known_dimensions, &constants); + + // 9. Handle 'align-content: stretch'. + debug_log!("handle_align_content_stretch"); + handle_align_content_stretch(&mut flex_lines, known_dimensions, &constants); + + // 10. Collapse visibility:collapse items. If any flex items have visibility: collapse, + // note the cross size of the line they’re in as the item’s strut size, and restart + // layout from the beginning. + // + // In this second layout round, when collecting items into lines, treat the collapsed + // items as having zero main size. For the rest of the algorithm following that step, + // ignore the collapsed items entirely (as if they were display:none) except that after + // calculating the cross size of the lines, if any line’s cross size is less than the + // largest strut size among all the collapsed items in the line, set its cross size to + // that strut size. + // + // Skip this step in the second layout round. + + // TODO implement once (if ever) we support visibility:collapse + + // 11. Determine the used cross size of each flex item. + debug_log!("determine_used_cross_size"); + determine_used_cross_size(tree, &mut flex_lines, &constants); + + // 9.5. Main-Axis Alignment + + // 12. Distribute any remaining free space. + debug_log!("distribute_remaining_free_space"); + distribute_remaining_free_space(&mut flex_lines, &constants); + + // 9.6. Cross-Axis Alignment + + // 13. Resolve cross-axis auto margins (also includes 14). + debug_log!("resolve_cross_axis_auto_margins"); + resolve_cross_axis_auto_margins(&mut flex_lines, &constants); + + // 15. Determine the flex container’s used cross size. + debug_log!("determine_container_cross_size"); + let total_line_cross_size = determine_container_cross_size(&flex_lines, known_dimensions, &mut constants); + + // We have the container size. + // If our caller does not care about performing layout we are done now. + if run_mode == RunMode::ComputeSize { + return LayoutOutput::from_outer_size(constants.container_size); + } + + // 16. Align all flex lines per align-content. + debug_log!("align_flex_lines_per_align_content"); + align_flex_lines_per_align_content(&mut flex_lines, &constants, total_line_cross_size); + + // Do a final layout pass and gather the resulting layouts + debug_log!("final_layout_pass"); + let inflow_content_size = final_layout_pass(tree, &mut flex_lines, &constants); + + // Before returning we perform absolute layout on all absolutely positioned children + debug_log!("perform_absolute_layout_on_absolute_children"); + let absolute_content_size = perform_absolute_layout_on_absolute_children(tree, node, &constants); + + debug_log!("hidden_layout"); + let len = tree.child_count(node); + for order in 0..len { + let child = tree.get_child_id(node, order); + if tree.get_flexbox_child_style(child).box_generation_mode() == BoxGenerationMode::None { + tree.set_unrounded_layout(child, &Layout::with_order(order as u32)); + tree.perform_child_layout( + child, + Size::NONE, + Size::NONE, + Size::MAX_CONTENT, + SizingMode::InherentSize, + Line::FALSE, + ); + } + } + + // 8.5. Flex Container Baselines: calculate the flex container's first baseline + // See https://www.w3.org/TR/css-flexbox-1/#flex-baselines + let first_vertical_baseline = if flex_lines.is_empty() { + None + } else { + flex_lines[0] + .items + .iter() + .find(|item| constants.is_column || item.align_self == AlignSelf::BASELINE) + .or_else(|| flex_lines[0].items.iter().next()) + .map(|child| { + let offset_vertical = if constants.is_row { child.offset_cross } else { child.offset_main }; + offset_vertical + child.baseline + }) + }; + + LayoutOutput::from_sizes_and_baselines( + constants.container_size, + inflow_content_size.f32_max(absolute_content_size), + Point { x: None, y: first_vertical_baseline }, + ) +} + +/// Compute constants that can be reused during the flexbox algorithm. +#[inline] +fn compute_constants( + tree: &impl LayoutFlexboxContainer, + style: impl FlexboxContainerStyle, + known_dimensions: Size>, + parent_size: Size>, +) -> AlgoConstants { + let dir = style.flex_direction(); + let is_row = dir.is_row(); + let is_column = dir.is_column(); + let is_wrap = matches!(style.flex_wrap(), FlexWrap::Wrap | FlexWrap::WrapReverse); + let is_wrap_reverse = style.flex_wrap() == FlexWrap::WrapReverse; + + 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_sum = padding.sum_axes() + border.sum_axes(); + let box_sizing_adjustment = + if style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO }; + + let align_items = style.align_items().unwrap_or(AlignItems::STRETCH); + let align_content = style.align_content().unwrap_or(AlignContent::STRETCH); + let justify_content = style.justify_content(); + let layout_direction = style.direction(); + + // 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 layout_direction { + Direction::Ltr => content_box_inset.right += scrollbar_gutter.x, + Direction::Rtl => content_box_inset.left += scrollbar_gutter.x, + }; + + let node_outer_size = known_dimensions; + let node_inner_size = node_outer_size.maybe_sub(content_box_inset.sum_axes()); + let gap = style.gap().resolve_or_zero(node_inner_size.or(Size::zero()), |val, basis| tree.calc(val, basis)); + + let container_size = Size::zero(); + let inner_container_size = Size::zero(); + + AlgoConstants { + dir, + layout_direction, + is_row, + is_column, + is_wrap, + is_wrap_reverse, + 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), + 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), + margin, + border, + gap, + content_box_inset, + scrollbar_gutter, + align_items, + align_content, + justify_content, + node_outer_size, + node_inner_size, + container_size, + inner_container_size, + } +} + +/// Generate anonymous flex items. +/// +/// # [9.1. Initial Setup](https://www.w3.org/TR/css-flexbox-1/#box-manip) +/// +/// - [**Generate anonymous flex items**](https://www.w3.org/TR/css-flexbox-1/#algo-anon-box) as described in [§4 Flex Items](https://www.w3.org/TR/css-flexbox-1/#flex-items). +#[inline] +fn generate_anonymous_flex_items( + tree: &impl LayoutFlexboxContainer, + node: NodeId, + constants: &AlgoConstants, +) -> Vec { + tree.child_ids(node) + .enumerate() + .map(|(index, child)| (index, child, tree.get_flexbox_child_style(child))) + .filter(|(_, _, style)| style.position() != Position::Absolute) + .filter(|(_, _, style)| style.box_generation_mode() != BoxGenerationMode::None) + .map(|(index, child, child_style)| { + let aspect_ratio = child_style.aspect_ratio(); + let padding = child_style + .padding() + .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis)); + let border = child_style + .border() + .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis)); + let pb_sum = (padding + border).sum_axes(); + let box_sizing_adjustment = + if child_style.box_sizing() == BoxSizing::ContentBox { pb_sum } else { Size::ZERO }; + FlexItem { + node: child, + order: index as u32, + size: child_style + .size() + .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment), + min_size: child_style + .min_size() + .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment), + max_size: child_style + .max_size() + .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment), + + inset: child_style + .inset() + .zip_size(constants.node_inner_size, |p, s| p.maybe_resolve(s, |val, basis| tree.calc(val, basis))), + margin: child_style + .margin() + .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis)), + margin_is_auto: child_style.margin().map(LengthPercentageAuto::is_auto), + padding: child_style + .padding() + .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis)), + border: child_style + .border() + .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis)), + align_self: child_style.align_self().unwrap_or(constants.align_items), + overflow: child_style.overflow(), + scrollbar_width: child_style.scrollbar_width(), + flex_grow: child_style.flex_grow(), + flex_shrink: child_style.flex_shrink(), + flex_basis: 0.0, + inner_flex_basis: 0.0, + violation: 0.0, + frozen: false, + + resolved_minimum_main_size: 0.0, + hypothetical_inner_size: Size::zero(), + hypothetical_outer_size: Size::zero(), + target_size: Size::zero(), + outer_target_size: Size::zero(), + content_flex_fraction: 0.0, + + baseline: 0.0, + + offset_main: 0.0, + offset_cross: 0.0, + } + }) + .collect() +} + +/// Determine the available main and cross space for the flex items. +/// +/// # [9.2. Line Length Determination](https://www.w3.org/TR/css-flexbox-1/#line-sizing) +/// +/// - [**Determine the available main and cross space for the flex items**](https://www.w3.org/TR/css-flexbox-1/#algo-available). +/// +/// For each dimension, if that dimension of the flex container’s content box is a definite size, use that; +/// if that dimension of the flex container is being sized under a min or max-content constraint, the available space in that dimension is that constraint; +/// otherwise, subtract the flex container’s margin, border, and padding from the space available to the flex container in that dimension and use that value. +/// **This might result in an infinite value**. +#[inline] +#[must_use] +fn determine_available_space( + known_dimensions: Size>, + outer_available_space: Size, + constants: &AlgoConstants, +) -> Size { + // Note: min/max/preferred size styles have already been applied to known_dimensions in the `compute` function above + let width = match known_dimensions.width { + Some(node_width) => AvailableSpace::Definite(node_width - constants.content_box_inset.horizontal_axis_sum()), + None => outer_available_space + .width + .maybe_sub(constants.margin.horizontal_axis_sum()) + .maybe_sub(constants.content_box_inset.horizontal_axis_sum()), + }; + + let height = match known_dimensions.height { + Some(node_height) => AvailableSpace::Definite(node_height - constants.content_box_inset.vertical_axis_sum()), + None => outer_available_space + .height + .maybe_sub(constants.margin.vertical_axis_sum()) + .maybe_sub(constants.content_box_inset.vertical_axis_sum()), + }; + + Size { width, height } +} + +/// Determine the flex base size and hypothetical main size of each item. +/// +/// # [9.2. Line Length Determination](https://www.w3.org/TR/css-flexbox-1/#line-sizing) +/// +/// - [**Determine the flex base size and hypothetical main size of each item:**](https://www.w3.org/TR/css-flexbox-1/#algo-main-item) +/// +/// - A. If the item has a definite used flex basis, that’s the flex base size. +/// +/// - B. If the flex item has ... +/// +/// - an intrinsic aspect ratio, +/// - a used flex basis of content, and +/// - a definite cross size, +/// +/// then the flex base size is calculated from its inner cross size and the flex item’s intrinsic aspect ratio. +/// +/// - C. If the used flex basis is content or depends on its available space, and the flex container is being sized under a min-content +/// or max-content constraint (e.g. when performing automatic table layout \[CSS21\]), size the item under that constraint. +/// The flex base size is the item’s resulting main size. +/// +/// - E. Otherwise, size the item into the available space using its used flex basis in place of its main size, treating a value of content as max-content. +/// If a cross size is needed to determine the main size (e.g. when the flex item’s main size is in its block axis) and the flex item’s cross size is auto and not definite, +/// in this calculation use fit-content as the flex item’s cross size. The flex base size is the item’s resulting main size. +/// +/// When determining the flex base size, the item’s min and max main sizes are ignored (no clamping occurs). +/// Furthermore, the sizing calculations that floor the content box size at zero when applying box-sizing are also ignored. +/// (For example, an item with a specified size of zero, positive padding, and box-sizing: border-box will have an outer flex base size of zero—and hence a negative inner flex base size.) +#[inline] +fn determine_flex_base_size( + tree: &mut impl LayoutFlexboxContainer, + constants: &AlgoConstants, + available_space: Size, + flex_items: &mut [FlexItem], +) { + let dir = constants.dir; + + for child in flex_items.iter_mut() { + let child_style = tree.get_flexbox_child_style(child.node); + + // Parent size for child sizing + let cross_axis_parent_size = constants.node_inner_size.cross(dir); + let child_parent_size = Size::from_cross(dir, cross_axis_parent_size); + + // Available space for child sizing + let cross_axis_margin_sum = constants.margin.cross_axis_sum(dir); + let child_min_cross = child.min_size.cross(dir).maybe_add(cross_axis_margin_sum); + let child_max_cross = child.max_size.cross(dir).maybe_add(cross_axis_margin_sum); + + // Clamp available space by min- and max- size + let cross_axis_available_space: AvailableSpace = match available_space.cross(dir) { + AvailableSpace::Definite(val) => AvailableSpace::Definite( + cross_axis_parent_size.unwrap_or(val).maybe_clamp(child_min_cross, child_max_cross), + ), + AvailableSpace::MinContent => match child_min_cross { + Some(min) => AvailableSpace::Definite(min), + None => AvailableSpace::MinContent, + }, + AvailableSpace::MaxContent => match child_max_cross { + Some(max) => AvailableSpace::Definite(max), + None => AvailableSpace::MaxContent, + }, + }; + + // Known dimensions for child sizing + let child_known_dimensions = { + let mut ckd = child.size.with_main(dir, None); + if child.align_self == AlignSelf::STRETCH + && !child.margin_is_auto.cross_start(constants.dir) + && !child.margin_is_auto.cross_end(constants.dir) + && ckd.cross(dir).is_none() + { + ckd.set_cross( + dir, + cross_axis_available_space.into_option().maybe_sub(child.margin.cross_axis_sum(dir)), + ); + } + ckd + }; + + let container_width = constants.node_inner_size.main(dir); + let box_sizing_adjustment = if child_style.box_sizing() == BoxSizing::ContentBox { + let padding = child_style.padding().resolve_or_zero(container_width, |val, basis| tree.calc(val, basis)); + let border = child_style.border().resolve_or_zero(container_width, |val, basis| tree.calc(val, basis)); + (padding + border).sum_axes() + } else { + Size::ZERO + } + .main(dir); + let flex_basis = child_style + .flex_basis() + .maybe_resolve(container_width, |val, basis| tree.calc(val, basis)) + .maybe_add(box_sizing_adjustment); + + drop(child_style); + + child.flex_basis = 'flex_basis: { + // A. If the item has a definite used flex basis, that’s the flex base size. + + // B. If the flex item has an intrinsic aspect ratio, + // a used flex basis of content, and a definite cross size, + // then the flex base size is calculated from its inner + // cross size and the flex item’s intrinsic aspect ratio. + + // Note: `child.size` has already been resolved against aspect_ratio in generate_anonymous_flex_items + // So B will just work here by using main_size without special handling for aspect_ratio + let main_size = child.size.main(dir); + if let Some(flex_basis) = flex_basis.or(main_size) { + break 'flex_basis flex_basis; + }; + + // C. If the used flex basis is content or depends on its available space, + // and the flex container is being sized under a min-content or max-content + // constraint (e.g. when performing automatic table layout [CSS21]), + // size the item under that constraint. The flex base size is the item’s + // resulting main size. + + // This is covered by the implementation of E below, which passes the available_space constraint + // through to the child size computation. It may need a separate implementation if/when D is implemented. + + // D. Otherwise, if the used flex basis is content or depends on its + // available space, the available main size is infinite, and the flex item’s + // inline axis is parallel to the main axis, lay the item out using the rules + // for a box in an orthogonal flow [CSS3-WRITING-MODES]. The flex base size + // is the item’s max-content main size. + + // TODO if/when vertical writing modes are supported + + // E. Otherwise, size the item into the available space using its used flex basis + // in place of its main size, treating a value of content as max-content. + // If a cross size is needed to determine the main size (e.g. when the + // flex item’s main size is in its block axis) and the flex item’s cross size + // is auto and not definite, in this calculation use fit-content as the + // flex item’s cross size. The flex base size is the item’s resulting main size. + + let child_available_space = Size::MAX_CONTENT + .with_main( + dir, + // Map AvailableSpace::Definite to AvailableSpace::MaxContent + if available_space.main(dir) == AvailableSpace::MinContent { + AvailableSpace::MinContent + } else { + AvailableSpace::MaxContent + }, + ) + .with_cross(dir, cross_axis_available_space); + + debug_log!("COMPUTE CHILD BASE SIZE:"); + break 'flex_basis tree.measure_child_size( + child.node, + child_known_dimensions, + child_parent_size, + child_available_space, + SizingMode::ContentSize, + dir.main_axis(), + Line::FALSE, + ); + }; + + // Floor flex-basis by the padding_border_sum (floors inner_flex_basis at zero) + // This seems to be in violation of the spec which explicitly states that the content box should not be floored at zero + // (like it usually is) when calculating the flex-basis. But including this matches both Chrome and Firefox's behaviour. + // + // TODO: resolve spec violation + // Spec: https://www.w3.org/TR/css-flexbox-1/#intrinsic-item-contributions + // Spec: https://www.w3.org/TR/css-flexbox-1/#change-2016-max-contribution + let padding_border_sum = child.padding.main_axis_sum(constants.dir) + child.border.main_axis_sum(constants.dir); + child.flex_basis = child.flex_basis.max(padding_border_sum); + + // The hypothetical main size is the item’s flex base size clamped according to its + // used min and max main sizes (and flooring the content box size at zero). + + child.inner_flex_basis = + child.flex_basis - child.padding.main_axis_sum(constants.dir) - child.border.main_axis_sum(constants.dir); + + let padding_border_axes_sums = (child.padding + child.border).sum_axes().map(Some); + + // Note that it is important that the `parent_size` parameter in the main axis is not set for this + // function call as it used for resolving percentages, and percentage size in an axis should not contribute + // to a min-content contribution in that same axis. However the `parent_size` and `available_space` *should* + // be set to their usual values in the cross axis so that wrapping content can wrap correctly. + // + // See https://drafts.csswg.org/css-sizing-3/#min-percentage-contribution + let style_min_main_size = + child.min_size.or(child.overflow.map(Overflow::maybe_into_automatic_min_size).into()).main(dir); + + child.resolved_minimum_main_size = style_min_main_size.unwrap_or({ + let min_content_main_size = { + let child_available_space = Size::MIN_CONTENT.with_cross(dir, cross_axis_available_space); + + debug_log!("COMPUTE CHILD MIN SIZE:"); + tree.measure_child_size( + child.node, + child_known_dimensions, + child_parent_size, + child_available_space, + SizingMode::ContentSize, + dir.main_axis(), + Line::FALSE, + ) + }; + + // 4.5. Automatic Minimum Size of Flex Items + // https://www.w3.org/TR/css-flexbox-1/#min-size-auto + let clamped_min_content_size = + min_content_main_size.maybe_min(child.size.main(dir)).maybe_min(child.max_size.main(dir)); + clamped_min_content_size.maybe_max(padding_border_axes_sums.main(dir)) + }); + + let hypothetical_inner_min_main = + child.resolved_minimum_main_size.maybe_max(padding_border_axes_sums.main(constants.dir)); + let hypothetical_inner_size = + child.flex_basis.maybe_clamp(Some(hypothetical_inner_min_main), child.max_size.main(constants.dir)); + let hypothetical_outer_size = hypothetical_inner_size + child.margin.main_axis_sum(constants.dir); + + child.hypothetical_inner_size.set_main(constants.dir, hypothetical_inner_size); + child.hypothetical_outer_size.set_main(constants.dir, hypothetical_outer_size); + } +} + +/// Collect flex items into flex lines. +/// +/// # [9.3. Main Size Determination](https://www.w3.org/TR/css-flexbox-1/#main-sizing) +/// +/// - [**Collect flex items into flex lines**](https://www.w3.org/TR/css-flexbox-1/#algo-line-break): +/// +/// - If the flex container is single-line, collect all the flex items into a single flex line. +/// +/// - Otherwise, starting from the first uncollected item, collect consecutive items one by one until the first time that the next collected item would not fit into the flex container’s inner main size +/// (or until a forced break is encountered, see [§10 Fragmenting Flex Layout](https://www.w3.org/TR/css-flexbox-1/#pagination)). +/// If the very first uncollected item wouldn't fit, collect just it into the line. +/// +/// For this step, the size of a flex item is its outer hypothetical main size. (**Note: This can be negative**.) +/// +/// Repeat until all flex items have been collected into flex lines. +/// +/// **Note that the "collect as many" line will collect zero-sized flex items onto the end of the previous line even if the last non-zero item exactly "filled up" the line**. +#[inline] +fn collect_flex_lines<'a>( + constants: &AlgoConstants, + available_space: Size, + flex_items: &'a mut Vec, +) -> Vec> { + if !constants.is_wrap { + let mut lines = new_vec_with_capacity(1); + lines.push(FlexLine { items: flex_items.as_mut_slice(), cross_size: 0.0, offset_cross: 0.0 }); + lines + } else { + let main_axis_available_space = match constants.max_size.main(constants.dir) { + Some(max_size) => AvailableSpace::Definite( + available_space + .main(constants.dir) + .into_option() + .unwrap_or(max_size) + .maybe_max(constants.min_size.main(constants.dir)), + ), + None => available_space.main(constants.dir), + }; + + match main_axis_available_space { + // If we're sizing under a max-content constraint then the flex items will never wrap + // (at least for now - future extensions to the CSS spec may add provisions for forced wrap points) + AvailableSpace::MaxContent => { + let mut lines = new_vec_with_capacity(1); + lines.push(FlexLine { items: flex_items.as_mut_slice(), cross_size: 0.0, offset_cross: 0.0 }); + lines + } + // If flex-wrap is Wrap and we're sizing under a min-content constraint, then we take every possible wrapping opportunity + // and place each item in it's own line + AvailableSpace::MinContent => { + let mut lines = new_vec_with_capacity(flex_items.len()); + let mut items = &mut flex_items[..]; + while !items.is_empty() { + let (line_items, rest) = items.split_at_mut(1); + lines.push(FlexLine { items: line_items, cross_size: 0.0, offset_cross: 0.0 }); + items = rest; + } + lines + } + AvailableSpace::Definite(main_axis_available_space) => { + let mut lines = new_vec_with_capacity(1); + let mut flex_items = &mut flex_items[..]; + let main_axis_gap = constants.gap.main(constants.dir); + + while !flex_items.is_empty() { + // Find index of the first item in the next line + // (or the last item if all remaining items are in the current line) + let mut line_length = 0.0; + let index = flex_items + .iter() + .enumerate() + .find(|&(idx, child)| { + // Gaps only occur between items (not before the first one or after the last one) + // So first item in the line does not contribute a gap to the line length + let gap_contribution = if idx == 0 { 0.0 } else { main_axis_gap }; + line_length += child.hypothetical_outer_size.main(constants.dir) + gap_contribution; + line_length > main_axis_available_space && idx != 0 + }) + .map(|(idx, _)| idx) + .unwrap_or(flex_items.len()); + + let (items, rest) = flex_items.split_at_mut(index); + lines.push(FlexLine { items, cross_size: 0.0, offset_cross: 0.0 }); + flex_items = rest; + } + lines + } + } + } +} + +/// Determine the container's main size (if not already known) +fn determine_container_main_size( + tree: &mut impl LayoutFlexboxContainer, + available_space: Size, + lines: &mut [FlexLine<'_>], + constants: &mut AlgoConstants, +) { + let dir = constants.dir; + let main_content_box_inset = constants.content_box_inset.main_axis_sum(constants.dir); + + let outer_main_size: f32 = constants.node_outer_size.main(constants.dir).unwrap_or_else(|| { + match available_space.main(dir) { + AvailableSpace::Definite(main_axis_available_space) => { + let longest_line_length: f32 = lines + .iter() + .map(|line| { + let line_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len()); + let total_target_size = line + .items + .iter() + .map(|child| { + let padding_border_sum = (child.padding + child.border).main_axis_sum(constants.dir); + (child.flex_basis.maybe_max(child.min_size.main(constants.dir)) + + child.margin.main_axis_sum(constants.dir)) + .max(padding_border_sum) + }) + .sum::(); + total_target_size + line_main_axis_gap + }) + .max_by(|a, b| a.total_cmp(b)) + .unwrap_or(0.0); + let size = longest_line_length + main_content_box_inset; + if lines.len() > 1 { + f32_max(size, main_axis_available_space) + } else { + size + } + } + AvailableSpace::MinContent if constants.is_wrap => { + let longest_line_length: f32 = lines + .iter() + .map(|line| { + let line_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len()); + let total_target_size = line + .items + .iter() + .map(|child| { + let padding_border_sum = (child.padding + child.border).main_axis_sum(constants.dir); + (child.flex_basis.maybe_max(child.min_size.main(constants.dir)) + + child.margin.main_axis_sum(constants.dir)) + .max(padding_border_sum) + }) + .sum::(); + total_target_size + line_main_axis_gap + }) + .max_by(|a, b| a.total_cmp(b)) + .unwrap_or(0.0); + longest_line_length + main_content_box_inset + } + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + // Define a base main_size variable. This is mutated once for iteration over the outer + // loop over the flex lines as: + // "The flex container’s max-content size is the largest sum of the afore-calculated sizes of all items within a single line." + let mut main_size = 0.0; + + for line in lines.iter_mut() { + for item in line.items.iter_mut() { + let style_min = item.min_size.main(constants.dir); + let style_preferred = item.size.main(constants.dir); + let style_max = item.max_size.main(constants.dir); + + // The spec seems a bit unclear on this point (my initial reading was that the `.maybe_max(style_preferred)` should + // not be included here), however this matches both Chrome and Firefox as of 9th March 2023. + // + // Spec: https://www.w3.org/TR/css-flexbox-1/#intrinsic-item-contributions + // Spec modification: https://www.w3.org/TR/css-flexbox-1/#change-2016-max-contribution + // Issue: https://github.com/w3c/csswg-drafts/issues/1435 + // Gentest: padding_border_overrides_size_flex_basis_0.html + let clamping_basis = Some(item.flex_basis).maybe_max(style_preferred); + let flex_basis_min = clamping_basis.filter(|_| item.flex_shrink == 0.0); + let flex_basis_max = clamping_basis.filter(|_| item.flex_grow == 0.0); + + let min_main_size = style_min + .maybe_max(flex_basis_min) + .or(flex_basis_min) + .unwrap_or(item.resolved_minimum_main_size) + .max(item.resolved_minimum_main_size); + let max_main_size = + style_max.maybe_min(flex_basis_max).or(flex_basis_max).unwrap_or(f32::INFINITY); + + let content_contribution = match (min_main_size, style_preferred, max_main_size) { + // If the clamping values are such that max <= min, then we can avoid the expensive step of computing the content size + // as we know that the clamping values will override it anyway + (min, Some(pref), max) if max <= min || max <= pref => { + pref.min(max).max(min) + item.margin.main_axis_sum(constants.dir) + } + (min, _, max) if max <= min => min + item.margin.main_axis_sum(constants.dir), + + // Else compute the min- or -max content size and apply the full formula for computing the + // min- or max- content contribution + _ if item.is_scroll_container() => { + item.flex_basis + item.margin.main_axis_sum(constants.dir) + } + _ => { + // Parent size for child sizing + let cross_axis_parent_size = constants.node_inner_size.cross(dir); + + // Available space for child sizing + let cross_axis_margin_sum = constants.margin.cross_axis_sum(dir); + let child_min_cross = item.min_size.cross(dir).maybe_add(cross_axis_margin_sum); + let child_max_cross = item.max_size.cross(dir).maybe_add(cross_axis_margin_sum); + let cross_axis_available_space: AvailableSpace = available_space + .cross(dir) + .map_definite_value(|val| cross_axis_parent_size.unwrap_or(val)) + .maybe_clamp(child_min_cross, child_max_cross); + + let child_available_space = available_space.with_cross(dir, cross_axis_available_space); + + // Known dimensions for child sizing + let child_known_dimensions = { + let mut ckd = item.size.with_main(dir, None); + if item.align_self == AlignSelf::STRETCH && ckd.cross(dir).is_none() { + ckd.set_cross( + dir, + cross_axis_available_space + .into_option() + .maybe_sub(item.margin.cross_axis_sum(dir)), + ); + } + ckd + }; + + // Either the min- or max- content size depending on which constraint we are sizing under. + // TODO: Optimise by using already computed values where available + debug_log!("COMPUTE CHILD BASE SIZE (for intrinsic main size):"); + let content_main_size = tree.measure_child_size( + item.node, + child_known_dimensions, + constants.node_inner_size, + child_available_space, + SizingMode::InherentSize, + dir.main_axis(), + Line::FALSE, + ) + item.margin.main_axis_sum(constants.dir); + + // This is somewhat bizarre in that it's asymmetrical depending whether the flex container is a column or a row. + // + // I *think* this might relate to https://drafts.csswg.org/css-flexbox-1/#algo-main-container: + // + // "The automatic block size of a block-level flex container is its max-content size." + // + // Which could suggest that flex-basis defining a vertical size does not shrink because it is in the block axis, and the automatic size + // in the block axis is a MAX content size. Whereas a flex-basis defining a horizontal size does shrink because the automatic size in + // inline axis is MIN content size (although I don't have a reference for that). + // + // Ultimately, this was not found by reading the spec, but by trial and error fixing tests to align with Webkit/Firefox output. + // (see the `flex_basis_unconstraint_row` and `flex_basis_uncontraint_column` generated tests which demonstrate this) + if constants.is_row { + content_main_size.maybe_clamp(style_min, style_max).max(main_content_box_inset) + } else { + content_main_size + .max(item.flex_basis) + .maybe_clamp(style_min, style_max) + .max(main_content_box_inset) + } + } + }; + item.content_flex_fraction = { + let diff = content_contribution - item.flex_basis; + if diff > 0.0 { + diff / f32_max(1.0, item.flex_grow) + } else if diff < 0.0 { + let scaled_shrink_factor = f32_max(1.0, item.flex_shrink * item.inner_flex_basis); + diff / scaled_shrink_factor + } else { + // We are assuming that diff is 0.0 here and that we haven't accidentally introduced a NaN + 0.0 + } + }; + } + + // TODO Spec says to scale everything by the line's max flex fraction. But neither Chrome nor firefox implement this + // so we don't either. But if we did want to, we'd need this computation here (and to use it below): + // + // Within each line, find the largest max-content flex fraction among all the flex items. + // let line_flex_fraction = line + // .items + // .iter() + // .map(|item| item.content_flex_fraction) + // .max_by(|a, b| a.total_cmp(b)) + // .unwrap_or(0.0); // Unwrap case never gets hit because there is always at least one item a line + + // Add each item’s flex base size to the product of: + // - its flex grow factor (or scaled flex shrink factor,if the chosen max-content flex fraction was negative) + // - the chosen max-content flex fraction + // then clamp that result by the max main size floored by the min main size. + // + // The flex container’s max-content size is the largest sum of the afore-calculated sizes of all items within a single line. + let item_main_size_sum = line + .items + .iter_mut() + .map(|item| { + let flex_fraction = item.content_flex_fraction; + // let flex_fraction = line_flex_fraction; + + let flex_contribution = if item.content_flex_fraction > 0.0 { + f32_max(1.0, item.flex_grow) * flex_fraction + } else if item.content_flex_fraction < 0.0 { + let scaled_shrink_factor = f32_max(1.0, item.flex_shrink) * item.inner_flex_basis; + scaled_shrink_factor * flex_fraction + } else { + 0.0 + }; + let size = item.flex_basis + flex_contribution; + item.outer_target_size.set_main(constants.dir, size); + item.target_size.set_main(constants.dir, size); + size + }) + .sum::(); + + let gap_sum = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len()); + main_size = f32_max(main_size, item_main_size_sum + gap_sum) + } + + main_size + main_content_box_inset + } + } + }); + + let outer_main_size = outer_main_size + .maybe_clamp(constants.min_size.main(constants.dir), constants.max_size.main(constants.dir)) + .max(main_content_box_inset - constants.scrollbar_gutter.main(constants.dir)); + + // let outer_main_size = inner_main_size + constants.padding_border.main_axis_sum(constants.dir); + let inner_main_size = f32_max(outer_main_size - main_content_box_inset, 0.0); + constants.container_size.set_main(constants.dir, outer_main_size); + constants.inner_container_size.set_main(constants.dir, inner_main_size); + constants.node_inner_size.set_main(constants.dir, Some(inner_main_size)); +} + +/// Resolve the flexible lengths of the items within a flex line. +/// Sets the `main` component of each item's `target_size` and `outer_target_size` +/// +/// # [9.7. Resolving Flexible Lengths](https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths) +#[inline] +fn resolve_flexible_lengths(line: &mut FlexLine, constants: &AlgoConstants) { + let total_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len()); + + // 1. Determine the used flex factor. Sum the outer hypothetical main sizes of all + // items on the line. If the sum is less than the flex container’s inner main size, + // use the flex grow factor for the rest of this algorithm; otherwise, use the + // flex shrink factor. + + let total_hypothetical_outer_main_size = + line.items.iter().map(|child| child.hypothetical_outer_size.main(constants.dir)).sum::(); + let used_flex_factor: f32 = total_main_axis_gap + total_hypothetical_outer_main_size; + let growing = used_flex_factor < constants.node_inner_size.main(constants.dir).unwrap_or(0.0); + let shrinking = used_flex_factor > constants.node_inner_size.main(constants.dir).unwrap_or(0.0); + let exactly_sized = !growing & !shrinking; + + // 2. Size inflexible items. Freeze, setting its target main size to its hypothetical main size + // - Any item that has a flex factor of zero + // - If using the flex grow factor: any item that has a flex base size + // greater than its hypothetical main size + // - If using the flex shrink factor: any item that has a flex base size + // smaller than its hypothetical main size + + for child in line.items.iter_mut() { + let inner_target_size = child.hypothetical_inner_size.main(constants.dir); + child.target_size.set_main(constants.dir, inner_target_size); + + if exactly_sized + || (child.flex_grow == 0.0 && child.flex_shrink == 0.0) + || (growing && child.flex_basis > child.hypothetical_inner_size.main(constants.dir)) + || (shrinking && child.flex_basis < child.hypothetical_inner_size.main(constants.dir)) + { + child.frozen = true; + let outer_target_size = inner_target_size + child.margin.main_axis_sum(constants.dir); + child.outer_target_size.set_main(constants.dir, outer_target_size); + } + } + + if exactly_sized { + return; + } + + // 3. Calculate initial free space. Sum the outer sizes of all items on the line, + // and subtract this from the flex container’s inner main size. For frozen items, + // use their outer target main size; for other items, use their outer flex base size. + + let used_space: f32 = total_main_axis_gap + + line + .items + .iter() + .map(|child| { + if child.frozen { + child.outer_target_size.main(constants.dir) + } else { + child.flex_basis + child.margin.main_axis_sum(constants.dir) + } + }) + .sum::(); + + let initial_free_space = constants.node_inner_size.main(constants.dir).maybe_sub(used_space).unwrap_or(0.0); + + // 4. Loop + + loop { + // a. Check for flexible items. If all the flex items on the line are frozen, + // free space has been distributed; exit this loop. + + if line.items.iter().all(|child| child.frozen) { + break; + } + + // b. Calculate the remaining free space as for initial free space, above. + // If the sum of the unfrozen flex items’ flex factors is less than one, + // multiply the initial free space by this sum. If the magnitude of this + // value is less than the magnitude of the remaining free space, use this + // as the remaining free space. + + let used_space: f32 = total_main_axis_gap + + line + .items + .iter() + .map(|child| { + if child.frozen { + child.outer_target_size.main(constants.dir) + } else { + child.flex_basis + child.margin.main_axis_sum(constants.dir) + } + }) + .sum::(); + + let mut unfrozen: Vec<&mut FlexItem> = line.items.iter_mut().filter(|child| !child.frozen).collect(); + + let (sum_flex_grow, sum_flex_shrink): (f32, f32) = + unfrozen.iter().fold((0.0, 0.0), |(flex_grow, flex_shrink), item| { + (flex_grow + item.flex_grow, flex_shrink + item.flex_shrink) + }); + + let free_space = if growing && sum_flex_grow < 1.0 { + (initial_free_space * sum_flex_grow - total_main_axis_gap) + .maybe_min(constants.node_inner_size.main(constants.dir).maybe_sub(used_space)) + } else if shrinking && sum_flex_shrink < 1.0 { + (initial_free_space * sum_flex_shrink - total_main_axis_gap) + .maybe_max(constants.node_inner_size.main(constants.dir).maybe_sub(used_space)) + } else { + (constants.node_inner_size.main(constants.dir).maybe_sub(used_space)) + .unwrap_or(used_flex_factor - used_space) + }; + + // c. Distribute free space proportional to the flex factors. + // - If the remaining free space is zero + // Do Nothing + // - If using the flex grow factor + // Find the ratio of the item’s flex grow factor to the sum of the + // flex grow factors of all unfrozen items on the line. Set the item’s + // target main size to its flex base size plus a fraction of the remaining + // free space proportional to the ratio. + // - If using the flex shrink factor + // For every unfrozen item on the line, multiply its flex shrink factor by + // its inner flex base size, and note this as its scaled flex shrink factor. + // Find the ratio of the item’s scaled flex shrink factor to the sum of the + // scaled flex shrink factors of all unfrozen items on the line. Set the item’s + // target main size to its flex base size minus a fraction of the absolute value + // of the remaining free space proportional to the ratio. Note this may result + // in a negative inner main size; it will be corrected in the next step. + // - Otherwise + // Do Nothing + + if free_space.is_normal() { + if growing && sum_flex_grow > 0.0 { + for child in &mut unfrozen { + child + .target_size + .set_main(constants.dir, child.flex_basis + free_space * (child.flex_grow / sum_flex_grow)); + } + } else if shrinking && sum_flex_shrink > 0.0 { + let sum_scaled_shrink_factor: f32 = + unfrozen.iter().map(|child| child.inner_flex_basis * child.flex_shrink).sum(); + + if sum_scaled_shrink_factor > 0.0 { + for child in &mut unfrozen { + let scaled_shrink_factor = child.inner_flex_basis * child.flex_shrink; + child.target_size.set_main( + constants.dir, + child.flex_basis + free_space * (scaled_shrink_factor / sum_scaled_shrink_factor), + ) + } + } + } + } + + // d. Fix min/max violations. Clamp each non-frozen item’s target main size by its + // used min and max main sizes and floor its content-box size at zero. If the + // item’s target main size was made smaller by this, it’s a max violation. + // If the item’s target main size was made larger by this, it’s a min violation. + + let total_violation = unfrozen.iter_mut().fold(0.0, |acc, child| -> f32 { + let resolved_min_main: Option = child.resolved_minimum_main_size.into(); + let max_main = child.max_size.main(constants.dir); + let clamped = child.target_size.main(constants.dir).maybe_clamp(resolved_min_main, max_main).max(0.0); + child.violation = clamped - child.target_size.main(constants.dir); + child.target_size.set_main(constants.dir, clamped); + child.outer_target_size.set_main( + constants.dir, + child.target_size.main(constants.dir) + child.margin.main_axis_sum(constants.dir), + ); + + acc + child.violation + }); + + // e. Freeze over-flexed items. The total violation is the sum of the adjustments + // from the previous step ∑(clamped size - unclamped size). If the total violation is: + // - Zero + // Freeze all items. + // - Positive + // Freeze all the items with min violations. + // - Negative + // Freeze all the items with max violations. + + for child in &mut unfrozen { + match total_violation { + v if v > 0.0 => child.frozen = child.violation > 0.0, + v if v < 0.0 => child.frozen = child.violation < 0.0, + _ => child.frozen = true, + } + } + + // f. Return to the start of this loop. + } +} + +/// Determine the hypothetical cross size of each item. +/// +/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing) +/// +/// - [**Determine the hypothetical cross size of each item**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-item) +/// by performing layout with the used main size and the available space, treating auto as fit-content. +#[inline] +fn determine_hypothetical_cross_size( + tree: &mut impl LayoutFlexboxContainer, + line: &mut FlexLine, + constants: &AlgoConstants, + available_space: Size, +) { + for child in line.items.iter_mut() { + let padding_border_sum = (child.padding + child.border).cross_axis_sum(constants.dir); + + let child_known_main = constants.container_size.main(constants.dir).into(); + + let child_cross = child + .size + .cross(constants.dir) + .maybe_clamp(child.min_size.cross(constants.dir), child.max_size.cross(constants.dir)) + .maybe_max(padding_border_sum); + + let child_available_cross = available_space + .cross(constants.dir) + .maybe_clamp(child.min_size.cross(constants.dir), child.max_size.cross(constants.dir)) + .maybe_max(padding_border_sum); + + let child_inner_cross = child_cross.unwrap_or_else(|| { + tree.measure_child_size( + child.node, + Size { + width: if constants.is_row { child.target_size.width.into() } else { child_cross }, + height: if constants.is_row { child_cross } else { child.target_size.height.into() }, + }, + constants.node_inner_size, + Size { + width: if constants.is_row { child_known_main } else { child_available_cross }, + height: if constants.is_row { child_available_cross } else { child_known_main }, + }, + SizingMode::ContentSize, + constants.dir.cross_axis(), + Line::FALSE, + ) + .maybe_clamp(child.min_size.cross(constants.dir), child.max_size.cross(constants.dir)) + .max(padding_border_sum) + }); + let child_outer_cross = child_inner_cross + child.margin.cross_axis_sum(constants.dir); + + child.hypothetical_inner_size.set_cross(constants.dir, child_inner_cross); + child.hypothetical_outer_size.set_cross(constants.dir, child_outer_cross); + } +} + +/// Calculate the base lines of the children. +#[inline] +fn calculate_children_base_lines( + tree: &mut impl LayoutFlexboxContainer, + node_size: Size>, + available_space: Size, + flex_lines: &mut [FlexLine], + constants: &AlgoConstants, +) { + // Only compute baselines for flex rows because we only support baseline alignment in the cross axis + // where that axis is also the inline axis + // TODO: this may need revisiting if/when we support vertical writing modes + if !constants.is_row { + return; + } + + for line in flex_lines { + // If a flex line has one or zero items participating in baseline alignment then baseline alignment is a no-op so we skip + let line_baseline_child_count = + line.items.iter().filter(|child| child.align_self == AlignSelf::BASELINE).count(); + if line_baseline_child_count <= 1 { + continue; + } + + for child in line.items.iter_mut() { + // Only calculate baselines for children participating in baseline alignment + if child.align_self != AlignSelf::BASELINE { + continue; + } + + let measured_size_and_baselines = tree.perform_child_layout( + child.node, + Size { + width: if constants.is_row { + child.target_size.width.into() + } else { + child.hypothetical_inner_size.width.into() + }, + height: if constants.is_row { + child.hypothetical_inner_size.height.into() + } else { + child.target_size.height.into() + }, + }, + constants.node_inner_size, + Size { + width: if constants.is_row { + constants.container_size.width.into() + } else { + available_space.width.maybe_set(node_size.width) + }, + height: if constants.is_row { + available_space.height.maybe_set(node_size.height) + } else { + constants.container_size.height.into() + }, + }, + SizingMode::ContentSize, + Line::FALSE, + ); + + let baseline = measured_size_and_baselines.first_baselines.y; + let height = measured_size_and_baselines.size.height; + + child.baseline = baseline.unwrap_or(height) + child.margin.top; + } + } +} + +/// Calculate the cross size of each flex line. +/// +/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing) +/// +/// - [**Calculate the cross size of each flex line**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-line). +#[inline] +fn calculate_cross_size(flex_lines: &mut [FlexLine], node_size: Size>, constants: &AlgoConstants) { + // If the flex container is single-line and has a definite cross size, + // the cross size of the flex line is the flex container’s inner cross size. + if !constants.is_wrap && node_size.cross(constants.dir).is_some() { + let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir); + let cross_min_size = constants.min_size.cross(constants.dir); + let cross_max_size = constants.max_size.cross(constants.dir); + flex_lines[0].cross_size = node_size + .cross(constants.dir) + .maybe_clamp(cross_min_size, cross_max_size) + .maybe_sub(cross_axis_padding_border) + .maybe_max(0.0) + .unwrap_or(0.0); + } else { + // Otherwise, for each flex line: + // + // 1. Collect all the flex items whose inline-axis is parallel to the main-axis, whose + // align-self is baseline, and whose cross-axis margins are both non-auto. Find the + // largest of the distances between each item’s baseline and its hypothetical outer + // cross-start edge, and the largest of the distances between each item’s baseline + // and its hypothetical outer cross-end edge, and sum these two values. + + // 2. Among all the items not collected by the previous step, find the largest + // outer hypothetical cross size. + + // 3. The used cross-size of the flex line is the largest of the numbers found in the + // previous two steps and zero. + for line in flex_lines.iter_mut() { + let max_baseline: f32 = line.items.iter().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x)); + line.cross_size = line + .items + .iter() + .map(|child| { + if child.align_self == AlignSelf::BASELINE + && !child.margin_is_auto.cross_start(constants.dir) + && !child.margin_is_auto.cross_end(constants.dir) + { + max_baseline - child.baseline + child.hypothetical_outer_size.cross(constants.dir) + } else { + child.hypothetical_outer_size.cross(constants.dir) + } + }) + .fold(0.0, |acc, x| acc.max(x)); + } + + // If the flex container is single-line, then clamp the line’s cross-size to be within the container’s computed min and max cross sizes. + // Note that if CSS 2.1’s definition of min/max-width/height applied more generally, this behavior would fall out automatically. + if !constants.is_wrap { + let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir); + let cross_min_size = constants.min_size.cross(constants.dir); + let cross_max_size = constants.max_size.cross(constants.dir); + flex_lines[0].cross_size = flex_lines[0].cross_size.maybe_clamp( + cross_min_size.maybe_sub(cross_axis_padding_border), + cross_max_size.maybe_sub(cross_axis_padding_border), + ); + } + } +} + +/// Handle 'align-content: stretch'. +/// +/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing) +/// +/// - [**Handle 'align-content: stretch'**](https://www.w3.org/TR/css-flexbox-1/#algo-line-stretch). If the flex container has a definite cross size, align-content is stretch, +/// and the sum of the flex lines' cross sizes is less than the flex container’s inner cross size, +/// increase the cross size of each flex line by equal amounts such that the sum of their cross sizes exactly equals the flex container’s inner cross size. +#[inline] +fn handle_align_content_stretch(flex_lines: &mut [FlexLine], node_size: Size>, constants: &AlgoConstants) { + if constants.align_content == AlignContent::STRETCH { + let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir); + let cross_min_size = constants.min_size.cross(constants.dir); + let cross_max_size = constants.max_size.cross(constants.dir); + let container_min_inner_cross = node_size + .cross(constants.dir) + .or(cross_min_size) + .maybe_clamp(cross_min_size, cross_max_size) + .maybe_sub(cross_axis_padding_border) + .maybe_max(0.0) + .unwrap_or(0.0); + + let total_cross_axis_gap = sum_axis_gaps(constants.gap.cross(constants.dir), flex_lines.len()); + let lines_total_cross: f32 = flex_lines.iter().map(|line| line.cross_size).sum::() + total_cross_axis_gap; + + if lines_total_cross < container_min_inner_cross { + let remaining = container_min_inner_cross - lines_total_cross; + let addition = remaining / flex_lines.len() as f32; + flex_lines.iter_mut().for_each(|line| line.cross_size += addition); + } + } +} + +/// Determine the used cross size of each flex item. +/// +/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing) +/// +/// - [**Determine the used cross size of each flex item**](https://www.w3.org/TR/css-flexbox-1/#algo-stretch). If a flex item has align-self: stretch, its computed cross size property is auto, +/// and neither of its cross-axis margins are auto, the used outer cross size is the used cross size of its flex line, clamped according to the item’s used min and max cross sizes. +/// Otherwise, the used cross size is the item’s hypothetical cross size. +/// +/// If the flex item has align-self: stretch, redo layout for its contents, treating this used size as its definite cross size so that percentage-sized children can be resolved. +/// +/// **Note that this step does not affect the main size of the flex item, even if it has an intrinsic aspect ratio**. +#[inline] +fn determine_used_cross_size( + tree: &impl LayoutFlexboxContainer, + flex_lines: &mut [FlexLine], + constants: &AlgoConstants, +) { + for line in flex_lines { + let line_cross_size = line.cross_size; + + for child in line.items.iter_mut() { + let child_style = tree.get_flexbox_child_style(child.node); + child.target_size.set_cross( + constants.dir, + if child.align_self == AlignSelf::STRETCH + && !child.margin_is_auto.cross_start(constants.dir) + && !child.margin_is_auto.cross_end(constants.dir) + && child_style.size().cross(constants.dir).is_auto() + { + // For some reason this particular usage of max_width is an exception to the rule that max_width's transfer + // using the aspect_ratio (if set). Both Chrome and Firefox agree on this. And reading the spec, it seems like + // a reasonable interpretation. Although it seems to me that the spec *should* apply aspect_ratio here. + let padding = child_style + .padding() + .resolve_or_zero(constants.node_inner_size, |val, basis| tree.calc(val, basis)); + let border = child_style + .border() + .resolve_or_zero(constants.node_inner_size, |val, basis| tree.calc(val, basis)); + let pb_sum = (padding + border).sum_axes(); + let box_sizing_adjustment = + if child_style.box_sizing() == BoxSizing::ContentBox { pb_sum } else { Size::ZERO }; + + let max_size_ignoring_aspect_ratio = child_style + .max_size() + .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis)) + .maybe_add(box_sizing_adjustment); + + (line_cross_size - child.margin.cross_axis_sum(constants.dir)).maybe_clamp( + child.min_size.cross(constants.dir), + max_size_ignoring_aspect_ratio.cross(constants.dir), + ) + } else { + child.hypothetical_inner_size.cross(constants.dir) + }, + ); + + child.outer_target_size.set_cross( + constants.dir, + child.target_size.cross(constants.dir) + child.margin.cross_axis_sum(constants.dir), + ); + } + } +} + +/// Distribute any remaining free space. +/// +/// # [9.5. Main-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#main-alignment) +/// +/// - [**Distribute any remaining free space**](https://www.w3.org/TR/css-flexbox-1/#algo-main-align). For each flex line: +/// +/// 1. If the remaining free space is positive and at least one main-axis margin on this line is `auto`, distribute the free space equally among these margins. +/// Otherwise, set all `auto` margins to zero. +/// +/// 2. Align the items along the main-axis per `justify-content`. +#[inline] +fn distribute_remaining_free_space(flex_lines: &mut [FlexLine], constants: &AlgoConstants) { + for line in flex_lines { + let total_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len()); + let used_space: f32 = total_main_axis_gap + + line.items.iter().map(|child| child.outer_target_size.main(constants.dir)).sum::(); + let free_space = constants.inner_container_size.main(constants.dir) - used_space; + let mut num_auto_margins = 0; + + for child in line.items.iter_mut() { + if child.margin_is_auto.main_start(constants.dir) { + num_auto_margins += 1; + } + if child.margin_is_auto.main_end(constants.dir) { + num_auto_margins += 1; + } + } + + if free_space > 0.0 && num_auto_margins > 0 { + let margin = free_space / num_auto_margins as f32; + + for child in line.items.iter_mut() { + if child.margin_is_auto.main_start(constants.dir) { + if constants.is_row { + child.margin.left = margin; + } else { + child.margin.top = margin; + } + } + if child.margin_is_auto.main_end(constants.dir) { + if constants.is_row { + child.margin.right = margin; + } else { + child.margin.bottom = margin; + } + } + } + } + + let num_items = line.items.len(); + let layout_reverse = constants.dir.is_reverse(); + let gap = constants.gap.main(constants.dir); + let raw_justify_content_mode = constants.justify_content.unwrap_or(JustifyContent::FLEX_START); + let justify_content_mode = apply_alignment_fallback(free_space, num_items, raw_justify_content_mode); + + let justify_item = |(i, child): (usize, &mut FlexItem)| { + child.offset_main = + compute_alignment_offset(free_space, num_items, gap, justify_content_mode, layout_reverse, i == 0); + }; + + if layout_reverse { + line.items.iter_mut().rev().enumerate().for_each(justify_item); + } else { + line.items.iter_mut().enumerate().for_each(justify_item); + } + } +} + +/// Resolve cross-axis `auto` margins. +/// +/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment) +/// +/// - [**Resolve cross-axis `auto` margins**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-margins). +/// If a flex item has auto cross-axis margins: +/// +/// - If its outer cross size (treating those auto margins as zero) is less than the cross size of its flex line, +/// distribute the difference in those sizes equally to the auto margins. +/// +/// - Otherwise, if the block-start or inline-start margin (whichever is in the cross axis) is auto, set it to zero. +/// Set the opposite margin so that the outer cross size of the item equals the cross size of its flex line. +#[inline] +fn resolve_cross_axis_auto_margins(flex_lines: &mut [FlexLine], constants: &AlgoConstants) { + for line in flex_lines { + let line_cross_size = line.cross_size; + let max_baseline: f32 = line.items.iter_mut().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x)); + + for child in line.items.iter_mut() { + let free_space = line_cross_size - child.outer_target_size.cross(constants.dir); + + if child.margin_is_auto.cross_start(constants.dir) && child.margin_is_auto.cross_end(constants.dir) { + if constants.is_row { + child.margin.top = free_space / 2.0; + child.margin.bottom = free_space / 2.0; + } else { + child.margin.left = free_space / 2.0; + child.margin.right = free_space / 2.0; + } + } else if child.margin_is_auto.cross_start(constants.dir) { + if constants.is_row { + child.margin.top = free_space; + } else { + child.margin.left = free_space; + } + } else if child.margin_is_auto.cross_end(constants.dir) { + if constants.is_row { + child.margin.bottom = free_space; + } else { + child.margin.right = free_space; + } + } else { + // 14. Align all flex items along the cross-axis. + child.offset_cross = align_flex_items_along_cross_axis(child, free_space, max_baseline, constants); + } + } + } +} + +/// Align all flex items along the cross-axis. +/// +/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment) +/// +/// - [**Align all flex items along the cross-axis**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-align) per `align-self`, +/// if neither of the item's cross-axis margins are `auto`. +#[inline] +fn align_flex_items_along_cross_axis( + child: &FlexItem, + free_space: f32, + max_baseline: f32, + constants: &AlgoConstants, +) -> f32 { + let cross_axis_should_reverse = constants.is_column && matches!(constants.layout_direction, Direction::Rtl); + + // If align-self uses a "safe" overflow-position keyword and the item would overflow its + // line cross size, fall back to logical Start to avoid data loss. See CSS Box Alignment 3 + // §4.3 . Otherwise, drop the safety + // field so the match below operates on a bare keyword and stays exhaustive. + let align_keyword = if child.align_self.is_safe() && free_space < 0.0 { + AlignItemsKeyword::Start + } else { + child.align_self.keyword + }; + + match align_keyword { + AlignItemsKeyword::Start => { + if cross_axis_should_reverse { + free_space + } else { + 0.0 + } + } + AlignItemsKeyword::FlexStart => { + if constants.is_wrap_reverse ^ cross_axis_should_reverse { + free_space + } else { + 0.0 + } + } + AlignItemsKeyword::End => { + if cross_axis_should_reverse { + 0.0 + } else { + free_space + } + } + AlignItemsKeyword::FlexEnd => { + if constants.is_wrap_reverse ^ cross_axis_should_reverse { + 0.0 + } else { + free_space + } + } + AlignItemsKeyword::Center => free_space / 2.0, + AlignItemsKeyword::Baseline => { + if constants.is_row { + max_baseline - child.baseline + } else { + // Until we support vertical writing modes, baseline alignment only makes sense if + // the constants.direction is row, so we treat it as flex-start alignment in columns. + let baseline_column_should_reverse = cross_axis_should_reverse && !constants.is_wrap; + if constants.is_wrap_reverse ^ baseline_column_should_reverse { + free_space + } else { + 0.0 + } + } + } + AlignItemsKeyword::Stretch => { + if constants.is_wrap_reverse ^ cross_axis_should_reverse { + free_space + } else { + 0.0 + } + } + } +} + +/// Determine the flex container’s used cross size. +/// +/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment) +/// +/// - [**Determine the flex container’s used cross size**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-container): +/// +/// - If the cross size property is a definite size, use that, clamped by the used min and max cross sizes of the flex container. +/// +/// - Otherwise, use the sum of the flex lines' cross sizes, clamped by the used min and max cross sizes of the flex container. +#[inline] +#[must_use] +fn determine_container_cross_size( + flex_lines: &[FlexLine], + node_size: Size>, + constants: &mut AlgoConstants, +) -> f32 { + let total_cross_axis_gap = sum_axis_gaps(constants.gap.cross(constants.dir), flex_lines.len()); + let total_line_cross_size: f32 = flex_lines.iter().map(|line| line.cross_size).sum::(); + + let padding_border_sum = constants.content_box_inset.cross_axis_sum(constants.dir); + let cross_scrollbar_gutter = constants.scrollbar_gutter.cross(constants.dir); + let min_cross_size = constants.min_size.cross(constants.dir); + let max_cross_size = constants.max_size.cross(constants.dir); + let outer_container_size = node_size + .cross(constants.dir) + .unwrap_or(total_line_cross_size + total_cross_axis_gap + padding_border_sum) + .maybe_clamp(min_cross_size, max_cross_size) + .max(padding_border_sum - cross_scrollbar_gutter); + let inner_container_size = f32_max(outer_container_size - padding_border_sum, 0.0); + + constants.container_size.set_cross(constants.dir, outer_container_size); + constants.inner_container_size.set_cross(constants.dir, inner_container_size); + + total_line_cross_size +} + +/// Align all flex lines per `align-content`. +/// +/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment) +/// +/// - [**Align all flex lines**](https://www.w3.org/TR/css-flexbox-1/#algo-line-align) per `align-content`. +#[inline] +fn align_flex_lines_per_align_content(flex_lines: &mut [FlexLine], constants: &AlgoConstants, total_cross_size: f32) { + let num_lines = flex_lines.len(); + let gap = constants.gap.cross(constants.dir); + let total_cross_axis_gap = sum_axis_gaps(gap, num_lines); + let free_space = constants.inner_container_size.cross(constants.dir) - total_cross_size - total_cross_axis_gap; + + let align_content_mode = apply_alignment_fallback(free_space, num_lines, constants.align_content); + + let align_line = |(i, line): (usize, &mut FlexLine)| { + line.offset_cross = + compute_alignment_offset(free_space, num_lines, gap, align_content_mode, constants.is_wrap_reverse, i == 0); + }; + + if constants.is_wrap_reverse { + flex_lines.iter_mut().rev().enumerate().for_each(align_line); + } else { + flex_lines.iter_mut().enumerate().for_each(align_line); + } +} + +/// Calculates the layout for a flex-item +#[allow(clippy::too_many_arguments)] +fn calculate_flex_item( + tree: &mut impl LayoutFlexboxContainer, + item: &mut FlexItem, + total_offset_main: &mut f32, + total_offset_cross: f32, + line_offset_cross: f32, + #[cfg(feature = "content_size")] total_content_size: &mut Size, + container_size: Size, + node_inner_size: Size>, + direction: FlexDirection, + layout_direction: Direction, +) { + let layout_output = tree.perform_child_layout( + item.node, + item.target_size.map(|s| s.into()), + node_inner_size, + container_size.map(|s| s.into()), + SizingMode::ContentSize, + Line::FALSE, + ); + let LayoutOutput { + size, + #[cfg(feature = "content_size")] + content_size, + .. + } = layout_output; + + let is_rtl_row = direction.is_row() && layout_direction.is_rtl(); + let is_rtl_column = direction.is_column() && layout_direction.is_rtl(); + let main_relative_inset = if is_rtl_row { + item.inset.main_end(direction).or(item.inset.main_start(direction).map(|pos| -pos)).unwrap_or(0.0) + } else { + item.inset.main_start(direction).or(item.inset.main_end(direction).map(|pos| -pos)).unwrap_or(0.0) + }; + let cross_relative_inset = if is_rtl_column { + item.inset.cross_end(direction).map(|pos| -pos).or(item.inset.cross_start(direction)).unwrap_or(0.0) + } else { + item.inset.cross_start(direction).or(item.inset.cross_end(direction).map(|pos| -pos)).unwrap_or(0.0) + }; + let effective_line_offset_cross = if is_rtl_column { 0.0 } else { line_offset_cross }; + + let offset_main = if is_rtl_row { + *total_offset_main - item.offset_main - item.margin.main_end(direction) - main_relative_inset - size.width + } else { + *total_offset_main + item.offset_main + item.margin.main_start(direction) + main_relative_inset + }; + + let offset_cross = total_offset_cross + + item.offset_cross + + effective_line_offset_cross + + item.margin.cross_start(direction) + + cross_relative_inset; + + if direction.is_row() { + let baseline_offset_cross = + total_offset_cross + item.offset_cross + effective_line_offset_cross + item.margin.cross_start(direction); + let inner_baseline = layout_output.first_baselines.y.unwrap_or(size.height); + item.baseline = baseline_offset_cross + inner_baseline; + } else { + let baseline_offset_main = *total_offset_main + item.offset_main + item.margin.main_start(direction); + let inner_baseline = layout_output.first_baselines.y.unwrap_or(size.height); + item.baseline = baseline_offset_main + inner_baseline; + } + + let location = if direction.is_row() { + Point { x: offset_main, y: offset_cross } + } else { + Point { x: offset_cross, y: offset_main } + }; + let scrollbar_size = Size { + width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 }, + height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 }, + }; + + tree.set_unrounded_layout( + item.node, + &Layout { + order: item.order, + size, + #[cfg(feature = "content_size")] + content_size, + scrollbar_size, + location, + padding: item.padding, + border: item.border, + margin: item.margin, + }, + ); + + if is_rtl_row { + *total_offset_main -= item.offset_main + item.margin.main_axis_sum(direction) + size.main(direction); + } else { + *total_offset_main += item.offset_main + item.margin.main_axis_sum(direction) + size.main(direction); + } + + #[cfg(feature = "content_size")] + { + let contribution_location = if layout_direction.is_rtl() { + Point { x: container_size.width - (location.x + size.width), y: location.y } + } else { + location + }; + *total_content_size = total_content_size.f32_max(compute_content_size_contribution( + contribution_location, + size, + content_size, + item.overflow, + )); + } +} + +/// Calculates the layout line +#[allow(clippy::too_many_arguments)] +fn calculate_layout_line( + tree: &mut impl LayoutFlexboxContainer, + line: &mut FlexLine, + total_offset_cross: &mut f32, + #[cfg(feature = "content_size")] content_size: &mut Size, + container_size: Size, + node_inner_size: Size>, + padding_border: Rect, + direction: FlexDirection, + layout_direction: Direction, +) { + let mut total_offset_main = if layout_direction.is_rtl() && direction.is_row() { + container_size.width - padding_border.main_end(direction) + } else { + padding_border.main_start(direction) + }; + let line_offset_cross = line.offset_cross; + + let is_rtl_column = layout_direction.is_rtl() && direction.is_column(); + if is_rtl_column { + *total_offset_cross -= line_offset_cross + line.cross_size; + } + + if direction.is_reverse() { + for item in line.items.iter_mut().rev() { + calculate_flex_item( + tree, + item, + &mut total_offset_main, + *total_offset_cross, + line_offset_cross, + #[cfg(feature = "content_size")] + content_size, + container_size, + node_inner_size, + direction, + layout_direction, + ); + } + } else { + for item in line.items.iter_mut() { + calculate_flex_item( + tree, + item, + &mut total_offset_main, + *total_offset_cross, + line_offset_cross, + #[cfg(feature = "content_size")] + content_size, + container_size, + node_inner_size, + direction, + layout_direction, + ); + } + } + + if !is_rtl_column { + *total_offset_cross += line_offset_cross + line.cross_size; + } +} + +/// Do a final layout pass and collect the resulting layouts. +#[inline] +fn final_layout_pass( + tree: &mut impl LayoutFlexboxContainer, + flex_lines: &mut [FlexLine], + constants: &AlgoConstants, +) -> Size { + let mut total_offset_cross = if constants.is_column && constants.layout_direction.is_rtl() { + constants.container_size.width - constants.content_box_inset.cross_end(constants.dir) + } else { + constants.content_box_inset.cross_start(constants.dir) + }; + + #[cfg_attr(not(feature = "content_size"), allow(unused_mut))] + let mut content_size = Size::ZERO; + + if constants.is_wrap_reverse { + for line in flex_lines.iter_mut().rev() { + calculate_layout_line( + tree, + line, + &mut total_offset_cross, + #[cfg(feature = "content_size")] + &mut content_size, + constants.container_size, + constants.node_inner_size, + constants.content_box_inset, + constants.dir, + constants.layout_direction, + ); + } + } else { + for line in flex_lines.iter_mut() { + calculate_layout_line( + tree, + line, + &mut total_offset_cross, + #[cfg(feature = "content_size")] + &mut content_size, + constants.container_size, + constants.node_inner_size, + constants.content_box_inset, + constants.dir, + constants.layout_direction, + ); + } + } + + content_size.width += if constants.layout_direction.is_rtl() { + constants.content_box_inset.left - constants.border.left - constants.scrollbar_gutter.x + } else { + constants.content_box_inset.right - constants.border.right - constants.scrollbar_gutter.x + }; + content_size.height += constants.content_box_inset.bottom - constants.border.bottom - constants.scrollbar_gutter.y; + + content_size +} + +/// Perform absolute layout on all absolutely positioned children. +#[inline] +fn perform_absolute_layout_on_absolute_children( + tree: &mut impl LayoutFlexboxContainer, + node: NodeId, + constants: &AlgoConstants, +) -> Size { + let container_width = constants.container_size.width; + let container_height = constants.container_size.height; + let inset_relative_size = + constants.container_size - constants.border.sum_axes() - constants.scrollbar_gutter.into(); + + #[cfg_attr(not(feature = "content_size"), allow(unused_mut))] + let mut content_size = Size::ZERO; + + for order in 0..tree.child_count(node) { + let child = tree.get_child_id(node, order); + let child_style = tree.get_flexbox_child_style(child); + + // Skip items that are display:none or are not position:absolute + if child_style.box_generation_mode() == BoxGenerationMode::None || child_style.position() != Position::Absolute + { + continue; + } + + let overflow = child_style.overflow(); + let scrollbar_width = child_style.scrollbar_width(); + let aspect_ratio = child_style.aspect_ratio(); + let align_self = child_style.align_self().unwrap_or(constants.align_items); + let margin = child_style + .margin() + .map(|margin| margin.resolve_to_option(inset_relative_size.width, |val, basis| tree.calc(val, basis))); + let padding = + child_style.padding().resolve_or_zero(Some(inset_relative_size.width), |val, basis| tree.calc(val, basis)); + let border = + child_style.border().resolve_or_zero(Some(inset_relative_size.width), |val, basis| tree.calc(val, basis)); + let padding_border_sum = (padding + border).sum_axes(); + let box_sizing_adjustment = + if child_style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO }; + + // Resolve inset + // Insets are resolved against the container size minus border + let left = + child_style.inset().left.maybe_resolve(inset_relative_size.width, |val, basis| tree.calc(val, basis)); + let right = + child_style.inset().right.maybe_resolve(inset_relative_size.width, |val, basis| tree.calc(val, basis)); + let top = child_style.inset().top.maybe_resolve(inset_relative_size.height, |val, basis| tree.calc(val, basis)); + let bottom = + child_style.inset().bottom.maybe_resolve(inset_relative_size.height, |val, basis| tree.calc(val, basis)); + + // Compute known dimensions from min/max/inherent size styles + let style_size = child_style + .size() + .maybe_resolve(inset_relative_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment); + let min_size = child_style + .min_size() + .maybe_resolve(inset_relative_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment) + .or(padding_border_sum.map(Some)) + .maybe_max(padding_border_sum); + let max_size = child_style + .max_size() + .maybe_resolve(inset_relative_size, |val, basis| tree.calc(val, basis)) + .maybe_apply_aspect_ratio(aspect_ratio) + .maybe_add(box_sizing_adjustment); + let mut known_dimensions = style_size.maybe_clamp(min_size, max_size); + + drop(child_style); + + // Fill in width from left/right and reapply aspect ratio if: + // - Width is not already known + // - Item has both left and right inset properties set + if let (None, Some(left), Some(right)) = (known_dimensions.width, left, right) { + let new_width_raw = inset_relative_size.width.maybe_sub(margin.left).maybe_sub(margin.right) - left - right; + known_dimensions.width = Some(f32_max(new_width_raw, 0.0)); + known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size); + } + + // Fill in height from top/bottom and reapply aspect ratio if: + // - Height is not already known + // - Item has both top and bottom inset properties set + if let (None, Some(top), Some(bottom)) = (known_dimensions.height, top, bottom) { + let new_height_raw = + inset_relative_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - top - bottom; + known_dimensions.height = Some(f32_max(new_height_raw, 0.0)); + known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size); + } + let measured_size = tree.measure_child_size_both( + child, + known_dimensions, + constants.node_inner_size, + Size { + width: AvailableSpace::Definite(container_width.maybe_clamp(min_size.width, max_size.width)), + height: AvailableSpace::Definite(container_height.maybe_clamp(min_size.height, max_size.height)), + }, + SizingMode::InherentSize, + Line::FALSE, + ); + let final_size = known_dimensions.unwrap_or(measured_size).maybe_clamp(min_size, max_size); + + let layout_output = tree.perform_child_layout( + child, + final_size.map(Some), + constants.node_inner_size, + Size { + width: AvailableSpace::Definite(container_width.maybe_clamp(min_size.width, max_size.width)), + height: AvailableSpace::Definite(container_height.maybe_clamp(min_size.height, max_size.height)), + }, + SizingMode::InherentSize, + Line::FALSE, + ); + + let non_auto_margin = margin.map(|m| m.unwrap_or(0.0)); + + let free_space = Size { + width: constants.container_size.width - final_size.width - non_auto_margin.horizontal_axis_sum(), + height: constants.container_size.height - final_size.height - non_auto_margin.vertical_axis_sum(), + } + .f32_max(Size::ZERO); + + // Expand auto margins to fill available space + let resolved_margin = { + let auto_margin_size = Size { + width: { + let auto_margin_count = margin.left.is_none() as u8 + margin.right.is_none() as u8; + if auto_margin_count > 0 { + free_space.width / auto_margin_count as f32 + } else { + 0.0 + } + }, + height: { + let auto_margin_count = margin.top.is_none() as u8 + margin.bottom.is_none() as u8; + if auto_margin_count > 0 { + free_space.height / auto_margin_count as f32 + } else { + 0.0 + } + }, + }; + + Rect { + left: margin.left.unwrap_or(auto_margin_size.width), + right: margin.right.unwrap_or(auto_margin_size.width), + top: margin.top.unwrap_or(auto_margin_size.height), + bottom: margin.bottom.unwrap_or(auto_margin_size.height), + } + }; + + // Determine flex-relative insets + let (start_main, end_main) = if constants.is_row { (left, right) } else { (top, bottom) }; + let (start_cross, end_cross) = if constants.is_row { (top, bottom) } else { (left, right) }; + let main_axis_is_horizontal = constants.is_row; + let cross_axis_is_horizontal = !constants.is_row; + let main_is_rtl = main_axis_is_horizontal && constants.layout_direction.is_rtl(); + let cross_is_rtl = cross_axis_is_horizontal && constants.layout_direction.is_rtl(); + let main_axis_flex_start_reversed = constants.dir.is_reverse() ^ main_is_rtl; + let cross_axis_flex_start_reversed = constants.is_wrap_reverse ^ cross_is_rtl; + let main_start_scrollbar_offset = + if main_is_rtl { constants.scrollbar_gutter.main(constants.dir) } else { 0.0 }; + let cross_start_scrollbar_offset = + if cross_is_rtl { constants.scrollbar_gutter.cross(constants.dir) } else { 0.0 }; + let main_end_scrollbar_offset = if main_is_rtl { 0.0 } else { constants.scrollbar_gutter.main(constants.dir) }; + let cross_end_scrollbar_offset = + if cross_is_rtl { 0.0 } else { constants.scrollbar_gutter.cross(constants.dir) }; + + // Apply main-axis alignment + // let free_main_space = free_space.main(constants.dir) - resolved_margin.main_axis_sum(constants.dir); + let offset_main = if start_main.is_some() || end_main.is_some() { + if main_is_rtl && end_main.is_some() { + constants.container_size.main(constants.dir) + - constants.border.main_end(constants.dir) + - main_end_scrollbar_offset + - final_size.main(constants.dir) + - end_main.unwrap_or(0.0) + - resolved_margin.main_end(constants.dir) + } else if let Some(start) = start_main { + start + + constants.border.main_start(constants.dir) + + main_start_scrollbar_offset + + resolved_margin.main_start(constants.dir) + } else { + constants.container_size.main(constants.dir) + - constants.border.main_end(constants.dir) + - main_end_scrollbar_offset + - final_size.main(constants.dir) + - end_main.unwrap_or(0.0) + - resolved_margin.main_end(constants.dir) + } + } else { + // Stretch is an invalid value for justify_content in the flexbox algorithm, so we + // treat it as if it wasn't set (and thus we default to FlexStart behaviour). + // + // The `safe` overflow-position keyword is intentionally NOT applied here, even when + // the abs-positioned item would overflow the main axis: Chrome does not apply safe + // fallback to `justify-content` on absolutely-positioned flex items (only the + // cross-axis `align-self` does so). Matching the layout authority over a strict + // spec read keeps gentest fixtures green; reconsider if Chromium changes behavior. + match (constants.justify_content.unwrap_or(JustifyContent::START).keyword(), main_axis_flex_start_reversed) + { + (AlignContentKeyword::SpaceBetween, _) + | (AlignContentKeyword::Stretch, false) + | (AlignContentKeyword::FlexStart, false) + | (AlignContentKeyword::FlexEnd, true) => { + constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir) + } + (AlignContentKeyword::Start, false) => { + constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir) + } + (AlignContentKeyword::Start, true) => { + constants.container_size.main(constants.dir) + - constants.content_box_inset.main_end(constants.dir) + - final_size.main(constants.dir) + - resolved_margin.main_end(constants.dir) + } + (AlignContentKeyword::End, false) => { + constants.container_size.main(constants.dir) + - constants.content_box_inset.main_end(constants.dir) + - final_size.main(constants.dir) + - resolved_margin.main_end(constants.dir) + } + (AlignContentKeyword::End, true) => { + constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir) + } + (AlignContentKeyword::FlexEnd, false) + | (AlignContentKeyword::FlexStart, true) + | (AlignContentKeyword::Stretch, true) => { + constants.container_size.main(constants.dir) + - constants.content_box_inset.main_end(constants.dir) + - final_size.main(constants.dir) + - resolved_margin.main_end(constants.dir) + } + (AlignContentKeyword::SpaceEvenly, _) + | (AlignContentKeyword::SpaceAround, _) + | (AlignContentKeyword::Center, _) => { + (constants.container_size.main(constants.dir) + + constants.content_box_inset.main_start(constants.dir) + - constants.content_box_inset.main_end(constants.dir) + - final_size.main(constants.dir) + + resolved_margin.main_start(constants.dir) + - resolved_margin.main_end(constants.dir)) + / 2.0 + } + } + }; + + // Apply cross-axis alignment + // let free_cross_space = free_space.cross(constants.dir) - resolved_margin.cross_axis_sum(constants.dir); + let offset_cross = if start_cross.is_some() || end_cross.is_some() { + if cross_is_rtl && end_cross.is_some() { + constants.container_size.cross(constants.dir) + - constants.border.cross_end(constants.dir) + - cross_end_scrollbar_offset + - final_size.cross(constants.dir) + - end_cross.unwrap_or(0.0) + - resolved_margin.cross_end(constants.dir) + } else if let Some(start) = start_cross { + start + + constants.border.cross_start(constants.dir) + + cross_start_scrollbar_offset + + resolved_margin.cross_start(constants.dir) + } else { + constants.container_size.cross(constants.dir) + - constants.border.cross_end(constants.dir) + - cross_end_scrollbar_offset + - final_size.cross(constants.dir) + - end_cross.unwrap_or(0.0) + - resolved_margin.cross_end(constants.dir) + } + } else { + let cross_overflows = final_size.cross(constants.dir) + resolved_margin.cross_axis_sum(constants.dir) + > constants.container_size.cross(constants.dir) + - constants.content_box_inset.cross_axis_sum(constants.dir); + let cross_keyword = resolve_self_alignment_safety(align_self, cross_overflows); + match (cross_keyword, cross_axis_flex_start_reversed) { + // Stretch alignment does not apply to absolutely positioned items + // See "Example 3" at https://www.w3.org/TR/css-flexbox-1/#abspos-items + // Note: Stretch should be FlexStart not Start when we support both + (AlignItemsKeyword::Start, false) => { + constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir) + } + (AlignItemsKeyword::Start, true) => { + constants.container_size.cross(constants.dir) + - constants.content_box_inset.cross_end(constants.dir) + - final_size.cross(constants.dir) + - resolved_margin.cross_end(constants.dir) + } + (AlignItemsKeyword::End, false) => { + constants.container_size.cross(constants.dir) + - constants.content_box_inset.cross_end(constants.dir) + - final_size.cross(constants.dir) + - resolved_margin.cross_end(constants.dir) + } + (AlignItemsKeyword::End, true) => { + constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir) + } + (AlignItemsKeyword::Baseline | AlignItemsKeyword::Stretch | AlignItemsKeyword::FlexStart, false) + | (AlignItemsKeyword::FlexEnd, true) => { + constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir) + } + (AlignItemsKeyword::Baseline | AlignItemsKeyword::Stretch | AlignItemsKeyword::FlexStart, true) + | (AlignItemsKeyword::FlexEnd, false) => { + constants.container_size.cross(constants.dir) + - constants.content_box_inset.cross_end(constants.dir) + - final_size.cross(constants.dir) + - resolved_margin.cross_end(constants.dir) + } + (AlignItemsKeyword::Center, _) => { + (constants.container_size.cross(constants.dir) + + constants.content_box_inset.cross_start(constants.dir) + - constants.content_box_inset.cross_end(constants.dir) + - final_size.cross(constants.dir) + + resolved_margin.cross_start(constants.dir) + - resolved_margin.cross_end(constants.dir)) + / 2.0 + } + } + }; + + let location = match constants.is_row { + true => Point { x: offset_main, y: offset_cross }, + false => Point { x: offset_cross, y: offset_main }, + }; + 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 }, + }; + tree.set_unrounded_layout( + child, + &Layout { + order: order as u32, + size: final_size, + #[cfg(feature = "content_size")] + content_size: layout_output.content_size, + scrollbar_size, + location, + padding, + border, + margin: resolved_margin, + }, + ); + + #[cfg(feature = "content_size")] + { + let size_content_size_contribution = Size { + width: match overflow.x { + Overflow::Visible => f32_max(final_size.width, layout_output.content_size.width), + _ => final_size.width, + }, + height: match overflow.y { + Overflow::Visible => f32_max(final_size.height, layout_output.content_size.height), + _ => final_size.height, + }, + }; + if size_content_size_contribution.has_non_zero_area() { + let absolute_area_offset = Point { + x: constants.border.left + + if constants.layout_direction.is_rtl() { constants.scrollbar_gutter.x } else { 0.0 }, + y: constants.border.top, + }; + let relative_location = + Point { x: location.x - absolute_area_offset.x, y: location.y - absolute_area_offset.y }; + let content_size_contribution = Size { + width: if constants.layout_direction.is_rtl() { + let overflow_extra_width = + f32_max(size_content_size_contribution.width - final_size.width, 0.0); + f32_max(inset_relative_size.width - relative_location.x, 0.0) + overflow_extra_width + } else { + relative_location.x + size_content_size_contribution.width + }, + height: relative_location.y + size_content_size_contribution.height, + }; + content_size = content_size.f32_max(content_size_contribution); + } + } + } + + content_size +} + +/// Computes the total space taken up by gaps in an axis given: +/// - The size of each gap +/// - The number of items (children or flex-lines) between which there are gaps +#[inline(always)] +fn sum_axis_gaps(gap: f32, num_items: usize) -> f32 { + // Gaps only exist between items, so... + if num_items <= 1 { + // ...if there are less than 2 items then there are no gaps + 0.0 + } else { + // ...otherwise there are (num_items - 1) gaps + gap * (num_items - 1) as f32 + } +} diff --git a/vendor/taffy/src/compute/float.rs b/vendor/taffy/src/compute/float.rs new file mode 100644 index 0000000..e565ac0 --- /dev/null +++ b/vendor/taffy/src/compute/float.rs @@ -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. +//! +//! + +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, + /// 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, + /// 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, 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, + /// A list of right-floated boxes within the context + right_floats: Vec, + /// A list of non-overlapping horizontal "segments" within the context. + /// Each segment has the same available width for it's entire height. + segments: Vec, + /// A closed-open range indicating which segment the last placed float + /// was placed(on each side). + last_placed_floats: [Range; 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(÷_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(÷_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) { + 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, + min_y: f32, + containing_block_insets: [f32; 2], + direction: FloatDirection, + clear: Clear, + ) -> Point { + 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, + 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 { + 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 { + 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, + ) -> 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 + } +} diff --git a/vendor/taffy/src/compute/grid/alignment.rs b/vendor/taffy/src/compute/grid/alignment.rs new file mode 100644 index 0000000..54770e7 --- /dev/null +++ b/vendor/taffy/src/compute/grid/alignment.rs @@ -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, + border: Line, + 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, + container_alignment_styles: InBothAbsAxis>, + baseline_shim: f32, + direction: Direction, +) -> (Size, 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, + alignment_style: AlignSelf, + resolved_size: f32, + position: Position, + inset: Line>, + margin: Line>, + baseline_shim: f32, + direction: Direction, +) -> (f32, Line) { + // 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) +} diff --git a/vendor/taffy/src/compute/grid/explicit_grid.rs b/vendor/taffy/src/compute/grid/explicit_grid.rs new file mode 100644 index 0000000..b32f2c9 --- /dev/null +++ b/vendor/taffy/src/compute/grid/explicit_grid.rs @@ -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, + 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::(); + + 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, + 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::(); + 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::(); + + // 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, + 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::() + }) + .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, + count: u16, + mut auto_tracks_iter: impl Iterator, + 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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)"); + } + } +} diff --git a/vendor/taffy/src/compute/grid/implicit_grid.rs b/vendor/taffy/src/compute/grid/implicit_grid.rs new file mode 100644 index 0000000..143e7c3 --- /dev/null +++ b/vendor/taffy/src/compute/grid/implicit_grid.rs @@ -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, +) -> (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, + 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::(col_line, explicit_col_count); + let (child_row_min, child_row_max, child_row_span) = + child_min_line_max_line_span::(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( + line: Line>, + 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::(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::(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); + } + } +} diff --git a/vendor/taffy/src/compute/grid/mod.rs b/vendor/taffy/src/compute/grid/mod.rs new file mode 100644 index 0000000..1a631a8 --- /dev/null +++ b/vendor/taffy/src/compute/grid/mod.rs @@ -0,0 +1,897 @@ +//! This module is a partial implementation of the CSS Grid Level 1 specification +//! +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: &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> = <::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, 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::(); + 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::(); + 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::>()); + debug_log!("initial_row_sum", dbg:initial_row_sum); + debug_log!(dbg: rows.iter().map(|track| track.base_size).collect::>()); + + // 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 = column + .min_track_sizing_function + .resolved_percentage_size(container_content_box.width, |val, basis| tree.calc(val, basis)); + let max: Option = 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 = row + .min_track_sizing_function + .resolved_percentage_size(container_content_box.height, |val, basis| tree.calc(val, basis)); + let max: Option = 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::(); + let final_row_sum = rows.iter().map(|track| track.base_size).sum::(); + + 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 { + /// + pub rows: DetailedGridTracksInfo, + /// + pub columns: DetailedGridTracksInfo, + /// + pub items: Vec, +} + +/// 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, + /// The used size of the tracks + pub sizes: Vec, +} + +#[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 { + 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 { + 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 { + 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) -> 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 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), + } + } +} diff --git a/vendor/taffy/src/compute/grid/placement.rs b/vendor/taffy/src/compute/grid/placement.rs new file mode 100644 index 0000000..ff24a03 --- /dev/null +++ b/vendor/taffy/src/compute/grid/placement.rs @@ -0,0 +1,764 @@ +//! Implements placing items in the grid and resolving the implicit grid. +//! +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 { + 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, explicit_col_count: u16) -> Line { + 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, + axis: AbsoluteAxis, + direction: Direction, + explicit_col_count: u16, +) -> Line { + 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, + children_iter: impl Fn() -> ChildIter, + direction: Direction, + grid_auto_flow: GridAutoFlow, + align_items: AlignItems, + justify_items: AlignItems, + named_line_resolver: &NamedLineResolver<::CustomIdent>, +) where + S: GridItemStyle + 'a, + ChildIter: Iterator, +{ + 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>, + primary_axis: AbsoluteAxis, + direction: Direction, + explicit_col_count: u16, +) -> (Line, Line) { + // 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>, + auto_flow: GridAutoFlow, + direction: Direction, + explicit_col_count: u16, +) -> (Line, Line) { + 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>, + auto_flow: GridAutoFlow, + grid_position: (OriginZeroLine, OriginZeroLine), + direction: Direction, + explicit_col_count: u16, +) -> (Line, Line) { + 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( + cell_occupancy_matrix: &mut CellOccupancyMatrix, + items: &mut Vec, + node: NodeId, + index: usize, + style: S, + parent_align_items: AlignItems, + parent_justify_items: AlignItems, + primary_axis: AbsoluteAxis, + primary_span: Line, + secondary_span: Line, + 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); + } + } +} diff --git a/vendor/taffy/src/compute/grid/track_sizing.rs b/vendor/taffy/src/compute/grid/track_sizing.rs new file mode 100644 index 0000000..10e3ea2 --- /dev/null +++ b/vendor/taffy/src/compute/grid/track_sizing.rs @@ -0,0 +1,1418 @@ +//! Implements the track sizing algorithm +//! +use super::types::{GridItem, GridTrack, TrackCounts}; +use crate::geometry::{AbstractAxis, Line, Size}; +use crate::style::{AlignContent, AlignContentKeyword, AlignSelf, AvailableSpace}; +use crate::style_helpers::TaffyMinContent; +use crate::tree::{LayoutPartialTree, LayoutPartialTreeExt, SizingMode}; +use crate::util::sys::{f32_max, f32_min, Vec}; +use crate::util::{MaybeMath, ResolveOrZero}; +use crate::CompactLength; +use core::cmp::Ordering; + +/// Takes an axis, and a list of grid items sorted firstly by whether they cross a flex track +/// in the specified axis (items that don't cross a flex track first) and then by the number +/// of tracks they cross in specified axis (ascending order). +struct ItemBatcher { + /// The axis in which the ItemBatcher is operating. Used when querying properties from items. + axis: AbstractAxis, + /// The starting index of the current batch + index_offset: usize, + /// The span of the items in the current batch + current_span: u16, + /// Whether the current batch of items cross a flexible track + current_is_flex: bool, +} + +impl ItemBatcher { + /// Create a new ItemBatcher for the specified axis + #[inline(always)] + fn new(axis: AbstractAxis) -> Self { + ItemBatcher { index_offset: 0, axis, current_span: 1, current_is_flex: false } + } + + /// This is basically a manual version of Iterator::next which passes `items` + /// in as a parameter on each iteration to work around borrow checker rules + #[inline] + fn next<'items>(&mut self, items: &'items mut [GridItem]) -> Option<(&'items mut [GridItem], bool)> { + if self.current_is_flex || self.index_offset >= items.len() { + return None; + } + + let item = &items[self.index_offset]; + self.current_span = item.span(self.axis); + self.current_is_flex = item.crosses_flexible_track(self.axis); + + let next_index_offset = if self.current_is_flex { + items.len() + } else { + items + .iter() + .position(|item: &GridItem| { + item.crosses_flexible_track(self.axis) || item.span(self.axis) > self.current_span + }) + .unwrap_or(items.len()) + }; + + let batch_range = self.index_offset..next_index_offset; + self.index_offset = next_index_offset; + + let batch = &mut items[batch_range]; + Some((batch, self.current_is_flex)) + } +} + +/// This struct captures a bunch of variables which are used to compute the intrinsic sizes of children so that those variables +/// don't have to be passed around all over the place below. It then has methods that implement the intrinsic sizing computations +struct IntrinsicSizeMeasurer<'tree, 'oat, Tree, EstimateFunction> +where + Tree: LayoutPartialTree, + EstimateFunction: Fn(&GridTrack, Option, &Tree) -> Option, +{ + /// The layout tree + tree: &'tree mut Tree, + /// The tracks in the opposite axis to the one we are currently sizing + other_axis_tracks: &'oat [GridTrack], + /// A function that computes an estimate of an other-axis track's size which is passed to + /// the child size measurement functions + get_track_size_estimate: EstimateFunction, + /// The axis we are currently sizing + axis: AbstractAxis, + /// The available grid space + inner_node_size: Size>, +} + +impl IntrinsicSizeMeasurer<'_, '_, Tree, EstimateFunction> +where + Tree: LayoutPartialTree, + EstimateFunction: Fn(&GridTrack, Option, &Tree) -> Option, +{ + /// 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 + #[inline(always)] + fn grid_area_size(&self, item: &mut GridItem, axis_tracks: &[GridTrack]) -> Size> { + item.grid_area_size_cached( + self.axis, + axis_tracks, + self.other_axis_tracks, + self.inner_node_size, + |track, basis| (self.get_track_size_estimate)(track, basis, self.tree), + &|val, basis| self.tree.calc(val, basis), + ) + } + + /// 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)] + fn margins_axis_sums_with_baseline_shims(&self, item: &GridItem, percentage_basis: Option) -> Size { + item.margins_axis_sums_with_baseline_shims(percentage_basis, self.tree) + } + + /// Simple pass-through function to `LayoutPartialTreeExt::calc` + #[inline(always)] + fn calc(&self, val: *const (), basis: f32) -> f32 { + self.tree.calc(val, basis) + } + + /// Retrieve the item's min content contribution from the cache or compute it using the provided parameters + #[inline(always)] + fn min_content_contribution(&mut self, item: &mut GridItem, axis_tracks: &[GridTrack]) -> f32 { + let grid_area_size = self.grid_area_size(item, axis_tracks); + let available_space = grid_area_size.with(self.axis, None); + let margin_axis_sums = self.margins_axis_sums_with_baseline_shims(item, available_space.width); + let contribution = item.min_content_contribution_cached(self.axis, self.tree, grid_area_size, available_space); + contribution + margin_axis_sums.get(self.axis) + } + + /// Retrieve the item's max content contribution from the cache or compute it using the provided parameters + #[inline(always)] + fn max_content_contribution(&mut self, item: &mut GridItem, axis_tracks: &[GridTrack]) -> f32 { + let grid_area_size = self.grid_area_size(item, axis_tracks); + let available_space = grid_area_size.with(self.axis, None); + let margin_axis_sums = self.margins_axis_sums_with_baseline_shims(item, available_space.width); + let contribution = item.max_content_contribution_cached(self.axis, self.tree, grid_area_size, available_space); + contribution + margin_axis_sums.get(self.axis) + } + + /// 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. + #[inline(always)] + fn minimum_contribution(&mut self, item: &mut GridItem, axis_tracks: &[GridTrack]) -> f32 { + let grid_area_size = self.grid_area_size(item, axis_tracks); + let available_space = grid_area_size.with(self.axis, None); + let margin_axis_sums = self.margins_axis_sums_with_baseline_shims(item, available_space.width); + let contribution = + item.minimum_contribution_cached(self.tree, self.axis, axis_tracks, grid_area_size, self.inner_node_size); + contribution + margin_axis_sums.get(self.axis) + } +} + +/// To make track sizing efficient we want to order tracks +/// Here a placement is either a Line representing a row-start/row-end or a column-start/column-end +#[inline(always)] +pub(super) fn cmp_by_cross_flex_then_span_then_start( + axis: AbstractAxis, +) -> impl FnMut(&GridItem, &GridItem) -> Ordering { + move |item_a: &GridItem, item_b: &GridItem| -> Ordering { + match (item_a.crosses_flexible_track(axis), item_b.crosses_flexible_track(axis)) { + (false, true) => Ordering::Less, + (true, false) => Ordering::Greater, + _ => { + let placement_a = item_a.placement(axis); + let placement_b = item_b.placement(axis); + match placement_a.span().cmp(&placement_b.span()) { + Ordering::Less => Ordering::Less, + Ordering::Greater => Ordering::Greater, + Ordering::Equal => placement_a.start.cmp(&placement_b.start), + } + } + } + } +} + +/// When applying the track sizing algorithm and estimating the size in the other axis for content sizing items +/// we should take into account align-content/justify-content if both the grid container and all items in the +/// other axis have definite sizes. This function computes such a per-gutter additional size adjustment. +#[inline(always)] +pub(super) fn compute_alignment_gutter_adjustment( + alignment: AlignContent, + axis_inner_node_size: Option, + get_track_size_estimate: impl Fn(&GridTrack, Option) -> Option, + tracks: &[GridTrack], +) -> f32 { + if tracks.len() <= 1 { + return 0.0; + } + + // As items never cross the outermost gutters in a grid, we can simplify our calculations by + // treating Start and End the same. The safety modifier doesn't influence gutter weight; + // overflow fallback is handled when offsets are computed. + let outer_gutter_weight = match alignment.keyword() { + AlignContentKeyword::Start + | AlignContentKeyword::FlexStart + | AlignContentKeyword::End + | AlignContentKeyword::FlexEnd + | AlignContentKeyword::Center => 1, + AlignContentKeyword::Stretch => 0, + AlignContentKeyword::SpaceBetween => 0, + AlignContentKeyword::SpaceAround => 1, + AlignContentKeyword::SpaceEvenly => 1, + }; + + let inner_gutter_weight = match alignment.keyword() { + AlignContentKeyword::FlexStart + | AlignContentKeyword::Start + | AlignContentKeyword::FlexEnd + | AlignContentKeyword::End + | AlignContentKeyword::Center + | AlignContentKeyword::Stretch => 0, + AlignContentKeyword::SpaceBetween => 1, + AlignContentKeyword::SpaceAround => 2, + AlignContentKeyword::SpaceEvenly => 1, + }; + + if inner_gutter_weight == 0 { + return 0.0; + } + + if let Some(axis_inner_node_size) = axis_inner_node_size { + let free_space = tracks + .iter() + .map(|track| get_track_size_estimate(track, Some(axis_inner_node_size))) + .sum::>() + .map(|track_size_sum| f32_max(0.0, axis_inner_node_size - track_size_sum)) + .unwrap_or(0.0); + + let weighted_track_count = + (((tracks.len() - 3) / 2) * inner_gutter_weight as usize) + (2 * outer_gutter_weight as usize); + + return (free_space / weighted_track_count as f32) * inner_gutter_weight as f32; + } + + 0.0 +} + +/// Convert origin-zero coordinates track placement in grid track vector indexes +#[inline(always)] +pub(super) fn resolve_item_track_indexes(items: &mut [GridItem], column_counts: TrackCounts, row_counts: TrackCounts) { + for item in items { + item.column_indexes = item.column.map(|line| line.into_track_vec_index(column_counts) as u16); + item.row_indexes = item.row.map(|line| line.into_track_vec_index(row_counts) as u16); + } +} + +/// Determine (in each axis) whether the item crosses any flexible tracks +#[inline(always)] +pub(super) fn determine_if_item_crosses_flexible_or_intrinsic_tracks( + items: &mut Vec, + columns: &[GridTrack], + rows: &[GridTrack], +) { + for item in items { + item.crosses_flexible_column = + item.track_range_excluding_lines(AbstractAxis::Inline).any(|i| columns[i].is_flexible()); + item.crosses_intrinsic_column = + item.track_range_excluding_lines(AbstractAxis::Inline).any(|i| columns[i].has_intrinsic_sizing_function()); + item.crosses_flexible_row = + item.track_range_excluding_lines(AbstractAxis::Block).any(|i| rows[i].is_flexible()); + item.crosses_intrinsic_row = + item.track_range_excluding_lines(AbstractAxis::Block).any(|i| rows[i].has_intrinsic_sizing_function()); + } +} + +/// Track sizing algorithm +/// Note: Gutters are treated as empty fixed-size tracks for the purpose of the track sizing algorithm. +#[allow(clippy::too_many_arguments)] +pub(super) fn track_sizing_algorithm( + tree: &mut Tree, + axis: AbstractAxis, + axis_min_size: Option, + axis_max_size: Option, + axis_alignment: AlignContent, + other_axis_alignment: AlignContent, + available_grid_space: Size, + inner_node_size: Size>, + axis_tracks: &mut [GridTrack], + other_axis_tracks: &mut [GridTrack], + items: &mut [GridItem], + get_track_size_estimate: fn(&GridTrack, Option, &Tree) -> Option, + has_baseline_aligned_item: bool, +) { + // 11.4 Initialise Track sizes + // Initialize each track’s base size and growth limit. + let percentage_basis = inner_node_size.get(axis).or(axis_min_size); + initialize_track_sizes(tree, axis_tracks, percentage_basis); + + // 11.5.1 Shim item baselines + if has_baseline_aligned_item { + resolve_item_baselines(tree, axis, items, inner_node_size); + } + + // If all tracks have base_size = growth_limit, then skip the rest of this function. + // Note: this can only happen both track sizing function have the same fixed track sizing function + if axis_tracks.iter().all(|track| track.base_size == track.growth_limit) { + return; + } + + // Pre-computations for 11.5 Resolve Intrinsic Track Sizes + + // Compute an additional amount to add to each spanned gutter when computing item's estimated size in the + // in the opposite axis based on the alignment, container size, and estimated track sizes in that axis + let gutter_alignment_adjustment = compute_alignment_gutter_adjustment( + other_axis_alignment, + inner_node_size.get(axis.other()), + |track, basis| get_track_size_estimate(track, basis, tree), + other_axis_tracks, + ); + if other_axis_tracks.len() > 3 { + let len = other_axis_tracks.len(); + let inner_gutter_tracks = other_axis_tracks[2..len].iter_mut().step_by(2); + for track in inner_gutter_tracks { + track.content_alignment_adjustment = gutter_alignment_adjustment; + } + } + + // 11.5 Resolve Intrinsic Track Sizes + resolve_intrinsic_track_sizes( + tree, + axis, + axis_tracks, + other_axis_tracks, + items, + available_grid_space.get(axis), + inner_node_size, + get_track_size_estimate, + ); + + // 11.6. Maximise Tracks + // Distributes free space (if any) to tracks with FINITE growth limits, up to their limits. + maximise_tracks(axis_tracks, inner_node_size.get(axis), available_grid_space.get(axis)); + + // For the purpose of the final two expansion steps ("Expand Flexible Tracks" and "Stretch auto Tracks"), we only want to expand + // into space generated by the grid container's size (as defined by either it's preferred size style or by it's parent node through + // something like stretch alignment), not just any available space. To do this we map definite available space to AvailableSpace::MaxContent + // in the case that inner_node_size is None + let axis_available_space_for_expansion = if let Some(available_space) = inner_node_size.get(axis) { + AvailableSpace::Definite(available_space) + } else { + match available_grid_space.get(axis) { + AvailableSpace::MinContent => AvailableSpace::MinContent, + AvailableSpace::MaxContent | AvailableSpace::Definite(_) => AvailableSpace::MaxContent, + } + }; + + // 11.7. Expand Flexible Tracks + // This step sizes flexible tracks using the largest value it can assign to an fr without exceeding the available space. + expand_flexible_tracks( + tree, + axis, + axis_tracks, + items, + axis_min_size, + axis_max_size, + axis_available_space_for_expansion, + ); + + // 11.8. Stretch auto Tracks + // This step expands tracks that have an auto max track sizing function by dividing any remaining positive, definite free space equally amongst them. + if axis_alignment == AlignContent::STRETCH { + stretch_auto_tracks(axis_tracks, axis_min_size, axis_available_space_for_expansion); + } +} + +/// Whether it is a minimum or maximum size's space being distributed +/// This controls behaviour of the space distribution algorithm when distributing beyond limits +/// See "distributing space beyond limits" at https://www.w3.org/TR/css-grid-1/#extra-space +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum IntrinsicContributionType { + /// It's a minimum size's space being distributed + Minimum, + /// It's a maximum size's space being distributed + Maximum, +} + +/// Add any planned base size increases to the base size after a round of distributing space to base sizes +/// Reset the planed base size increase to zero ready for the next round. +#[inline(always)] +fn flush_planned_base_size_increases(tracks: &mut [GridTrack]) { + for track in tracks { + track.base_size += track.base_size_planned_increase; + track.base_size_planned_increase = 0.0; + } +} + +/// Add any planned growth limit increases to the growth limit after a round of distributing space to growth limits +/// Reset the planed growth limit increase to zero ready for the next round. +#[inline(always)] +fn flush_planned_growth_limit_increases(tracks: &mut [GridTrack], set_infinitely_growable: bool) { + for track in tracks { + if track.growth_limit_planned_increase > 0.0 { + track.growth_limit = if track.growth_limit == f32::INFINITY { + track.base_size + track.growth_limit_planned_increase + } else { + track.growth_limit + track.growth_limit_planned_increase + }; + track.infinitely_growable = set_infinitely_growable; + } else { + track.infinitely_growable = false; + } + track.growth_limit_planned_increase = 0.0 + } +} + +/// 11.4 Initialise Track sizes +/// Initialize each track’s base size and growth limit. +#[inline(always)] +fn initialize_track_sizes( + tree: &impl LayoutPartialTree, + axis_tracks: &mut [GridTrack], + axis_inner_node_size: Option, +) { + for track in axis_tracks.iter_mut() { + // For each track, if the track’s min track sizing function is: + // - A fixed sizing function + // Resolve to an absolute length and use that size as the track’s initial base size. + // Note: Indefinite lengths cannot occur, as they’re treated as auto. + // - An intrinsic sizing function + // Use an initial base size of zero. + track.base_size = track + .min_track_sizing_function + .definite_value(axis_inner_node_size, |val, basis| tree.calc(val, basis)) + .unwrap_or(0.0); + + // For each track, if the track’s max track sizing function is: + // - A fixed sizing function + // Resolve to an absolute length and use that size as the track’s initial growth limit. + // - An intrinsic sizing function + // Use an initial growth limit of infinity. + // - A flexible sizing function + // Use an initial growth limit of infinity. + track.growth_limit = track + .max_track_sizing_function + .definite_value(axis_inner_node_size, |val, basis| tree.calc(val, basis)) + .unwrap_or(f32::INFINITY); + + // In all cases, if the growth limit is less than the base size, increase the growth limit to match the base size. + if track.growth_limit < track.base_size { + track.growth_limit = track.base_size; + } + } +} + +/// 11.5.1 Shim baseline-aligned items so their intrinsic size contributions reflect their baseline alignment. +fn resolve_item_baselines( + tree: &mut impl LayoutPartialTree, + axis: AbstractAxis, + items: &mut [GridItem], + inner_node_size: Size>, +) { + // Sort items by track in the other axis (row) start position so that we can iterate items in groups which + // are in the same track in the other axis (row) + let other_axis = axis.other(); + items.sort_by_key(|item| item.placement(other_axis).start); + + // Iterate over grid rows + let mut remaining_items = &mut items[0..]; + while !remaining_items.is_empty() { + // Get the row index of the current row + let current_row = remaining_items[0].placement(other_axis).start; + + // Find the item index of the first item that is in a different row (or None if we've reached the end of the list) + let next_row_first_item = + remaining_items.iter().position(|item| item.placement(other_axis).start != current_row); + + // Use this index to split the `remaining_items` slice in two slices: + // - A `row_items` slice containing the items (that start) in the current row + // - A new `remaining_items` consisting of the remainder of the `remaining_items` slice + // that hasn't been split off into `row_items + let row_items = if let Some(index) = next_row_first_item { + let (row_items, tail) = remaining_items.split_at_mut(index); + remaining_items = tail; + row_items + } else { + let row_items = remaining_items; + remaining_items = &mut []; + row_items + }; + + // Count how many items in *this row* are baseline aligned + // If a row has one or zero items participating in baseline alignment then baseline alignment is a no-op + // for those items and we skip further computations for that row + let row_baseline_item_count = row_items.iter().filter(|item| item.align_self == AlignSelf::BASELINE).count(); + if row_baseline_item_count <= 1 { + continue; + } + + // Compute the baselines of all items in the row + for item in row_items.iter_mut() { + let measured_size_and_baselines = tree.perform_child_layout( + item.node, + Size::NONE, + inner_node_size, + Size::MIN_CONTENT, + SizingMode::InherentSize, + Line::FALSE, + ); + + let baseline = measured_size_and_baselines.first_baselines.y; + let height = measured_size_and_baselines.size.height; + + item.baseline = Some( + baseline.unwrap_or(height) + + item.margin.top.resolve_or_zero(inner_node_size.width, |val, basis| tree.calc(val, basis)), + ); + } + + // Compute the max baseline of all items in the row + let row_max_baseline = + row_items.iter().map(|item| item.baseline.unwrap_or(0.0)).max_by(|a, b| a.total_cmp(b)).unwrap(); + + // Compute the baseline shim for each item in the row + for item in row_items.iter_mut() { + item.baseline_shim = row_max_baseline - item.baseline.unwrap_or(0.0); + } + } +} + +/// 11.5 Resolve Intrinsic Track Sizes +#[allow(clippy::too_many_arguments)] +fn resolve_intrinsic_track_sizes( + tree: &mut Tree, + axis: AbstractAxis, + axis_tracks: &mut [GridTrack], + other_axis_tracks: &[GridTrack], + items: &mut [GridItem], + axis_available_grid_space: AvailableSpace, + inner_node_size: Size>, + get_track_size_estimate: impl Fn(&GridTrack, Option, &Tree) -> Option, +) { + // Step 1. Shim baseline-aligned items so their intrinsic size contributions reflect their baseline alignment. + + // Already done at this point. See resolve_item_baselines function. + + // Step 2. + + // The track sizing algorithm requires us to iterate through the items in ascending order of the number of + // tracks they span (first items that span 1 track, then items that span 2 tracks, etc). + // To avoid having to do multiple iterations of the items, we pre-sort them into this order. + items.sort_by(cmp_by_cross_flex_then_span_then_start(axis)); + + // Step 2, Step 3 and Step 4 + // 2 & 3. Iterate over items that don't cross a flex track. Items should have already been sorted in ascending order + // of the number of tracks they span. Step 2 is the 1 track case and has an optimised implementation + // 4. Next, repeat the previous step instead considering (together, rather than grouped by span size) all items + // that do span a track with a flexible sizing function while + + // Compute item's intrinsic (content-based) sizes + // Note: For items with a specified minimum size of auto (the initial value), the minimum contribution is usually equivalent + // to the min-content contribution—but can differ in some cases, see §6.6 Automatic Minimum Size of Grid Items. + // Also, minimum contribution <= min-content contribution <= max-content contribution. + + let axis_inner_node_size = inner_node_size.get(axis); + let flex_factor_sum = axis_tracks.iter().map(|track| track.flex_factor()).sum::(); + let mut item_sizer = + IntrinsicSizeMeasurer { tree, other_axis_tracks, axis, inner_node_size, get_track_size_estimate }; + + let mut batched_item_iterator = ItemBatcher::new(axis); + while let Some((batch, is_flex)) = batched_item_iterator.next(items) { + // 2. Size tracks to fit non-spanning items: For each track with an intrinsic track sizing function and not a flexible sizing function, + // consider the items in it with a span of 1: + let batch_span = batch[0].placement(axis).span(); + if !is_flex && batch_span == 1 { + for item in batch.iter_mut() { + let track_index = item.placement_indexes(axis).start + 1; + let track = &axis_tracks[track_index as usize]; + + // Handle base sizes + let new_base_size = match track.min_track_sizing_function.0.tag() { + CompactLength::MIN_CONTENT_TAG => { + f32_max(track.base_size, item_sizer.min_content_contribution(item, axis_tracks)) + } + // If the container size is indefinite and has not yet been resolved then percentage sized + // tracks should be treated as min-content (this matches Chrome's behaviour and seems sensible) + CompactLength::PERCENT_TAG => { + if axis_inner_node_size.is_none() { + f32_max(track.base_size, item_sizer.min_content_contribution(item, axis_tracks)) + } else { + track.base_size + } + } + CompactLength::MAX_CONTENT_TAG => { + f32_max(track.base_size, item_sizer.max_content_contribution(item, axis_tracks)) + } + CompactLength::AUTO_TAG => { + let space = match axis_available_grid_space { + // QUIRK: The spec says that: + // + // If the grid container is being sized under a min- or max-content constraint, use the items’ limited + // min-content contributions in place of their minimum contributions here. + // + // However, in practice browsers only seem to apply this rule if the item is not a scroll container + // (note that overflow:hidden counts as a scroll container), giving the automatic minimum size of scroll + // containers (zero) precedence over the min-content contributions. + AvailableSpace::MinContent | AvailableSpace::MaxContent + if !item.overflow.get(axis).is_scroll_container() => + { + let axis_minimum_size = item_sizer.minimum_contribution(item, axis_tracks); + let axis_min_content_size = item_sizer.min_content_contribution(item, axis_tracks); + let limit = track + .max_track_sizing_function + .definite_limit(axis_inner_node_size, |val, basis| item_sizer.calc(val, basis)); + axis_min_content_size.maybe_min(limit).max(axis_minimum_size) + } + _ => item_sizer.minimum_contribution(item, axis_tracks), + }; + f32_max(track.base_size, space) + } + CompactLength::LENGTH_TAG => { + // Do nothing as it's not an intrinsic track sizing function + track.base_size + } + // Handle calc() like percentage + #[cfg(feature = "calc")] + _ if track.min_track_sizing_function.0.is_calc() => { + if axis_inner_node_size.is_none() { + f32_max(track.base_size, item_sizer.min_content_contribution(item, axis_tracks)) + } else { + track.base_size + } + } + _ => unreachable!(), + }; + let growth_limit_min_content_contribution = if !item.overflow.get(axis).is_scroll_container() { + Some(item_sizer.min_content_contribution(item, axis_tracks)) + } else { + None + }; + let growth_limit_max_content_contribution = item_sizer.max_content_contribution(item, axis_tracks); + let growth_limit_intrinsic_min_content_contribution = + item_sizer.min_content_contribution(item, axis_tracks); + let track = &mut axis_tracks[track_index as usize]; + track.base_size = new_base_size; + + // Handle growth limits + if track.max_track_sizing_function.is_fit_content() { + // If item is not a scroll container, then increase the growth limit to at least the + // size of the min-content contribution + if let Some(min_content_contribution) = growth_limit_min_content_contribution { + track.growth_limit_planned_increase = + f32_max(track.growth_limit_planned_increase, min_content_contribution); + } + + // Always increase the growth limit to at least the size of the *fit-content limited* + // max-content contribution + let fit_content_limit = track.fit_content_limit(axis_inner_node_size); + let max_content_contribution = f32_min(growth_limit_max_content_contribution, fit_content_limit); + track.growth_limit_planned_increase = + f32_max(track.growth_limit_planned_increase, max_content_contribution); + } else if track.max_track_sizing_function.is_max_content_alike() + || track.max_track_sizing_function.uses_percentage() && axis_inner_node_size.is_none() + { + // If the container size is indefinite and has not yet been resolved then percentage sized + // tracks should be treated as auto (this matches Chrome's behaviour and seems sensible) + track.growth_limit_planned_increase = + f32_max(track.growth_limit_planned_increase, growth_limit_max_content_contribution); + } else if track.max_track_sizing_function.is_intrinsic() { + track.growth_limit_planned_increase = + f32_max(track.growth_limit_planned_increase, growth_limit_intrinsic_min_content_contribution); + } + } + + for track in axis_tracks.iter_mut() { + if track.growth_limit_planned_increase > 0.0 { + track.growth_limit = if track.growth_limit == f32::INFINITY { + track.growth_limit_planned_increase + } else { + f32_max(track.growth_limit, track.growth_limit_planned_increase) + }; + } + track.infinitely_growable = false; + track.growth_limit_planned_increase = 0.0; + if track.growth_limit < track.base_size { + track.growth_limit = track.base_size; + } + } + + continue; + } + + let use_flex_factor_for_distribution = is_flex && flex_factor_sum != 0.0; + + // 1. For intrinsic minimums: + // First increase the base size of tracks with an intrinsic min track sizing function + for item in batch.iter_mut().filter(|item| item.crosses_intrinsic_track(axis)) { + // ...by distributing extra space as needed to accommodate these items’ minimum contributions. + // + // QUIRK: The spec says that: + // + // If the grid container is being sized under a min- or max-content constraint, use the items’ limited min-content contributions + // in place of their minimum contributions here. + // + // However, in practice browsers only seem to apply this rule if the item is not a scroll container (note that overflow:hidden counts as + // a scroll container), giving the automatic minimum size of scroll containers (zero) precedence over the min-content contributions. + let space = match axis_available_grid_space { + AvailableSpace::MinContent | AvailableSpace::MaxContent + if !item.overflow.get(axis).is_scroll_container() => + { + let axis_minimum_size = item_sizer.minimum_contribution(item, axis_tracks); + let axis_min_content_size = item_sizer.min_content_contribution(item, axis_tracks); + let limit = item.spanned_track_limit(axis, axis_tracks, axis_inner_node_size, &|val, basis| { + item_sizer.calc(val, basis) + }); + axis_min_content_size.maybe_min(limit).max(axis_minimum_size) + } + _ => item_sizer.minimum_contribution(item, axis_tracks), + }; + let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; + if space > 0.0 { + let has_intrinsic_min_track_sizing_function = |track: &GridTrack| { + track + .min_track_sizing_function + .definite_value(axis_inner_node_size, |val, basis| item_sizer.calc(val, basis)) + .is_none() + }; + if item.overflow.get(axis).is_scroll_container() { + let fit_content_limit = + move |track: &GridTrack| track.fit_content_limited_growth_limit(axis_inner_node_size); + distribute_item_space_to_base_size( + is_flex, + use_flex_factor_for_distribution, + space, + tracks, + has_intrinsic_min_track_sizing_function, + fit_content_limit, + IntrinsicContributionType::Minimum, + ); + } else { + distribute_item_space_to_base_size( + is_flex, + use_flex_factor_for_distribution, + space, + tracks, + has_intrinsic_min_track_sizing_function, + |track| track.growth_limit, + IntrinsicContributionType::Minimum, + ); + } + } + } + flush_planned_base_size_increases(axis_tracks); + + // 2. For content-based minimums: + // Next continue to increase the base size of tracks with a min track sizing function of min-content or max-content + // by distributing extra space as needed to account for these items' min-content contributions. + let has_min_or_max_content_min_track_sizing_function = + move |track: &GridTrack| track.min_track_sizing_function.is_min_or_max_content(); + for item in batch.iter_mut() { + let space = item_sizer.min_content_contribution(item, axis_tracks); + let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; + if space > 0.0 { + if item.overflow.get(axis).is_scroll_container() { + let fit_content_limit = + move |track: &GridTrack| track.fit_content_limited_growth_limit(axis_inner_node_size); + distribute_item_space_to_base_size( + is_flex, + use_flex_factor_for_distribution, + space, + tracks, + has_min_or_max_content_min_track_sizing_function, + fit_content_limit, + IntrinsicContributionType::Minimum, + ); + } else { + distribute_item_space_to_base_size( + is_flex, + use_flex_factor_for_distribution, + space, + tracks, + has_min_or_max_content_min_track_sizing_function, + |track| track.growth_limit, + IntrinsicContributionType::Minimum, + ); + } + } + } + flush_planned_base_size_increases(axis_tracks); + + // 3. For max-content minimums: + + // If the grid container is being sized under a max-content constraint, continue to increase the base size of tracks with + // a min track sizing function of auto or max-content by distributing extra space as needed to account for these items' + // limited max-content contributions. + + // Define fit_content_limited_growth_limit function. This is passed to the distribute_space_up_to_limits + // helper function, and is used to compute the limit to distribute up to for each track. + // Wrapping the method on GridTrack is necessary in order to resolve percentage fit-content arguments. + if axis_available_grid_space == AvailableSpace::MaxContent { + /// Whether a track: + /// - has an Auto MIN track sizing function + /// - Does not have a MinContent MAX track sizing function + /// + /// The latter condition was added in order to match Chrome. But I believe it is due to the provision + /// under minmax here https://www.w3.org/TR/css-grid-1/#track-sizes which states that: + /// + /// "If the max is less than the min, then the max will be floored by the min (essentially yielding minmax(min, min))" + #[inline(always)] + fn has_auto_min_track_sizing_function(track: &GridTrack) -> bool { + track.min_track_sizing_function.is_auto() && !track.max_track_sizing_function.is_min_content() + } + + /// Whether a track has a MaxContent min track sizing function + #[inline(always)] + fn has_max_content_min_track_sizing_function(track: &GridTrack) -> bool { + track.min_track_sizing_function.is_max_content() + } + + for item in batch.iter_mut() { + let axis_max_content_size = item_sizer.max_content_contribution(item, axis_tracks); + let limit = item.spanned_track_limit(axis, axis_tracks, axis_inner_node_size, &|val, basis| { + item_sizer.calc(val, basis) + }); + let space = axis_max_content_size.maybe_min(limit); + let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; + if space > 0.0 { + // If any of the tracks spanned by the item have a MaxContent min track sizing function then + // distribute space only to those tracks. Otherwise distribute space to tracks with an Auto min + // track sizing function. + // + // Note: this prioritisation of MaxContent over Auto is not mentioned in the spec (which suggests that + // we ought to distribute space evenly between MaxContent and Auto tracks). But it is implemented like + // this in both Chrome and Firefox (and it does have a certain logic to it), so we implement it too for + // compatibility. + // + // See: https://www.w3.org/TR/css-grid-1/#track-size-max-content-min + if tracks.iter().any(has_max_content_min_track_sizing_function) { + distribute_item_space_to_base_size( + is_flex, + use_flex_factor_for_distribution, + space, + tracks, + has_max_content_min_track_sizing_function, + |_| f32::INFINITY, + IntrinsicContributionType::Maximum, + ); + } else { + let fit_content_limited_growth_limit = + move |track: &GridTrack| track.fit_content_limited_growth_limit(axis_inner_node_size); + distribute_item_space_to_base_size( + is_flex, + use_flex_factor_for_distribution, + space, + tracks, + has_auto_min_track_sizing_function, + fit_content_limited_growth_limit, + IntrinsicContributionType::Maximum, + ); + } + } + } + flush_planned_base_size_increases(axis_tracks); + } + + // In all cases, continue to increase the base size of tracks with a min track sizing function of max-content by distributing + // extra space as needed to account for these items' max-content contributions. + let has_max_content_min_track_sizing_function = + move |track: &GridTrack| track.min_track_sizing_function.is_max_content(); + for item in batch.iter_mut() { + let axis_max_content_size = item_sizer.max_content_contribution(item, axis_tracks); + let space = axis_max_content_size; + let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; + if space > 0.0 { + distribute_item_space_to_base_size( + is_flex, + use_flex_factor_for_distribution, + space, + tracks, + has_max_content_min_track_sizing_function, + |track| track.growth_limit, + IntrinsicContributionType::Maximum, + ); + } + } + flush_planned_base_size_increases(axis_tracks); + + // 4. If at this point any track’s growth limit is now less than its base size, increase its growth limit to match its base size. + for track in axis_tracks.iter_mut() { + if track.growth_limit < track.base_size { + track.growth_limit = track.base_size; + } + } + + // If a track is a flexible track, then it has flexible max track sizing function + // It cannot also have an intrinsic max track sizing function, so these steps do not apply. + if !is_flex { + // 5. For intrinsic maximums: Next increase the growth limit of tracks with an intrinsic max track sizing function by + // distributing extra space as needed to account for these items' min-content contributions. + let has_intrinsic_max_track_sizing_function = + move |track: &GridTrack| !track.max_track_sizing_function.has_definite_value(axis_inner_node_size); + for item in batch.iter_mut() { + let axis_min_content_size = item_sizer.min_content_contribution(item, axis_tracks); + let space = axis_min_content_size; + let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; + if space > 0.0 { + distribute_item_space_to_growth_limit( + space, + tracks, + has_intrinsic_max_track_sizing_function, + inner_node_size.get(axis), + ); + } + } + // Mark any tracks whose growth limit changed from infinite to finite in this step as infinitely growable for the next step. + flush_planned_growth_limit_increases(axis_tracks, true); + + // 6. For max-content maximums: Lastly continue to increase the growth limit of tracks with a max track sizing function of max-content + // by distributing extra space as needed to account for these items' max-content contributions. However, limit the growth of any + // fit-content() tracks by their fit-content() argument. + let has_max_content_max_track_sizing_function = |track: &GridTrack| { + track.max_track_sizing_function.is_max_content_alike() + || (track.max_track_sizing_function.uses_percentage() && axis_inner_node_size.is_none()) + }; + for item in batch.iter_mut() { + let axis_max_content_size = item_sizer.max_content_contribution(item, axis_tracks); + let space = axis_max_content_size; + let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; + if space > 0.0 { + distribute_item_space_to_growth_limit( + space, + tracks, + has_max_content_max_track_sizing_function, + inner_node_size.get(axis), + ); + } + } + // Mark any tracks whose growth limit changed from infinite to finite in this step as infinitely growable for the next step. + flush_planned_growth_limit_increases(axis_tracks, false); + } + } + + // Step 5. If any track still has an infinite growth limit (because, for example, it had no items placed + // in it or it is a flexible track), set its growth limit to its base size. + // NOTE: this step is super-important to ensure that the "Maximise Tracks" step doesn't affect flexible tracks + axis_tracks + .iter_mut() + .filter(|track| track.growth_limit == f32::INFINITY) + .for_each(|track| track.growth_limit = track.base_size); +} + +/// 11.5.1. Distributing Extra Space Across Spanned Tracks +/// https://www.w3.org/TR/css-grid-1/#extra-space +#[inline(always)] +fn distribute_item_space_to_base_size( + is_flex: bool, + use_flex_factor_for_distribution: bool, + space: f32, + tracks: &mut [GridTrack], + track_is_affected: impl Fn(&GridTrack) -> bool, + track_limit: impl Fn(&GridTrack) -> f32, + intrinsic_contribution_type: IntrinsicContributionType, +) { + if is_flex { + let filter = |track: &GridTrack| track.is_flexible() && track_is_affected(track); + if use_flex_factor_for_distribution { + distribute_item_space_to_base_size_inner( + space, + tracks, + filter, + |track| track.flex_factor(), + track_limit, + intrinsic_contribution_type, + ) + } else { + distribute_item_space_to_base_size_inner( + space, + tracks, + filter, + |_| 1.0, + track_limit, + intrinsic_contribution_type, + ) + } + } else { + distribute_item_space_to_base_size_inner( + space, + tracks, + track_is_affected, + |_| 1.0, + track_limit, + intrinsic_contribution_type, + ) + } + + /// Inner function that doesn't account for differences due to distributing to flex items + /// This difference is handled by the closure passed in above + fn distribute_item_space_to_base_size_inner( + space: f32, + tracks: &mut [GridTrack], + track_is_affected: impl Fn(&GridTrack) -> bool, + track_distribution_proportion: impl Fn(&GridTrack) -> f32, + track_limit: impl Fn(&GridTrack) -> f32, + intrinsic_contribution_type: IntrinsicContributionType, + ) { + // Skip this distribution if there is either + // - no space to distribute + // - no affected tracks to distribute space to + if space == 0.0 || !tracks.iter().any(&track_is_affected) { + return; + } + + // Define get_base_size function. This is passed to the distribute_space_up_to_limits helper function + // to indicate that it is the base size that is being distributed to. + let get_base_size = |track: &GridTrack| track.base_size; + + // 1. Find the space to distribute + let track_sizes: f32 = tracks.iter().map(|track| track.base_size).sum(); + let extra_space: f32 = f32_max(0.0, space - track_sizes); + + // 2. Distribute space up to limits: + // Note: there are two exit conditions to this loop: + // - We run out of space to distribute (extra_space falls below THRESHOLD) + // - We run out of growable tracks to distribute to + + /// Define a small constant to avoid infinite loops due to rounding errors. Rather than stopping distributing + /// extra space when it gets to exactly zero, we will stop when it falls below this amount + const THRESHOLD: f32 = 0.000001; + + let extra_space = distribute_space_up_to_limits( + extra_space, + tracks, + &track_is_affected, + &track_distribution_proportion, + get_base_size, + &track_limit, + ); + + // 3. Distribute remaining span beyond limits (if any) + if extra_space > THRESHOLD { + // When accommodating minimum contributions or accommodating min-content contributions: + // - any affected track that happens to also have an intrinsic max track sizing function; + // When accommodating max-content contributions: + // - any affected track that happens to also have a max-content max track sizing function + let mut filter = match intrinsic_contribution_type { + IntrinsicContributionType::Minimum => { + (|track: &GridTrack| track.max_track_sizing_function.is_intrinsic()) as fn(&GridTrack) -> bool + } + IntrinsicContributionType::Maximum => { + (|track: &GridTrack| { + track.min_track_sizing_function.is_max_content() + || track.max_track_sizing_function.is_max_or_fit_content() + }) as fn(&GridTrack) -> bool + } + }; + + // If there are no such tracks (matching filter above), then use all affected tracks. + let number_of_tracks = + tracks.iter().filter(|track| track_is_affected(track)).filter(|track| filter(track)).count(); + if number_of_tracks == 0 { + filter = (|_| true) as fn(&GridTrack) -> bool; + } + + distribute_space_up_to_limits( + extra_space, + tracks, + filter, + &track_distribution_proportion, + get_base_size, + &track_limit, // Should apply only fit-content limit here? + ); + } + + // 4. For each affected track, if the track’s item-incurred increase is larger than the track’s planned increase + // set the track’s planned increase to that value. + for track in tracks.iter_mut() { + if track.item_incurred_increase > track.base_size_planned_increase { + track.base_size_planned_increase = track.item_incurred_increase; + } + + // Reset the item_incurresed increase ready for the next space distribution + track.item_incurred_increase = 0.0; + } + } +} + +/// 11.5.1. Distributing Extra Space Across Spanned Tracks +/// This is simplified (and faster) version of the algorithm for growth limits +/// https://www.w3.org/TR/css-grid-1/#extra-space +fn distribute_item_space_to_growth_limit( + space: f32, + tracks: &mut [GridTrack], + track_is_affected: impl Fn(&GridTrack) -> bool, + axis_inner_node_size: Option, +) { + // Skip this distribution if there is either + // - no space to distribute + // - no affected tracks to distribute space to + if space == 0.0 || tracks.iter().filter(|track| track_is_affected(track)).count() == 0 { + return; + } + + // 1. Find the space to distribute + let track_sizes: f32 = tracks + .iter() + .map(|track| if track.growth_limit == f32::INFINITY { track.base_size } else { track.growth_limit }) + .sum(); + let extra_space: f32 = f32_max(0.0, space - track_sizes); + + // 2. Distribute space up to limits: + // For growth limits the limit is either Infinity, or the growth limit itself. Which means that: + // - If there are any tracks with infinite limits then all space will be distributed to those track(s). + // - Otherwise no space will be distributed as part of this step + let number_of_growable_tracks = tracks + .iter() + .filter(|track| track_is_affected(track)) + .filter(|track| { + track.infinitely_growable || track.fit_content_limited_growth_limit(axis_inner_node_size) == f32::INFINITY + }) + .count(); + if number_of_growable_tracks > 0 { + let item_incurred_increase = extra_space / number_of_growable_tracks as f32; + for track in tracks.iter_mut().filter(|track| track_is_affected(track)).filter(|track| { + track.infinitely_growable || track.fit_content_limited_growth_limit(axis_inner_node_size) == f32::INFINITY + }) { + track.item_incurred_increase = item_incurred_increase; + } + } else { + // 3. Distribute space beyond limits + // If space remains after all tracks are frozen, unfreeze and continue to distribute space to the item-incurred increase + // ...when handling any intrinsic growth limit: all affected tracks. + distribute_space_up_to_limits( + extra_space, + tracks, + track_is_affected, + |_| 1.0, + |track| if track.growth_limit == f32::INFINITY { track.base_size } else { track.growth_limit }, + move |track| track.fit_content_limit(axis_inner_node_size), + ); + }; + + // 4. For each affected track, if the track’s item-incurred increase is larger than the track’s planned increase + // set the track’s planned increase to that value. + for track in tracks.iter_mut() { + if track.item_incurred_increase > track.growth_limit_planned_increase { + track.growth_limit_planned_increase = track.item_incurred_increase; + } + + // Reset the item_incurresed increase ready for the next space distribution + track.item_incurred_increase = 0.0; + } +} + +/// 11.6 Maximise Tracks +/// Distributes free space (if any) to tracks with FINITE growth limits, up to their limits. +#[inline(always)] +fn maximise_tracks( + axis_tracks: &mut [GridTrack], + axis_inner_node_size: Option, + axis_available_grid_space: AvailableSpace, +) { + let used_space: f32 = axis_tracks.iter().map(|track| track.base_size).sum(); + let free_space = axis_available_grid_space.compute_free_space(used_space); + if free_space == f32::INFINITY { + axis_tracks.iter_mut().for_each(|track| track.base_size = track.growth_limit); + } else if free_space > 0.0 { + distribute_space_up_to_limits( + free_space, + axis_tracks, + |_| true, + |_| 1.0, + |track| track.base_size, + move |track: &GridTrack| track.fit_content_limited_growth_limit(axis_inner_node_size), + ); + for track in axis_tracks.iter_mut() { + track.base_size += track.item_incurred_increase; + track.item_incurred_increase = 0.0; + } + } +} + +/// 11.7. Expand Flexible Tracks +/// This step sizes flexible tracks using the largest value it can assign to an fr without exceeding the available space. +#[allow(clippy::too_many_arguments)] +#[inline(always)] +fn expand_flexible_tracks( + tree: &mut impl LayoutPartialTree, + axis: AbstractAxis, + axis_tracks: &mut [GridTrack], + items: &mut [GridItem], + axis_min_size: Option, + axis_max_size: Option, + axis_available_space_for_expansion: AvailableSpace, +) { + // First, find the grid’s used flex fraction: + let flex_fraction = match axis_available_space_for_expansion { + // If the free space is zero: + // The used flex fraction is zero. + // Otherwise, if the free space is a definite length: + // The used flex fraction is the result of finding the size of an fr using all of the grid tracks and + // a space to fill of the available grid space. + AvailableSpace::Definite(available_space) => { + let used_space: f32 = axis_tracks.iter().map(|track| track.base_size).sum(); + let free_space = available_space - used_space; + if free_space <= 0.0 { + 0.0 + } else { + find_size_of_fr(axis_tracks, available_space) + } + } + // If ... sizing the grid container under a min-content constraint the used flex fraction is zero. + AvailableSpace::MinContent => 0.0, + // Otherwise, if the free space is an indefinite length: + AvailableSpace::MaxContent => { + // The used flex fraction is the maximum of: + let flex_fraction = f32_max( + // For each flexible track, if the flexible track’s flex factor is greater than one, + // the result of dividing the track’s base size by its flex factor; otherwise, the track’s base size. + axis_tracks + .iter() + .filter(|track| track.max_track_sizing_function.is_fr()) + .map(|track| { + let flex_factor = track.flex_factor(); + if flex_factor > 1.0 { + track.base_size / flex_factor + } else { + track.base_size + } + }) + .max_by(|a, b| a.total_cmp(b)) + .unwrap_or(0.0), + // For each grid item that crosses a flexible track, the result of finding the size of an fr using all the grid tracks + // that the item crosses and a space to fill of the item’s max-content contribution. + items + .iter_mut() + .filter(|item| item.crosses_flexible_track(axis)) + .map(|item| { + let tracks = &axis_tracks[item.track_range_excluding_lines(axis)]; + // TODO: plumb estimate of other axis size (known_dimensions) in here rather than just passing Size::NONE? + let max_content_contribution = + item.max_content_contribution_cached(axis, tree, Size::NONE, Size::NONE); + find_size_of_fr(tracks, max_content_contribution) + }) + .max_by(|a, b| a.total_cmp(b)) + .unwrap_or(0.0), + ); + + // If using this flex fraction would cause the grid to be smaller than the grid container’s min-width/height (or larger than the + // grid container’s max-width/height), then redo this step, treating the free space as definite and the available grid space as equal + // to the grid container’s inner size when it’s sized to its min-width/height (max-width/height). + // (Note: min_size takes precedence over max_size) + let hypothetical_grid_size: f32 = axis_tracks + .iter() + .map(|track| { + if track.max_track_sizing_function.is_fr() { + let track_flex_factor = track.max_track_sizing_function.0.value(); + f32_max(track.base_size, track_flex_factor * flex_fraction) + } else { + track.base_size + } + }) + .sum(); + let axis_min_size = axis_min_size.unwrap_or(0.0); + let axis_max_size = axis_max_size.unwrap_or(f32::INFINITY); + if hypothetical_grid_size < axis_min_size { + find_size_of_fr(axis_tracks, axis_min_size) + } else if hypothetical_grid_size > axis_max_size { + find_size_of_fr(axis_tracks, axis_max_size) + } else { + flex_fraction + } + } + }; + + // For each flexible track, if the product of the used flex fraction and the track’s flex factor is greater + // than the track’s base size, set its base size to that product. + for track in axis_tracks.iter_mut().filter(|track| track.max_track_sizing_function.is_fr()) { + let track_flex_factor = track.max_track_sizing_function.0.value(); + track.base_size = f32_max(track.base_size, track_flex_factor * flex_fraction); + } +} + +/// 11.7.1. Find the Size of an fr +/// This algorithm finds the largest size that an fr unit can be without exceeding the target size. +/// It must be called with a set of grid tracks and some quantity of space to fill. +#[inline(always)] +fn find_size_of_fr(tracks: &[GridTrack], space_to_fill: f32) -> f32 { + // Handle the trivial case where there is no space to fill + // Do not remove as otherwise the loop below will loop infinitely + if space_to_fill == 0.0 { + return 0.0; + } + + // If the product of the hypothetical fr size (computed below) and any flexible track’s flex factor + // is less than the track’s base size, then we must restart this algorithm treating all such tracks as inflexible. + // We therefore wrap the entire algorithm in a loop, with an hypothetical_fr_size of INFINITY such that the above + // condition can never be true for the first iteration. + let mut hypothetical_fr_size = f32::INFINITY; + let mut previous_iter_hypothetical_fr_size; + loop { + // Let leftover space be the space to fill minus the base sizes of the non-flexible grid tracks. + // Let flex factor sum be the sum of the flex factors of the flexible tracks. If this value is less than 1, set it to 1 instead. + // We compute both of these in a single loop to avoid iterating over the data twice + let mut used_space = 0.0; + let mut naive_flex_factor_sum = 0.0; + for track in tracks.iter() { + // Tracks for which flex_factor * hypothetical_fr_size < track.base_size are treated as inflexible + if track.max_track_sizing_function.is_fr() + && track.max_track_sizing_function.0.value() * hypothetical_fr_size >= track.base_size + { + naive_flex_factor_sum += track.max_track_sizing_function.0.value(); + } else { + used_space += track.base_size; + }; + } + let leftover_space = space_to_fill - used_space; + let flex_factor = f32_max(naive_flex_factor_sum, 1.0); + + // Let the hypothetical fr size be the leftover space divided by the flex factor sum. + previous_iter_hypothetical_fr_size = hypothetical_fr_size; + hypothetical_fr_size = leftover_space / flex_factor; + + // If the product of the hypothetical fr size and a flexible track’s flex factor is less than the track’s base size, + // restart this algorithm treating all such tracks as inflexible. + // We keep track of the hypothetical_fr_size + let hypothetical_fr_size_is_valid = tracks.iter().all(|track| { + if track.max_track_sizing_function.is_fr() { + let flex_factor = track.max_track_sizing_function.0.value(); + flex_factor * hypothetical_fr_size >= track.base_size + || flex_factor * previous_iter_hypothetical_fr_size < track.base_size + } else { + true + } + }); + if hypothetical_fr_size_is_valid { + break; + } + } + + // Return the hypothetical fr size. + hypothetical_fr_size +} + +/// 11.8. Stretch auto Tracks +/// This step expands tracks that have an auto max track sizing function by dividing any remaining positive, definite free space equally amongst them. +#[inline(always)] +fn stretch_auto_tracks( + axis_tracks: &mut [GridTrack], + axis_min_size: Option, + axis_available_space_for_expansion: AvailableSpace, +) { + let num_auto_tracks = axis_tracks.iter().filter(|track| track.max_track_sizing_function.is_auto()).count(); + if num_auto_tracks > 0 { + let used_space: f32 = axis_tracks.iter().map(|track| track.base_size).sum(); + + // If the free space is indefinite, but the grid container has a definite min-width/height + // use that size to calculate the free space for this step instead. + let free_space = if axis_available_space_for_expansion.is_definite() { + axis_available_space_for_expansion.compute_free_space(used_space) + } else { + match axis_min_size { + Some(size) => size - used_space, + None => 0.0, + } + }; + if free_space > 0.0 { + let extra_space_per_auto_track = free_space / num_auto_tracks as f32; + axis_tracks + .iter_mut() + .filter(|track| track.max_track_sizing_function.is_auto()) + .for_each(|track| track.base_size += extra_space_per_auto_track); + } + } +} + +/// Helper function for distributing space to tracks evenly +/// Used by both distribute_item_space_to_base_size and maximise_tracks steps +#[inline(always)] +fn distribute_space_up_to_limits( + space_to_distribute: f32, + tracks: &mut [GridTrack], + track_is_affected: impl Fn(&GridTrack) -> bool, + track_distribution_proportion: impl Fn(&GridTrack) -> f32, + track_affected_property: impl Fn(&GridTrack) -> f32, + track_limit: impl Fn(&GridTrack) -> f32, +) -> f32 { + /// Define a small constant to avoid infinite loops due to rounding errors. Rather than stopping distributing + /// extra space when it gets to exactly zero, we will stop when it falls below this amount + const THRESHOLD: f32 = 0.01; + + let mut space_to_distribute = space_to_distribute; + while space_to_distribute > THRESHOLD { + let track_distribution_proportion_sum: f32 = tracks + .iter() + .filter(|track| track_affected_property(track) + track.item_incurred_increase < track_limit(track)) + .filter(|track| track_is_affected(track)) + .map(&track_distribution_proportion) + .sum(); + + if track_distribution_proportion_sum == 0.0 { + break; + } + + // Compute item-incurred increase for this iteration + let min_increase_limit = tracks + .iter() + .filter(|track| track_affected_property(track) + track.item_incurred_increase < track_limit(track)) + .filter(|track| track_is_affected(track)) + .map(|track| (track_limit(track) - track_affected_property(track)) / track_distribution_proportion(track)) + .min_by(|a, b| a.total_cmp(b)) + .unwrap(); // We will never pass an empty track list to this function + let iteration_item_incurred_increase = + f32_min(min_increase_limit, space_to_distribute / track_distribution_proportion_sum); + + for track in tracks.iter_mut().filter(|track| track_is_affected(track)) { + let increase = iteration_item_incurred_increase * track_distribution_proportion(track); + if increase > 0.0 && track_affected_property(track) + increase <= track_limit(track) + THRESHOLD { + track.item_incurred_increase += increase; + space_to_distribute -= increase; + } + } + } + + space_to_distribute +} diff --git a/vendor/taffy/src/compute/grid/types/cell_occupancy.rs b/vendor/taffy/src/compute/grid/types/cell_occupancy.rs new file mode 100644 index 0000000..b840871 --- /dev/null +++ b/vendor/taffy/src/compute/grid/types/cell_occupancy.rs @@ -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, + /// 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, + secondary_range: Range, + ) -> 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, col_range: Range) { + // 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, + secondary_span: Line, + 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, + secondary_span: Line, + ) -> 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, + secondary_range: Range, + ) -> 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 { + 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 { + 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)) + } +} diff --git a/vendor/taffy/src/compute/grid/types/coordinates.rs b/vendor/taffy/src/compute/grid/types/coordinates.rs new file mode 100644 index 0000000..8543c08 --- /dev/null +++ b/vendor/taffy/src/compute/grid/types/coordinates.rs @@ -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 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 for OriginZeroLine { + type Output = Self; + fn add(self, rhs: OriginZeroLine) -> Self::Output { + OriginZeroLine(self.0 + rhs.0) + } +} +impl Sub for OriginZeroLine { + type Output = Self; + fn sub(self, rhs: OriginZeroLine) -> Self::Output { + OriginZeroLine(self.0 - rhs.0) + } +} + +// Add and Sub with u16 +impl Add for OriginZeroLine { + type Output = Self; + fn add(self, rhs: u16) -> Self::Output { + OriginZeroLine(self.0 + rhs as i16) + } +} +impl AddAssign for OriginZeroLine { + fn add_assign(&mut self, rhs: u16) { + self.0 += rhs as i16; + } +} +impl Sub 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 { + // 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 { + /// 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 {} diff --git a/vendor/taffy/src/compute/grid/types/grid_item.rs b/vendor/taffy/src/compute/grid/types/grid_item.rs new file mode 100644 index 0000000..1ed6e24 --- /dev/null +++ b/vendor/taffy/src/compute/grid/types/grid_item.rs @@ -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, + /// The items definite column-start and column-end, as resolved by the placement algorithm + /// (in origin-zero coordinates) + pub column: Line, + + /// 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, + /// The item's box_sizing style + pub box_sizing: BoxSizing, + /// The item's size style + pub size: Size, + /// The item's min_size style + pub min_size: Size, + /// The item's max_size style + pub max_size: Size, + /// The item's aspect_ratio style + pub aspect_ratio: Option, + /// The item's padding style + pub padding: Rect, + /// The item's border style + pub border: Rect, + /// The item's margin style + pub margin: Rect, + /// 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, + /// 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 stored in a grid's AbstractAxisTracks) + pub row_indexes: Line, + /// The items definite column-start and column-end (same as `column` field, except in a different coordinate system) + /// (as indexes into the Vec stored in a grid's AbstractAxisTracks) + pub column_indexes: Line, + + /// 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>>, + /// Cache for the min-content size + pub min_content_contribution_cache: Size>, + /// Cache for the minimum contribution + pub minimum_contribution_cache: Size>, + /// Cache for the max-content size + pub max_content_contribution_cache: Size>, + + /// 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( + node: NodeId, + col_span: Line, + row_span: Line, + 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 { + 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 { + 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 { + 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, + resolve_calc_value: &dyn Fn(*const (), f32) -> f32, + ) -> Option { + 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, + resolve_calc_value: &dyn Fn(*const (), f32) -> f32, + ) -> Option { + 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>, + ) -> Size> { + 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>, + get_track_size_estimate: impl Fn(&GridTrack, Option) -> Option, + resolve_calc_value: &impl Fn(*const (), f32) -> f32, + ) -> Size> { + 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::>(), + ); + + 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::>(), + ); + + 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>, + get_track_size_estimate: impl Fn(&GridTrack, Option) -> Option, + resolve_calc_value: &impl Fn(*const (), f32) -> f32, + ) -> Size> { + 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, + tree: &impl LayoutPartialTree, + ) -> Size { + 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>, + available_space: Size>, + ) -> 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>, + available_space: Size>, + ) -> 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>, + available_space: Size>, + ) -> 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>, + available_space: Size>, + ) -> 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>, + inner_node_size: Size>, + ) -> 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>, + inner_node_size: Size>, + ) -> 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 + }) + } +} diff --git a/vendor/taffy/src/compute/grid/types/grid_track.rs b/vendor/taffy/src/compute/grid/types/grid_track.rs new file mode 100644 index 0000000..af23c40 --- /dev/null +++ b/vendor/taffy/src/compute/grid/types/grid_track.rs @@ -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 }, +} + +/// 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 { + 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_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 + } + } +} diff --git a/vendor/taffy/src/compute/grid/types/grid_track_counts.rs b/vendor/taffy/src/compute/grid/types/grid_track_counts.rs new file mode 100644 index 0000000..079533d --- /dev/null +++ b/vendor/taffy/src/compute/grid/types/grid_track_counts.rs @@ -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) -> Range { + 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) -> Line { + 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 } + } +} diff --git a/vendor/taffy/src/compute/grid/types/mod.rs b/vendor/taffy/src/compute/grid/types/mod.rs new file mode 100644 index 0000000..398aebb --- /dev/null +++ b/vendor/taffy/src/compute/grid/types/mod.rs @@ -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; diff --git a/vendor/taffy/src/compute/grid/types/named.rs b/vendor/taffy/src/compute/grid/types/named.rs new file mode 100644 index 0000000..9b784c0 --- /dev/null +++ b/vendor/taffy/src/compute/grid/types/named.rs @@ -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` 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(pub T); +impl PartialOrd for StrHasher { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for StrHasher { + fn cmp(&self, other: &Self) -> Ordering { + self.0.as_ref().cmp(other.0.as_ref()) + } +} +impl PartialEq for StrHasher { + fn eq(&self, other: &Self) -> bool { + other.0.as_ref() == self.0.as_ref() + } +} +impl Eq for StrHasher {} +#[cfg(feature = "std")] +impl std::hash::Hash for StrHasher { + fn hash(&self, state: &mut H) { + self.0.as_ref().hash(state) + } +} +impl Borrow for StrHasher { + 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 { + /// Map of row line names to line numbers. Each line name may correspond to multiple lines + /// so we store a `Vec` + row_lines: Map, Vec>, + /// Map of column line names to line numbers. Each line name may correspond to multiple lines + /// so we store a `Vec` + column_lines: Map, Vec>, + /// Map of area names to area definitions (start and end lines numbers in each axis) + areas: Map, GridTemplateArea>, + /// 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(map: &mut Map, Vec>, key: S, value: u16) { + map.entry(StrHasher(key)).and_modify(|lines| lines.push(value)).or_insert_with(|| single_value_vec(value)); +} + +impl NamedLineResolver { + /// Create and initialise a new `NamedLineResolver` + pub(crate) fn new( + style: &impl GridContainerStyle, + column_auto_repetitions: u16, + row_auto_repetitions: u16, + ) -> Self { + let mut areas: Map, GridTemplateArea<_>> = Map::new(); + let mut column_lines: Map, Vec> = Map::new(); + let mut row_lines: Map, Vec> = 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| 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| 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>) -> Line { + 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>) -> Line { + 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>, + axis: GridAreaAxis, + ) -> Line { + 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 ; + // - otherwise its grid span is 1. + // + // + 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: + 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 Debug for NamedLineResolver { + 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(()) + } +} diff --git a/vendor/taffy/src/compute/grid/util/mod.rs b/vendor/taffy/src/compute/grid/util/mod.rs new file mode 100644 index 0000000..ea6a8ed --- /dev/null +++ b/vendor/taffy/src/compute/grid/util/mod.rs @@ -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}; diff --git a/vendor/taffy/src/compute/grid/util/test_helpers.rs b/vendor/taffy/src/compute/grid/util/test_helpers.rs new file mode 100644 index 0000000..8830550 --- /dev/null +++ b/vendor/taffy/src/compute/grid/util/test_helpers.rs @@ -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, GridPlacement, GridPlacement, GridPlacement) +{ + fn into_grid_child(self) -> Style { + 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)) + } +} diff --git a/vendor/taffy/src/compute/leaf.rs b/vendor/taffy/src/compute/leaf.rs new file mode 100644 index 0000000..4f1d979 --- /dev/null +++ b/vendor/taffy/src/compute/leaf.rs @@ -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( + inputs: LayoutInput, + style: &impl CoreStyle, + resolve_calc_value: impl Fn(*const (), f32) -> f32, + measure_function: MeasureFunction, +) -> LayoutOutput +where + MeasureFunction: FnOnce(Size>, Size) -> Size, +{ + 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, + } +} diff --git a/vendor/taffy/src/compute/mod.rs b/vendor/taffy/src/compute/mod.rs new file mode 100644 index 0000000..d70c77c --- /dev/null +++ b/vendor/taffy/src/compute/mod.rs @@ -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) { + 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: &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 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, + 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()); + } + } +} diff --git a/vendor/taffy/src/geometry.rs b/vendor/taffy/src/geometry.rs new file mode 100644 index 0000000..61eaa5b --- /dev/null +++ b/vendor/taffy/src/geometry.rs @@ -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 Size { + #[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 Rect { + #[inline(always)] + /// Get either the width or height depending on the AbsoluteAxis passed in + pub fn grid_axis_sum(self, axis: AbsoluteAxis) -> ::Output { + match axis { + AbsoluteAxis::Horizontal => self.left + self.right, + AbsoluteAxis::Vertical => self.top + self.bottom, + } + } +} + +/// The CSS abstract axis +/// +#[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 { + /// The item in the horizontal axis + pub horizontal: T, + /// The item in the vertical axis + pub vertical: T, +} + +impl InBothAbsAxis { + #[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 { + /// 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> Add> for Rect { + type Output = Rect; + + fn add(self, rhs: Rect) -> Self::Output { + Rect { + left: self.left + rhs.left, + right: self.right + rhs.right, + top: self.top + rhs.top, + bottom: self.bottom + rhs.bottom, + } + } +} + +impl Rect { + /// 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(self, size: Size, f: F) -> Rect + 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` into a `Rect`. + pub fn map(self, f: F) -> Rect + 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` representing the left and right properties of the Rect + pub fn horizontal_components(self) -> Line { + Line { start: self.left, end: self.right } + } + + /// Returns a `Line` containing the top and bottom properties of the Rect + pub fn vertical_components(self) -> Line { + Line { start: self.top, end: self.bottom } + } +} + +impl Rect +where + T: Add + 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` + /// + /// **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 { + 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 Rect +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 { + /// Creates a new Rect with `0.0` as all parameters + pub const ZERO: Rect = 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 { + /// The start position of a line + pub start: T, + /// The end position of a line + pub end: T, +} + +impl Line { + /// Applies the function `f` to both the width and height + /// + /// This is used to transform a `Line` into a `Line`. + pub fn map(self, f: F) -> Line + where + F: Fn(T) -> R, + { + Line { start: f(self.start), end: f(self.end) } + } +} + +impl Line { + /// A `Line` with both start and end set to `true` + pub const TRUE: Self = Line { start: true, end: true }; + /// A `Line` with both start and end set to `false` + pub const FALSE: Self = Line { start: false, end: false }; +} + +impl Line { + /// Adds the start and end values together and returns the result + pub fn sum(&self) -> ::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 { + /// The x extent of the rectangle + pub width: T, + /// The y extent of the rectangle + pub height: T, +} + +// Generic Add impl for Size + Size where T + U has an Add impl +impl> Add> for Size { + type Output = Size<>::Output>; + + fn add(self, rhs: Size) -> Self::Output { + Size { width: self.width + rhs.width, height: self.height + rhs.height } + } +} + +// Generic Sub impl for Size + Size where T + U has an Sub impl +impl> Sub> for Size { + type Output = Size<>::Output>; + + fn sub(self, rhs: Size) -> 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 Size { + /// Applies the function `f` to both the width and height + /// + /// This is used to transform a `Size` into a `Size`. + pub fn map(self, f: F) -> Size + 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(self, f: F) -> Size + where + F: Fn(T) -> T, + { + Size { width: f(self.width), height: self.height } + } + + /// Applies the function `f` to the height + pub fn map_height(self, f: F) -> Size + 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(self, other: Size, f: Func) -> Size + 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 { + /// A [`Size`] with zero width and height + pub const ZERO: Size = Self { width: 0.0, height: 0.0 }; + + /// Applies f32_max to each component separately + #[inline(always)] + pub fn f32_max(self, rhs: Size) -> Size { + 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) -> Size { + 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> { + /// A [`Size`] with `None` width and height + pub const NONE: Size> = Self { width: None, height: None }; + + /// A [`Size>`] 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>`] with either the width or height set based on the provided `direction` + #[cfg(feature = "flexbox")] + pub const fn from_cross(direction: FlexDirection, value: Option) -> 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) -> Size> { + 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 Size> { + /// Performs Option::unwrap_or on each component separately + pub fn unwrap_or(self, alt: Size) -> Size { + 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>) -> Size> { + 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 { + /// Generates a [`Size`] 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`] 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 { + /// The x-coordinate + pub x: T, + /// The y-coordinate + pub y: T, +} + +impl Point { + /// A [`Point`] with values (0,0), representing the origin + pub const ZERO: Self = Self { x: 0.0, y: 0.0 }; +} + +impl Point> { + /// A [`Point`] with values (None, None) + pub const NONE: Self = Self { x: None, y: None }; +} + +// Generic Add impl for Point + Point where T + U has an Add impl +impl> Add> for Point { + type Output = Point<>::Output>; + + fn add(self, rhs: Point) -> Self::Output { + Point { x: self.x + rhs.x, y: self.y + rhs.y } + } +} + +impl Point { + /// Applies the function `f` to both the x and y + /// + /// This is used to transform a `Point` into a `Point`. + pub fn map(self, f: F) -> Point + 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 { + 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 From> for Size { + fn from(value: Point) -> 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 { + /// The value representing the minimum + pub min: Min, + /// The value representing the maximum + pub max: Max, +} diff --git a/vendor/taffy/src/lib.rs b/vendor/taffy/src/lib.rs new file mode 100644 index 0000000..51f6ba1 --- /dev/null +++ b/vendor/taffy/src/lib.rs @@ -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 +#[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::*; diff --git a/vendor/taffy/src/prelude.rs b/vendor/taffy/src/prelude.rs new file mode 100644 index 0000000..cc3381d --- /dev/null +++ b/vendor/taffy/src/prelude.rs @@ -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; diff --git a/vendor/taffy/src/style/alignment.rs b/vendor/taffy/src/style/alignment.rs new file mode 100644 index 0000000..eda57a0 --- /dev/null +++ b/vendor/taffy/src/style/alignment.rs @@ -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(&self, serializer: S) -> Result { + 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>(deserializer: D) -> Result { + 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(self, v: &str) -> Result { + 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(&self, serializer: S) -> Result { + 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>(deserializer: D) -> Result { + 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(self, v: &str) -> Result { + 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::() <= 2, "AlignItems grew to {}", size_of::()); + assert!(size_of::() <= 2, "AlignContent grew to {}", size_of::()); + assert!(size_of::>() <= 3); + assert!(size_of::>() <= 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::().unwrap(), AlignItems::START); + assert_eq!("end".parse::().unwrap(), AlignItems::END); + assert_eq!("flex-start".parse::().unwrap(), AlignItems::FLEX_START); + assert_eq!("flex-end".parse::().unwrap(), AlignItems::FLEX_END); + assert_eq!("center".parse::().unwrap(), AlignItems::CENTER); + assert_eq!("baseline".parse::().unwrap(), AlignItems::BASELINE); + assert_eq!("stretch".parse::().unwrap(), AlignItems::STRETCH); + } + + #[cfg(feature = "parse")] + #[test] + fn parse_align_items_safe() { + assert_eq!("safe start".parse::().unwrap(), AlignItems::SAFE_START); + assert_eq!("safe end".parse::().unwrap(), AlignItems::SAFE_END); + assert_eq!("safe flex-start".parse::().unwrap(), AlignItems::SAFE_FLEX_START); + assert_eq!("safe flex-end".parse::().unwrap(), AlignItems::SAFE_FLEX_END); + assert_eq!("safe center".parse::().unwrap(), AlignItems::SAFE_CENTER); + } + + #[cfg(feature = "parse")] + #[test] + fn parse_align_items_safe_case_insensitive() { + assert_eq!("SAFE Start".parse::().unwrap(), AlignItems::SAFE_START); + assert_eq!("Safe FLEX-end".parse::().unwrap(), AlignItems::SAFE_FLEX_END); + } + + #[cfg(feature = "parse")] + #[test] + fn parse_align_items_unsafe_drops_modifier() { + assert_eq!("unsafe start".parse::().unwrap(), AlignItems::START); + assert_eq!("unsafe end".parse::().unwrap(), AlignItems::END); + assert_eq!("unsafe center".parse::().unwrap(), AlignItems::CENTER); + } + + #[cfg(feature = "parse")] + #[test] + fn parse_align_items_rejects_invalid_safe_combos() { + assert!("safe stretch".parse::().is_err()); + assert!("safe baseline".parse::().is_err()); + assert!("safe space-between".parse::().is_err()); + assert!("safe".parse::().is_err()); + assert!("safe garbage".parse::().is_err()); + assert!("unsafe stretch".parse::().is_err()); + assert!("unsafe baseline".parse::().is_err()); + } + + #[cfg(feature = "parse")] + #[test] + fn parse_align_content_plain() { + assert_eq!("start".parse::().unwrap(), AlignContent::START); + assert_eq!("space-between".parse::().unwrap(), AlignContent::SPACE_BETWEEN); + assert_eq!("space-evenly".parse::().unwrap(), AlignContent::SPACE_EVENLY); + assert_eq!("space-around".parse::().unwrap(), AlignContent::SPACE_AROUND); + assert_eq!("stretch".parse::().unwrap(), AlignContent::STRETCH); + } + + #[cfg(feature = "parse")] + #[test] + fn parse_align_content_safe() { + assert_eq!("safe start".parse::().unwrap(), AlignContent::SAFE_START); + assert_eq!("safe end".parse::().unwrap(), AlignContent::SAFE_END); + assert_eq!("safe flex-start".parse::().unwrap(), AlignContent::SAFE_FLEX_START); + assert_eq!("safe flex-end".parse::().unwrap(), AlignContent::SAFE_FLEX_END); + assert_eq!("safe center".parse::().unwrap(), AlignContent::SAFE_CENTER); + } + + #[cfg(feature = "parse")] + #[test] + fn parse_align_content_unsafe_drops_modifier() { + assert_eq!("unsafe start".parse::().unwrap(), AlignContent::START); + assert_eq!("unsafe flex-end".parse::().unwrap(), AlignContent::FLEX_END); + } + + #[cfg(feature = "parse")] + #[test] + fn parse_align_content_rejects_invalid_safe_combos() { + assert!("safe stretch".parse::().is_err()); + assert!("safe space-between".parse::().is_err()); + assert!("safe space-evenly".parse::().is_err()); + assert!("safe space-around".parse::().is_err()); + assert!("safe".parse::().is_err()); + assert!("unsafe stretch".parse::().is_err()); + assert!("unsafe space-between".parse::().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::("\"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::("\"NotAVariant\"").is_err()); + } +} diff --git a/vendor/taffy/src/style/available_space.rs b/vendor/taffy/src/style/available_space.rs new file mode 100644 index 0000000..2f2715f --- /dev/null +++ b/vendor/taffy/src/style/available_space.rs @@ -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 +/// +#[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 + 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 { + 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) -> 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 for AvailableSpace { + fn from(value: f32) -> Self { + Self::Definite(value) + } +} + +impl From> for AvailableSpace { + fn from(option: Option) -> Self { + match option { + Some(value) => Self::Definite(value), + None => Self::MaxContent, + } + } +} + +impl Size { + /// Convert `Size` into `Size>` + pub fn into_options(self) -> Size> { + 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>) -> Size { + Size { width: self.width.maybe_set(value.width), height: self.height.maybe_set(value.height) } + } +} diff --git a/vendor/taffy/src/style/block.rs b/vendor/taffy/src/style/block.rs new file mode 100644 index 0000000..954792c --- /dev/null +++ b/vendor/taffy/src/style/block.rs @@ -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::::DEFAULT.text_align + } + + /// How children of this block container are aligned in the block (cross) axis + #[inline(always)] + fn align_content(&self) -> Option { + Style::::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 `
` and `
` +#[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, +); diff --git a/vendor/taffy/src/style/compact_length.rs b/vendor/taffy/src/style/compact_length.rs new file mode 100644 index 0000000..93c85c9 --- /dev/null +++ b/vendor/taffy/src/style/compact_length.rs @@ -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: + #[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: + #[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 { + 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 + Copy>(value: Input) -> Self { + Self::length(value.into()) + } +} +impl FromPercent for CompactLength { + fn from_percent + Copy>(value: Input) -> Self { + Self::percent(value.into()) + } +} +impl FromFr for CompactLength { + fn from_fr + 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(&self, serializer: S) -> Result + 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(deserializer: D) -> Result + 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")) + } + } +} diff --git a/vendor/taffy/src/style/dimension.rs b/vendor/taffy/src/style/dimension.rs new file mode 100644 index 0000000..444bd97 --- /dev/null +++ b/vendor/taffy/src/style/dimension.rs @@ -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`](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 + Copy>(value: Input) -> Self { + Self::length(value.into()) + } +} +impl FromPercent for LengthPercentage { + fn from_percent + 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(deserializer: D) -> Result + 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`](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 + Copy>(value: Input) -> Self { + Self::length(value.into()) + } +} +impl FromPercent for LengthPercentageAuto { + fn from_percent + Copy>(value: Input) -> Self { + Self::percent(value.into()) + } +} +impl From 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 { + 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(deserializer: D) -> Result + 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`](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 + Copy>(value: Input) -> Self { + Self::length(value.into()) + } +} +impl FromPercent for Dimension { + fn from_percent + Copy>(value: Input) -> Self { + Self::percent(value.into()) + } +} +impl From for Dimension { + fn from(input: LengthPercentage) -> Self { + Self(input.0) + } +} +impl From 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 { + 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(deserializer: D) -> Result + 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 { + /// 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)), + } + } +} diff --git a/vendor/taffy/src/style/flex.rs b/vendor/taffy/src/style/flex.rs new file mode 100644 index 0000000..e4c7654 --- /dev/null +++ b/vendor/taffy/src/style/flex.rs @@ -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::::DEFAULT.flex_direction + } + /// Should elements wrap, or stay in a single line? + #[inline(always)] + fn flex_wrap(&self) -> FlexWrap { + Style::::DEFAULT.flex_wrap + } + + /// How large should the gaps between items in a grid or flex container be? + #[inline(always)] + fn gap(&self) -> Size { + Style::::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 { + Style::::DEFAULT.align_content + } + /// How this node's children aligned in the cross/block axis? + #[inline(always)] + fn align_items(&self) -> Option { + Style::::DEFAULT.align_items + } + /// How this node's children should be aligned in the inline axis + #[inline(always)] + fn justify_content(&self) -> Option { + Style::::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::::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::::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::::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 { + Style::::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()); + } + } +} diff --git a/vendor/taffy/src/style/float.rs b/vendor/taffy/src/style/float.rs new file mode 100644 index 0000000..6a8fb51 --- /dev/null +++ b/vendor/taffy/src/style/float.rs @@ -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 +#[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`ca + pub fn float_direction(&self) -> Option { + 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 +#[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, +); diff --git a/vendor/taffy/src/style/grid.rs b/vendor/taffy/src/style/grid.rs new file mode 100644 index 0000000..1e0a8b0 --- /dev/null +++ b/vendor/taffy/src/style/grid.rs @@ -0,0 +1,1607 @@ +//! Style types for CSS Grid layout +use super::{ + AlignContent, AlignItems, AlignSelf, CheapCloneStr, CompactLength, CoreStyle, Dimension, JustifyContent, + LengthPercentage, LengthPercentageAuto, Style, +}; +use crate::compute::grid::{GridCoordinate, GridLine, OriginZeroLine}; +use crate::geometry::{AbsoluteAxis, AbstractAxis, Line, MinMax, Size}; +use crate::style_helpers::*; +use crate::sys::{DefaultCheapStr, Vec}; +use core::cmp::{max, min}; +use core::fmt::Debug; + +#[cfg(feature = "parse")] +use crate::util::parse::{ + from_str_from_css, parse_css_str_entirely, CssParseResult, FromCss, ParseError, Parser, Token, +}; + +/// Defines a grid area +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct GridTemplateArea { + /// The name of the grid area which + #[cfg_attr(feature = "serde", serde(deserialize_with = "crate::util::deserialize_from_str"))] + pub name: CustomIdent, + /// The index of the row at which the grid area starts in grid coordinates. + pub row_start: u16, + /// The index of the row at which the grid area ends in grid coordinates. + pub row_end: u16, + /// The index of the column at which the grid area starts in grid coordinates. + pub column_start: u16, + /// The index of the column at which the grid area end in grid coordinates. + pub column_end: u16, +} + +/// Defines a named grid line +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct NamedGridLine { + /// The name of the grid area which + #[cfg_attr(feature = "serde", serde(deserialize_with = "crate::util::deserialize_from_str"))] + pub name: CustomIdent, + /// The index of the row at which the grid area starts in grid coordinates. + pub index: u16, +} + +/// Axis as `Row` or `Column` +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum GridAreaAxis { + /// The `Row` axis + Row, + /// The `Column` axis + Column, +} + +/// Logical end (`Start` or `End`) +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum GridAreaEnd { + /// The `Start` end + Start, + /// The `End` end + End, +} + +/// A trait to represent a `repeat()` clause in a `grid-template-*` definition +pub trait GenericRepetition { + /// The type that represents ``s (for named lines) + type CustomIdent: CheapCloneStr; + /// The type which represents an iterator over the list of repeated tracks + type RepetitionTrackList<'a>: Iterator + ExactSizeIterator + Clone + where + Self: 'a; + + /// A nested iterator of line names (nested because each line may have multiple associated names) + type TemplateLineNames<'a>: TemplateLineNames<'a, Self::CustomIdent> + where + Self: 'a; + /// The repetition count (integer, auto-fill, or auto-fit) + fn count(&self) -> RepetitionCount; + /// Get an iterator over the repeated tracks + fn tracks(&self) -> Self::RepetitionTrackList<'_>; + /// Returns the number of repeated tracks + fn track_count(&self) -> u16 { + self.tracks().len() as u16 + } + /// Returns an iterator over the lines names + fn lines_names(&self) -> Self::TemplateLineNames<'_>; +} + +/// A nested list of line names. This is effectively a generic representation of `Vec>` that allows +/// both the collection and string type to be customised. +#[rustfmt::skip] +pub trait TemplateLineNames<'a, S: CheapCloneStr> : Iterator> + ExactSizeIterator + Clone where Self: 'a { + /// A simple list line names. This is effectively a generic representation of `VecString>` that allows + /// both the collection and string type to be customised. + type LineNameSet<'b>: Iterator + ExactSizeIterator + Clone where Self: 'b; +} + +impl<'a, S: CheapCloneStr> TemplateLineNames<'a, S> + for core::iter::Map>, fn(&Vec) -> core::slice::Iter<'_, S>> +{ + type LineNameSet<'b> + = core::slice::Iter<'b, S> + where + Self: 'b; +} + +#[derive(Copy, Clone)] +/// A type representing a component in a `grid-template-*` defintion where the type +/// representing `repeat()`s is generic +pub enum GenericGridTemplateComponent +where + S: CheapCloneStr, + Repetition: GenericRepetition, +{ + /// A single track sizing function + Single(TrackSizingFunction), + /// A `repeat()` + Repeat(Repetition), +} + +impl GenericGridTemplateComponent +where + S: CheapCloneStr, + Repetition: GenericRepetition, +{ + /// Whether the track definition is a auto-repeated fragment + pub fn is_auto_repetition(&self) -> bool { + match self { + Self::Single(_) => false, + Self::Repeat(repeat) => matches!(repeat.count(), RepetitionCount::AutoFit | RepetitionCount::AutoFill), + } + } +} + +/// The set of styles required for a CSS Grid container +pub trait GridContainerStyle: CoreStyle { + /// The type for a `repeat()` within a grid_template_rows or grid_template_columns + type Repetition<'a>: GenericRepetition + where + Self: 'a; + + /// The type returned by grid_template_rows and grid_template_columns + type TemplateTrackList<'a>: Iterator>> + + ExactSizeIterator + + Clone + where + Self: 'a; + + /// The type returned by grid_auto_rows and grid_auto_columns + type AutoTrackList<'a>: Iterator + ExactSizeIterator + Clone + where + Self: 'a; + + /// The type returned by grid_template_row_names and grid_template_column_names + //IntoIterator> + type TemplateLineNames<'a>: TemplateLineNames<'a, Self::CustomIdent> + where + Self: 'a; + + /// The type of custom identifiers used to identify named grid lines and areas + type GridTemplateAreas<'a>: IntoIterator> + where + Self: 'a; + + // FIXME: re-add default implemenations for grid_{template,auto}_{rows,columns} once the + // associated_type_defaults feature (https://github.com/rust-lang/rust/issues/29661) is stabilised. + + /// Defines the track sizing functions (heights) of the grid rows + fn grid_template_rows(&self) -> Option>; + /// Defines the track sizing functions (widths) of the grid columns + fn grid_template_columns(&self) -> Option>; + /// Defines the size of implicitly created rows + fn grid_auto_rows(&self) -> Self::AutoTrackList<'_>; + /// Defined the size of implicitly created columns + fn grid_auto_columns(&self) -> Self::AutoTrackList<'_>; + + /// Named grid areas + fn grid_template_areas(&self) -> Option>; + /// Defines the line names for row lines + fn grid_template_column_names(&self) -> Option>; + /// Defines the size of implicitly created rows + fn grid_template_row_names(&self) -> Option>; + + /// Controls how items get placed into the grid for auto-placed items + #[inline(always)] + fn grid_auto_flow(&self) -> GridAutoFlow { + Style::::DEFAULT.grid_auto_flow + } + + /// How large should the gaps between items in a grid or flex container be? + #[inline(always)] + fn gap(&self) -> Size { + Style::::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 { + Style::::DEFAULT.align_content + } + /// How should contained within this item be aligned in the main/inline axis + #[inline(always)] + fn justify_content(&self) -> Option { + Style::::DEFAULT.justify_content + } + /// How this node's children aligned in the cross/block axis? + #[inline(always)] + fn align_items(&self) -> Option { + Style::::DEFAULT.align_items + } + /// How this node's children should be aligned in the inline axis + #[inline(always)] + fn justify_items(&self) -> Option { + Style::::DEFAULT.justify_items + } + + /// Get a grid item's row or column placement depending on the axis passed + #[inline(always)] + fn grid_template_tracks(&self, axis: AbsoluteAxis) -> Option> { + match axis { + AbsoluteAxis::Horizontal => self.grid_template_columns(), + AbsoluteAxis::Vertical => self.grid_template_rows(), + } + } + + /// Get a grid container's align-content or justify-content alignment depending on the axis passed + #[inline(always)] + fn grid_align_content(&self, axis: AbstractAxis) -> AlignContent { + match axis { + AbstractAxis::Inline => self.justify_content().unwrap_or(AlignContent::STRETCH), + AbstractAxis::Block => self.align_content().unwrap_or(AlignContent::STRETCH), + } + } +} + +/// The set of styles required for a CSS Grid item (child of a CSS Grid container) +pub trait GridItemStyle: CoreStyle { + /// Defines which row in the grid the item should start and end at + #[inline(always)] + fn grid_row(&self) -> Line> { + Default::default() + } + /// Defines which column in the grid the item should start and end at + #[inline(always)] + fn grid_column(&self) -> Line> { + Default::default() + } + + /// 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 { + Style::::DEFAULT.align_self + } + /// How this node should be aligned in the inline axis + /// Falls back to the parents [`super::JustifyItems`] if not set + #[inline(always)] + fn justify_self(&self) -> Option { + Style::::DEFAULT.justify_self + } + + /// Get a grid item's row or column placement depending on the axis passed + #[inline(always)] + fn grid_placement(&self, axis: AbsoluteAxis) -> Line> { + match axis { + AbsoluteAxis::Horizontal => self.grid_column(), + AbsoluteAxis::Vertical => self.grid_row(), + } + } +} + +/// Controls whether grid items are placed row-wise or column-wise. And whether the sparse or dense packing algorithm is used. +/// +/// The "dense" packing algorithm attempts to fill in holes earlier in the grid, if smaller items come up later. This may cause items to appear out-of-order, when doing so would fill in holes left by larger items. +/// +/// Defaults to [`GridAutoFlow::Row`] +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-flow) +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum GridAutoFlow { + /// Items are placed by filling each row in turn, adding new rows as necessary + #[default] + Row, + /// Items are placed by filling each column in turn, adding new columns as necessary. + Column, + /// Combines `Row` with the dense packing algorithm. + RowDense, + /// Combines `Column` with the dense packing algorithm. + ColumnDense, +} + +#[cfg(feature = "parse")] +impl FromCss for GridAutoFlow { + fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> { + let mut axis: Option<&'static str> = None; + let mut dense = false; + + for _ in 0..2 { + if let Ok(ident) = parser.try_parse(|parser| parser.expect_ident_cloned()) { + match &*ident { + "row" => { + axis = Some("row"); + } + "column" => { + axis = Some("column"); + } + "dense" => dense = true, + _ => { + return Err(parser.new_unexpected_token_error(Token::Ident(ident))); + } + } + } else { + break; + } + } + + match (axis, dense) { + (Some("row"), false) => Ok(Self::Row), + (Some("row") | None, true) => Ok(Self::RowDense), + (Some("column"), false) => Ok(Self::Column), + (Some("column"), true) => Ok(Self::ColumnDense), + (None, false) => { + let token = parser.next().cloned()?; + Err(parser.new_unexpected_token_error(token)) + } + _ => unreachable!(), + } + } +} +#[cfg(feature = "parse")] +from_str_from_css!(GridAutoFlow); + +impl GridAutoFlow { + /// Whether grid auto placement uses the sparse placement algorithm or the dense placement algorithm + /// See: + pub const fn is_dense(&self) -> bool { + match self { + Self::Row | Self::Column => false, + Self::RowDense | Self::ColumnDense => true, + } + } + + /// Whether grid auto placement fills areas row-wise or column-wise + /// See: + pub const fn primary_axis(&self) -> AbsoluteAxis { + match self { + Self::Row | Self::RowDense => AbsoluteAxis::Horizontal, + Self::Column | Self::ColumnDense => AbsoluteAxis::Vertical, + } + } +} + +/// A grid line placement specification which is generic over the coordinate system that it uses to define +/// grid line positions. +/// +/// `GenericGridPlacement` is aliased as GridPlacement and is exposed to users of Taffy to define styles. +/// `GenericGridPlacement` is aliased as OriginZeroGridPlacement and is used internally for placement computations. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum GenericGridPlacement { + /// Place item according to the auto-placement algorithm, and the parent's grid_auto_flow property + Auto, + /// Place item at specified line (column or row) index + Line(LineType), + /// Item should span specified number of tracks (columns or rows) + Span(u16), +} + +/// A grid line placement using the normalized OriginZero coordinates to specify line positions. +pub(crate) type OriginZeroGridPlacement = GenericGridPlacement; + +/// A grid line placement using CSS grid line coordinates to specify line positions. This uses the same coordinate +/// system as the public `GridPlacement` type but doesn't support named lines (these are expected to have already +/// been resolved by the time values of this type are constructed). +pub(crate) type NonNamedGridPlacement = GenericGridPlacement; + +/// A grid line placement specification. Used for grid-[row/column]-[start/end]. Named tracks are not implemented. +/// +/// Defaults to `GridPlacement::Auto` +/// +/// [Specification](https://www.w3.org/TR/css3-grid-layout/#typedef-grid-row-start-grid-line) +#[derive(Clone, PartialEq, Debug, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum GridPlacement { + /// Place item according to the auto-placement algorithm, and the parent's grid_auto_flow property + #[default] + Auto, + /// Place item at specified line (column or row) index + Line(GridLine), + /// Place item at specified named line (column or row) + NamedLine(S, i16), + /// Item should span specified number of tracks (columns or rows) + Span(u16), + /// Item should span until the nth line named ``. + /// + /// If there are less than n lines named `` in the specified direction then + /// all implicit lines will be counted. + NamedSpan(S, u16), +} +impl TaffyAuto for GridPlacement { + const AUTO: Self = Self::Auto; +} +impl TaffyGridLine for GridPlacement { + fn from_line_index(index: i16) -> Self { + GridPlacement::::Line(GridLine::from(index)) + } +} +impl TaffyGridLine for Line> { + fn from_line_index(index: i16) -> Self { + Line { start: GridPlacement::::from_line_index(index), end: GridPlacement::::Auto } + } +} +impl TaffyGridSpan for GridPlacement { + fn from_span(span: u16) -> Self { + GridPlacement::::Span(span) + } +} +impl TaffyGridSpan for Line> { + fn from_span(span: u16) -> Self { + Line { start: GridPlacement::::from_span(span), end: GridPlacement::::Auto } + } +} + +#[cfg(feature = "parse")] +impl FromCss for GridPlacement { + fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> { + let mut span = false; + let mut number = None; + let mut ident = None; + + while !parser.is_exhausted() { + let token = parser.next()?.clone(); + match &token { + Token::Ident(s) => match s.as_ref() { + "auto" => { + if span || number.is_some() || ident.is_some() { + return Err(parser.new_unexpected_token_error(token)); + } + parser.expect_exhausted()?; + return Ok(Self::Auto); + } + "span" => { + if span { + return Err(parser.new_unexpected_token_error(token)); + } + span = true; + } + other => { + if ident.is_some() { + return Err(parser.new_unexpected_token_error(token)); + } + ident = Some(S::from(other)); + } + }, + Token::Number { int_value: Some(value), .. } if *value != 0 => { + if number.is_some() { + return Err(parser.new_unexpected_token_error(token)); + } + number = Some(*value); + } + _ => return Err(parser.new_unexpected_token_error(token)), + }; + } + + match (span, number, ident) { + (true, None, None) => Ok(Self::Span(0)), + (true, Some(number), None) => Ok(Self::Span(number as u16)), + (true, None, Some(ident)) => Ok(Self::NamedSpan(ident, 0)), + (true, Some(number), Some(ident)) => Ok(Self::NamedSpan(ident, number as u16)), + (false, Some(number), None) => Ok(Self::Line(GridLine::from(number as i16))), + (false, Some(number), Some(ident)) => Ok(Self::NamedLine(ident, number as i16)), + (false, None, Some(ident)) => Ok(Self::NamedLine(ident, 0)), + (false, None, None) => Err(parser.new_error(cssparser::BasicParseErrorKind::EndOfInput)), + } + } +} + +#[cfg(feature = "parse")] +impl core::str::FromStr for GridPlacement { + type Err = ParseError; + fn from_str(input: &str) -> Result { + parse_css_str_entirely(input) + } +} + +impl GridPlacement { + /// Apply a mapping function if the [`GridPlacement`] is a `Line`. Otherwise return `self` unmodified. + pub fn into_origin_zero_placement_ignoring_named(&self, explicit_track_count: u16) -> OriginZeroGridPlacement { + match self { + Self::Auto => OriginZeroGridPlacement::Auto, + Self::Span(span) => OriginZeroGridPlacement::Span(*span), + // Grid line zero is an invalid index, so it gets treated as Auto + // See: https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row-start#values + Self::Line(line) => match line.as_i16() { + 0 => OriginZeroGridPlacement::Auto, + _ => OriginZeroGridPlacement::Line(line.into_origin_zero_line(explicit_track_count)), + }, + Self::NamedLine(_, _) => OriginZeroGridPlacement::Auto, + Self::NamedSpan(_, _) => OriginZeroGridPlacement::Auto, + } + } +} + +impl Line> { + /// Apply a mapping function if the [`GridPlacement`] is a `Line`. Otherwise return `self` unmodified. + pub fn into_origin_zero_ignoring_named(&self, explicit_track_count: u16) -> Line { + Line { + start: self.start.into_origin_zero_placement_ignoring_named(explicit_track_count), + end: self.end.into_origin_zero_placement_ignoring_named(explicit_track_count), + } + } +} + +impl NonNamedGridPlacement { + /// Apply a mapping function if the [`GridPlacement`] is a `Track`. Otherwise return `self` unmodified. + pub fn into_origin_zero_placement( + &self, + explicit_track_count: u16, + // resolve_named: impl Fn(&str) -> Option + ) -> OriginZeroGridPlacement { + match self { + Self::Auto => OriginZeroGridPlacement::Auto, + Self::Span(span) => OriginZeroGridPlacement::Span(*span), + // Grid line zero is an invalid index, so it gets treated as Auto + // See: https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row-start#values + Self::Line(line) => match line.as_i16() { + 0 => OriginZeroGridPlacement::Auto, + _ => OriginZeroGridPlacement::Line(line.into_origin_zero_line(explicit_track_count)), + }, + } + } +} + +impl Line> { + /// Resolves the span for an indefinite placement (a placement that does not consist of two `Track`s). + /// Panics if called on a definite placement + pub const fn indefinite_span(&self) -> u16 { + use GenericGridPlacement as GP; + match (self.start, self.end) { + (GP::Line(_), GP::Auto) => 1, + (GP::Auto, GP::Line(_)) => 1, + (GP::Auto, GP::Auto) => 1, + (GP::Line(_), GP::Span(span)) => span, + (GP::Span(span), GP::Line(_)) => span, + (GP::Span(span), GP::Auto) => span, + (GP::Auto, GP::Span(span)) => span, + (GP::Span(span), GP::Span(_)) => span, + (GP::Line(_), GP::Line(_)) => panic!("indefinite_span should only be called on indefinite grid tracks"), + } + } +} + +impl Line> { + #[inline] + /// Whether the track position is definite in this axis (or the item will need auto placement) + /// The track position is definite if least one of the start and end positions is a NON-ZERO track index + /// (0 is an invalid line in GridLine coordinates, and falls back to "auto" which is indefinite) + pub fn is_definite(&self) -> bool { + match (&self.start, &self.end) { + (GridPlacement::Line(line), _) if line.as_i16() != 0 => true, + (_, GridPlacement::Line(line)) if line.as_i16() != 0 => true, + (GridPlacement::NamedLine(_, _), _) => true, + (_, GridPlacement::NamedLine(_, _)) => true, + _ => false, + } + } +} + +impl Line { + #[inline] + /// Whether the track position is definite in this axis (or the item will need auto placement) + /// The track position is definite if least one of the start and end positions is a NON-ZERO track index + /// (0 is an invalid line in GridLine coordinates, and falls back to "auto" which is indefinite) + pub fn is_definite(&self) -> bool { + match (&self.start, &self.end) { + (GenericGridPlacement::Line(line), _) if line.as_i16() != 0 => true, + (_, GenericGridPlacement::Line(line)) if line.as_i16() != 0 => true, + _ => false, + } + } + + /// Apply a mapping function if the [`GridPlacement`] is a `Track`. Otherwise return `self` unmodified. + pub fn into_origin_zero(&self, explicit_track_count: u16) -> Line { + Line { + start: self.start.into_origin_zero_placement(explicit_track_count), + end: self.end.into_origin_zero_placement(explicit_track_count), + } + } +} + +impl Line { + #[inline] + /// Whether the track position is definite in this axis (or the item will need auto placement) + /// The track position is definite if least one of the start and end positions is a track index + pub const fn is_definite(&self) -> bool { + matches!((self.start, self.end), (GenericGridPlacement::Line(_), _) | (_, GenericGridPlacement::Line(_))) + } + + /// If at least one of the of the start and end positions is a track index then the other end can be resolved + /// into a track index purely based on the information contained with the placement specification + pub fn resolve_definite_grid_lines(&self) -> Line { + use OriginZeroGridPlacement as GP; + match (self.start, self.end) { + (GP::Line(line1), GP::Line(line2)) => { + if line1 == line2 { + Line { start: line1, end: line1 + 1 } + } else { + Line { start: min(line1, line2), end: max(line1, line2) } + } + } + (GP::Line(line), GP::Span(span)) => Line { start: line, end: line + span }, + (GP::Line(line), GP::Auto) => Line { start: line, end: line + 1 }, + (GP::Span(span), GP::Line(line)) => Line { start: line - span, end: line }, + (GP::Auto, GP::Line(line)) => Line { start: line - 1, end: line }, + _ => panic!("resolve_definite_grid_tracks should only be called on definite grid tracks"), + } + } + + /// For absolutely positioned items: + /// - Tracks resolve to definite tracks + /// - For Spans: + /// - If the other position is a Track, they resolve to a definite track relative to the other track + /// - Else resolve to None + /// - Auto resolves to None + /// + /// When finally positioning the item, a value of None means that the item's grid area is bounded by the grid + /// container's border box on that side. + pub fn resolve_absolutely_positioned_grid_tracks(&self) -> Line> { + use OriginZeroGridPlacement as GP; + match (self.start, self.end) { + (GP::Line(track1), GP::Line(track2)) => { + if track1 == track2 { + Line { start: Some(track1), end: Some(track1 + 1) } + } else { + Line { start: Some(min(track1, track2)), end: Some(max(track1, track2)) } + } + } + (GP::Line(track), GP::Span(span)) => Line { start: Some(track), end: Some(track + span) }, + (GP::Line(track), GP::Auto) => Line { start: Some(track), end: None }, + (GP::Span(span), GP::Line(track)) => Line { start: Some(track - span), end: Some(track) }, + (GP::Auto, GP::Line(track)) => Line { start: None, end: Some(track) }, + _ => Line { start: None, end: None }, + } + } + + /// If neither of the start and end positions is a track index then the other end can be resolved + /// into a track index if a definite start position is supplied externally + pub fn resolve_indefinite_grid_tracks(&self, start: OriginZeroLine) -> Line { + use OriginZeroGridPlacement as GP; + match (self.start, self.end) { + (GP::Auto, GP::Auto) => Line { start, end: start + 1 }, + (GP::Span(span), GP::Auto) => Line { start, end: start + span }, + (GP::Auto, GP::Span(span)) => Line { start, end: start + span }, + (GP::Span(span), GP::Span(_)) => Line { start, end: start + span }, + _ => panic!("resolve_indefinite_grid_tracks should only be called on indefinite grid tracks"), + } + } +} + +/// Represents the start and end points of a GridItem within a given axis +impl Default for Line> { + fn default() -> Self { + Line { start: GridPlacement::::Auto, end: GridPlacement::::Auto } + } +} + +/// Maximum track sizing function +/// +/// Specifies the maximum size of a grid track. A grid track will automatically size between it's minimum and maximum size based +/// on the size of it's contents, the amount of available space, and the sizing constraint the grid is being size under. +/// See +#[derive(Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub struct MaxTrackSizingFunction(pub(crate) CompactLength); +impl TaffyZero for MaxTrackSizingFunction { + const ZERO: Self = Self(CompactLength::ZERO); +} +impl TaffyAuto for MaxTrackSizingFunction { + const AUTO: Self = Self(CompactLength::AUTO); +} +impl TaffyMinContent for MaxTrackSizingFunction { + const MIN_CONTENT: Self = Self(CompactLength::MIN_CONTENT); +} +impl TaffyMaxContent for MaxTrackSizingFunction { + const MAX_CONTENT: Self = Self(CompactLength::MAX_CONTENT); +} +impl FromLength for MaxTrackSizingFunction { + fn from_length + Copy>(value: Input) -> Self { + Self::length(value.into()) + } +} +impl FromPercent for MaxTrackSizingFunction { + fn from_percent + Copy>(value: Input) -> Self { + Self::percent(value.into()) + } +} +impl TaffyFitContent for MaxTrackSizingFunction { + fn fit_content(argument: LengthPercentage) -> Self { + Self(CompactLength::fit_content(argument)) + } +} +impl FromFr for MaxTrackSizingFunction { + fn from_fr + Copy>(value: Input) -> Self { + Self::fr(value.into()) + } +} +impl From for MaxTrackSizingFunction { + fn from(input: LengthPercentage) -> Self { + Self(input.0) + } +} +impl From for MaxTrackSizingFunction { + fn from(input: LengthPercentageAuto) -> Self { + Self(input.0) + } +} +impl From for MaxTrackSizingFunction { + fn from(input: Dimension) -> Self { + Self(input.0) + } +} +impl From for MaxTrackSizingFunction { + fn from(input: MinTrackSizingFunction) -> Self { + Self(input.0) + } +} + +#[cfg(feature = "parse")] +impl FromCss for MaxTrackSizingFunction { + fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> { + let token = parser.next()?.clone(); + match token { + Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)), + Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)), + Token::Dimension { unit, value, .. } if unit == "fr" && value.is_sign_positive() => Ok(Self::fr(value)), + Token::Ident(ref ident) => match ident.as_ref() { + "auto" => Ok(Self::auto()), + "min-content" => Ok(Self::min_content()), + "max-content" => Ok(Self::max_content()), + _ => Err(parser.new_unexpected_token_error(token))?, + }, + Token::Function(ref name) if name.as_ref() == "fit-content" => parser.parse_nested_block(|parser| { + let token = parser.next()?.clone(); + match token { + Token::Percentage { unit_value, .. } => Ok(Self::fit_content_percent(unit_value)), + Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::fit_content_px(value)), + token => Err(parser.new_unexpected_token_error(token))?, + } + }), + token => Err(parser.new_unexpected_token_error(token))?, + } + } +} + +#[cfg(feature = "parse")] +from_str_from_css!(MaxTrackSizingFunction); + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for MaxTrackSizingFunction { + fn deserialize(deserializer: D) -> Result + 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 + | CompactLength::MIN_CONTENT_TAG + | CompactLength::MAX_CONTENT_TAG + | CompactLength::FIT_CONTENT_PX_TAG + | CompactLength::FIT_CONTENT_PERCENT_TAG + | CompactLength::FR_TAG + ) { + Ok(Self(inner)) + } else { + Err(serde::de::Error::custom("Invalid tag")) + } + } +} + +impl MaxTrackSizingFunction { + /// 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()) + } + + /// 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(CompactLength::min_content()) + } + + /// 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(CompactLength::max_content()) + } + + /// 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(CompactLength::fit_content_px(limit)) + } + + /// 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(CompactLength::fit_content_percent(limit)) + } + + /// 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: + #[inline(always)] + pub const fn fr(val: f32) -> Self { + Self(CompactLength::fr(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] + #[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 unsafe fn from_raw(val: CompactLength) -> Self { + Self(val) + } + + /// Get the underlying `CompactLength` representation of the value + pub fn into_raw(self) -> CompactLength { + self.0 + } + + /// Returns true if the max track sizing function is `MinContent`, `MaxContent`, `FitContent` or `Auto`, else false. + #[inline(always)] + pub fn is_intrinsic(&self) -> bool { + self.0.is_intrinsic() + } + + /// 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: + #[inline(always)] + pub fn is_max_content_alike(&self) -> bool { + self.0.is_max_content_alike() + } + + /// Returns true if the an Fr value, else false. + #[inline(always)] + pub fn is_fr(&self) -> bool { + self.0.is_fr() + } + + /// Returns true if the is `Auto`, else false. + #[inline(always)] + pub fn is_auto(&self) -> bool { + self.0.is_auto() + } + + /// Returns true if value is MinContent + #[inline(always)] + pub fn is_min_content(&self) -> bool { + self.0.is_min_content() + } + + /// Returns true if value is MaxContent + #[inline(always)] + pub fn is_max_content(&self) -> bool { + self.0.is_max_content() + } + + /// Returns true if value is FitContent(...) + #[inline(always)] + pub fn is_fit_content(&self) -> bool { + self.0.is_fit_content() + } + + /// Returns true if value is MaxContent or FitContent(...) + #[inline(always)] + pub fn is_max_or_fit_content(&self) -> bool { + self.0.is_max_or_fit_content() + } + + /// Returns whether the value can be resolved using `Self::definite_value` + #[inline(always)] + pub fn has_definite_value(self, parent_size: Option) -> bool { + match self.0.tag() { + CompactLength::LENGTH_TAG => true, + CompactLength::PERCENT_TAG => parent_size.is_some(), + #[cfg(feature = "calc")] + _ if self.0.is_calc() => parent_size.is_some(), + _ => false, + } + } + + /// Returns fixed point values directly. Attempts to resolve percentage values against + /// the passed available_space and returns if this results in a concrete value (which it + /// will if the available_space is `Some`). Otherwise returns None. + #[inline(always)] + pub fn definite_value( + self, + parent_size: Option, + calc_resolver: impl Fn(*const (), f32) -> f32, + ) -> Option { + match self.0.tag() { + CompactLength::LENGTH_TAG => Some(self.0.value()), + CompactLength::PERCENT_TAG => parent_size.map(|size| self.0.value() * size), + #[cfg(feature = "calc")] + _ if self.0.is_calc() => parent_size.map(|size| calc_resolver(self.0.calc_value(), size)), + _ => None, + } + } + + /// Resolve the maximum size of the track as defined by either: + /// - A fixed track sizing function + /// - A percentage track sizing function (with definite available space) + /// - A fit-content sizing function with fixed argument + /// - A fit-content sizing function with percentage argument (with definite available space) + /// All other kinds of track sizing function return None. + #[inline(always)] + pub fn definite_limit( + self, + parent_size: Option, + calc_resolver: impl Fn(*const (), f32) -> f32, + ) -> Option { + match self.0.tag() { + CompactLength::FIT_CONTENT_PX_TAG => Some(self.0.value()), + CompactLength::FIT_CONTENT_PERCENT_TAG => parent_size.map(|size| self.0.value() * size), + _ => self.definite_value(parent_size, calc_resolver), + } + } + + /// 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 { + self.0.resolved_percentage_size(parent_size, calc_resolver) + } + + /// Whether the track sizing functions depends on the size of the parent node + #[inline(always)] + pub fn uses_percentage(self) -> bool { + self.0.uses_percentage() + } +} + +/// Minimum track sizing function +/// +/// Specifies the minimum size of a grid track. A grid track will automatically size between it's minimum and maximum size based +/// on the size of it's contents, the amount of available space, and the sizing constraint the grid is being size under. +/// See +#[derive(Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub struct MinTrackSizingFunction(pub(crate) CompactLength); +impl TaffyZero for MinTrackSizingFunction { + const ZERO: Self = Self(CompactLength::ZERO); +} +impl TaffyAuto for MinTrackSizingFunction { + const AUTO: Self = Self(CompactLength::AUTO); +} +impl TaffyMinContent for MinTrackSizingFunction { + const MIN_CONTENT: Self = Self(CompactLength::MIN_CONTENT); +} +impl TaffyMaxContent for MinTrackSizingFunction { + const MAX_CONTENT: Self = Self(CompactLength::MAX_CONTENT); +} +impl FromLength for MinTrackSizingFunction { + fn from_length + Copy>(value: Input) -> Self { + Self::length(value.into()) + } +} +impl FromPercent for MinTrackSizingFunction { + fn from_percent + Copy>(value: Input) -> Self { + Self::percent(value.into()) + } +} +impl From for MinTrackSizingFunction { + fn from(input: LengthPercentage) -> Self { + Self(input.0) + } +} +impl From for MinTrackSizingFunction { + fn from(input: LengthPercentageAuto) -> Self { + Self(input.0) + } +} +impl From for MinTrackSizingFunction { + fn from(input: Dimension) -> Self { + Self(input.0) + } +} + +impl From for MinTrackSizingFunction { + fn from(input: MaxTrackSizingFunction) -> Self { + if input.is_fr() || input.is_fit_content() { + return Self::auto(); + } + Self(input.0) + } +} + +#[cfg(feature = "parse")] +impl FromCss for MinTrackSizingFunction { + fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> { + let token = parser.next()?.clone(); + match token { + Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)), + Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)), + Token::Ident(ref ident) => match ident.as_ref() { + "auto" => Ok(Self::auto()), + "min-content" => Ok(Self::min_content()), + "max-content" => Ok(Self::max_content()), + _ => Err(parser.new_unexpected_token_error(token))?, + }, + token => Err(parser.new_unexpected_token_error(token))?, + } + } +} + +#[cfg(feature = "parse")] +from_str_from_css!(MinTrackSizingFunction); + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for MinTrackSizingFunction { + fn deserialize(deserializer: D) -> Result + 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 + | CompactLength::MIN_CONTENT_TAG + | CompactLength::MAX_CONTENT_TAG + | CompactLength::FIT_CONTENT_PX_TAG + | CompactLength::FIT_CONTENT_PERCENT_TAG + ) { + Ok(Self(inner)) + } else { + Err(serde::de::Error::custom("Invalid tag")) + } + } +} + +impl MinTrackSizingFunction { + /// 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()) + } + + /// 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(CompactLength::min_content()) + } + + /// 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(CompactLength::max_content()) + } + + /// 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 unsafe fn from_raw(val: CompactLength) -> Self { + Self(val) + } + + /// Get the underlying `CompactLength` representation of the value + pub fn into_raw(self) -> CompactLength { + self.0 + } + + /// Returns true if the min track sizing function is `MinContent`, `MaxContent` or `Auto`, else false. + #[inline(always)] + pub fn is_intrinsic(&self) -> bool { + self.0.is_intrinsic() + } + + /// 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 { + self.0.is_min_or_max_content() + } + + /// Returns true if the value is an fr value + #[inline(always)] + pub fn is_fr(&self) -> bool { + self.0.is_fr() + } + + /// Returns true if the is `Auto`, else false. + #[inline(always)] + pub fn is_auto(&self) -> bool { + self.0.is_auto() + } + + /// Returns true if value is MinContent + #[inline(always)] + pub fn is_min_content(&self) -> bool { + self.0.is_min_content() + } + + /// Returns true if value is MaxContent + #[inline(always)] + pub fn is_max_content(&self) -> bool { + self.0.is_max_content() + } + + /// Returns fixed point values directly. Attempts to resolve percentage values against + /// the passed available_space and returns if this results in a concrete value (which it + /// will if the available_space is `Some`). Otherwise returns `None`. + #[inline(always)] + pub fn definite_value( + self, + parent_size: Option, + calc_resolver: impl Fn(*const (), f32) -> f32, + ) -> Option { + match self.0.tag() { + CompactLength::LENGTH_TAG => Some(self.0.value()), + CompactLength::PERCENT_TAG => parent_size.map(|size| self.0.value() * size), + #[cfg(feature = "calc")] + _ if self.0.is_calc() => parent_size.map(|size| calc_resolver(self.0.calc_value(), size)), + _ => None, + } + } + + /// 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 { + self.0.resolved_percentage_size(parent_size, calc_resolver) + } + + /// 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.0.tag(), CompactLength::PERCENT_TAG) || self.0.is_calc() + } + #[cfg(not(feature = "calc"))] + { + matches!(self.0.tag(), CompactLength::PERCENT_TAG) + } + } +} + +/// The sizing function for a grid track (row/column) +/// +/// May either be a MinMax variant which specifies separate values for the min-/max- track sizing functions +/// or a scalar value which applies to both track sizing functions. +pub type TrackSizingFunction = MinMax; +impl TrackSizingFunction { + /// Extract the min track sizing function + pub fn min_sizing_function(&self) -> MinTrackSizingFunction { + self.min + } + /// Extract the max track sizing function + pub fn max_sizing_function(&self) -> MaxTrackSizingFunction { + self.max + } + /// Determine whether at least one of the components ("min" and "max") are fixed sizing function + pub fn has_fixed_component(&self) -> bool { + self.min.0.is_length_or_percentage() || self.max.0.is_length_or_percentage() + } +} +impl TaffyAuto for TrackSizingFunction { + const AUTO: Self = Self { min: MinTrackSizingFunction::AUTO, max: MaxTrackSizingFunction::AUTO }; +} +impl TaffyMinContent for TrackSizingFunction { + const MIN_CONTENT: Self = + Self { min: MinTrackSizingFunction::MIN_CONTENT, max: MaxTrackSizingFunction::MIN_CONTENT }; +} +impl TaffyMaxContent for TrackSizingFunction { + const MAX_CONTENT: Self = + Self { min: MinTrackSizingFunction::MAX_CONTENT, max: MaxTrackSizingFunction::MAX_CONTENT }; +} +impl TaffyFitContent for TrackSizingFunction { + fn fit_content(argument: LengthPercentage) -> Self { + Self { min: MinTrackSizingFunction::AUTO, max: MaxTrackSizingFunction::fit_content(argument) } + } +} +impl TaffyZero for TrackSizingFunction { + const ZERO: Self = Self { min: MinTrackSizingFunction::ZERO, max: MaxTrackSizingFunction::ZERO }; +} +impl FromLength for TrackSizingFunction { + fn from_length + Copy>(value: Input) -> Self { + Self { min: MinTrackSizingFunction::from_length(value), max: MaxTrackSizingFunction::from_length(value) } + } +} +impl FromPercent for TrackSizingFunction { + fn from_percent + Copy>(percent: Input) -> Self { + Self { min: MinTrackSizingFunction::from_percent(percent), max: MaxTrackSizingFunction::from_percent(percent) } + } +} +impl FromFr for TrackSizingFunction { + fn from_fr + Copy>(flex: Input) -> Self { + Self { min: MinTrackSizingFunction::AUTO, max: MaxTrackSizingFunction::from_fr(flex) } + } +} +impl From for TrackSizingFunction { + fn from(input: LengthPercentage) -> Self { + Self { min: input.into(), max: input.into() } + } +} +impl From for TrackSizingFunction { + fn from(input: LengthPercentageAuto) -> Self { + Self { min: input.into(), max: input.into() } + } +} +impl From for TrackSizingFunction { + fn from(input: Dimension) -> Self { + Self { min: input.into(), max: input.into() } + } +} + +#[cfg(feature = "parse")] +impl FromCss for TrackSizingFunction { + fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> { + // Try to parse a minmax() function + if let Ok(value) = parser.try_parse(|parser| { + parser.expect_function_matching("minmax")?; + parser.parse_nested_block(|parser| { + let min = MinTrackSizingFunction::from_css(parser)?; + parser.expect_comma()?; + let max = MaxTrackSizingFunction::from_css(parser)?; + + Ok(Self { min, max }) + }) + }) { + return Ok(value); + } + + // Else parse a max track sizing function + let max = MaxTrackSizingFunction::from_css(parser)?; + let min = max.into(); + Ok(Self { min, max }) + } +} + +#[cfg(feature = "parse")] +from_str_from_css!(TrackSizingFunction); + +/// The first argument to a repeated track definition. This type represents the type of automatic repetition to perform. +/// +/// See for an explanation of how auto-repeated track definitions work +/// and the difference between AutoFit and AutoFill. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum RepetitionCount { + /// Auto-repeating tracks should be generated to fit the container + /// See: + AutoFill, + /// Auto-repeating tracks should be generated to fit the container + /// See: + AutoFit, + /// The specified tracks should be repeated exacts N times + Count(u16), +} +impl From for RepetitionCount { + fn from(value: u16) -> Self { + Self::Count(value) + } +} + +/// Error returned when trying to convert a string to a GridTrackRepetition and that string is not +/// either "auto-fit" or "auto-fill" +#[derive(Debug)] +pub struct InvalidStringRepetitionValue; +#[cfg(feature = "std")] +impl std::error::Error for InvalidStringRepetitionValue {} +impl core::fmt::Display for InvalidStringRepetitionValue { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("&str can only be converted to GridTrackRepetition if it's value is 'auto-fit' or 'auto-fill'") + } +} +impl TryFrom<&str> for RepetitionCount { + type Error = InvalidStringRepetitionValue; + fn try_from(value: &str) -> Result { + match value { + "auto-fit" => Ok(Self::AutoFit), + "auto-fill" => Ok(Self::AutoFill), + _ => Err(InvalidStringRepetitionValue), + } + } +} + +#[cfg(feature = "parse")] +impl FromCss for RepetitionCount { + fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> { + match parser.next()?.clone() { + Token::Number { int_value: Some(value), .. } if value.is_positive() => Ok(Self::Count(value as _)), + Token::Ident(ident) if ident == "auto-fit" => Ok(Self::AutoFit), + Token::Ident(ident) if ident == "auto-fill" => Ok(Self::AutoFill), + token => Err(parser.new_unexpected_token_error(token))?, + } + } +} +#[cfg(feature = "parse")] +from_str_from_css!(RepetitionCount); + +/// A typed representation of a `repeat(..)` in `grid-template-*` value +#[derive(Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct GridTemplateRepetition { + /// The number of the times the repeat is repeated + pub count: RepetitionCount, + /// The tracks to repeat + pub tracks: Vec, + /// The line names for the repeated tracks + pub line_names: Vec>, +} + +#[rustfmt::skip] +impl GenericRepetition for &'_ GridTemplateRepetition { + type CustomIdent = S; + type RepetitionTrackList<'a> = core::iter::Copied> where Self: 'a; + type TemplateLineNames<'a> = core::iter::Map>, fn(&Vec) -> core::slice::Iter<'_, S>> where Self: 'a; + #[inline(always)] + fn count(&self) -> RepetitionCount { + self.count + } + #[inline(always)] + fn track_count(&self) -> u16 { + self.tracks.len() as u16 + } + #[inline(always)] + fn tracks(&self) -> Self::RepetitionTrackList<'_> { + self.tracks.iter().copied() + } + #[inline(always)] + fn lines_names(&self) -> Self::TemplateLineNames<'_> { + self.line_names.iter().map(|names| names.iter()) + } +} + +/// An element in a `grid-template-columns` or `grid-template-rows` definition. +/// Either a track sizing function or a repeat(). +/// +/// See +#[derive(Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum GridTemplateComponent { + /// A single non-repeated track + Single(TrackSizingFunction), + /// Automatically generate grid tracks to fit the available space using the specified definite track lengths + /// Only valid if every track in template (not just the repetition) has a fixed size. + Repeat(GridTemplateRepetition), +} + +impl GridTemplateComponent { + /// Convert a `GridTemplateComponent` into a `GridTemplateComponentRef` + pub fn as_component_ref(&self) -> GenericGridTemplateComponent> { + match self { + GridTemplateComponent::Single(size) => GenericGridTemplateComponent::Single(*size), + GridTemplateComponent::Repeat(repetition) => GenericGridTemplateComponent::Repeat(repetition), + } + } +} + +impl GridTemplateComponent { + /// Whether the track definition is a auto-repeated fragment + pub fn is_auto_repetition(&self) -> bool { + matches!( + self, + Self::Repeat(GridTemplateRepetition { count: RepetitionCount::AutoFit | RepetitionCount::AutoFill, .. }) + ) + } +} +impl TaffyAuto for GridTemplateComponent { + const AUTO: Self = Self::Single(TrackSizingFunction::AUTO); +} +impl TaffyMinContent for GridTemplateComponent { + const MIN_CONTENT: Self = Self::Single(TrackSizingFunction::MIN_CONTENT); +} +impl TaffyMaxContent for GridTemplateComponent { + const MAX_CONTENT: Self = Self::Single(TrackSizingFunction::MAX_CONTENT); +} +impl TaffyFitContent for GridTemplateComponent { + fn fit_content(argument: LengthPercentage) -> Self { + Self::Single(TrackSizingFunction::fit_content(argument)) + } +} +impl TaffyZero for GridTemplateComponent { + const ZERO: Self = Self::Single(TrackSizingFunction::ZERO); +} +impl FromLength for GridTemplateComponent { + fn from_length + Copy>(value: Input) -> Self { + Self::Single(TrackSizingFunction::from_length(value)) + } +} +impl FromPercent for GridTemplateComponent { + fn from_percent + Copy>(percent: Input) -> Self { + Self::Single(TrackSizingFunction::from_percent(percent)) + } +} +impl FromFr for GridTemplateComponent { + fn from_fr + Copy>(flex: Input) -> Self { + Self::Single(TrackSizingFunction::from_fr(flex)) + } +} +impl From> for GridTemplateComponent { + fn from(input: MinMax) -> Self { + Self::Single(input) + } +} + +#[cfg(feature = "parse")] +impl FromCss for GridTemplateComponent { + fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> { + // Try to parse a minmax() function + if let Ok(value) = parser.try_parse(|parser| { + parser.expect_function_matching("repeat")?; + parser.parse_nested_block(|parser| { + let count = RepetitionCount::from_css(parser)?; + parser.expect_comma()?; + let tracks = GridTemplateTracks::::from_css(parser)?; + + Ok(Self::Repeat(GridTemplateRepetition { count, tracks: tracks.tracks, line_names: tracks.line_names })) + }) + }) { + return Ok(value); + } + + // Else parse a track sizing function + let track_sizing_function = TrackSizingFunction::from_css(parser)?; + Ok(Self::Single(track_sizing_function)) + } +} +#[cfg(feature = "parse")] +impl core::str::FromStr for GridTemplateComponent { + type Err = ParseError; + fn from_str(input: &str) -> Result { + parse_css_str_entirely(input) + } +} + +#[derive(Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[doc(hidden)] +pub struct GridTemplateTracks { + /// The tracks to repeat + pub tracks: Vec, + /// The line names for the repeated tracks + pub line_names: Vec>, +} + +impl Default for GridTemplateTracks { + fn default() -> Self { + Self { tracks: Vec::new(), line_names: Vec::new() } + } +} + +#[cfg(feature = "parse")] +impl FromCss for GridTemplateTracks { + fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> { + fn try_parse_line_names<'i, S: CheapCloneStr>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Vec> { + parser.try_parse(|parser| { + parser.expect_square_bracket_block()?; + parser.parse_nested_block(|parser| { + let mut line_names = Vec::new(); + while !parser.is_exhausted() { + line_names.push(S::from(parser.expect_ident_cloned()?.as_ref())); + } + Ok(line_names) + }) + }) + } + + let mut tracks = Self::default(); + if let Ok(line_names) = try_parse_line_names(parser) { + tracks.line_names.push(line_names); + } + + while !parser.is_exhausted() { + tracks.tracks.push(Track::from_css(parser)?); + if let Ok(line_names) = try_parse_line_names(parser) { + tracks.line_names.push(line_names); + } + } + + if tracks.tracks.is_empty() { + return Err(parser.new_error(cssparser::BasicParseErrorKind::EndOfInput)); + } + + Ok(tracks) + } +} +#[cfg(feature = "parse")] +impl core::str::FromStr for GridTemplateTracks { + type Err = ParseError; + fn from_str(input: &str) -> Result { + parse_css_str_entirely(input) + } +} + +#[derive(Default)] +#[doc(hidden)] +pub struct GridAutoTracks(pub Vec); + +#[cfg(feature = "parse")] +impl FromCss for GridAutoTracks { + fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> { + let mut tracks = Self::default(); + while !parser.is_exhausted() { + tracks.0.push(TrackSizingFunction::from_css(parser)?); + } + if tracks.0.is_empty() { + return Err(parser.new_error(cssparser::BasicParseErrorKind::EndOfInput)); + } + Ok(tracks) + } +} +#[cfg(feature = "parse")] +from_str_from_css!(GridAutoTracks); diff --git a/vendor/taffy/src/style/mod.rs b/vendor/taffy/src/style/mod.rs new file mode 100644 index 0000000..beba852 --- /dev/null +++ b/vendor/taffy/src/style/mod.rs @@ -0,0 +1,1357 @@ +//! A typed representation of [CSS style properties](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) in Rust. Used as input to layout computation. +mod alignment; +mod available_space; +mod compact_length; +mod dimension; + +#[cfg(feature = "block_layout")] +mod block; +#[cfg(feature = "flexbox")] +mod flex; +#[cfg(feature = "float_layout")] +mod float; +#[cfg(feature = "grid")] +mod grid; + +pub use self::alignment::{ + AlignContent, AlignContentKeyword, AlignItems, AlignItemsKeyword, AlignSelf, AlignmentSafety, JustifyContent, + JustifyItems, JustifySelf, +}; +pub use self::available_space::AvailableSpace; +pub use self::compact_length::CompactLength; +pub use self::dimension::{Dimension, LengthPercentage, LengthPercentageAuto}; +use crate::sys::DefaultCheapStr; + +#[cfg(feature = "block_layout")] +pub use self::block::{BlockContainerStyle, BlockItemStyle, TextAlign}; +#[cfg(feature = "flexbox")] +pub use self::flex::{FlexDirection, FlexWrap, FlexboxContainerStyle, FlexboxItemStyle}; +#[cfg(feature = "float_layout")] +pub use self::float::{Clear, Float, FloatDirection}; +#[cfg(feature = "grid")] +pub use self::grid::{ + GenericGridPlacement, GenericGridTemplateComponent, GenericRepetition, GridAutoFlow, GridAutoTracks, + GridContainerStyle, GridItemStyle, GridPlacement, GridTemplateComponent, GridTemplateRepetition, + GridTemplateTracks, MaxTrackSizingFunction, MinTrackSizingFunction, RepetitionCount, TrackSizingFunction, +}; +#[cfg(feature = "grid")] +pub(crate) use self::grid::{GridAreaAxis, GridAreaEnd}; +#[cfg(feature = "grid")] +pub use self::grid::{GridTemplateArea, NamedGridLine, TemplateLineNames}; +#[cfg(feature = "grid")] +pub(crate) use self::grid::{NonNamedGridPlacement, OriginZeroGridPlacement}; + +use crate::geometry::{Point, Rect, Size}; +use crate::style_helpers::TaffyAuto as _; +use core::fmt::Debug; + +#[cfg(feature = "grid")] +use crate::geometry::Line; +#[cfg(feature = "serde")] +use crate::style_helpers; +#[cfg(feature = "grid")] +use crate::util::sys::GridTrackVec; + +use crate::sys::String; + +/// Trait that represents a cheaply clonable string. If you're unsure what to use here +/// consider `Arc` or `string_cache::Atom`. +#[cfg(any(feature = "alloc", feature = "std"))] +pub trait CheapCloneStr: + AsRef + for<'a> From<&'a str> + From + PartialEq + Eq + Clone + Default + Debug + 'static +{ +} +#[cfg(any(feature = "alloc", feature = "std"))] +impl CheapCloneStr for T where + T: AsRef + for<'a> From<&'a str> + From + PartialEq + Eq + Clone + Default + Debug + 'static +{ +} + +/// Trait that represents a cheaply clonable string. If you're unsure what to use here +/// consider `Arc` or `string_cache::Atom`. +#[cfg(not(any(feature = "alloc", feature = "std")))] +pub trait CheapCloneStr {} +#[cfg(not(any(feature = "alloc", feature = "std")))] +impl CheapCloneStr for T {} + +/// The core set of styles that are shared between all CSS layout nodes +/// +/// Note that all methods come with a default implementation which simply returns the default value for that style property +/// but this is a just a convenience to save on boilerplate for styles that your implementation doesn't support. You will need +/// to override the default implementation for each style property that your style type actually supports. +pub trait CoreStyle { + /// The type of custom identifiers used to identify named grid lines and areas + type CustomIdent: CheapCloneStr; + + /// Which box generation mode should be used + #[inline(always)] + fn box_generation_mode(&self) -> BoxGenerationMode { + BoxGenerationMode::DEFAULT + } + /// Is block layout? + #[inline(always)] + fn is_block(&self) -> bool { + false + } + /// Is it a compressible replaced element? + /// + #[inline(always)] + fn is_compressible_replaced(&self) -> bool { + false + } + /// Which box do size styles apply to + #[inline(always)] + fn box_sizing(&self) -> BoxSizing { + BoxSizing::BorderBox + } + + /// The direction of text, table and grid columns, and horizontal overflow. + #[inline(always)] + fn direction(&self) -> Direction { + Direction::Ltr + } + + // Overflow properties + /// How children overflowing their container should affect layout + #[inline(always)] + fn overflow(&self) -> Point { + Style::::DEFAULT.overflow + } + /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. + #[inline(always)] + fn scrollbar_width(&self) -> f32 { + 0.0 + } + + // Position properties + /// What should the `position` value of this struct use as a base offset? + #[inline(always)] + fn position(&self) -> Position { + Style::::DEFAULT.position + } + /// How should the position of this element be tweaked relative to the layout defined? + #[inline(always)] + fn inset(&self) -> Rect { + Style::::DEFAULT.inset + } + + // Size properies + /// Sets the initial size of the item + #[inline(always)] + fn size(&self) -> Size { + Style::::DEFAULT.size + } + /// Controls the minimum size of the item + #[inline(always)] + fn min_size(&self) -> Size { + Style::::DEFAULT.min_size + } + /// Controls the maximum size of the item + #[inline(always)] + fn max_size(&self) -> Size { + Style::::DEFAULT.max_size + } + /// Sets the preferred aspect ratio for the item + /// The ratio is calculated as width divided by height. + #[inline(always)] + fn aspect_ratio(&self) -> Option { + Style::::DEFAULT.aspect_ratio + } + + // Spacing Properties + /// How large should the margin be on each side? + #[inline(always)] + fn margin(&self) -> Rect { + Style::::DEFAULT.margin + } + /// How large should the padding be on each side? + #[inline(always)] + fn padding(&self) -> Rect { + Style::::DEFAULT.padding + } + /// How large should the border be on each side? + #[inline(always)] + fn border(&self) -> Rect { + Style::::DEFAULT.border + } +} + +/// Sets the layout used for the children of this node +/// +/// The default values depends on on which feature flags are enabled. The order of precedence is: Flex, Grid, Block, None. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Display { + /// The children will follow the block layout algorithm + #[cfg(feature = "block_layout")] + Block, + /// The children will follow the flexbox layout algorithm + #[cfg(feature = "flexbox")] + Flex, + /// The children will follow the CSS Grid layout algorithm + #[cfg(feature = "grid")] + Grid, + /// The node is hidden, and it's children will also be hidden + None, +} + +impl Display { + /// The default Display mode + #[cfg(feature = "flexbox")] + pub const DEFAULT: Display = Display::Flex; + + /// The default Display mode + #[cfg(all(feature = "grid", not(feature = "flexbox")))] + pub const DEFAULT: Display = Display::Grid; + + /// The default Display mode + #[cfg(all(feature = "block_layout", not(feature = "flexbox"), not(feature = "grid")))] + pub const DEFAULT: Display = Display::Block; + + /// The default Display mode + #[cfg(all(not(feature = "flexbox"), not(feature = "grid"), not(feature = "block_layout")))] + pub const DEFAULT: Display = Display::None; +} + +impl Default for Display { + fn default() -> Self { + Self::DEFAULT + } +} + +#[cfg(feature = "parse")] +crate::util::parse::impl_parse_for_keyword_enum!(Display, + "none" => None, + #[cfg(feature = "flexbox")] + "flex" => Flex, + #[cfg(feature = "grid")] + "grid" => Grid, + #[cfg(feature = "block_layout")] + "block" => Block, +); + +impl core::fmt::Display for Display { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Display::None => write!(f, "NONE"), + #[cfg(feature = "block_layout")] + Display::Block => write!(f, "BLOCK"), + #[cfg(feature = "flexbox")] + Display::Flex => write!(f, "FLEX"), + #[cfg(feature = "grid")] + Display::Grid => write!(f, "GRID"), + } + } +} + +/// An abstracted version of the CSS `display` property where any value other than "none" is represented by "normal" +/// See: +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum BoxGenerationMode { + /// The node generates a box in the regular way + Normal, + /// The node and it's descendants generate no boxes (they are hidden) + None, +} + +impl BoxGenerationMode { + /// The default of BoxGenerationMode + pub const DEFAULT: BoxGenerationMode = BoxGenerationMode::Normal; +} + +impl Default for BoxGenerationMode { + fn default() -> Self { + Self::DEFAULT + } +} + +/// The positioning strategy for this item. +/// +/// This controls both how the origin is determined for the [`Style::position`] field, +/// and whether or not the item will be controlled by flexbox's layout algorithm. +/// +/// WARNING: this enum follows the behavior of [CSS's `position` property](https://developer.mozilla.org/en-US/docs/Web/CSS/position), +/// which can be unintuitive. +/// +/// [`Position::Relative`] is the default value, in contrast to the default behavior in CSS. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Position { + /// The offset is computed relative to the final position given by the layout algorithm. + /// Offsets do not affect the position of any other items; they are effectively a correction factor applied at the end. + #[default] + Relative, + /// The offset is computed relative to this item's closest positioned ancestor, if any. + /// Otherwise, it is placed relative to the origin. + /// No space is created for the item in the page layout, and its size will not be altered. + /// + /// WARNING: to opt-out of layouting entirely, you must use [`Display::None`] instead on your [`Style`] object. + Absolute, +} + +#[cfg(feature = "parse")] +crate::util::parse::impl_parse_for_keyword_enum!(Position, + "relative" => Relative, + "absolute" => Absolute, +); + +/// Specifies whether size styles for this node are assigned to the node's "content box" or "border box" +/// +/// - The "content box" is the node's inner size excluding padding, border and margin +/// - The "border box" is the node's outer size including padding and border (but still excluding margin) +/// +/// This property modifies the application of the following styles: +/// +/// - `size` +/// - `min_size` +/// - `max_size` +/// - `flex_basis` +/// +/// See +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum BoxSizing { + /// Size styles such size, min_size, max_size specify the box's "border box" (the size excluding margin but including padding/border) + #[default] + BorderBox, + /// Size styles such size, min_size, max_size specify the box's "content box" (the size excluding padding/border/margin) + ContentBox, +} + +#[cfg(feature = "parse")] +crate::util::parse::impl_parse_for_keyword_enum!(BoxSizing, + "border-box" => BorderBox, + "content-box" => ContentBox, +); + +/// How children overflowing their container should affect layout +/// +/// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should +/// be displayed anyway, be clipped, or trigger the container to become a scroll container. However it also has secondary effects on layout, +/// the main ones being: +/// +/// - The automatic minimum size Flexbox/CSS Grid items with non-`Visible` overflow is `0` rather than being content based +/// - `Overflow::Scroll` nodes have space in the layout reserved for a scrollbar (width controlled by the `scrollbar_width` property) +/// +/// In Taffy, we only implement the layout related secondary effects as we are not concerned with drawing/painting. The amount of space reserved for +/// a scrollbar is controlled by the `scrollbar_width` property. If this is `0` then `Scroll` behaves identically to `Hidden`. +/// +/// +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Overflow { + /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content. + /// Content that overflows this node *should* contribute to the scroll region of its parent. + #[default] + Visible, + /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content. + /// Content that overflows this node should *not* contribute to the scroll region of its parent. + Clip, + /// The automatic minimum size of this node as a flexbox/grid item should be `0`. + /// Content that overflows this node should *not* contribute to the scroll region of its parent. + Hidden, + /// The automatic minimum size of this node as a flexbox/grid item should be `0`. Additionally, space should be reserved + /// for a scrollbar. The amount of space reserved is controlled by the `scrollbar_width` property. + /// Content that overflows this node should *not* contribute to the scroll region of its parent. + Scroll, +} + +impl Overflow { + /// Returns true for overflow modes that contain their contents (`Overflow::Hidden`, `Overflow::Scroll`, `Overflow::Auto`) + /// or else false for overflow modes that allow their contains to spill (`Overflow::Visible`). + #[inline(always)] + pub fn is_scroll_container(self) -> bool { + match self { + Self::Visible | Self::Clip => false, + Self::Hidden | Self::Scroll => true, + } + } + + /// Returns `Some(0.0)` if the overflow mode would cause the automatic minimum size of a Flexbox or CSS Grid item + /// to be `0`. Else returns None. + #[inline(always)] + pub(crate) fn maybe_into_automatic_min_size(self) -> Option { + match self.is_scroll_container() { + true => Some(0.0), + false => None, + } + } +} + +#[cfg(feature = "parse")] +crate::util::parse::impl_parse_for_keyword_enum!(Overflow, + "visible" => Visible, + "hidden" => Hidden, + "clip" => Clip, + "scroll" => Scroll, +); + +/// Sets the direction of text, table and grid columns, and horizontal overflow. +/// +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Direction { + #[default] + /// Left-to-right + Ltr, + /// Right-to-left + Rtl, +} + +impl Direction { + /// Returns true if the direction is right-to-left + #[inline] + pub(crate) fn is_rtl(&self) -> bool { + matches!(self, Direction::Rtl) + } +} + +#[cfg(feature = "parse")] +crate::util::parse::impl_parse_for_keyword_enum!(Direction, + "ltr" => Ltr, + "rtl" => Rtl, +); + +/// A typed representation of the CSS style information for a single node. +/// +/// The most important idea in flexbox is the notion of a "main" and "cross" axis, which are always perpendicular to each other. +/// The orientation of these axes are controlled via the [`FlexDirection`] field of this struct. +/// +/// This struct follows the [CSS equivalent](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox) directly; +/// information about the behavior on the web should transfer directly. +/// +/// Detailed information about the exact behavior of each of these fields +/// can be found on [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS) by searching for the field name. +/// The distinction between margin, padding and border is explained well in +/// this [introduction to the box model](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Introduction_to_the_CSS_box_model). +/// +/// If the behavior does not match the flexbox layout algorithm on the web, please file a bug! +#[derive(Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct Style { + /// This is a dummy field which is necessary to make Taffy compile with the `grid` feature disabled + /// It should always be set to `core::marker::PhantomData`. + pub dummy: core::marker::PhantomData, + /// What layout strategy should be used? + pub display: Display, + /// Whether a child is display:table or not. This affects children of block layouts. + /// This should really be part of `Display`, but it is currently seperate because table layout isn't implemented + pub item_is_table: bool, + /// Is it a replaced element like an image or form field? + /// + pub item_is_replaced: bool, + /// Should size styles apply to the content box or the border box of the node + pub box_sizing: BoxSizing, + /// Sets the direction of text, table and grid columns, and horizontal overflow. + pub direction: Direction, + + // Overflow properties + /// How children overflowing their container should affect layout + pub overflow: Point, + /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. + pub scrollbar_width: f32, + + #[cfg(feature = "float_layout")] + /// Should the box be floated + pub float: Float, + #[cfg(feature = "float_layout")] + /// Should the box clear floats + pub clear: Clear, + + // Position properties + /// What should the `position` value of this struct use as a base offset? + pub position: Position, + /// How should the position of this element be tweaked relative to the layout defined? + #[cfg_attr(feature = "serde", serde(default = "style_helpers::auto"))] + pub inset: Rect, + + // Size properties + /// Sets the initial size of the item + #[cfg_attr(feature = "serde", serde(default = "style_helpers::auto"))] + pub size: Size, + /// Controls the minimum size of the item + #[cfg_attr(feature = "serde", serde(default = "style_helpers::auto"))] + pub min_size: Size, + /// Controls the maximum size of the item + #[cfg_attr(feature = "serde", serde(default = "style_helpers::auto"))] + pub max_size: Size, + /// Sets the preferred aspect ratio for the item + /// + /// The ratio is calculated as width divided by height. + pub aspect_ratio: Option, + + // Spacing Properties + /// How large should the margin be on each side? + #[cfg_attr(feature = "serde", serde(default = "style_helpers::zero"))] + pub margin: Rect, + /// How large should the padding be on each side? + #[cfg_attr(feature = "serde", serde(default = "style_helpers::zero"))] + pub padding: Rect, + /// How large should the border be on each side? + #[cfg_attr(feature = "serde", serde(default = "style_helpers::zero"))] + pub border: Rect, + + // Alignment properties + /// How this node's children aligned in the cross/block axis? + #[cfg(any(feature = "flexbox", feature = "grid"))] + pub align_items: Option, + /// How this node should be aligned in the cross/block axis + /// Falls back to the parents [`AlignItems`] if not set + #[cfg(any(feature = "flexbox", feature = "grid"))] + pub align_self: Option, + /// How this node's children should be aligned in the inline axis + #[cfg(feature = "grid")] + pub justify_items: Option, + /// How this node should be aligned in the inline axis + /// Falls back to the parents [`JustifyItems`] if not set + #[cfg(feature = "grid")] + pub justify_self: Option, + /// How should content contained within this item be aligned in the cross/block axis + #[cfg(any(feature = "flexbox", feature = "grid", feature = "block_layout"))] + pub align_content: Option, + /// How should content contained within this item be aligned in the main/inline axis + #[cfg(any(feature = "flexbox", feature = "grid"))] + pub justify_content: Option, + /// How large should the gaps between items in a grid or flex container be? + #[cfg(any(feature = "flexbox", feature = "grid"))] + #[cfg_attr(feature = "serde", serde(default = "style_helpers::zero"))] + pub gap: Size, + + // Block container properties + /// How items elements should aligned in the inline axis + #[cfg(feature = "block_layout")] + pub text_align: TextAlign, + + // Flexbox container properties + /// Which direction does the main axis flow in? + #[cfg(feature = "flexbox")] + pub flex_direction: FlexDirection, + /// Should elements wrap, or stay in a single line? + #[cfg(feature = "flexbox")] + pub flex_wrap: FlexWrap, + + // Flexbox item properties + /// Sets the initial main axis size of the item + #[cfg(feature = "flexbox")] + pub flex_basis: Dimension, + /// The relative rate at which this item grows when it is expanding to fill space + /// + /// 0.0 is the default value, and this value must be positive. + #[cfg(feature = "flexbox")] + pub flex_grow: f32, + /// The relative rate at which this item shrinks when it is contracting to fit into space + /// + /// 1.0 is the default value, and this value must be positive. + #[cfg(feature = "flexbox")] + pub flex_shrink: f32, + + // Grid container properies + /// Defines the track sizing functions (heights) of the grid rows + #[cfg(feature = "grid")] + pub grid_template_rows: GridTrackVec>, + /// Defines the track sizing functions (widths) of the grid columns + #[cfg(feature = "grid")] + pub grid_template_columns: GridTrackVec>, + /// Defines the size of implicitly created rows + #[cfg(feature = "grid")] + pub grid_auto_rows: GridTrackVec, + /// Defined the size of implicitly created columns + #[cfg(feature = "grid")] + pub grid_auto_columns: GridTrackVec, + /// Controls how items get placed into the grid for auto-placed items + #[cfg(feature = "grid")] + pub grid_auto_flow: GridAutoFlow, + + // Grid container named properties + /// Defines the rectangular grid areas + #[cfg(feature = "grid")] + pub grid_template_areas: GridTrackVec>, + /// The named lines between the columns + #[cfg(feature = "grid")] + pub grid_template_column_names: GridTrackVec>, + /// The named lines between the rows + #[cfg(feature = "grid")] + pub grid_template_row_names: GridTrackVec>, + + // Grid child properties + /// Defines which row in the grid the item should start and end at + #[cfg(feature = "grid")] + pub grid_row: Line>, + /// Defines which column in the grid the item should start and end at + #[cfg(feature = "grid")] + pub grid_column: Line>, +} + +impl Style { + /// The [`Default`] layout, in a form that can be used in const functions + pub const DEFAULT: Style = Style { + dummy: core::marker::PhantomData, + display: Display::DEFAULT, + item_is_table: false, + item_is_replaced: false, + box_sizing: BoxSizing::BorderBox, + direction: Direction::Ltr, + overflow: Point { x: Overflow::Visible, y: Overflow::Visible }, + scrollbar_width: 0.0, + #[cfg(feature = "float_layout")] + float: Float::None, + #[cfg(feature = "float_layout")] + clear: Clear::None, + position: Position::Relative, + inset: Rect::auto(), + margin: Rect::zero(), + padding: Rect::zero(), + border: Rect::zero(), + size: Size::auto(), + min_size: Size::auto(), + max_size: Size::auto(), + aspect_ratio: None, + #[cfg(any(feature = "flexbox", feature = "grid"))] + gap: Size::zero(), + // Alignment + #[cfg(any(feature = "flexbox", feature = "grid"))] + align_items: None, + #[cfg(any(feature = "flexbox", feature = "grid"))] + align_self: None, + #[cfg(feature = "grid")] + justify_items: None, + #[cfg(feature = "grid")] + justify_self: None, + #[cfg(any(feature = "flexbox", feature = "grid", feature = "block_layout"))] + align_content: None, + #[cfg(any(feature = "flexbox", feature = "grid"))] + justify_content: None, + // Block + #[cfg(feature = "block_layout")] + text_align: TextAlign::Auto, + // Flexbox + #[cfg(feature = "flexbox")] + flex_direction: FlexDirection::Row, + #[cfg(feature = "flexbox")] + flex_wrap: FlexWrap::NoWrap, + #[cfg(feature = "flexbox")] + flex_grow: 0.0, + #[cfg(feature = "flexbox")] + flex_shrink: 1.0, + #[cfg(feature = "flexbox")] + flex_basis: Dimension::AUTO, + // Grid + #[cfg(feature = "grid")] + grid_template_rows: GridTrackVec::new(), + #[cfg(feature = "grid")] + grid_template_columns: GridTrackVec::new(), + #[cfg(feature = "grid")] + grid_template_areas: GridTrackVec::new(), + #[cfg(feature = "grid")] + grid_template_column_names: GridTrackVec::new(), + #[cfg(feature = "grid")] + grid_template_row_names: GridTrackVec::new(), + #[cfg(feature = "grid")] + grid_auto_rows: GridTrackVec::new(), + #[cfg(feature = "grid")] + grid_auto_columns: GridTrackVec::new(), + #[cfg(feature = "grid")] + grid_auto_flow: GridAutoFlow::Row, + #[cfg(feature = "grid")] + grid_row: Line { start: GridPlacement::::Auto, end: GridPlacement::::Auto }, + #[cfg(feature = "grid")] + grid_column: Line { start: GridPlacement::::Auto, end: GridPlacement::::Auto }, + }; +} + +impl Default for Style { + fn default() -> Self { + Style::DEFAULT + } +} + +impl CoreStyle for Style { + type CustomIdent = S; + + #[inline(always)] + fn box_generation_mode(&self) -> BoxGenerationMode { + match self.display { + Display::None => BoxGenerationMode::None, + _ => BoxGenerationMode::Normal, + } + } + #[inline(always)] + #[cfg(feature = "block_layout")] + fn is_block(&self) -> bool { + matches!(self.display, Display::Block) + } + #[inline(always)] + fn is_compressible_replaced(&self) -> bool { + self.item_is_replaced + } + #[inline(always)] + fn box_sizing(&self) -> BoxSizing { + self.box_sizing + } + #[inline(always)] + fn direction(&self) -> Direction { + self.direction + } + #[inline(always)] + fn overflow(&self) -> Point { + self.overflow + } + #[inline(always)] + fn scrollbar_width(&self) -> f32 { + self.scrollbar_width + } + #[inline(always)] + fn position(&self) -> Position { + self.position + } + #[inline(always)] + fn inset(&self) -> Rect { + self.inset + } + #[inline(always)] + fn size(&self) -> Size { + self.size + } + #[inline(always)] + fn min_size(&self) -> Size { + self.min_size + } + #[inline(always)] + fn max_size(&self) -> Size { + self.max_size + } + #[inline(always)] + fn aspect_ratio(&self) -> Option { + self.aspect_ratio + } + #[inline(always)] + fn margin(&self) -> Rect { + self.margin + } + #[inline(always)] + fn padding(&self) -> Rect { + self.padding + } + #[inline(always)] + fn border(&self) -> Rect { + self.border + } +} + +impl CoreStyle for &'_ T { + type CustomIdent = T::CustomIdent; + + #[inline(always)] + fn box_generation_mode(&self) -> BoxGenerationMode { + (*self).box_generation_mode() + } + #[inline(always)] + fn is_block(&self) -> bool { + (*self).is_block() + } + #[inline(always)] + fn is_compressible_replaced(&self) -> bool { + (*self).is_compressible_replaced() + } + #[inline(always)] + fn box_sizing(&self) -> BoxSizing { + (*self).box_sizing() + } + #[inline(always)] + fn direction(&self) -> Direction { + (*self).direction() + } + #[inline(always)] + fn overflow(&self) -> Point { + (*self).overflow() + } + #[inline(always)] + fn scrollbar_width(&self) -> f32 { + (*self).scrollbar_width() + } + #[inline(always)] + fn position(&self) -> Position { + (*self).position() + } + #[inline(always)] + fn inset(&self) -> Rect { + (*self).inset() + } + #[inline(always)] + fn size(&self) -> Size { + (*self).size() + } + #[inline(always)] + fn min_size(&self) -> Size { + (*self).min_size() + } + #[inline(always)] + fn max_size(&self) -> Size { + (*self).max_size() + } + #[inline(always)] + fn aspect_ratio(&self) -> Option { + (*self).aspect_ratio() + } + #[inline(always)] + fn margin(&self) -> Rect { + (*self).margin() + } + #[inline(always)] + fn padding(&self) -> Rect { + (*self).padding() + } + #[inline(always)] + fn border(&self) -> Rect { + (*self).border() + } +} + +#[cfg(feature = "block_layout")] +impl BlockContainerStyle for Style { + #[inline(always)] + fn text_align(&self) -> TextAlign { + self.text_align + } + + #[inline(always)] + fn align_content(&self) -> Option { + self.align_content + } +} + +#[cfg(feature = "block_layout")] +impl BlockContainerStyle for &'_ T { + #[inline(always)] + fn text_align(&self) -> TextAlign { + (*self).text_align() + } + + #[inline(always)] + fn align_content(&self) -> Option { + (*self).align_content() + } +} + +#[cfg(feature = "block_layout")] +impl BlockItemStyle for Style { + #[inline(always)] + fn is_table(&self) -> bool { + self.item_is_table + } + + #[cfg(feature = "float_layout")] + #[inline(always)] + fn float(&self) -> Float { + self.float + } + + #[cfg(feature = "float_layout")] + #[inline(always)] + fn clear(&self) -> Clear { + self.clear + } +} + +#[cfg(feature = "block_layout")] +impl BlockItemStyle for &'_ T { + #[inline(always)] + fn is_table(&self) -> bool { + (*self).is_table() + } + + #[cfg(feature = "float_layout")] + #[inline(always)] + fn float(&self) -> Float { + (*self).float() + } + + #[cfg(feature = "float_layout")] + #[inline(always)] + fn clear(&self) -> Clear { + (*self).clear() + } +} + +#[cfg(feature = "flexbox")] +impl FlexboxContainerStyle for Style { + #[inline(always)] + fn flex_direction(&self) -> FlexDirection { + self.flex_direction + } + #[inline(always)] + fn flex_wrap(&self) -> FlexWrap { + self.flex_wrap + } + #[inline(always)] + fn gap(&self) -> Size { + self.gap + } + #[inline(always)] + fn align_content(&self) -> Option { + self.align_content + } + #[inline(always)] + fn align_items(&self) -> Option { + self.align_items + } + #[inline(always)] + fn justify_content(&self) -> Option { + self.justify_content + } +} + +#[cfg(feature = "flexbox")] +impl FlexboxContainerStyle for &'_ T { + #[inline(always)] + fn flex_direction(&self) -> FlexDirection { + (*self).flex_direction() + } + #[inline(always)] + fn flex_wrap(&self) -> FlexWrap { + (*self).flex_wrap() + } + #[inline(always)] + fn gap(&self) -> Size { + (*self).gap() + } + #[inline(always)] + fn align_content(&self) -> Option { + (*self).align_content() + } + #[inline(always)] + fn align_items(&self) -> Option { + (*self).align_items() + } + #[inline(always)] + fn justify_content(&self) -> Option { + (*self).justify_content() + } +} + +#[cfg(feature = "flexbox")] +impl FlexboxItemStyle for Style { + #[inline(always)] + fn flex_basis(&self) -> Dimension { + self.flex_basis + } + #[inline(always)] + fn flex_grow(&self) -> f32 { + self.flex_grow + } + #[inline(always)] + fn flex_shrink(&self) -> f32 { + self.flex_shrink + } + #[inline(always)] + fn align_self(&self) -> Option { + self.align_self + } +} + +#[cfg(feature = "flexbox")] +impl FlexboxItemStyle for &'_ T { + #[inline(always)] + fn flex_basis(&self) -> Dimension { + (*self).flex_basis() + } + #[inline(always)] + fn flex_grow(&self) -> f32 { + (*self).flex_grow() + } + #[inline(always)] + fn flex_shrink(&self) -> f32 { + (*self).flex_shrink() + } + #[inline(always)] + fn align_self(&self) -> Option { + (*self).align_self() + } +} + +#[cfg(feature = "grid")] +impl GridContainerStyle for Style { + type Repetition<'a> + = &'a GridTemplateRepetition + where + Self: 'a; + + type TemplateTrackList<'a> + = core::iter::Map< + core::slice::Iter<'a, GridTemplateComponent>, + fn(&'a GridTemplateComponent) -> GenericGridTemplateComponent>, + > + where + Self: 'a; + + type AutoTrackList<'a> + = core::iter::Copied> + where + Self: 'a; + + #[cfg(feature = "grid")] + type TemplateLineNames<'a> + = core::iter::Map>, fn(&GridTrackVec) -> core::slice::Iter<'_, S>> + where + Self: 'a; + #[cfg(feature = "grid")] + type GridTemplateAreas<'a> + = core::iter::Cloned>> + where + Self: 'a; + + #[inline(always)] + fn grid_template_rows(&self) -> Option> { + Some(self.grid_template_rows.iter().map(|c| c.as_component_ref())) + } + #[inline(always)] + fn grid_template_columns(&self) -> Option> { + Some(self.grid_template_columns.iter().map(|c| c.as_component_ref())) + } + #[inline(always)] + fn grid_auto_rows(&self) -> Self::AutoTrackList<'_> { + self.grid_auto_rows.iter().copied() + } + #[inline(always)] + fn grid_auto_columns(&self) -> Self::AutoTrackList<'_> { + self.grid_auto_columns.iter().copied() + } + #[inline(always)] + fn grid_auto_flow(&self) -> GridAutoFlow { + self.grid_auto_flow + } + #[inline(always)] + fn gap(&self) -> Size { + self.gap + } + #[inline(always)] + fn align_content(&self) -> Option { + self.align_content + } + #[inline(always)] + fn justify_content(&self) -> Option { + self.justify_content + } + #[inline(always)] + fn align_items(&self) -> Option { + self.align_items + } + #[inline(always)] + fn justify_items(&self) -> Option { + self.justify_items + } + + #[inline(always)] + #[cfg(feature = "grid")] + fn grid_template_areas(&self) -> Option> { + Some(self.grid_template_areas.iter().cloned()) + } + + #[inline(always)] + #[cfg(feature = "grid")] + fn grid_template_column_names(&self) -> Option> { + Some(self.grid_template_column_names.iter().map(|names| names.iter())) + } + + #[inline(always)] + #[cfg(feature = "grid")] + fn grid_template_row_names(&self) -> Option> { + Some(self.grid_template_row_names.iter().map(|names| names.iter())) + } +} + +#[cfg(feature = "grid")] +impl GridContainerStyle for &'_ T { + type Repetition<'a> + = T::Repetition<'a> + where + Self: 'a; + + type TemplateTrackList<'a> + = T::TemplateTrackList<'a> + where + Self: 'a; + + type AutoTrackList<'a> + = T::AutoTrackList<'a> + where + Self: 'a; + + /// The type returned by grid_template_row_names and grid_template_column_names + #[cfg(feature = "grid")] + type TemplateLineNames<'a> + = T::TemplateLineNames<'a> + where + Self: 'a; + #[cfg(feature = "grid")] + type GridTemplateAreas<'a> + = T::GridTemplateAreas<'a> + where + Self: 'a; + + #[inline(always)] + fn grid_template_rows(&self) -> Option> { + (*self).grid_template_rows() + } + #[inline(always)] + fn grid_template_columns(&self) -> Option> { + (*self).grid_template_columns() + } + #[inline(always)] + fn grid_auto_rows(&self) -> Self::AutoTrackList<'_> { + (*self).grid_auto_rows() + } + #[inline(always)] + fn grid_auto_columns(&self) -> Self::AutoTrackList<'_> { + (*self).grid_auto_columns() + } + #[cfg(feature = "grid")] + #[inline(always)] + fn grid_template_areas(&self) -> Option> { + (*self).grid_template_areas() + } + #[cfg(feature = "grid")] + #[inline(always)] + fn grid_template_column_names(&self) -> Option> { + (*self).grid_template_column_names() + } + #[cfg(feature = "grid")] + #[inline(always)] + fn grid_template_row_names(&self) -> Option> { + (*self).grid_template_row_names() + } + #[inline(always)] + fn grid_auto_flow(&self) -> GridAutoFlow { + (*self).grid_auto_flow() + } + #[inline(always)] + fn gap(&self) -> Size { + (*self).gap() + } + #[inline(always)] + fn align_content(&self) -> Option { + (*self).align_content() + } + #[inline(always)] + fn justify_content(&self) -> Option { + (*self).justify_content() + } + #[inline(always)] + fn align_items(&self) -> Option { + (*self).align_items() + } + #[inline(always)] + fn justify_items(&self) -> Option { + (*self).justify_items() + } +} + +#[cfg(feature = "grid")] +impl GridItemStyle for Style { + #[inline(always)] + fn grid_row(&self) -> Line> { + // TODO: Investigate eliminating clone + self.grid_row.clone() + } + #[inline(always)] + fn grid_column(&self) -> Line> { + // TODO: Investigate eliminating clone + self.grid_column.clone() + } + #[inline(always)] + fn align_self(&self) -> Option { + self.align_self + } + #[inline(always)] + fn justify_self(&self) -> Option { + self.justify_self + } +} + +#[cfg(feature = "grid")] +impl GridItemStyle for &'_ T { + #[inline(always)] + fn grid_row(&self) -> Line> { + (*self).grid_row() + } + #[inline(always)] + fn grid_column(&self) -> Line> { + (*self).grid_column() + } + #[inline(always)] + fn align_self(&self) -> Option { + (*self).align_self() + } + #[inline(always)] + fn justify_self(&self) -> Option { + (*self).justify_self() + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::Style; + use crate::sys::DefaultCheapStr; + use crate::{geometry::*, style_helpers::TaffyAuto as _}; + + #[test] + fn defaults_match() { + #[cfg(feature = "grid")] + use super::GridPlacement; + + let old_defaults: Style = Style { + dummy: core::marker::PhantomData, + display: Default::default(), + item_is_table: false, + item_is_replaced: false, + box_sizing: Default::default(), + #[cfg(feature = "float_layout")] + float: Default::default(), + #[cfg(feature = "float_layout")] + clear: Default::default(), + direction: Default::default(), + overflow: Default::default(), + scrollbar_width: 0.0, + position: Default::default(), + #[cfg(feature = "flexbox")] + flex_direction: Default::default(), + #[cfg(feature = "flexbox")] + flex_wrap: Default::default(), + #[cfg(any(feature = "flexbox", feature = "grid"))] + align_items: Default::default(), + #[cfg(any(feature = "flexbox", feature = "grid"))] + align_self: Default::default(), + #[cfg(feature = "grid")] + justify_items: Default::default(), + #[cfg(feature = "grid")] + justify_self: Default::default(), + #[cfg(any(feature = "flexbox", feature = "grid", feature = "block_layout"))] + align_content: Default::default(), + #[cfg(any(feature = "flexbox", feature = "grid"))] + justify_content: Default::default(), + inset: Rect::auto(), + margin: Rect::zero(), + padding: Rect::zero(), + border: Rect::zero(), + gap: Size::zero(), + #[cfg(feature = "block_layout")] + text_align: Default::default(), + #[cfg(feature = "flexbox")] + flex_grow: 0.0, + #[cfg(feature = "flexbox")] + flex_shrink: 1.0, + #[cfg(feature = "flexbox")] + flex_basis: super::Dimension::AUTO, + size: Size::auto(), + min_size: Size::auto(), + max_size: Size::auto(), + aspect_ratio: Default::default(), + #[cfg(feature = "grid")] + grid_template_rows: Default::default(), + #[cfg(feature = "grid")] + grid_template_columns: Default::default(), + #[cfg(feature = "grid")] + grid_template_row_names: Default::default(), + #[cfg(feature = "grid")] + grid_template_column_names: Default::default(), + #[cfg(feature = "grid")] + grid_template_areas: Default::default(), + #[cfg(feature = "grid")] + grid_auto_rows: Default::default(), + #[cfg(feature = "grid")] + grid_auto_columns: Default::default(), + #[cfg(feature = "grid")] + grid_auto_flow: Default::default(), + #[cfg(feature = "grid")] + grid_row: Line { start: GridPlacement::Auto, end: GridPlacement::Auto }, + #[cfg(feature = "grid")] + grid_column: Line { start: GridPlacement::Auto, end: GridPlacement::Auto }, + }; + + assert_eq!(Style::DEFAULT, Style::::default()); + assert_eq!(Style::DEFAULT, old_defaults); + } + + // NOTE: Please feel free the update the sizes in this test as required. This test is here to prevent unintentional size changes + // and to serve as accurate up-to-date documentation on the sizes. + #[test] + fn style_sizes() { + use super::*; + type S = crate::sys::DefaultCheapStr; + + fn assert_type_size(expected_size: usize) { + let name = ::core::any::type_name::(); + let name = name.replace("taffy::geometry::", ""); + let name = name.replace("taffy::style::dimension::", ""); + let name = name.replace("taffy::style::alignment::", ""); + let name = name.replace("taffy::style::flex::", ""); + let name = name.replace("taffy::style::grid::", ""); + + assert_eq!( + ::core::mem::size_of::(), + expected_size, + "Expected {} for be {} byte(s) but it was {} byte(s)", + name, + expected_size, + ::core::mem::size_of::(), + ); + } + + // Display and Position + assert_type_size::(1); + assert_type_size::(1); + assert_type_size::(1); + assert_type_size::(1); + + // Dimensions and aggregations of Dimensions + assert_type_size::(4); + assert_type_size::(8); + assert_type_size::(8); + assert_type_size::(8); + assert_type_size::>(16); + assert_type_size::>(16); + assert_type_size::>(16); + assert_type_size::>(32); + assert_type_size::>(32); + assert_type_size::>(32); + + // Alignment — `AlignContent` and `AlignItems` are structs of two `#[repr(u8)]` enums + // (position keyword + safety modifier). Niche-packing in the safety byte (only 2 of + // 256 values used) lets `Option<_>` stay the same size as the bare struct. + assert_type_size::(1); + assert_type_size::(1); + assert_type_size::(1); + assert_type_size::(2); + assert_type_size::(2); + assert_type_size::>(2); + assert_type_size::>(2); + + // Flexbox Container + assert_type_size::(1); + assert_type_size::(1); + + // CSS Grid Container + assert_type_size::(1); + assert_type_size::(8); + assert_type_size::(8); + assert_type_size::(16); + assert_type_size::>(24); + assert_type_size::>>(24); + + // String-type dependent (String) + assert_type_size::>(56); + assert_type_size::>(32); + assert_type_size::>>(64); + assert_type_size::>(544); + + // String-type dependent (Arc) + assert_type_size::>>(56); + assert_type_size::>>(24); + assert_type_size::>>>(48); + assert_type_size::>>(512); + } +} diff --git a/vendor/taffy/src/style_helpers.rs b/vendor/taffy/src/style_helpers.rs new file mode 100644 index 0000000..1b3915e --- /dev/null +++ b/vendor/taffy/src/style_helpers.rs @@ -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(repetition_kind: Input, tracks: Vec) -> GridTemplateComponent +where + Input: TryInto, + >::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(count: u16) -> Vec> { + 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(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(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(min: MinTrackSizingFunction, max: MaxTrackSizingFunction) -> Output +where + Output: From>, +{ + 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(flex_fraction: Input) -> Output +where + Input: Into + Copy, + Output: From>, +{ + MinMax { min: zero(), max: fr(flex_fraction.into()) }.into() +} + +/// Returns the zero value for that type +pub const fn zero() -> 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 TaffyZero for Option { + const ZERO: Option = Some(T::ZERO); +} +impl TaffyZero for Point { + const ZERO: Point = Point { x: T::ZERO, y: T::ZERO }; +} +impl Point { + /// 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::() + } +} +impl TaffyZero for Line { + const ZERO: Line = Line { start: T::ZERO, end: T::ZERO }; +} +impl Line { + /// 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::() + } +} +impl TaffyZero for Size { + const ZERO: Size = Size { width: T::ZERO, height: T::ZERO }; +} +impl Size { + /// 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::() + } +} +impl TaffyZero for Rect { + const ZERO: Rect = Rect { left: T::ZERO, right: T::ZERO, top: T::ZERO, bottom: T::ZERO }; +} +impl Rect { + /// 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::() + } +} + +/// Returns the auto value for that type +pub const fn auto() -> T { + T::AUTO +} + +/// Trait to abstract over auto values +pub trait TaffyAuto { + /// The auto value for type implementing TaffyAuto + const AUTO: Self; +} +impl TaffyAuto for Option { + const AUTO: Option = Some(T::AUTO); +} +impl TaffyAuto for Point { + const AUTO: Point = Point { x: T::AUTO, y: T::AUTO }; +} +impl Point { + /// 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::() + } +} +impl TaffyAuto for Line { + const AUTO: Line = Line { start: T::AUTO, end: T::AUTO }; +} +impl Line { + /// 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::() + } +} +impl TaffyAuto for Size { + const AUTO: Size = Size { width: T::AUTO, height: T::AUTO }; +} +impl Size { + /// 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::() + } +} +impl TaffyAuto for Rect { + const AUTO: Rect = Rect { left: T::AUTO, right: T::AUTO, top: T::AUTO, bottom: T::AUTO }; +} +impl Rect { + /// 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::() + } +} + +/// Returns the auto value for that type +pub const fn min_content() -> 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 TaffyMinContent for Option { + const MIN_CONTENT: Option = Some(T::MIN_CONTENT); +} +impl TaffyMinContent for Point { + const MIN_CONTENT: Point = Point { x: T::MIN_CONTENT, y: T::MIN_CONTENT }; +} +impl Point { + /// 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::() + } +} +impl TaffyMinContent for Line { + const MIN_CONTENT: Line = Line { start: T::MIN_CONTENT, end: T::MIN_CONTENT }; +} +impl Line { + /// 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::() + } +} +impl TaffyMinContent for Size { + const MIN_CONTENT: Size = Size { width: T::MIN_CONTENT, height: T::MIN_CONTENT }; +} +impl Size { + /// 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::() + } +} +impl TaffyMinContent for Rect { + const MIN_CONTENT: Rect = + Rect { left: T::MIN_CONTENT, right: T::MIN_CONTENT, top: T::MIN_CONTENT, bottom: T::MIN_CONTENT }; +} +impl Rect { + /// 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::() + } +} + +/// Returns the auto value for that type +pub const fn max_content() -> 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 TaffyMaxContent for Option { + const MAX_CONTENT: Option = Some(T::MAX_CONTENT); +} +impl TaffyMaxContent for Point { + const MAX_CONTENT: Point = Point { x: T::MAX_CONTENT, y: T::MAX_CONTENT }; +} +impl Point { + /// 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::() + } +} +impl TaffyMaxContent for Line { + const MAX_CONTENT: Line = Line { start: T::MAX_CONTENT, end: T::MAX_CONTENT }; +} +impl Line { + /// 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::() + } +} +impl TaffyMaxContent for Size { + const MAX_CONTENT: Size = Size { width: T::MAX_CONTENT, height: T::MAX_CONTENT }; +} +impl Size { + /// 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::() + } +} +impl TaffyMaxContent for Rect { + const MAX_CONTENT: Rect = + Rect { left: T::MAX_CONTENT, right: T::MAX_CONTENT, top: T::MAX_CONTENT, bottom: T::MAX_CONTENT }; +} +impl Rect { + /// 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::() + } +} + +/// Returns a value of the inferred type which represent a `fit-content(…)` value +/// with the given argument. +pub fn fit_content(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 TaffyFitContent for Point { + fn fit_content(argument: LengthPercentage) -> Self { + Point { x: T::fit_content(argument), y: T::fit_content(argument) } + } +} +impl Point { + /// 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 TaffyFitContent for Line { + fn fit_content(argument: LengthPercentage) -> Self { + Line { start: T::fit_content(argument), end: T::fit_content(argument) } + } +} +impl Line { + /// 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 TaffyFitContent for Size { + fn fit_content(argument: LengthPercentage) -> Self { + Size { width: T::fit_content(argument), height: T::fit_content(argument) } + } +} +impl Size { + /// 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 TaffyFitContent for Rect { + 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 Rect { + /// 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 + 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` into Self + fn from_length + Copy>(value: Input) -> Self; +} +impl FromLength for f32 { + fn from_length + Copy>(value: Input) -> Self { + value.into() + } +} +impl FromLength for Option { + fn from_length + Copy>(value: Input) -> Self { + Some(value.into()) + } +} +impl FromLength for Point { + fn from_length + Copy>(value: Input) -> Self { + Point { x: T::from_length(value.into()), y: T::from_length(value.into()) } + } +} +impl Point { + /// Returns a Point where x and y values are the same given absolute length + pub fn length + Copy>(value: Input) -> Self { + length::(value) + } +} +impl FromLength for Line { + fn from_length + Copy>(value: Input) -> Self { + Line { start: T::from_length(value.into()), end: T::from_length(value.into()) } + } +} +impl Line { + /// Returns a Line where both the start and end values are the same given absolute length + pub fn length + Copy>(value: Input) -> Self { + length::(value) + } +} +impl FromLength for Size { + fn from_length + Copy>(value: Input) -> Self { + Size { width: T::from_length(value.into()), height: T::from_length(value.into()) } + } +} +impl Size { + /// Returns a Size where both the width and height values the same given absolute length + pub fn length + Copy>(value: Input) -> Self { + length::(value) + } +} +impl FromLength for Rect { + fn from_length + 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 Rect { + /// Returns a Rect where the left, right, top and bottom values are all the same given absolute length + pub fn length + Copy>(value: Input) -> Self { + length::(value) + } +} + +/// Returns a value of the inferred type which represent a percentage +pub fn percent + 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` into Self + fn from_percent + Copy>(percent: Input) -> Self; +} +impl FromPercent for f32 { + fn from_percent + Copy>(percent: Input) -> Self { + percent.into() + } +} +impl FromPercent for Option { + fn from_percent + Copy>(percent: Input) -> Self { + Some(percent.into()) + } +} +impl FromPercent for Point { + fn from_percent + Copy>(percent: Input) -> Self { + Point { x: T::from_percent(percent.into()), y: T::from_percent(percent.into()) } + } +} +impl Point { + /// 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 + Copy>(percent_value: Input) -> Self { + percent::(percent_value) + } +} +impl FromPercent for Line { + fn from_percent + Copy>(percent: Input) -> Self { + Line { start: T::from_percent(percent.into()), end: T::from_percent(percent.into()) } + } +} +impl Line { + /// 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 + Copy>(percent_value: Input) -> Self { + percent::(percent_value) + } +} +impl FromPercent for Size { + fn from_percent + Copy>(percent: Input) -> Self { + Size { width: T::from_percent(percent.into()), height: T::from_percent(percent.into()) } + } +} +impl Size { + /// 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 + Copy>(percent_value: Input) -> Self { + percent::(percent_value) + } +} +impl FromPercent for Rect { + fn from_percent + 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 Rect { + /// 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 + Copy>(percent_value: Input) -> Self { + percent::(percent_value) + } +} + +/// Create a `Fraction` track sizing function (`fr` in CSS) +#[cfg(feature = "grid")] +pub fn fr + 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` into Self + fn from_fr + 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 = 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(), + }) + ); + } +} diff --git a/vendor/taffy/src/test.rs b/vendor/taffy/src/test.rs new file mode 100644 index 0000000..81eb0c3 --- /dev/null +++ b/vendor/taffy/src/test.rs @@ -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) -> 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), + /// 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>, + available_space: Size, + _node_id: NodeId, + context: Option<&mut TestNodeContext>, + _style: &Style, +) -> Size { + 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>) -> Size { + 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>, + available_space: taffy::Size, + ) -> taffy::Size { + 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 }, + } + } +} diff --git a/vendor/taffy/src/tree/cache.rs b/vendor/taffy/src/tree/cache.rs new file mode 100644 index 0000000..5187419 --- /dev/null +++ b/vendor/taffy/src/tree/cache.rs @@ -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` into `u32` +#[inline(always)] +fn option_cache_key(input: Option) -> u32 { + match input { + Some(value) => value.to_bits(), + None => INFINITY_BITS, + } +} + +/// Pack `Size>` into `u64` +#[inline(always)] +fn size_option_cache_key(input: Size>) -> 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` into `u64` +#[inline(always)] +#[allow(dead_code)] +fn size_available_space_cache_key(input: Size) -> 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) and `AvailableSpace` in +/// a single dimension into a cache key in a single dimension. +#[inline(always)] +fn mixed_cache_key(kd: Option, avs: AvailableSpace) -> u32 { + kd.map(|kd| kd.to_bits()).unwrap_or_else(|| available_space_cache_key(avs)) +} + +/// Encodes combination of a `known_dimension` (Option) and `AvailableSpace` in +/// two dimensions into a cache key in a single dimension. +#[inline(always)] +fn size_mixed_cache_key(kd: Size>, avs: Size) -> 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 { + /// 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>, + /// The cache entries for the node's preliminary size measurements + measure_entries: [Option>>; 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>, available_space: Size) -> 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 { + 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, +} diff --git a/vendor/taffy/src/tree/layout.rs b/vendor/taffy/src/tree/layout.rs new file mode 100644 index 0000000..3049537 --- /dev/null +++ b/vendor/taffy/src/tree/layout.rs @@ -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 for RequestedAxis { + fn from(value: AbsoluteAxis) -> Self { + match value { + AbsoluteAxis::Horizontal => RequestedAxis::Horizontal, + AbsoluteAxis::Vertical => RequestedAxis::Vertical, + } + } +} +impl TryFrom for AbsoluteAxis { + type Error = (); + fn try_from(value: RequestedAxis) -> Result { + 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>, + /// Parent size dimensions are intended to be used for percentage resolution. + pub parent_size: Size>, + /// 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, + /// 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, +} + +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 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, + #[cfg(feature = "content_size")] + /// The size of the content within the node + pub content_size: Size, + /// The first baseline of the node in each dimension, if any + pub first_baselines: Point>, + /// 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, + #[cfg_attr(not(feature = "content_size"), allow(unused_variables))] content_size: Size, + first_baselines: Point>, + ) -> 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, content_size: Size) -> 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) -> 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, + /// The width and height of the node + pub size: Size, + #[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, + /// The size of the scrollbars in each dimension. If there is no scrollbar then the size will be zero. + pub scrollbar_size: Size, + /// The size of the borders of the node + pub border: Rect, + /// The size of the padding of the node + pub padding: Rect, + /// The size of the margin of the node + pub margin: Rect, +} + +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 { + 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), + /// For node that hasn't had any detailed information yet + None, +} diff --git a/vendor/taffy/src/tree/mod.rs b/vendor/taffy/src/tree/mod.rs new file mode 100644 index 0000000..2096b6f --- /dev/null +++ b/vendor/taffy/src/tree/mod.rs @@ -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; diff --git a/vendor/taffy/src/tree/node.rs b/vendor/taffy/src/tree/node.rs new file mode 100644 index 0000000..9285c22 --- /dev/null +++ b/vendor/taffy/src/tree/node.rs @@ -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 for NodeId { + #[inline] + fn from(raw: u64) -> Self { + Self(raw) + } +} +impl From for u64 { + #[inline] + fn from(id: NodeId) -> Self { + id.0 + } +} +impl From for NodeId { + #[inline] + fn from(raw: usize) -> Self { + Self(raw as u64) + } +} +impl From for usize { + #[inline] + fn from(id: NodeId) -> Self { + id.0 as usize + } +} + +#[cfg(feature = "taffy_tree")] +impl From for NodeId { + #[inline] + fn from(key: DefaultKey) -> Self { + Self(key.data().as_ffi()) + } +} + +#[cfg(feature = "taffy_tree")] +impl From for DefaultKey { + #[inline] + fn from(key: NodeId) -> Self { + KeyData::from_ffi(key.0).into() + } +} diff --git a/vendor/taffy/src/tree/taffy_tree.rs b/vendor/taffy/src/tree/taffy_tree.rs new file mode 100644 index 0000000..ab9beb0 --- /dev/null +++ b/vendor/taffy/src/tree/taffy_tree.rs @@ -0,0 +1,1405 @@ +//! Contains [TaffyTree](crate::tree::TaffyTree): the default implementation of [LayoutTree](crate::tree::LayoutTree), and the error type for Taffy. +#[cfg(not(feature = "std"))] +use slotmap::SecondaryMap; +#[cfg(feature = "std")] +use slotmap::SparseSecondaryMap as SecondaryMap; +use slotmap::{DefaultKey, SlotMap}; + +#[cfg(feature = "block_layout")] +use crate::block::BlockContext; +use crate::geometry::Size; +use crate::style::{AvailableSpace, Display, Style}; +use crate::sys::DefaultCheapStr; +use crate::tree::{ + Cache, ClearState, Layout, LayoutInput, LayoutOutput, LayoutPartialTree, NodeId, PrintTree, RoundTree, RunMode, + TraversePartialTree, TraverseTree, +}; +use crate::util::debug::{debug_log, debug_log_node}; +use crate::util::sys::{new_vec_with_capacity, ChildrenVec, Vec}; + +use crate::compute::{ + compute_cached_layout, compute_hidden_layout, compute_leaf_layout, compute_root_layout, round_layout, +}; +use crate::CacheTree; + +#[cfg(feature = "block_layout")] +use crate::{compute::compute_block_layout, LayoutBlockContainer}; +#[cfg(feature = "flexbox")] +use crate::{compute::compute_flexbox_layout, LayoutFlexboxContainer}; +#[cfg(feature = "grid")] +use crate::{compute::compute_grid_layout, LayoutGridContainer}; + +#[cfg(all(feature = "detailed_layout_info", feature = "grid"))] +use crate::compute::grid::DetailedGridInfo; +#[cfg(feature = "detailed_layout_info")] +use crate::tree::layout::DetailedLayoutInfo; + +/// The error Taffy generates on invalid operations +pub type TaffyResult = Result; + +/// An error that occurs while trying to access or modify a node's children by index. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TaffyError { + /// The parent node does not have a child at `child_index`. It only has `child_count` children + ChildIndexOutOfBounds { + /// The parent node whose child was being looked up + parent: NodeId, + /// The index that was looked up + child_index: usize, + /// The total number of children the parent has + child_count: usize, + }, + /// The parent node was not found in the [`TaffyTree`](crate::TaffyTree) instance. + InvalidParentNode(NodeId), + /// The child node was not found in the [`TaffyTree`](crate::TaffyTree) instance. + InvalidChildNode(NodeId), + /// The supplied node was not found in the [`TaffyTree`](crate::TaffyTree) instance. + InvalidInputNode(NodeId), +} + +impl core::fmt::Display for TaffyError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + TaffyError::ChildIndexOutOfBounds { parent, child_index, child_count } => { + write!(f, "Index (is {child_index}) should be < child_count ({child_count}) for parent node {parent:?}") + } + TaffyError::InvalidParentNode(parent) => { + write!(f, "Parent Node {parent:?} is not in the TaffyTree instance") + } + TaffyError::InvalidChildNode(child) => write!(f, "Child Node {child:?} is not in the TaffyTree instance"), + TaffyError::InvalidInputNode(node) => write!(f, "Supplied Node {node:?} is not in the TaffyTree instance"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for TaffyError {} + +/// Global configuration values for a TaffyTree instance +#[derive(Debug, Clone, Copy)] +pub(crate) struct TaffyConfig { + /// Whether to round layout values + pub(crate) use_rounding: bool, +} + +impl Default for TaffyConfig { + fn default() -> Self { + Self { use_rounding: true } + } +} + +/// Layout information for a given [`Node`](crate::node::Node) +/// +/// Stored in a [`TaffyTree`]. +#[derive(Debug, Clone, PartialEq)] +struct NodeData { + /// The layout strategy used by this node + pub(crate) style: Style, + + /// The always unrounded results of the layout computation. We must store this separately from the rounded + /// layout to avoid errors from rounding already-rounded values. See . + pub(crate) unrounded_layout: Layout, + + /// The final results of the layout computation. + /// These may be rounded or unrounded depending on what the `use_rounding` config setting is set to. + pub(crate) final_layout: Layout, + + /// Whether the node has context data associated with it or not + pub(crate) has_context: bool, + + /// The cached results of the layout computation + pub(crate) cache: Cache, + + /// The computation result from layout algorithm + #[cfg(feature = "detailed_layout_info")] + pub(crate) detailed_layout_info: DetailedLayoutInfo, +} + +impl NodeData { + /// Create the data for a new node + #[must_use] + pub const fn new(style: Style) -> Self { + Self { + style, + cache: Cache::new(), + unrounded_layout: Layout::new(), + final_layout: Layout::new(), + has_context: false, + #[cfg(feature = "detailed_layout_info")] + detailed_layout_info: DetailedLayoutInfo::None, + } + } + + /// Marks a node and all of its ancestors as requiring relayout + /// + /// This clears any cached data and signals that the data must be recomputed. + /// If the node was already marked as dirty, returns true + #[inline] + pub fn mark_dirty(&mut self) -> ClearState { + self.cache.clear() + } +} + +/// An entire tree of UI nodes. The entry point to Taffy's high-level API. +/// +/// Allows you to build a tree of UI nodes, run Taffy's layout algorithms over that tree, and then access the resultant layout.] +#[derive(Debug, Clone)] +pub struct TaffyTree { + /// The [`NodeData`] for each node stored in this tree + nodes: SlotMap, + + /// Functions/closures that compute the intrinsic size of leaf nodes + node_context_data: SecondaryMap, + + /// The children of each node + /// + /// The indexes in the outer vector correspond to the position of the parent [`NodeData`] + children: SlotMap>, + + /// The parents of each node + /// + /// The indexes in the outer vector correspond to the position of the child [`NodeData`] + parents: SlotMap>, + + /// Layout mode configuration + config: TaffyConfig, +} + +impl Default for TaffyTree { + fn default() -> TaffyTree<()> { + TaffyTree::new() + } +} + +/// Iterator that wraps a slice of nodes, lazily converting them to u64 +pub struct TaffyTreeChildIter<'a>(core::slice::Iter<'a, NodeId>); +impl Iterator for TaffyTreeChildIter<'_> { + type Item = NodeId; + + #[inline] + fn next(&mut self) -> Option { + self.0.next().copied() + } +} + +// TraversePartialTree impl for TaffyTree +impl TraversePartialTree for TaffyTree { + type ChildIter<'a> + = TaffyTreeChildIter<'a> + where + Self: 'a; + + #[inline(always)] + fn child_ids(&self, parent_node_id: NodeId) -> Self::ChildIter<'_> { + TaffyTreeChildIter(self.children[parent_node_id.into()].iter()) + } + + #[inline(always)] + fn child_count(&self, parent_node_id: NodeId) -> usize { + self.children[parent_node_id.into()].len() + } + + #[inline(always)] + fn get_child_id(&self, parent_node_id: NodeId, id: usize) -> NodeId { + self.children[parent_node_id.into()][id] + } +} + +// TraverseTree impl for TaffyTree +impl TraverseTree for TaffyTree {} + +// CacheTree impl for TaffyTree +impl CacheTree for TaffyTree { + fn cache_get(&self, node_id: NodeId, input: &LayoutInput) -> Option { + self.nodes[node_id.into()].cache.get(input) + } + + fn cache_store(&mut self, node_id: NodeId, input: &LayoutInput, layout_output: LayoutOutput) { + self.nodes[node_id.into()].cache.store(input, layout_output) + } + + fn cache_clear(&mut self, node_id: NodeId) { + self.nodes[node_id.into()].cache.clear(); + } +} + +// PrintTree impl for TaffyTree +impl PrintTree for TaffyTree { + #[inline(always)] + fn get_debug_label(&self, node_id: NodeId) -> &'static str { + let node = &self.nodes[node_id.into()]; + let display = node.style.display; + let num_children = self.child_count(node_id); + + match (num_children, display) { + (_, Display::None) => "NONE", + (0, _) => "LEAF", + #[cfg(feature = "block_layout")] + (_, Display::Block) => "BLOCK", + #[cfg(feature = "flexbox")] + (_, Display::Flex) => { + use crate::FlexDirection; + match node.style.flex_direction { + FlexDirection::Row | FlexDirection::RowReverse => "FLEX ROW", + FlexDirection::Column | FlexDirection::ColumnReverse => "FLEX COL", + } + } + #[cfg(feature = "grid")] + (_, Display::Grid) => "GRID", + } + } + + #[inline(always)] + fn get_final_layout(&self, node_id: NodeId) -> Layout { + if self.config.use_rounding { + self.nodes[node_id.into()].final_layout + } else { + self.nodes[node_id.into()].unrounded_layout + } + } +} + +/// View over the Taffy tree that holds the tree itself along with a reference to the context +/// and implements LayoutTree. This allows the context to be stored outside of the TaffyTree struct +/// which makes the lifetimes of the context much more flexible. +pub(crate) struct TaffyView<'t, NodeContext, MeasureFunction> +where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, +{ + /// A reference to the TaffyTree + pub(crate) taffy: &'t mut TaffyTree, + /// The context provided for passing to measure functions if layout is run over this struct + pub(crate) measure_function: MeasureFunction, +} + +impl TaffyView<'_, NodeContext, MeasureFunction> +where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, +{ + #[inline(always)] + /// Unified implementation that both `LayoutPartialTree::compute_child_layout` + /// and `LayoutBlockContainer::compute_block_child_layout` delegate to. + fn compute_child_layout( + &mut self, + node_id: NodeId, + inputs: LayoutInput, + #[cfg(feature = "block_layout")] block_ctx: Option<&mut BlockContext<'_>>, + ) -> LayoutOutput { + // If RunMode is PerformHiddenLayout then this indicates that an ancestor node is `Display::None` + // and thus that we should lay out this node using hidden layout regardless of it's own display style. + if inputs.run_mode == RunMode::PerformHiddenLayout { + debug_log!("HIDDEN"); + return compute_hidden_layout(self, node_id); + } + + // We run the following wrapped in "compute_cached_layout", which will check the cache for an entry matching the node and inputs and: + // - Return that entry if exists + // - Else call the passed closure (below) to compute the result + // + // If there was no cache match and a new result needs to be computed then that result will be added to the cache + compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| { + let display_mode = tree.taffy.nodes[node_id.into()].style.display; + let has_children = tree.child_count(node_id) > 0; + + debug_log!(display_mode); + debug_log_node!(inputs); + + // Dispatch to a layout algorithm based on the node's display style and whether the node has children or not. + match (display_mode, has_children) { + (Display::None, _) => compute_hidden_layout(tree, node_id), + #[cfg(feature = "block_layout")] + (Display::Block, true) => compute_block_layout(tree, node_id, inputs, block_ctx), + #[cfg(feature = "flexbox")] + (Display::Flex, true) => compute_flexbox_layout(tree, node_id, inputs), + #[cfg(feature = "grid")] + (Display::Grid, true) => compute_grid_layout(tree, node_id, inputs), + (_, false) => { + let node_key = node_id.into(); + let style = &tree.taffy.nodes[node_key].style; + let has_context = tree.taffy.nodes[node_key].has_context; + let node_context = has_context.then(|| tree.taffy.node_context_data.get_mut(node_key)).flatten(); + let measure_function = |known_dimensions, available_space| { + (tree.measure_function)(known_dimensions, available_space, node_id, node_context, style) + }; + compute_leaf_layout(inputs, style, |_, _| 0.0, measure_function) + } + } + }) + } +} + +// TraversePartialTree impl for TaffyView +impl TraversePartialTree for TaffyView<'_, NodeContext, MeasureFunction> +where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, +{ + type ChildIter<'a> + = TaffyTreeChildIter<'a> + where + Self: 'a; + + #[inline(always)] + fn child_ids(&self, parent_node_id: NodeId) -> Self::ChildIter<'_> { + self.taffy.child_ids(parent_node_id) + } + + #[inline(always)] + fn child_count(&self, parent_node_id: NodeId) -> usize { + self.taffy.child_count(parent_node_id) + } + + #[inline(always)] + fn get_child_id(&self, parent_node_id: NodeId, child_index: usize) -> NodeId { + self.taffy.get_child_id(parent_node_id, child_index) + } +} + +// TraverseTree impl for TaffyView +impl TraverseTree for TaffyView<'_, NodeContext, MeasureFunction> where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size +{ +} + +// LayoutPartialTree impl for TaffyView +impl LayoutPartialTree for TaffyView<'_, NodeContext, MeasureFunction> +where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, +{ + type CoreContainerStyle<'a> + = &'a Style + where + Self: 'a; + + type CustomIdent = DefaultCheapStr; + + #[inline(always)] + fn get_core_container_style(&self, node_id: NodeId) -> Self::CoreContainerStyle<'_> { + &self.taffy.nodes[node_id.into()].style + } + + #[inline(always)] + fn set_unrounded_layout(&mut self, node_id: NodeId, layout: &Layout) { + self.taffy.nodes[node_id.into()].unrounded_layout = *layout; + } + + #[inline(always)] + fn resolve_calc_value(&self, _val: *const (), _basis: f32) -> f32 { + 0.0 + } + + #[inline(always)] + fn compute_child_layout(&mut self, node_id: NodeId, inputs: LayoutInput) -> LayoutOutput { + self.compute_child_layout( + node_id, + inputs, + #[cfg(feature = "block_layout")] + None, + ) + } +} + +impl CacheTree for TaffyView<'_, NodeContext, MeasureFunction> +where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, +{ + fn cache_get(&self, node_id: NodeId, input: &LayoutInput) -> Option { + self.taffy.nodes[node_id.into()].cache.get(input) + } + + fn cache_store(&mut self, node_id: NodeId, input: &LayoutInput, layout_output: LayoutOutput) { + self.taffy.nodes[node_id.into()].cache.store(input, layout_output) + } + + fn cache_clear(&mut self, node_id: NodeId) { + self.taffy.nodes[node_id.into()].cache.clear(); + } +} + +#[cfg(feature = "block_layout")] +impl LayoutBlockContainer for TaffyView<'_, NodeContext, MeasureFunction> +where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, +{ + type BlockContainerStyle<'a> + = &'a Style + where + Self: 'a; + type BlockItemStyle<'a> + = &'a Style + where + Self: 'a; + + #[inline(always)] + fn get_block_container_style(&self, node_id: NodeId) -> Self::BlockContainerStyle<'_> { + self.get_core_container_style(node_id) + } + + #[inline(always)] + fn get_block_child_style(&self, child_node_id: NodeId) -> Self::BlockItemStyle<'_> { + self.get_core_container_style(child_node_id) + } + + #[inline(always)] + fn compute_block_child_layout( + &mut self, + node_id: NodeId, + inputs: LayoutInput, + block_ctx: Option<&mut BlockContext<'_>>, + ) -> LayoutOutput { + self.compute_child_layout(node_id, inputs, block_ctx) + } +} + +#[cfg(feature = "flexbox")] +impl LayoutFlexboxContainer for TaffyView<'_, NodeContext, MeasureFunction> +where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, +{ + type FlexboxContainerStyle<'a> + = &'a Style + where + Self: 'a; + type FlexboxItemStyle<'a> + = &'a Style + where + Self: 'a; + + #[inline(always)] + fn get_flexbox_container_style(&self, node_id: NodeId) -> Self::FlexboxContainerStyle<'_> { + &self.taffy.nodes[node_id.into()].style + } + + #[inline(always)] + fn get_flexbox_child_style(&self, child_node_id: NodeId) -> Self::FlexboxItemStyle<'_> { + &self.taffy.nodes[child_node_id.into()].style + } +} + +#[cfg(feature = "grid")] +impl LayoutGridContainer for TaffyView<'_, NodeContext, MeasureFunction> +where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, +{ + type GridContainerStyle<'a> + = &'a Style + where + Self: 'a; + type GridItemStyle<'a> + = &'a Style + where + Self: 'a; + + #[inline(always)] + fn get_grid_container_style(&self, node_id: NodeId) -> Self::GridContainerStyle<'_> { + &self.taffy.nodes[node_id.into()].style + } + + #[inline(always)] + fn get_grid_child_style(&self, child_node_id: NodeId) -> Self::GridItemStyle<'_> { + &self.taffy.nodes[child_node_id.into()].style + } + + #[inline(always)] + #[cfg(feature = "detailed_layout_info")] + fn set_detailed_grid_info(&mut self, node_id: NodeId, detailed_grid_info: DetailedGridInfo) { + self.taffy.nodes[node_id.into()].detailed_layout_info = DetailedLayoutInfo::Grid(Box::new(detailed_grid_info)); + } +} + +// RoundTree impl for TaffyView +impl RoundTree for TaffyView<'_, NodeContext, MeasureFunction> +where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, +{ + #[inline(always)] + fn get_unrounded_layout(&self, node: NodeId) -> Layout { + self.taffy.nodes[node.into()].unrounded_layout + } + + #[inline(always)] + fn set_final_layout(&mut self, node_id: NodeId, layout: &Layout) { + self.taffy.nodes[node_id.into()].final_layout = *layout; + } +} + +#[allow(clippy::iter_cloned_collect)] // due to no-std support, we need to use `iter_cloned` instead of `collect` +impl TaffyTree { + /// Creates a new [`TaffyTree`] + /// + /// The default capacity of a [`TaffyTree`] is 16 nodes. + #[must_use] + pub fn new() -> Self { + Self::with_capacity(16) + } + + /// Creates a new [`TaffyTree`] that can store `capacity` nodes before reallocation + #[must_use] + pub fn with_capacity(capacity: usize) -> Self { + TaffyTree { + // TODO: make this method const upstream, + // so constructors here can be const + nodes: SlotMap::with_capacity(capacity), + children: SlotMap::with_capacity(capacity), + parents: SlotMap::with_capacity(capacity), + node_context_data: SecondaryMap::with_capacity(capacity), + config: TaffyConfig::default(), + } + } + + /// Enable rounding of layout values. Rounding is enabled by default. + pub fn enable_rounding(&mut self) { + self.config.use_rounding = true; + } + + /// Disable rounding of layout values. Rounding is enabled by default. + pub fn disable_rounding(&mut self) { + self.config.use_rounding = false; + } + + /// Creates and adds a new unattached leaf node to the tree, and returns the node of the new node + pub fn new_leaf(&mut self, layout: Style) -> TaffyResult { + let id = self.nodes.insert(NodeData::new(layout)); + let _ = self.children.insert(new_vec_with_capacity(0)); + let _ = self.parents.insert(None); + + Ok(id.into()) + } + + /// Creates and adds a new unattached leaf node to the tree, and returns the [`NodeId`] of the new node + /// + /// Creates and adds a new leaf node with a supplied context + pub fn new_leaf_with_context(&mut self, layout: Style, context: NodeContext) -> TaffyResult { + let mut data = NodeData::new(layout); + data.has_context = true; + + let id = self.nodes.insert(data); + self.node_context_data.insert(id, context); + + let _ = self.children.insert(new_vec_with_capacity(0)); + let _ = self.parents.insert(None); + + Ok(id.into()) + } + + /// Creates and adds a new node, which may have any number of `children` + pub fn new_with_children(&mut self, layout: Style, children: &[NodeId]) -> TaffyResult { + let id = NodeId::from(self.nodes.insert(NodeData::new(layout))); + + for child in children { + self.parents[(*child).into()] = Some(id); + } + + let _ = self.children.insert(children.iter().copied().collect::<_>()); + let _ = self.parents.insert(None); + + Ok(id) + } + + /// Drops all nodes in the tree + pub fn clear(&mut self) { + self.nodes.clear(); + self.children.clear(); + self.parents.clear(); + } + + /// Remove a specific node from the tree and drop it + /// + /// Returns the id of the node removed. + pub fn remove(&mut self, node: NodeId) -> TaffyResult { + let key = node.into(); + if let Some(parent) = self.parents[key] { + if let Some(children) = self.children.get_mut(parent.into()) { + children.retain(|f| *f != node); + } + } + + // Remove "parent" references to a node when removing that node + if let Some(children) = self.children.get(key) { + for child in children.iter().copied() { + self.parents[child.into()] = None; + } + } + + let _ = self.children.remove(key); + let _ = self.parents.remove(key); + let _ = self.nodes.remove(key); + + Ok(node) + } + + /// Sets the context data associated with the node + #[inline] + pub fn set_node_context(&mut self, node: NodeId, measure: Option) -> TaffyResult<()> { + let key = node.into(); + if let Some(measure) = measure { + self.nodes[key].has_context = true; + self.node_context_data.insert(key, measure); + } else { + self.nodes[key].has_context = false; + self.node_context_data.remove(key); + } + + self.mark_dirty(node)?; + + Ok(()) + } + + /// Gets a reference to the the context data associated with the node + #[inline] + pub fn get_node_context(&self, node: NodeId) -> Option<&NodeContext> { + self.node_context_data.get(node.into()) + } + + /// Gets a mutable reference to the the context data associated with the node + #[inline] + pub fn get_node_context_mut(&mut self, node: NodeId) -> Option<&mut NodeContext> { + self.node_context_data.get_mut(node.into()) + } + + /// Gets mutable references to the the context data associated with the nodes. All keys must be valid and disjoint, otherwise None is returned. + pub fn get_disjoint_node_context_mut( + &mut self, + keys: [NodeId; N], + ) -> Option<[&mut NodeContext; N]> { + self.node_context_data.get_disjoint_mut(keys.map(|k| k.into())) + } + + /// Adds a `child` node under the supplied `parent` + pub fn add_child(&mut self, parent: NodeId, child: NodeId) -> TaffyResult<()> { + let parent_key = parent.into(); + let child_key = child.into(); + self.parents[child_key] = Some(parent); + self.children[parent_key].push(child); + self.mark_dirty(parent)?; + + Ok(()) + } + + /// Inserts a `child` node at the given `child_index` under the supplied `parent`, shifting all children after it to the right. + pub fn insert_child_at_index(&mut self, parent: NodeId, child_index: usize, child: NodeId) -> TaffyResult<()> { + let parent_key = parent.into(); + + let child_count = self.children[parent_key].len(); + if child_index > child_count { + return Err(TaffyError::ChildIndexOutOfBounds { parent, child_index, child_count }); + } + + self.parents[child.into()] = Some(parent); + self.children[parent_key].insert(child_index, child); + self.mark_dirty(parent)?; + + Ok(()) + } + + /// Directly sets the `children` of the supplied `parent` + pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> TaffyResult<()> { + let parent_key = parent.into(); + + // Remove node as parent from all its current children. + for child in &self.children[parent_key] { + self.parents[(*child).into()] = None; + } + + // Build up relation node <-> child + for &child in children { + // Remove child from previous parent + if let Some(previous_parent) = self.parents[child.into()] { + self.remove_child(previous_parent, child).unwrap(); + } + self.parents[child.into()] = Some(parent); + } + + let parent_children = &mut self.children[parent_key]; + parent_children.clear(); + children.iter().for_each(|child| parent_children.push(*child)); + + self.mark_dirty(parent)?; + + Ok(()) + } + + /// Removes the `child` of the parent `node` + /// + /// The child is not removed from the tree entirely, it is simply no longer attached to its previous parent. + pub fn remove_child(&mut self, parent: NodeId, child: NodeId) -> TaffyResult { + let index = self.children[parent.into()].iter().position(|n| *n == child).unwrap(); + self.remove_child_at_index(parent, index) + } + + /// Removes the child at the given `index` from the `parent` + /// + /// The child is not removed from the tree entirely, it is simply no longer attached to its previous parent. + pub fn remove_child_at_index(&mut self, parent: NodeId, child_index: usize) -> TaffyResult { + let parent_key = parent.into(); + let child_count = self.children[parent_key].len(); + if child_index >= child_count { + return Err(TaffyError::ChildIndexOutOfBounds { parent, child_index, child_count }); + } + + let child = self.children[parent_key].remove(child_index); + self.parents[child.into()] = None; + + self.mark_dirty(parent)?; + + Ok(child) + } + + /// Removes children at the given range from the `parent` + /// + /// Children are not removed from the tree entirely, they are simply no longer attached to their previous parent. + /// + /// Function will panic if given range is invalid. See [`core::slice::range`] + pub fn remove_children_range(&mut self, parent: NodeId, range: R) -> TaffyResult<()> + where + R: core::ops::RangeBounds, + { + let parent_key = parent.into(); + for child in self.children[parent_key].drain(range) { + self.parents[child.into()] = None; + } + + self.mark_dirty(parent)?; + Ok(()) + } + + /// Replaces the child at the given `child_index` from the `parent` node with the new `child` node + /// + /// The child is not removed from the tree entirely, it is simply no longer attached to its previous parent. + pub fn replace_child_at_index( + &mut self, + parent: NodeId, + child_index: usize, + new_child: NodeId, + ) -> TaffyResult { + let parent_key = parent.into(); + + let child_count = self.children[parent_key].len(); + if child_index >= child_count { + return Err(TaffyError::ChildIndexOutOfBounds { parent, child_index, child_count }); + } + + self.parents[new_child.into()] = Some(parent); + let old_child = core::mem::replace(&mut self.children[parent_key][child_index], new_child); + self.parents[old_child.into()] = None; + + self.mark_dirty(parent)?; + + Ok(old_child) + } + + /// Returns the child node of the parent `node` at the provided `child_index` + #[inline] + pub fn child_at_index(&self, parent: NodeId, child_index: usize) -> TaffyResult { + let parent_key = parent.into(); + let child_count = self.children[parent_key].len(); + if child_index >= child_count { + return Err(TaffyError::ChildIndexOutOfBounds { parent, child_index, child_count }); + } + + Ok(self.children[parent_key][child_index]) + } + + /// Returns the total number of nodes in the tree + #[inline] + pub fn total_node_count(&self) -> usize { + self.nodes.len() + } + + /// Returns the `NodeId` of the parent node of the specified node (if it exists) + /// + /// - Return None if the specified node has no parent + /// - Panics if the specified node does not exist + #[inline] + pub fn parent(&self, child_id: NodeId) -> Option { + self.parents[child_id.into()] + } + + /// Returns a list of children that belong to the parent node + pub fn children(&self, parent: NodeId) -> TaffyResult> { + Ok(self.children[parent.into()].clone()) + } + + /// Sets the [`Style`] of the provided `node` + #[inline] + pub fn set_style(&mut self, node: NodeId, style: Style) -> TaffyResult<()> { + self.nodes[node.into()].style = style; + self.mark_dirty(node)?; + Ok(()) + } + + /// Gets the [`Style`] of the provided `node` + #[inline] + pub fn style(&self, node: NodeId) -> TaffyResult<&Style> { + Ok(&self.nodes[node.into()].style) + } + + /// Return this node layout relative to its parent + #[inline] + pub fn layout(&self, node: NodeId) -> TaffyResult<&Layout> { + if self.config.use_rounding { + Ok(&self.nodes[node.into()].final_layout) + } else { + Ok(&self.nodes[node.into()].unrounded_layout) + } + } + + /// Returns this node layout with unrounded values relative to its parent. + #[inline] + pub fn unrounded_layout(&self, node: NodeId) -> &Layout { + &self.nodes[node.into()].unrounded_layout + } + + /// Get the "detailed layout info" for a node. + /// + /// Currently this is only implemented for CSS Grid containers where it contains + /// the computed size of each grid track and the computed placement of each grid item + #[cfg(feature = "detailed_layout_info")] + #[inline] + pub fn detailed_layout_info(&self, node_id: NodeId) -> &DetailedLayoutInfo { + &self.nodes[node_id.into()].detailed_layout_info + } + + /// Marks the layout of this node and its ancestors as outdated + pub fn mark_dirty(&mut self, node: NodeId) -> TaffyResult<()> { + fn mark_dirty_recursive( + nodes: &mut SlotMap, + parents: &SlotMap>, + node_key: DefaultKey, + ) { + match nodes[node_key].mark_dirty() { + ClearState::AlreadyEmpty => { + // Node was already marked as dirty. + // No need to visit ancestors + // as they should be marked as dirty already. + } + ClearState::Cleared => { + if let Some(Some(node)) = parents.get(node_key) { + mark_dirty_recursive(nodes, parents, (*node).into()); + } + } + } + } + + mark_dirty_recursive(&mut self.nodes, &self.parents, node.into()); + + Ok(()) + } + + /// Indicates whether the layout of this node needs to be recomputed + #[inline] + pub fn dirty(&self, node: NodeId) -> TaffyResult { + Ok(self.nodes[node.into()].cache.is_empty()) + } + + /// Updates the stored layout of the provided `node` and its children + pub fn compute_layout_with_measure( + &mut self, + node_id: NodeId, + available_space: Size, + measure_function: MeasureFunction, + ) -> Result<(), TaffyError> + where + MeasureFunction: + FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, + { + let use_rounding = self.config.use_rounding; + let mut taffy_view = TaffyView { taffy: self, measure_function }; + compute_root_layout(&mut taffy_view, node_id, available_space); + if use_rounding { + round_layout(&mut taffy_view, node_id); + } + Ok(()) + } + + /// Updates the stored layout of the provided `node` and its children + pub fn compute_layout(&mut self, node: NodeId, available_space: Size) -> Result<(), TaffyError> { + self.compute_layout_with_measure(node, available_space, |_, _, _, _, _| Size::ZERO) + } + + /// Prints a debug representation of the tree's layout + #[cfg(feature = "std")] + pub fn print_tree(&mut self, root: NodeId) { + crate::util::print_tree(self, root) + } + + /// Returns an instance of LayoutTree representing the TaffyTree + #[cfg(test)] + pub(crate) fn as_layout_tree(&mut self) -> impl LayoutPartialTree + CacheTree + '_ { + TaffyView { taffy: self, measure_function: |_, _, _, _, _| Size::ZERO } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::style::{Dimension, Display, FlexDirection}; + use crate::style_helpers::*; + use crate::util::sys; + + fn size_measure_function( + known_dimensions: Size>, + _available_space: Size, + _node_id: NodeId, + node_context: Option<&mut Size>, + _style: &Style, + ) -> Size { + known_dimensions.unwrap_or(node_context.cloned().unwrap_or(Size::ZERO)) + } + + #[test] + fn new_should_allocate_default_capacity() { + const DEFAULT_CAPACITY: usize = 16; // This is the capacity defined in the `impl Default` + let taffy: TaffyTree<()> = TaffyTree::new(); + + assert!(taffy.children.capacity() >= DEFAULT_CAPACITY); + assert!(taffy.parents.capacity() >= DEFAULT_CAPACITY); + assert!(taffy.nodes.capacity() >= DEFAULT_CAPACITY); + } + + #[test] + fn test_with_capacity() { + const CAPACITY: usize = 8; + let taffy: TaffyTree<()> = TaffyTree::with_capacity(CAPACITY); + + assert!(taffy.children.capacity() >= CAPACITY); + assert!(taffy.parents.capacity() >= CAPACITY); + assert!(taffy.nodes.capacity() >= CAPACITY); + } + + #[test] + fn test_new_leaf() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + let res = taffy.new_leaf(Style::default()); + assert!(res.is_ok()); + let node = res.unwrap(); + + // node should be in the taffy tree and have no children + assert!(taffy.child_count(node) == 0); + } + + #[test] + fn new_leaf_with_context() { + let mut taffy: TaffyTree> = TaffyTree::new(); + + let res = taffy.new_leaf_with_context(Style::default(), Size::ZERO); + assert!(res.is_ok()); + let node = res.unwrap(); + + // node should be in the taffy tree and have no children + assert!(taffy.child_count(node) == 0); + } + + /// Test that new_with_children works as expected + #[test] + fn test_new_with_children() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + let node = taffy.new_with_children(Style::default(), &[child0, child1]).unwrap(); + + // node should have two children + assert_eq!(taffy.child_count(node), 2); + assert_eq!(taffy.children(node).unwrap()[0], child0); + assert_eq!(taffy.children(node).unwrap()[1], child1); + } + + #[test] + fn remove_node_should_remove() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + let node = taffy.new_leaf(Style::default()).unwrap(); + + let _ = taffy.remove(node).unwrap(); + } + + #[test] + fn remove_node_should_detach_hierarchy() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + // Build a linear tree layout: <0> <- <1> <- <2> + let node2 = taffy.new_leaf(Style::default()).unwrap(); + let node1 = taffy.new_with_children(Style::default(), &[node2]).unwrap(); + let node0 = taffy.new_with_children(Style::default(), &[node1]).unwrap(); + + // Both node0 and node1 should have 1 child nodes + assert_eq!(taffy.children(node0).unwrap().as_slice(), &[node1]); + assert_eq!(taffy.children(node1).unwrap().as_slice(), &[node2]); + + // Disconnect the tree: <0> <2> + let _ = taffy.remove(node1).unwrap(); + + // Both remaining nodes should have no child nodes + assert!(taffy.children(node0).unwrap().is_empty()); + assert!(taffy.children(node2).unwrap().is_empty()); + } + + #[test] + fn remove_last_node() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + let parent = taffy.new_leaf(Style::default()).unwrap(); + let child = taffy.new_leaf(Style::default()).unwrap(); + taffy.add_child(parent, child).unwrap(); + + taffy.remove(child).unwrap(); + taffy.remove(parent).unwrap(); + } + + #[test] + fn set_measure() { + let mut taffy: TaffyTree> = TaffyTree::new(); + let node = taffy.new_leaf_with_context(Style::default(), Size { width: 200.0, height: 200.0 }).unwrap(); + taffy.compute_layout_with_measure(node, Size::MAX_CONTENT, size_measure_function).unwrap(); + assert_eq!(taffy.layout(node).unwrap().size.width, 200.0); + + taffy.set_node_context(node, Some(Size { width: 100.0, height: 100.0 })).unwrap(); + taffy.compute_layout_with_measure(node, Size::MAX_CONTENT, size_measure_function).unwrap(); + assert_eq!(taffy.layout(node).unwrap().size.width, 100.0); + } + + #[test] + fn set_measure_of_previously_unmeasured_node() { + let mut taffy: TaffyTree> = TaffyTree::new(); + let node = taffy.new_leaf(Style::default()).unwrap(); + taffy.compute_layout_with_measure(node, Size::MAX_CONTENT, size_measure_function).unwrap(); + assert_eq!(taffy.layout(node).unwrap().size.width, 0.0); + + taffy.set_node_context(node, Some(Size { width: 100.0, height: 100.0 })).unwrap(); + taffy.compute_layout_with_measure(node, Size::MAX_CONTENT, size_measure_function).unwrap(); + assert_eq!(taffy.layout(node).unwrap().size.width, 100.0); + } + + /// Test that adding `add_child()` works + #[test] + fn add_child() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let node = taffy.new_leaf(Style::default()).unwrap(); + assert_eq!(taffy.child_count(node), 0); + + let child0 = taffy.new_leaf(Style::default()).unwrap(); + taffy.add_child(node, child0).unwrap(); + assert_eq!(taffy.child_count(node), 1); + + let child1 = taffy.new_leaf(Style::default()).unwrap(); + taffy.add_child(node, child1).unwrap(); + assert_eq!(taffy.child_count(node), 2); + } + + #[test] + fn insert_child_at_index() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + let child2 = taffy.new_leaf(Style::default()).unwrap(); + + let node = taffy.new_leaf(Style::default()).unwrap(); + assert_eq!(taffy.child_count(node), 0); + + taffy.insert_child_at_index(node, 0, child0).unwrap(); + assert_eq!(taffy.child_count(node), 1); + assert_eq!(taffy.children(node).unwrap()[0], child0); + + taffy.insert_child_at_index(node, 0, child1).unwrap(); + assert_eq!(taffy.child_count(node), 2); + assert_eq!(taffy.children(node).unwrap()[0], child1); + assert_eq!(taffy.children(node).unwrap()[1], child0); + + taffy.insert_child_at_index(node, 1, child2).unwrap(); + assert_eq!(taffy.child_count(node), 3); + assert_eq!(taffy.children(node).unwrap()[0], child1); + assert_eq!(taffy.children(node).unwrap()[1], child2); + assert_eq!(taffy.children(node).unwrap()[2], child0); + } + + #[test] + fn set_children() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + let node = taffy.new_with_children(Style::default(), &[child0, child1]).unwrap(); + + assert_eq!(taffy.child_count(node), 2); + assert_eq!(taffy.children(node).unwrap()[0], child0); + assert_eq!(taffy.children(node).unwrap()[1], child1); + + let child2 = taffy.new_leaf(Style::default()).unwrap(); + let child3 = taffy.new_leaf(Style::default()).unwrap(); + taffy.set_children(node, &[child2, child3]).unwrap(); + + assert_eq!(taffy.child_count(node), 2); + assert_eq!(taffy.children(node).unwrap()[0], child2); + assert_eq!(taffy.children(node).unwrap()[1], child3); + } + + /// Test that removing a child works + #[test] + fn remove_child() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + let node = taffy.new_with_children(Style::default(), &[child0, child1]).unwrap(); + + assert_eq!(taffy.child_count(node), 2); + + taffy.remove_child(node, child0).unwrap(); + assert_eq!(taffy.child_count(node), 1); + assert_eq!(taffy.children(node).unwrap()[0], child1); + + taffy.remove_child(node, child1).unwrap(); + assert_eq!(taffy.child_count(node), 0); + } + + #[test] + fn remove_child_at_index() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + let node = taffy.new_with_children(Style::default(), &[child0, child1]).unwrap(); + + assert_eq!(taffy.child_count(node), 2); + + taffy.remove_child_at_index(node, 0).unwrap(); + assert_eq!(taffy.child_count(node), 1); + assert_eq!(taffy.children(node).unwrap()[0], child1); + + taffy.remove_child_at_index(node, 0).unwrap(); + assert_eq!(taffy.child_count(node), 0); + } + + #[test] + fn remove_children_range() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + let child2 = taffy.new_leaf(Style::default()).unwrap(); + let child3 = taffy.new_leaf(Style::default()).unwrap(); + let node = taffy.new_with_children(Style::default(), &[child0, child1, child2, child3]).unwrap(); + + assert_eq!(taffy.child_count(node), 4); + + taffy.remove_children_range(node, 1..=2).unwrap(); + assert_eq!(taffy.child_count(node), 2); + assert_eq!(taffy.children(node).unwrap(), [child0, child3]); + for child in [child0, child3] { + assert_eq!(taffy.parent(child), Some(node)); + } + for child in [child1, child2] { + assert_eq!(taffy.parent(child), None); + } + } + + // Related to: https://github.com/DioxusLabs/taffy/issues/510 + #[test] + fn remove_child_updates_parents() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + let parent = taffy.new_leaf(Style::default()).unwrap(); + let child = taffy.new_leaf(Style::default()).unwrap(); + + taffy.add_child(parent, child).unwrap(); + + taffy.remove(parent).unwrap(); + + // Once the parent is removed this shouldn't panic. + assert!(taffy.set_children(child, &[]).is_ok()); + } + + #[test] + fn replace_child_at_index() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + + let node = taffy.new_with_children(Style::default(), &[child0]).unwrap(); + assert_eq!(taffy.child_count(node), 1); + assert_eq!(taffy.children(node).unwrap()[0], child0); + + taffy.replace_child_at_index(node, 0, child1).unwrap(); + assert_eq!(taffy.child_count(node), 1); + assert_eq!(taffy.children(node).unwrap()[0], child1); + } + #[test] + fn test_child_at_index() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + let child2 = taffy.new_leaf(Style::default()).unwrap(); + let node = taffy.new_with_children(Style::default(), &[child0, child1, child2]).unwrap(); + + assert!(if let Ok(result) = taffy.child_at_index(node, 0) { result == child0 } else { false }); + assert!(if let Ok(result) = taffy.child_at_index(node, 1) { result == child1 } else { false }); + assert!(if let Ok(result) = taffy.child_at_index(node, 2) { result == child2 } else { false }); + } + #[test] + fn test_child_count() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + let node = taffy.new_with_children(Style::default(), &[child0, child1]).unwrap(); + + assert!(taffy.child_count(node) == 2); + assert!(taffy.child_count(child0) == 0); + assert!(taffy.child_count(child1) == 0); + } + + #[allow(clippy::vec_init_then_push)] + #[test] + fn test_children() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + let node = taffy.new_with_children(Style::default(), &[child0, child1]).unwrap(); + + let mut children = sys::Vec::new(); + children.push(child0); + children.push(child1); + + let children_result = taffy.children(node).unwrap(); + assert_eq!(children_result, children); + + assert!(taffy.children(child0).unwrap().is_empty()); + } + #[test] + fn test_set_style() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + let node = taffy.new_leaf(Style::default()).unwrap(); + assert_eq!(taffy.style(node).unwrap().display, Display::Flex); + + taffy.set_style(node, Style { display: Display::None, ..Style::default() }).unwrap(); + assert_eq!(taffy.style(node).unwrap().display, Display::None); + } + #[test] + fn test_style() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + let style = Style { display: Display::None, flex_direction: FlexDirection::RowReverse, ..Default::default() }; + + let node = taffy.new_leaf(style.clone()).unwrap(); + + let res = taffy.style(node); + assert!(res.is_ok()); + assert!(res.unwrap() == &style); + } + #[test] + fn test_layout() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let node = taffy.new_leaf(Style::default()).unwrap(); + + // TODO: Improve this test? + let res = taffy.layout(node); + assert!(res.is_ok()); + } + + #[test] + fn test_mark_dirty() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let child0 = taffy.new_leaf(Style::default()).unwrap(); + let child1 = taffy.new_leaf(Style::default()).unwrap(); + let node = taffy.new_with_children(Style::default(), &[child0, child1]).unwrap(); + + taffy.compute_layout(node, Size::MAX_CONTENT).unwrap(); + + assert_eq!(taffy.dirty(child0), Ok(false)); + assert_eq!(taffy.dirty(child1), Ok(false)); + assert_eq!(taffy.dirty(node), Ok(false)); + + taffy.mark_dirty(node).unwrap(); + assert_eq!(taffy.dirty(child0), Ok(false)); + assert_eq!(taffy.dirty(child1), Ok(false)); + assert_eq!(taffy.dirty(node), Ok(true)); + + taffy.compute_layout(node, Size::MAX_CONTENT).unwrap(); + taffy.mark_dirty(child0).unwrap(); + assert_eq!(taffy.dirty(child0), Ok(true)); + assert_eq!(taffy.dirty(child1), Ok(false)); + assert_eq!(taffy.dirty(node), Ok(true)); + } + + #[test] + fn compute_layout_should_produce_valid_result() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let node_result = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(10f32), height: Dimension::from_length(10f32) }, + ..Default::default() + }); + assert!(node_result.is_ok()); + let node = node_result.unwrap(); + let layout_result = taffy.compute_layout( + node, + Size { width: AvailableSpace::Definite(100.), height: AvailableSpace::Definite(100.) }, + ); + assert!(layout_result.is_ok()); + } + + #[test] + fn make_sure_layout_location_is_top_left() { + use crate::prelude::*; + + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + let node = taffy + .new_leaf(Style { + size: Size { width: Dimension::from_percent(1f32), height: Dimension::from_percent(1f32) }, + ..Default::default() + }) + .unwrap(); + + let root = taffy + .new_with_children( + Style { + size: Size { width: Dimension::from_length(100f32), height: Dimension::from_length(100f32) }, + padding: Rect { + left: length(10f32), + right: length(20f32), + top: length(30f32), + bottom: length(40f32), + }, + ..Default::default() + }, + &[node], + ) + .unwrap(); + + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + + // If Layout::location represents top-left coord, 'node' location + // must be (due applied 'root' padding): {x: 10, y: 30}. + // + // It's important, since result will be different for each other + // coordinate space: + // - bottom-left: {x: 10, y: 40} + // - top-right: {x: 20, y: 30} + // - bottom-right: {x: 20, y: 40} + let layout = taffy.layout(node).unwrap(); + assert_eq!(layout.location.x, 10f32); + assert_eq!(layout.location.y, 30f32); + } + + #[test] + fn set_children_reparents() { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + let child = taffy.new_leaf(Style::default()).unwrap(); + let old_parent = taffy.new_with_children(Style::default(), &[child]).unwrap(); + + let new_parent = taffy.new_leaf(Style::default()).unwrap(); + taffy.set_children(new_parent, &[child]).unwrap(); + + assert!(taffy.children(old_parent).unwrap().is_empty()); + } +} diff --git a/vendor/taffy/src/tree/traits.rs b/vendor/taffy/src/tree/traits.rs new file mode 100644 index 0000000..b91f9ca --- /dev/null +++ b/vendor/taffy/src/tree/traits.rs @@ -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)
[`compute_grid_layout`](crate::compute_grid_layout)
[`compute_block_layout`](crate::compute_block_layout)
[`compute_root_layout`](crate::compute_root_layout)
[`compute_leaf_layout`](crate::compute_leaf_layout)
[`compute_hidden_layout`](crate::compute_hidden_layout)
[`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 +//! 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`
+//! **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 + 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 + 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`. + 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; + + /// 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 + where + Self: 'a; + + /// The style type representing each CSS Grid item's styles + type GridItemStyle<'a>: GridItemStyle + 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>, + parent_size: Size>, + available_space: Size, + sizing_mode: SizingMode, + axis: AbsoluteAxis, + vertical_margins_are_collapsible: Line, + ) -> 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>, + parent_size: Size>, + available_space: Size, + sizing_mode: SizingMode, + vertical_margins_are_collapsible: Line, + ) -> Size { + 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>, + parent_size: Size>, + available_space: Size, + sizing_mode: SizingMode, + vertical_margins_are_collapsible: Line, + ) -> 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 LayoutPartialTreeExt for T {} diff --git a/vendor/taffy/src/util/debug.rs b/vendor/taffy/src/util/debug.rs new file mode 100644 index 0000000..ad69324 --- /dev/null +++ b/vendor/taffy/src/util/debug.rs @@ -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>, +} + +#[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}; diff --git a/vendor/taffy/src/util/math.rs b/vendor/taffy/src/util/math.rs new file mode 100644 index 0000000..35f5f60 --- /dev/null +++ b/vendor/taffy/src/util/math.rs @@ -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 { + /// 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> for Option { + fn maybe_min(self, rhs: Option) -> Option { + 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) -> Option { + 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, max: Option) -> Option { + 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) -> Option { + 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) -> Option { + match (self, rhs) { + (Some(l), Some(r)) => Some(l - r), + (Some(_l), None) => self, + (None, Some(_r)) => None, + (None, None) => None, + } + } +} + +impl MaybeMath> for Option { + fn maybe_min(self, rhs: f32) -> Option { + self.map(|val| val.min(rhs)) + } + + fn maybe_max(self, rhs: f32) -> Option { + self.map(|val| val.max(rhs)) + } + + fn maybe_clamp(self, min: f32, max: f32) -> Option { + self.map(|val| val.min(max).max(min)) + } + + fn maybe_add(self, rhs: f32) -> Option { + self.map(|val| val + rhs) + } + + fn maybe_sub(self, rhs: f32) -> Option { + self.map(|val| val - rhs) + } +} + +impl MaybeMath, f32> for f32 { + fn maybe_min(self, rhs: Option) -> f32 { + match rhs { + Some(val) => self.min(val), + None => self, + } + } + + fn maybe_max(self, rhs: Option) -> f32 { + match rhs { + Some(val) => self.max(val), + None => self, + } + } + + fn maybe_clamp(self, min: Option, max: Option) -> 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 { + match rhs { + Some(val) => self + val, + None => self, + } + } + + fn maybe_sub(self, rhs: Option) -> f32 { + match rhs { + Some(val) => self - val, + None => self, + } + } +} + +impl MaybeMath 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, AvailableSpace> for AvailableSpace { + fn maybe_min(self, rhs: Option) -> 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) -> 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, max: Option) -> 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) -> 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) -> 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> MaybeMath, Size> for Size { + fn maybe_min(self, rhs: Size) -> Size { + Size { width: self.width.maybe_min(rhs.width), height: self.height.maybe_min(rhs.height) } + } + + fn maybe_max(self, rhs: Size) -> Size { + Size { width: self.width.maybe_max(rhs.width), height: self.height.maybe_max(rhs.height) } + } + + fn maybe_clamp(self, min: Size, max: Size) -> Size { + 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) -> Size { + Size { width: self.width.maybe_add(rhs.width), height: self.height.maybe_add(rhs.height) } + } + + fn maybe_sub(self, rhs: Size) -> Size { + 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); + } + } +} diff --git a/vendor/taffy/src/util/mod.rs b/vendor/taffy/src/util/mod.rs new file mode 100644 index 0000000..0201f38 --- /dev/null +++ b/vendor/taffy/src/util/mod.rs @@ -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 +where + S: for<'a> From<&'a str>, + D: serde::Deserializer<'de>, +{ + let s: String = serde::Deserialize::deserialize(deserializer)?; + Ok(S::from(&s)) +} diff --git a/vendor/taffy/src/util/parse.rs b/vendor/taffy/src/util/parse.rs new file mode 100644 index 0000000..24743fb --- /dev/null +++ b/vendor/taffy/src/util/parse.rs @@ -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>; + +/// 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> for ParseError { + fn from(value: CssParseError<'i>) -> Self { + Self(value.to_string()) + } +} +impl<'i> From> 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 = Result; + +/// 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(input: &str) -> Result { + 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 { + $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 { + $crate::util::parse::parse_css_str_entirely(input) + } + } + }; +} +pub(crate) use impl_parse_for_keyword_enum; diff --git a/vendor/taffy/src/util/print.rs b/vendor/taffy/src/util/print.rs new file mode 100644 index 0000000..d536d43 --- /dev/null +++ b/vendor/taffy/src/util/print.rs @@ -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::::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(()) +} diff --git a/vendor/taffy/src/util/resolve.rs b/vendor/taffy/src/util/resolve.rs new file mode 100644 index 0000000..b07b610 --- /dev/null +++ b/vendor/taffy/src/util/resolve.rs @@ -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 { + /// 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 { + /// 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> for LengthPercentage { + /// Converts the given [`LengthPercentage`] into an absolute length + /// Can return `None` + fn maybe_resolve(self, context: Option, calc: impl Fn(*const (), f32) -> f32) -> Option { + 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> for LengthPercentageAuto { + /// Converts the given [`LengthPercentageAuto`] into an absolute length + /// Can return `None` + fn maybe_resolve(self, context: Option, calc: impl Fn(*const (), f32) -> f32) -> Option { + 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> for Dimension { + /// Converts the given [`Dimension`] into an absolute length + /// + /// Can return `None` + fn maybe_resolve(self, context: Option, calc: impl Fn(*const (), f32) -> f32) -> Option { + 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 context +impl, Option>> MaybeResolve> 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 { + self.maybe_resolve(Some(context), calc) + } +} + +// Generic MaybeResolve for Size +impl> MaybeResolve, Size> for Size { + /// Converts any `parent`-relative values for size into an absolute size + fn maybe_resolve(self, context: Size, calc: impl Fn(*const (), f32) -> f32) -> Size { + Size { + width: self.width.maybe_resolve(context.width, &calc), + height: self.height.maybe_resolve(context.height, &calc), + } + } +} + +impl ResolveOrZero, f32> for LengthPercentage { + /// Will return a default value of result is evaluated to `None` + fn resolve_or_zero(self, context: Option, calc: impl Fn(*const (), f32) -> f32) -> f32 { + self.maybe_resolve(context, calc).unwrap_or(0.0) + } +} + +impl ResolveOrZero, f32> for LengthPercentageAuto { + /// Will return a default value of result is evaluated to `None` + fn resolve_or_zero(self, context: Option, calc: impl Fn(*const (), f32) -> f32) -> f32 { + self.maybe_resolve(context, calc).unwrap_or(0.0) + } +} + +impl ResolveOrZero, f32> for Dimension { + /// Will return a default value of result is evaluated to `None` + fn resolve_or_zero(self, context: Option, calc: impl Fn(*const (), f32) -> f32) -> f32 { + self.maybe_resolve(context, calc).unwrap_or(0.0) + } +} + +// Generic ResolveOrZero for Size +impl> ResolveOrZero, Size> for Size { + /// Converts any `parent`-relative values for size into an absolute size + fn resolve_or_zero(self, context: Size, calc: impl Fn(*const (), f32) -> f32) -> Size { + 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> ResolveOrZero, Rect> for Rect { + /// Converts any `parent`-relative values for Rect into an absolute Rect + fn resolve_or_zero(self, context: Size, calc: impl Fn(*const (), f32) -> f32) -> Rect { + 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>> ResolveOrZero, Rect> for Rect { + /// Converts any `parent`-relative values for Rect into an absolute Rect + fn resolve_or_zero(self, context: Option, calc: impl Fn(*const (), f32) -> f32) -> Rect { + 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> ResolveOrZero> for Rect { + /// 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 { + 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(input: Lhs, context: Rhs, expected: Out) + where + Lhs: MaybeResolve, + Out: PartialEq + Debug, + { + assert_eq!(input.maybe_resolve(context, |_, _| 42.42), expected); + } + + // ResolveOrZero test runner + fn roz_case(input: Lhs, context: Rhs, expected: Out) + where + Lhs: ResolveOrZero, + 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 should always return Size + /// + /// The parent / context should not affect the outcome. + #[test] + fn maybe_resolve_auto() { + mr_case(Size::::auto(), Size::NONE, Size::NONE); + mr_case(Size::::auto(), Size::new(5.0, 5.0), Size::NONE); + mr_case(Size::::auto(), Size::new(-5.0, -5.0), Size::NONE); + mr_case(Size::::auto(), Size::new(0.0, 0.0), Size::NONE); + } + + /// Size should always return a Size + /// 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` should return `Size` if context is `Size`. + /// Otherwise it should return `Size` + /// 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::::auto(), Size::NONE, Rect::zero()); + roz_case(Rect::::auto(), Size::new(5.0, 5.0), Rect::zero()); + roz_case(Rect::::auto(), Size::new(-5.0, -5.0), Rect::zero()); + roz_case(Rect::::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::::auto(), None, Rect::zero()); + roz_case(Rect::::auto(), Some(5.0), Rect::zero()); + roz_case(Rect::::auto(), Some(-5.0), Rect::zero()); + roz_case(Rect::::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()); + } + } +} diff --git a/vendor/taffy/src/util/sys.rs b/vendor/taffy/src/util/sys.rs new file mode 100644 index 0000000..3397a87 --- /dev/null +++ b/vendor/taffy/src/util/sys.rs @@ -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 = std::collections::HashMap; + /// An allocation-backend agnostic vector type + pub(crate) type Vec = std::vec::Vec; + /// A vector of child nodes + pub(crate) type ChildrenVec = std::vec::Vec; + #[cfg(feature = "grid")] + /// A vector of grid tracks + pub(crate) type GridTrackVec = std::vec::Vec; + + /// 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(capacity: usize) -> Vec { + 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(value: A) -> Vec { + 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 = alloc::collections::BTreeMap; + /// An allocation-backend agnostic vector type + pub(crate) type Vec = alloc::vec::Vec; + /// A vector of child nodes + pub(crate) type ChildrenVec = alloc::vec::Vec; + #[cfg(feature = "grid")] + /// A vector of grid tracks + pub(crate) type GridTrackVec = alloc::vec::Vec; + + /// 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(capacity: usize) -> Vec { + 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(value: A) -> Vec { + 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 = arrayvec::ArrayVec; + /// A vector of child nodes, whose length cannot exceed [`MAX_CHILD_COUNT`] + pub(crate) type ChildrenVec = arrayvec::ArrayVec; + #[cfg(feature = "grid")] + /// A vector of grid tracks + pub(crate) type GridTrackVec = arrayvec::ArrayVec; + + /// 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(_capacity: usize) -> arrayvec::ArrayVec { + 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(value: A) -> arrayvec::ArrayVec { + 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 + } + } +}