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 @@
+
+
+
+
+
+
+
+[](https://github.com/DioxusLabs/taffy/actions/workflows/ci.yml)
+[](https://crates.io/crates/taffy)
+[](https://docs.rs/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