// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
suberror TaffyError {
InvalidNodeId(Int)
CycleDetected(parent~ : Int, child~ : Int)
ChildNotFound(parent~ : Int, child~ : Int)
}
///|
pub type NodeId = Int
///|
pub struct Layout {
location : Point[Double]
size : Size[Double]
}
///|
pub fn Layout::zero() -> Layout {
Layout::{ location: Point::zero(), size: Size::zero() }
}
///|
priv struct LeafMeasureCacheEntry {
known_dimensions : Size[Double?]
available_space : Size[AvailableSpace]
measured : Size[Double]
}
///|
priv struct NodeLayoutCacheEntry {
known_dimensions : Size[Double?]
available_space : Size[AvailableSpace]
absolute_origin : Point[Double]
is_layout_root : Bool
location : Point[Double]
size : Size[Double]
effective_margin_top : Double
effective_margin_bottom : Double
effective_margin_top_max_pos : Double
effective_margin_top_min_neg : Double
effective_margin_bottom_max_pos : Double
effective_margin_bottom_min_neg : Double
}
///|
struct Node[C] {
mut style : Style
children : Array[NodeId]
mut parent : NodeId?
mut layout : Layout
mut context : C?
leaf_measure_cache : Array[LeafMeasureCacheEntry]
node_layout_cache : Array[NodeLayoutCacheEntry]
mut effective_margin_top : Double
mut effective_margin_bottom : Double
mut effective_margin_top_max_pos : Double
mut effective_margin_top_min_neg : Double
mut effective_margin_bottom_max_pos : Double
mut effective_margin_bottom_min_neg : Double
mut alive : Bool
mut dirty : Bool
}
///|
pub struct TaffyTree[C] {
priv nodes : Array[Node[C]]
}
///|
pub fn[C] TaffyTree::new() -> TaffyTree[C] {
TaffyTree::{ nodes: [] }
}
///|
pub fn[C] TaffyTree::with_capacity(_capacity : Int) -> TaffyTree[C] {
// Array capacity is backend-dependent; keep API surface but ignore the hint for now.
TaffyTree::new()
}
///|
pub fn[C] TaffyTree::new_leaf(self : TaffyTree[C], style : Style) -> NodeId {
let id = self.nodes.length()
self.nodes.append([
Node::{
style,
children: [],
parent: None,
layout: Layout::zero(),
context: None,
leaf_measure_cache: [],
node_layout_cache: [],
effective_margin_top: 0.0,
effective_margin_bottom: 0.0,
effective_margin_top_max_pos: 0.0,
effective_margin_top_min_neg: 0.0,
effective_margin_bottom_max_pos: 0.0,
effective_margin_bottom_min_neg: 0.0,
alive: true,
dirty: true,
},
])
id
}
///|
pub fn[C] TaffyTree::new_leaf_with_context(
self : TaffyTree[C],
style : Style,
context : C,
) -> NodeId {
let id = self.nodes.length()
self.nodes.append([
Node::{
style,
children: [],
parent: None,
layout: Layout::zero(),
context: Some(context),
leaf_measure_cache: [],
node_layout_cache: [],
effective_margin_top: 0.0,
effective_margin_bottom: 0.0,
effective_margin_top_max_pos: 0.0,
effective_margin_top_min_neg: 0.0,
effective_margin_bottom_max_pos: 0.0,
effective_margin_bottom_min_neg: 0.0,
alive: true,
dirty: true,
},
])
id
}
///|
pub fn[C] TaffyTree::new_with_children(
self : TaffyTree[C],
style : Style,
children : Array[NodeId],
) -> NodeId {
let id = self.nodes.length()
self.nodes.append([
Node::{
style,
children,
parent: None,
layout: Layout::zero(),
context: None,
leaf_measure_cache: [],
node_layout_cache: [],
effective_margin_top: 0.0,
effective_margin_bottom: 0.0,
effective_margin_top_max_pos: 0.0,
effective_margin_top_min_neg: 0.0,
effective_margin_bottom_max_pos: 0.0,
effective_margin_bottom_min_neg: 0.0,
alive: true,
dirty: true,
},
])
for child_id in children {
match self.nodes.get(child_id) {
Some(_) => {
if not(self.nodes[child_id].alive) {
continue
}
match self.nodes[child_id].parent {
Some(old_parent) =>
if old_parent != id {
let old_children = self.nodes[old_parent].children
match old_children.search_by(fn(x) { x == child_id }) {
Some(idx) => ignore(old_children.remove(idx))
None => ()
}
} else {
()
}
None => ()
}
self.nodes[child_id].parent = Some(id)
}
None => ()
}
}
id
}
///|
pub fn[C] TaffyTree::add_child(
self : TaffyTree[C],
parent : NodeId,
child : NodeId,
) -> Unit raise TaffyError {
match self.nodes.get(parent) {
Some(_) => ()
None => raise InvalidNodeId(parent)
}
match self.nodes.get(child) {
Some(_) => ()
None => raise InvalidNodeId(child)
}
if not(self.nodes[parent].alive) {
raise InvalidNodeId(parent)
}
if not(self.nodes[child].alive) {
raise InvalidNodeId(child)
}
if parent == child {
raise CycleDetected(parent~, child~)
}
// Reject cycles: `child` cannot be an ancestor of `parent`.
let mut cur = self.nodes[parent].parent
while true {
match cur {
None => break
Some(p) =>
if p == child {
raise CycleDetected(parent~, child~)
} else {
cur = self.nodes[p].parent
}
}
}
// Detach child from its old parent, if any.
match self.nodes[child].parent {
Some(old_parent) =>
if old_parent != parent {
let old_children = self.nodes[old_parent].children
match old_children.search_by(fn(x) { x == child }) {
Some(idx) => ignore(old_children.remove(idx))
None => ()
}
} else {
()
}
None => ()
}
self.nodes[child].parent = Some(parent)
self.nodes[parent].children.push(child)
self.mark_dirty(parent)
}
///|
pub fn[C] TaffyTree::set_children(
self : TaffyTree[C],
parent : NodeId,
children : Array[NodeId],
) -> Unit raise TaffyError {
match self.nodes.get(parent) {
Some(_) => ()
None => raise InvalidNodeId(parent)
}
if not(self.nodes[parent].alive) {
raise InvalidNodeId(parent)
}
// Validate nodes and reject cycles.
for child_id in children {
match self.nodes.get(child_id) {
Some(_) => ()
None => raise InvalidNodeId(child_id)
}
if not(self.nodes[child_id].alive) {
raise InvalidNodeId(child_id)
}
if child_id == parent {
raise CycleDetected(parent~, child=child_id)
}
let mut cur = Some(parent)
while true {
match cur {
None => break
Some(p) =>
if p == child_id {
raise CycleDetected(parent~, child=child_id)
} else {
cur = self.nodes[p].parent
}
}
}
}
// Detach current children.
let old_children = self.nodes[parent].children
for child_id in old_children {
self.nodes[child_id].parent = None
}
old_children.clear()
// Attach new children (detaching from any previous parents).
for child_id in children {
match self.nodes[child_id].parent {
Some(old_parent) =>
if old_parent != parent {
let old_siblings = self.nodes[old_parent].children
match old_siblings.search_by(fn(x) { x == child_id }) {
Some(idx) => ignore(old_siblings.remove(idx))
None => ()
}
} else {
()
}
None => ()
}
self.nodes[child_id].parent = Some(parent)
old_children.push(child_id)
}
self.mark_dirty(parent)
}
///|
pub fn[C] TaffyTree::remove_child(
self : TaffyTree[C],
parent : NodeId,
child : NodeId,
) -> NodeId raise TaffyError {
match self.nodes.get(parent) {
Some(_) => ()
None => raise InvalidNodeId(parent)
}
match self.nodes.get(child) {
Some(_) => ()
None => raise InvalidNodeId(child)
}
if not(self.nodes[parent].alive) {
raise InvalidNodeId(parent)
}
if not(self.nodes[child].alive) {
raise InvalidNodeId(child)
}
let siblings = self.nodes[parent].children
match siblings.search_by(fn(x) { x == child }) {
Some(idx) => {
ignore(siblings.remove(idx))
self.nodes[child].parent = None
self.mark_dirty(parent)
child
}
None => raise ChildNotFound(parent~, child~)
}
}
///|
pub fn[C] TaffyTree::mark_dirty(
self : TaffyTree[C],
node : NodeId,
) -> Unit raise TaffyError {
match self.nodes.get(node) {
Some(_) => ()
None => raise InvalidNodeId(node)
}
if not(self.nodes[node].alive) {
raise InvalidNodeId(node)
}
let mut cur : NodeId? = Some(node)
while true {
match cur {
None => break
Some(id) => {
self.nodes[id].dirty = true
cur = self.nodes[id].parent
}
}
}
}
///|
pub fn[C] TaffyTree::dirty(
self : TaffyTree[C],
node : NodeId,
) -> Bool raise TaffyError {
match self.nodes.get(node) {
Some(_) =>
if self.nodes[node].alive {
self.nodes[node].dirty
} else {
raise InvalidNodeId(node)
}
None => raise InvalidNodeId(node)
}
}
///|
pub fn[C] TaffyTree::remove(
self : TaffyTree[C],
node : NodeId,
) -> Unit raise TaffyError {
match self.nodes.get(node) {
Some(_) => ()
None => raise InvalidNodeId(node)
}
if not(self.nodes[node].alive) {
raise InvalidNodeId(node)
}
// Detach from parent.
let old_parent = self.nodes[node].parent
match old_parent {
Some(parent_id) => {
let siblings = self.nodes[parent_id].children
match siblings.search_by(fn(x) { x == node }) {
Some(idx) => ignore(siblings.remove(idx))
None => ()
}
self.nodes[node].parent = None
}
None => ()
}
// Orphan children.
let kids = self.nodes[node].children
for child_id in kids {
self.nodes[child_id].parent = None
}
kids.clear()
self.nodes[node].alive = false
match old_parent {
Some(p) => self.mark_dirty(p)
None => ()
}
}
///|
pub fn[C] TaffyTree::remove_subtree(
self : TaffyTree[C],
node : NodeId,
) -> Unit raise TaffyError {
match self.nodes.get(node) {
Some(_) => ()
None => raise InvalidNodeId(node)
}
if not(self.nodes[node].alive) {
raise InvalidNodeId(node)
}
// Detach root of subtree from its parent.
let old_parent = self.nodes[node].parent
match old_parent {
Some(parent_id) => {
let siblings = self.nodes[parent_id].children
match siblings.search_by(fn(x) { x == node }) {
Some(idx) => ignore(siblings.remove(idx))
None => ()
}
self.nodes[node].parent = None
}
None => ()
}
let stack : Array[NodeId] = [node]
while true {
match stack.pop() {
None => break
Some(cur) => {
if not(self.nodes[cur].alive) {
continue
}
let kids = self.nodes[cur].children
for child_id in kids {
stack.push(child_id)
}
kids.clear()
self.nodes[cur].parent = None
self.nodes[cur].context = None
self.nodes[cur].leaf_measure_cache.clear()
self.nodes[cur].node_layout_cache.clear()
self.nodes[cur].layout = Layout::zero()
self.nodes[cur].alive = false
}
}
}
match old_parent {
Some(p) => self.mark_dirty(p)
None => ()
}
}
///|
pub fn[C] TaffyTree::set_node_context(
self : TaffyTree[C],
node : NodeId,
context : C,
) -> Unit raise TaffyError {
match self.nodes.get(node) {
Some(_) =>
if self.nodes[node].alive {
self.nodes[node].context = Some(context)
self.nodes[node].leaf_measure_cache.clear()
self.nodes[node].node_layout_cache.clear()
self.mark_dirty(node)
} else {
raise InvalidNodeId(node)
}
None => raise InvalidNodeId(node)
}
}
///|
pub fn[C] TaffyTree::clear_node_context(
self : TaffyTree[C],
node : NodeId,
) -> Unit raise TaffyError {
match self.nodes.get(node) {
Some(_) =>
if self.nodes[node].alive {
self.nodes[node].context = None
self.nodes[node].leaf_measure_cache.clear()
self.nodes[node].node_layout_cache.clear()
self.mark_dirty(node)
} else {
raise InvalidNodeId(node)
}
None => raise InvalidNodeId(node)
}
}
///|
pub fn[C] TaffyTree::node_context(
self : TaffyTree[C],
node : NodeId,
) -> C? raise TaffyError {
let node_ref = match self.nodes.get(node) {
Some(n) => n
None => raise InvalidNodeId(node)
}
if not(node_ref.alive) {
raise InvalidNodeId(node)
}
node_ref.context
}
///|
pub fn[C] TaffyTree::layout(
self : TaffyTree[C],
node : NodeId,
) -> Layout raise TaffyError {
let node_ref = match self.nodes.get(node) {
Some(n) => n
None => raise InvalidNodeId(node)
}
if not(node_ref.alive) {
raise InvalidNodeId(node)
}
let layout = node_ref.layout
match node_ref.parent {
None => layout
Some(parent_id) => {
let parent = match self.nodes.get(parent_id) {
Some(p) => p
None => raise InvalidNodeId(parent_id)
}
if not(parent.alive) {
raise InvalidNodeId(parent_id)
}
let parent_layout = parent.layout
Layout::{
location: Point::new(
x=layout.location.x - parent_layout.location.x,
y=layout.location.y - parent_layout.location.y,
),
size: layout.size,
}
}
}
}
///|
pub fn[C] TaffyTree::set_style(
self : TaffyTree[C],
node : NodeId,
style : Style,
) -> Unit raise TaffyError {
match self.nodes.get(node) {
Some(_) =>
if self.nodes[node].alive {
self.nodes[node].style = style
self.nodes[node].leaf_measure_cache.clear()
self.nodes[node].node_layout_cache.clear()
self.mark_dirty(node)
} else {
raise InvalidNodeId(node)
}
None => raise InvalidNodeId(node)
}
}
///|
pub fn[C] TaffyTree::compute_layout(
self : TaffyTree[C],
root : NodeId,
available_space : Size[AvailableSpace],
) -> Unit raise TaffyError {
fn default_measure(
_known_dimensions : Size[Double?],
_available_space : Size[AvailableSpace],
_node_id : NodeId,
_context : C?,
_style : Style,
) -> Size[Double] {
Size::zero()
}
self.compute_layout_with_measure(root, available_space, default_measure)
}
///|
pub fn[C] TaffyTree::compute_layout_with_measure(
self : TaffyTree[C],
root : NodeId,
available_space : Size[AvailableSpace],
measure_function : (Size[Double?], Size[AvailableSpace], NodeId, C?, Style) -> Size[
Double,
],
) -> Unit raise TaffyError {
match self.nodes.get(root) {
Some(_) =>
if not(self.nodes[root].alive) {
raise InvalidNodeId(root)
} else {
()
}
None => raise InvalidNodeId(root)
}
for i in 0.. break
Some(cur) => {
if not(self.nodes[cur].alive) {
continue
}
let layout = self.nodes[cur].layout
let rounded_left = layout.location.x.round()
let rounded_top = layout.location.y.round()
let rounded_right = (layout.location.x + layout.size.width).round()
let rounded_bottom = (layout.location.y + layout.size.height).round()
self.nodes[cur].layout = Layout::{
location: Point::new(x=rounded_left, y=rounded_top),
size: Size::new(
width=max_double(rounded_right - rounded_left, 0.0),
height=max_double(rounded_bottom - rounded_top, 0.0),
),
}
self.nodes[cur].dirty = false
for child_id in self.nodes[cur].children {
stack.push(child_id)
}
}
}
}
}
///|
fn resolve_dimension(d : Dimension, available : AvailableSpace) -> Double {
match d {
DimAuto => 0.0
DimLength(v) => v
DimPercent(p) =>
match available {
AvailDefinite(v) => v * p
AvailMinContent => 0.0
AvailMaxContent => 0.0
}
DimFr(_) => 0.0
DimMinMax(_, max) => resolve_dimension(max, available)
DimMinContent => 0.0
DimMaxContent => 0.0
DimFitContent(_) => 0.0
DimRepeat(_, _) => 0.0
}
}
///|
fn max_double(a : Double, b : Double) -> Double {
if a > b {
a
} else {
b
}
}
///|
fn min_double(a : Double, b : Double) -> Double {
if a < b {
a
} else {
b
}
}
///|
fn resolve_optional_dimension(
d : Dimension,
available : AvailableSpace,
) -> Double? {
match d {
DimAuto => None
DimLength(v) => Some(v)
DimPercent(p) =>
match available {
AvailDefinite(v) => Some(v * p)
_ => None
}
DimFr(_) => None
DimMinMax(_, max) => resolve_optional_dimension(max, available)
DimMinContent => None
DimMaxContent => None
DimFitContent(_) => None
DimRepeat(_, _) => None
}
}
///|
fn clamp_dimension(
value : Double,
min_size : Dimension,
max_size : Dimension,
available : AvailableSpace,
) -> Double {
let min_v = resolve_optional_dimension(min_size, available)
let max_v0 = resolve_optional_dimension(max_size, available)
let max_v = match (min_v, max_v0) {
(Some(min_v), Some(max_v)) =>
if max_v < min_v {
Some(min_v)
} else {
Some(max_v)
}
_ => max_v0
}
let v0 = match min_v {
Some(min_v) => max_double(value, min_v)
None => value
}
match max_v {
Some(max_v) => if v0 > max_v { max_v } else { v0 }
None => v0
}
}
///|
fn resolve_dimension_width_basis(d : Dimension, basis_width : Double) -> Double {
match d {
DimAuto => 0.0
DimLength(v) => v
DimPercent(p) => basis_width * p
DimFr(_) => 0.0
DimMinMax(_, max) => resolve_dimension_width_basis(max, basis_width)
DimMinContent => 0.0
DimMaxContent => 0.0
DimFitContent(_) => 0.0
DimRepeat(_, _) => 0.0
}
}
///|
fn resolve_rect_width_basis(
rect : Rect[Dimension],
available : Size[AvailableSpace],
) -> Rect[Double] {
let basis_width = match available.width {
AvailDefinite(v) => v
_ => 0.0
}
Rect::new(
left=resolve_dimension_width_basis(rect.left, basis_width),
right=resolve_dimension_width_basis(rect.right, basis_width),
top=resolve_dimension_width_basis(rect.top, basis_width),
bottom=resolve_dimension_width_basis(rect.bottom, basis_width),
)
}
///|
fn abs_double(v : Double) -> Double {
if v < 0.0 {
-v
} else {
v
}
}
///|
fn margin_collapse_state_add_margin(
state : (Double, Double),
margin : Double,
) -> (Double, Double) {
let mut max_pos = state.0
let mut min_neg = state.1
if margin > 0.0 {
if margin > max_pos {
max_pos = margin
} else {
()
}
} else if margin < 0.0 {
if margin < min_neg {
min_neg = margin
} else {
()
}
} else {
()
}
(max_pos, min_neg)
}
///|
fn margin_collapse_state_from(margin : Double) -> (Double, Double) {
margin_collapse_state_add_margin((0.0, 0.0), margin)
}
///|
fn margin_collapse_state_value(state : (Double, Double)) -> Double {
state.0 + state.1
}
///|
fn[C] set_effective_margin_states(
tree : TaffyTree[C],
node_id : NodeId,
top_state : (Double, Double),
bottom_state : (Double, Double),
) -> Unit {
tree.nodes[node_id].effective_margin_top_max_pos = top_state.0
tree.nodes[node_id].effective_margin_top_min_neg = top_state.1
tree.nodes[node_id].effective_margin_bottom_max_pos = bottom_state.0
tree.nodes[node_id].effective_margin_bottom_min_neg = bottom_state.1
tree.nodes[node_id].effective_margin_top = margin_collapse_state_value(
top_state,
)
tree.nodes[node_id].effective_margin_bottom = margin_collapse_state_value(
bottom_state,
)
}
///|
fn double_approx_equal(a : Double, b : Double) -> Bool {
abs_double(a - b) <= 0.000001
}
///|
fn optional_double_equal(a : Double?, b : Double?) -> Bool {
match (a, b) {
(None, None) => true
(Some(x), Some(y)) => double_approx_equal(x, y)
_ => false
}
}
///|
fn available_space_equal(a : AvailableSpace, b : AvailableSpace) -> Bool {
match (a, b) {
(AvailDefinite(x), AvailDefinite(y)) => double_approx_equal(x, y)
(AvailMinContent, AvailMinContent) => true
(AvailMaxContent, AvailMaxContent) => true
_ => false
}
}
///|
fn known_dimensions_equal(a : Size[Double?], b : Size[Double?]) -> Bool {
optional_double_equal(a.width, b.width) &&
optional_double_equal(a.height, b.height)
}
///|
fn available_size_equal(
a : Size[AvailableSpace],
b : Size[AvailableSpace],
) -> Bool {
available_space_equal(a.width, b.width) &&
available_space_equal(a.height, b.height)
}
///|
fn point_equal(a : Point[Double], b : Point[Double]) -> Bool {
double_approx_equal(a.x, b.x) && double_approx_equal(a.y, b.y)
}
///|
fn find_leaf_measure_cache(
cache : Array[LeafMeasureCacheEntry],
known_dimensions : Size[Double?],
available_space : Size[AvailableSpace],
) -> Size[Double]? {
for entry in cache {
if known_dimensions_equal(entry.known_dimensions, known_dimensions) &&
available_size_equal(entry.available_space, available_space) {
return Some(entry.measured)
}
}
None
}
///|
fn find_node_layout_cache(
cache : Array[NodeLayoutCacheEntry],
known_dimensions : Size[Double?],
available_space : Size[AvailableSpace],
absolute_origin : Point[Double],
is_layout_root : Bool,
) -> NodeLayoutCacheEntry? {
for entry in cache {
if known_dimensions_equal(entry.known_dimensions, known_dimensions) &&
available_size_equal(entry.available_space, available_space) &&
point_equal(entry.absolute_origin, absolute_origin) &&
entry.is_layout_root == is_layout_root {
return Some(entry)
}
}
None
}
///|
fn resolve_track_dimension(d : Dimension, available : Double) -> Double {
match d {
DimAuto => 0.0
DimLength(v) => v
DimPercent(p) => available * p
DimFr(_) => 0.0
DimMinMax(_, max) => resolve_track_dimension(max, available)
DimMinContent => 0.0
DimMaxContent => 0.0
DimFitContent(limit) => resolve_track_dimension(limit, available)
DimRepeat(_, _) => 0.0
}
}
///|
fn is_near_int(v : Double) -> Bool {
abs_double(v - v.floor()) < 0.000001
}
///|
///|
fn compute_grid_track_sizes_with_contributions(
tracks : Array[Dimension],
available : Double,
gap : Double,
min_contrib : Array[Double],
max_contrib : Array[Double],
expand_to_fill : Bool,
) -> Array[Double] {
let count = tracks.length()
let sizes : Array[Double] = Array::make(count, 0.0)
let min_sizes : Array[Double] = Array::make(count, 0.0)
let fr_weights : Array[Double] = Array::make(count, 0.0)
let is_auto : Array[Bool] = Array::make(count, false)
let max_limits : Array[Double] = Array::make(count, 0.0)
let inf = 1.0e30
fn clamp_minmax(v : Double, lo : Double, hi : Double) -> Double {
let v = if v < lo { lo } else { v }
if v > hi {
hi
} else {
v
}
}
fn min_function_value(
d : Dimension,
available : Double,
min_c : Double,
max_c : Double,
) -> Double {
match d {
DimAuto => 0.0
DimMinContent => min_c
DimMaxContent => max_c
DimFitContent(limit) => {
let lim = resolve_track_dimension(limit, available)
let max_limited = if max_c < lim { max_c } else { lim }
if max_limited < min_c {
min_c
} else {
max_limited
}
}
DimFr(_) => 0.0
DimRepeat(_, _) => 0.0
_ => {
let v = resolve_track_dimension(d, available)
if v > 0.0 {
v
} else {
0.0
}
}
}
}
fn max_function_value_and_limit(
d : Dimension,
available : Double,
min_c : Double,
max_c : Double,
inf : Double,
) -> (Double, Double, Double) {
match d {
DimAuto => (max_c, inf, 0.0)
DimMinContent => (min_c, inf, 0.0)
DimMaxContent => (max_c, inf, 0.0)
DimFitContent(limit) => {
let lim = resolve_track_dimension(limit, available)
let preferred = if max_c < lim { max_c } else { lim }
let preferred = if preferred < min_c { min_c } else { preferred }
(preferred, preferred, 0.0)
}
DimFr(w) => (0.0, inf, if w > 0.0 { w } else { 0.0 })
DimRepeat(_, _) => (0.0, inf, 0.0)
_ => {
let v = resolve_track_dimension(d, available)
let v = if v > 0.0 { v } else { 0.0 }
(v, v, 0.0)
}
}
}
for i in 0.. {
min_sizes[i] = min_c
sizes[i] = max_c
max_limits[i] = inf
is_auto[i] = true
}
DimMinContent => {
min_sizes[i] = min_c
sizes[i] = min_c
max_limits[i] = inf
is_auto[i] = true
}
DimMaxContent => {
min_sizes[i] = max_c
sizes[i] = max_c
max_limits[i] = inf
is_auto[i] = true
}
DimFitContent(limit) => {
let lim = resolve_track_dimension(limit, available)
let preferred = if max_c < lim { max_c } else { lim }
let preferred = if preferred < min_c { min_c } else { preferred }
min_sizes[i] = min_c
sizes[i] = preferred
max_limits[i] = preferred
is_auto[i] = true
}
DimLength(v) => {
sizes[i] = v
min_sizes[i] = v
max_limits[i] = v
}
DimPercent(p) => {
let v = available * p
sizes[i] = v
min_sizes[i] = v
max_limits[i] = v
}
DimFr(w) => {
fr_weights[i] = if w > 0.0 { w } else { 0.0 }
let base = if w > 0.0 { min_c } else { max_c }
sizes[i] = base
min_sizes[i] = base
max_limits[i] = inf
}
DimMinMax(min_d, max_d) => {
let min_v = min_function_value(min_d, available, min_c, max_c)
let max_r = max_function_value_and_limit(
max_d, available, min_c, max_c, inf,
)
let max_v = max_r.0
let max_lim = max_r.1
let fr_w = max_r.2
min_sizes[i] = min_v
if fr_w > 0.0 {
fr_weights[i] = fr_w
sizes[i] = min_v
max_limits[i] = inf
} else {
let preferred = if max_v < min_v { min_v } else { max_v }
sizes[i] = preferred
max_limits[i] = if max_lim < min_v { min_v } else { max_lim }
is_auto[i] = match max_d {
DimAuto | DimMinContent | DimMaxContent | DimFitContent(_) => true
_ => false
}
}
}
DimRepeat(_, _) => {
// Expanded earlier; treat as auto when encountered.
min_sizes[i] = min_c
sizes[i] = max_c
max_limits[i] = inf
is_auto[i] = true
}
}
}
// Intrinsic sizing for flexible tracks: derive a flex fraction from content contributions.
// This matches taffy 0.5 behavior for `fr` tracks under indefinite available space.
if not(expand_to_fill) {
let mut flex_fraction = 0.0
for i in 0.. 0.0 {
let max_c = if i < max_contrib.length() { max_contrib[i] } else { 0.0 }
let ratio = max_c / w
if ratio > flex_fraction {
flex_fraction = ratio
}
}
}
if flex_fraction > 0.0 {
for i in 0.. 0.0 {
let flex_size = flex_fraction * w
sizes[i] = if flex_size < min_sizes[i] {
min_sizes[i]
} else {
flex_size
}
min_sizes[i] = if i < min_contrib.length() {
min_contrib[i]
} else {
0.0
}
max_limits[i] = inf
is_auto[i] = true
}
}
}
}
// Shrink tracks down to their min-content contributions if the sum of preferred sizes overflows.
let mut used = 0.0
for s in sizes {
used = used + s
}
if count > 1 {
used = used + gap * (count - 1).to_double()
}
if used > available {
let eps = 0.000001
let mut over = used - available
let mut shrinkable : Array[Int] = []
for i in 0.. min_sizes[i] + eps {
shrinkable.push(i)
}
}
while over > eps && shrinkable.length() > 0 {
let per = over / shrinkable.length().to_double()
let mut shrunk = 0.0
let next : Array[Int] = []
for idx in shrinkable {
let can = sizes[idx] - min_sizes[idx]
let s = if can < per { can } else { per }
if s > 0.0 {
sizes[idx] = sizes[idx] - s
shrunk = shrunk + s
}
if sizes[idx] > min_sizes[idx] + eps {
next.push(idx)
}
}
if shrunk <= 0.0 {
break
}
over = over - shrunk
shrinkable = next
}
}
// Distribute any remaining free space (only when the grid container has a definite size).
used = 0.0
for s in sizes {
used = used + s
}
if count > 1 {
used = used + gap * (count - 1).to_double()
}
let free = available - used
if expand_to_fill && free > 0.0 {
let mut sum_fr = 0.0
for w in fr_weights {
sum_fr = sum_fr + w
}
if sum_fr > 0.0 {
let mut fixed_used = 0.0
for i in 0.. 0.0 {
unresolved.push(i)
sizes[i] = min_sizes[i]
}
}
while unresolved.length() > 0 {
let mut unresolved_sum = 0.0
for idx in unresolved {
unresolved_sum = unresolved_sum + fr_weights[idx]
}
let denom = if unresolved_sum < 1.0 { 1.0 } else { unresolved_sum }
let flex_fraction = remaining_for_fr / denom
let mut froze_any = false
let next_unresolved : Array[Int] = []
for idx in unresolved {
let proposed = flex_fraction * fr_weights[idx]
if proposed + 0.000001 < min_sizes[idx] {
sizes[idx] = min_sizes[idx]
remaining_for_fr = max_double(
remaining_for_fr - min_sizes[idx],
0.0,
)
froze_any = true
} else {
next_unresolved.push(idx)
}
}
if not(froze_any) {
let total_fr_space = if unresolved_sum < 1.0 {
remaining_for_fr * unresolved_sum
} else {
remaining_for_fr
}
if is_near_int(total_fr_space) {
let total_int = total_fr_space.floor().to_int()
let raw_bases : Array[Int] = []
let raw_fracs : Array[Double] = []
let mut sum_bases = 0
for idx in unresolved {
let raw = flex_fraction * fr_weights[idx]
let base = raw.floor().to_int()
raw_bases.push(base)
raw_fracs.push(raw - base.to_double())
sum_bases = sum_bases + base
}
let mut remainder = total_int - sum_bases
while remainder > 0 {
let mut best_j = 0
let mut best_frac = -1.0
for j in 0.. best_frac {
best_frac = raw_fracs[j]
best_j = j
}
}
raw_bases[best_j] = raw_bases[best_j] + 1
raw_fracs[best_j] = -1.0
remainder = remainder - 1
}
for j in 0.. 0.000001 && growable.length() > 0 {
let extra = remaining / growable.length().to_double()
let mut used_extra = 0.0
let next : Array[Int] = []
for idx in growable {
let cap = max_limits[idx] - sizes[idx]
let inc = if cap < extra { cap } else { extra }
if inc > 0.0 {
sizes[idx] = sizes[idx] + inc
used_extra = used_extra + inc
}
if sizes[idx] + 0.000001 < max_limits[idx] {
next.push(idx)
}
}
if used_extra <= 0.0 {
break
}
remaining = remaining - used_extra
growable = next
}
}
}
// Ensure no track is below its min size.
for i in 0.. Size[
Double,
],
is_layout_root : Bool,
) -> Unit raise TaffyError {
ignore(is_layout_root)
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
let can_use_layout_cache = node.children.length() <= 1 &&
double_approx_equal(absolute_origin.x, 0.0) &&
double_approx_equal(absolute_origin.y, 0.0)
if can_use_layout_cache {
match
find_node_layout_cache(
node.node_layout_cache,
known_dimensions,
available_space,
absolute_origin,
is_layout_root,
) {
Some(entry) => {
tree.nodes[node_id].layout = Layout::{
location: entry.location,
size: entry.size,
}
tree.nodes[node_id].effective_margin_top = entry.effective_margin_top
tree.nodes[node_id].effective_margin_bottom = entry.effective_margin_bottom
tree.nodes[node_id].effective_margin_top_max_pos = entry.effective_margin_top_max_pos
tree.nodes[node_id].effective_margin_top_min_neg = entry.effective_margin_top_min_neg
tree.nodes[node_id].effective_margin_bottom_max_pos = entry.effective_margin_bottom_max_pos
tree.nodes[node_id].effective_margin_bottom_min_neg = entry.effective_margin_bottom_min_neg
return
}
None => ()
}
}
match node.style.display {
DisplayNone => compute_hidden_layout(tree, node_id, absolute_origin)
DisplayGrid =>
compute_grid_layout_with_measure(
tree, node_id, known_dimensions, available_space, absolute_origin, measure_function,
is_layout_root,
)
DisplayBlock =>
if node.children.length() == 0 {
compute_leaf_layout_with_measure(
tree, node_id, known_dimensions, available_space, absolute_origin, measure_function,
)
} else {
compute_block_layout_with_measure(
tree, node_id, known_dimensions, available_space, absolute_origin, measure_function,
is_layout_root,
)
}
_ =>
if node.children.length() == 0 {
compute_leaf_layout_with_measure(
tree, node_id, known_dimensions, available_space, absolute_origin, measure_function,
)
} else {
compute_flex_layout_with_measure(
tree, node_id, known_dimensions, available_space, absolute_origin, measure_function,
)
}
}
// Position::Relative: apply inset as a final offset without affecting sibling layout.
// `display: none` nodes must remain at zero-size/zero-offset.
if !(node.style.display is DisplayNone) {
match node.style.position {
PosRelative => {
let inset = node.style.inset
let dx = match
resolve_optional_dimension(inset.left, available_space.width) {
Some(v) => v
None =>
match
resolve_optional_dimension(inset.right, available_space.width) {
Some(v) => -v
None => 0.0
}
}
let dy = match
resolve_optional_dimension(inset.top, available_space.height) {
Some(v) => v
None =>
match
resolve_optional_dimension(inset.bottom, available_space.height) {
Some(v) => -v
None => 0.0
}
}
if dx != 0.0 || dy != 0.0 {
offset_subtree(tree, node_id, dx, dy)
}
}
PosAbsolute => ()
}
}
if can_use_layout_cache {
let layout = tree.nodes[node_id].layout
tree.nodes[node_id].node_layout_cache.push(NodeLayoutCacheEntry::{
known_dimensions,
available_space,
absolute_origin,
is_layout_root,
location: layout.location,
size: layout.size,
effective_margin_top: tree.nodes[node_id].effective_margin_top,
effective_margin_bottom: tree.nodes[node_id].effective_margin_bottom,
effective_margin_top_max_pos: tree.nodes[node_id].effective_margin_top_max_pos,
effective_margin_top_min_neg: tree.nodes[node_id].effective_margin_top_min_neg,
effective_margin_bottom_max_pos: tree.nodes[node_id].effective_margin_bottom_max_pos,
effective_margin_bottom_min_neg: tree.nodes[node_id].effective_margin_bottom_min_neg,
})
}
}
///|
fn[C] compute_hidden_layout(
tree : TaffyTree[C],
node_id : NodeId,
absolute_origin : Point[Double],
) -> Unit raise TaffyError {
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::zero(),
}
tree.nodes[node_id].effective_margin_top = 0.0
tree.nodes[node_id].effective_margin_bottom = 0.0
tree.nodes[node_id].effective_margin_top_max_pos = 0.0
tree.nodes[node_id].effective_margin_top_min_neg = 0.0
tree.nodes[node_id].effective_margin_bottom_max_pos = 0.0
tree.nodes[node_id].effective_margin_bottom_min_neg = 0.0
for child_id in node.children {
compute_hidden_layout(tree, child_id, absolute_origin)
}
}
///|
fn[C] offset_subtree(
tree : TaffyTree[C],
node_id : NodeId,
dx : Double,
dy : Double,
) -> Unit raise TaffyError {
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
let layout = node.layout
tree.nodes[node_id].layout = Layout::{
location: Point::{ x: layout.location.x + dx, y: layout.location.y + dy },
size: layout.size,
}
for child_id in node.children {
offset_subtree(tree, child_id, dx, dy)
}
}
///|
fn[C] subtree_has_measure_context(
tree : TaffyTree[C],
node_id : NodeId,
) -> Bool raise TaffyError {
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
if node.context is Some(_) {
return true
}
for child_id in node.children {
if subtree_has_measure_context(tree, child_id) {
return true
}
}
false
}
///|
fn[C] compute_leaf_layout_with_measure(
tree : TaffyTree[C],
node_id : NodeId,
known_dimensions : Size[Double?],
available_space : Size[AvailableSpace],
absolute_origin : Point[Double],
measure_function : (Size[Double?], Size[AvailableSpace], NodeId, C?, Style) -> Size[
Double,
],
) -> Unit raise TaffyError {
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
// CSS compatibility: vertical padding/border percentages are resolved against the width.
let resolved_padding = resolve_rect_width_basis(
node.style.padding,
available_space,
)
let resolved_border = resolve_rect_width_basis(
node.style.border,
available_space,
)
let scrollbar_w = node.style.scrollbar_width
let scrollbar_x = match node.style.overflow.x {
OverflowScroll => scrollbar_w
_ => 0.0
}
let scrollbar_y = match node.style.overflow.y {
OverflowScroll => scrollbar_w
_ => 0.0
}
let horiz_non_scroll_inset = resolved_padding.left +
resolved_padding.right +
resolved_border.left +
resolved_border.right
let vert_non_scroll_inset = resolved_padding.top +
resolved_padding.bottom +
resolved_border.top +
resolved_border.bottom
let horiz_inset = horiz_non_scroll_inset + scrollbar_y
let vert_inset = vert_non_scroll_inset + scrollbar_x
let mut specified_width = resolve_optional_dimension(
node.style.size.width,
available_space.width,
)
let mut specified_height = resolve_optional_dimension(
node.style.size.height,
available_space.height,
)
let known_width = known_dimensions.width
let known_height = known_dimensions.height
// aspect-ratio: apply to the node's preferred size (not to known_dimensions).
match node.style.aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match (specified_width, specified_height) {
(Some(w), None) => specified_height = Some((w / ratio).round())
(None, Some(h)) => specified_width = Some((h * ratio).round())
_ => ()
}
}
None => ()
}
let mut measure_known_dimensions = Size::new(
width=match known_width {
Some(w) => Some(w)
None => specified_width
},
height=match known_height {
Some(h) => Some(h)
None => specified_height
},
)
let resolved_max_width = resolve_optional_dimension(
node.style.max_size.width,
available_space.width,
)
let resolved_max_height = resolve_optional_dimension(
node.style.max_size.height,
available_space.height,
)
fn clamp_opt_to_max(v : Double?, max_v : Double?) -> Double? {
match (v, max_v) {
(Some(v0), Some(max0)) => Some(if v0 > max0 { max0 } else { v0 })
_ => v
}
}
measure_known_dimensions = Size::new(
width=clamp_opt_to_max(measure_known_dimensions.width, resolved_max_width),
height=clamp_opt_to_max(
measure_known_dimensions.height,
resolved_max_height,
),
)
let measure_available_space = Size::new(
width=match (available_space.width, resolved_max_width) {
(AvailDefinite(v), Some(max0)) =>
AvailDefinite(if v > max0 { max0 } else { v })
_ => available_space.width
},
height=match (available_space.height, resolved_max_height) {
(AvailDefinite(v), Some(max0)) =>
AvailDefinite(if v > max0 { max0 } else { v })
_ => available_space.height
},
)
let measured = match
find_leaf_measure_cache(
node.leaf_measure_cache,
measure_known_dimensions,
measure_available_space,
) {
Some(v) => v
None => {
let m = measure_function(
measure_known_dimensions,
measure_available_space,
node_id,
node.context,
node.style,
)
tree.nodes[node_id].leaf_measure_cache.push(LeafMeasureCacheEntry::{
known_dimensions: measure_known_dimensions,
available_space: measure_available_space,
measured: m,
})
m
}
}
let raw_width = match known_width {
Some(w) => w
None =>
match specified_width {
Some(w) => w
None => max_double(measured.width + horiz_inset, horiz_inset)
}
}
let raw_height = match known_height {
Some(h) => h
None =>
match specified_height {
Some(h) => h
None => max_double(measured.height + vert_inset, vert_inset)
}
}
let raw_border_box = Size::new(width=raw_width, height=raw_height)
let mut border_box_size = Size::new(
width=max_double(
clamp_dimension(
raw_border_box.width,
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
),
horiz_non_scroll_inset,
),
height=max_double(
clamp_dimension(
raw_border_box.height,
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
),
vert_non_scroll_inset,
),
)
// aspect-ratio: when width is auto and height is constrained by min/max,
// prefer deriving the width from the clamped height.
match node.style.aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
let width_is_auto = known_width is None && specified_width is None
let height_is_auto = known_height is None && specified_height is None
let resolved_min_height = resolve_optional_dimension(
node.style.min_size.height,
available_space.height,
)
let resolved_max_height = resolve_optional_dimension(
node.style.max_size.height,
available_space.height,
)
let height_is_definite = known_height is Some(_) ||
specified_height is Some(_) ||
(match resolved_min_height {
Some(min_h) => raw_border_box.height <= min_h
None => false
}) ||
(match resolved_max_height {
Some(max_h) => raw_border_box.height >= max_h
None => false
})
if width_is_auto && height_is_definite {
let ratio_width = max_double(
(border_box_size.height * ratio).round(),
horiz_inset,
)
border_box_size = Size::new(
width=max_double(
clamp_dimension(
ratio_width,
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
),
horiz_inset,
),
height=border_box_size.height,
)
} else {
()
}
if height_is_auto {
let min_height = max_double(
(border_box_size.width / ratio).round(),
vert_inset,
)
if min_height > border_box_size.height {
border_box_size = Size::new(
width=border_box_size.width,
height=min_height,
)
} else {
()
}
} else {
()
}
border_box_size = Size::new(
width=max_double(
clamp_dimension(
border_box_size.width,
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
),
horiz_non_scroll_inset,
),
height=max_double(
clamp_dimension(
border_box_size.height,
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
),
vert_non_scroll_inset,
),
)
}
None => ()
}
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: border_box_size,
}
let resolved_margin = resolve_rect_width_basis(
node.style.margin,
available_space,
)
set_effective_margin_states(
tree,
node_id,
margin_collapse_state_from(resolved_margin.top),
margin_collapse_state_from(resolved_margin.bottom),
)
}
///|
fn is_column(direction : FlexDirection) -> Bool {
match direction {
FlexRow => false
FlexRowReverse => false
FlexColumn => true
FlexColumnReverse => true
}
}
///|
fn get_main(size : Size[Double], is_col : Bool) -> Double {
if is_col {
size.height
} else {
size.width
}
}
///|
fn get_cross(size : Size[Double], is_col : Bool) -> Double {
if is_col {
size.width
} else {
size.height
}
}
///|
fn make_size_from_main_cross(
main : Double,
cross : Double,
is_col : Bool,
) -> Size[Double] {
if is_col {
Size::new(width=cross, height=main)
} else {
Size::new(width=main, height=cross)
}
}
///|
fn resolve_gap_main(
gap : Size[Dimension],
is_col : Bool,
available_main : AvailableSpace,
) -> Double {
if is_col {
resolve_dimension(gap.height, available_main)
} else {
resolve_dimension(gap.width, available_main)
}
}
///|
fn resolve_gap_cross(
gap : Size[Dimension],
is_col : Bool,
available_cross : AvailableSpace,
) -> Double {
if is_col {
resolve_dimension(gap.width, available_cross)
} else {
resolve_dimension(gap.height, available_cross)
}
}
///|
fn resolve_available_for_percent(
is_definite : Bool,
value : Double,
) -> AvailableSpace {
if is_definite {
AvailDefinite(value)
} else {
AvailMaxContent
}
}
///|
fn resolve_justify_start_main(
justify : AlignContent,
leftover_main : Double,
is_reverse : Bool,
) -> Double {
match justify {
AlignCenter => leftover_main / 2.0
AlignStart => if is_reverse { leftover_main } else { 0.0 }
AlignEnd => if is_reverse { 0.0 } else { leftover_main }
AlignFlexEnd => leftover_main
_ => 0.0
}
}
///|
fn expand_grid_template_axis(
template : Array[Dimension],
content_size : Double,
gap : Double,
) -> (Array[Dimension], Int, Bool) {
let expanded : Array[Dimension] = []
let mut used = 0.0
let mut first = true
let mut non_auto_fit_count = 0
let mut has_auto_fit = false
fn push_track(
expanded : Array[Dimension],
track : Dimension,
content_size : Double,
gap : Double,
used : Double,
first : Bool,
) -> (Double, Bool) {
let mut u = used
if not(first) {
u = u + gap
}
u = u + resolve_track_dimension(track, content_size)
expanded.push(track)
(u, false)
}
for item in template {
match item {
DimRepeat(rep, tracks) => {
if tracks.length() == 0 {
continue
}
match rep {
RepeatCount(n) => {
let count = if n > 0 { n } else { 0 }
for _i in 0.. {
let is_auto_fit = rep is RepeatAutoFit
if is_auto_fit {
has_auto_fit = true
}
let mut reps = 0
while true {
// Try appending one full track-list.
let mut u_try = used
let mut first_try = first
for t in tracks {
if not(first_try) {
u_try = u_try + gap
}
u_try = u_try + resolve_track_dimension(t, content_size)
first_try = false
}
let fits = u_try <= content_size + 0.000001
if reps > 0 && not(fits) {
break
}
// Always append at least once.
for t in tracks {
let r = push_track(expanded, t, content_size, gap, used, first)
used = r.0
first = r.1
if not(is_auto_fit) {
non_auto_fit_count = non_auto_fit_count + 1
}
}
reps = reps + 1
if not(fits) {
break
}
}
}
}
}
_ => {
let r = push_track(expanded, item, content_size, gap, used, first)
used = r.0
first = r.1
non_auto_fit_count = non_auto_fit_count + 1
}
}
}
(expanded, non_auto_fit_count, has_auto_fit)
}
///|
fn[C] compute_grid_layout_with_measure(
tree : TaffyTree[C],
node_id : NodeId,
known_dimensions : Size[Double?],
available_space : Size[AvailableSpace],
absolute_origin : Point[Double],
measure_function : (Size[Double?], Size[AvailableSpace], NodeId, C?, Style) -> Size[
Double,
],
is_layout_root : Bool,
) -> Unit raise TaffyError {
ignore(is_layout_root)
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
fn origin_zero_line(value : Int, explicit_track_count : Int) -> Int? {
if value == 0 {
None
} else if value > 0 {
Some(value - 1)
} else {
// Negative indices count back from the end of the explicit grid.
let explicit_line_count = explicit_track_count + 1
Some(value + explicit_line_count)
}
}
fn merged_placement_line(
placement : Line[GridPlacement],
legacy_start : Int?,
) -> Line[GridPlacement] {
match legacy_start {
Some(v) => Line::new(start=PlaceLine(v), end=placement.end)
None => placement
}
}
fn placement_span_value(span : Int) -> Int {
if span > 0 {
span
} else {
1
}
}
fn axis_resolve_start_line_and_span(
placement : Line[GridPlacement],
explicit_track_count : Int,
) -> (Int?, Int) {
let start_line = match placement.start {
PlaceLine(v) => origin_zero_line(v, explicit_track_count)
_ => None
}
let end_line = match placement.end {
PlaceLine(v) => origin_zero_line(v, explicit_track_count)
_ => None
}
let explicit_span = match placement.start {
PlaceSpan(s) => Some(placement_span_value(s))
_ =>
match placement.end {
PlaceSpan(s) => Some(placement_span_value(s))
_ => None
}
}
match explicit_span {
Some(span) =>
match (start_line, end_line) {
(Some(sl), _) => (Some(sl), span)
(None, Some(el)) => (Some(el - span), span)
_ => (None, span)
}
None =>
match (start_line, end_line) {
(Some(sl), Some(el)) => {
let span = if el > sl { el - sl } else { 1 }
(Some(sl), span)
}
(Some(sl), None) => (Some(sl), 1)
(None, Some(el)) => (Some(el - 1), 1)
_ => (None, 1)
}
}
}
fn axis_auto_track_list(auto_tracks : Array[Dimension]) -> Array[Dimension] {
if auto_tracks.length() == 0 {
[DimAuto]
} else {
auto_tracks
}
}
fn negative_auto_track_at(
auto_tracks : Array[Dimension],
offset_from_explicit : Int,
) -> Dimension {
let len = auto_tracks.length()
if len == 0 {
DimAuto
} else {
// `offset_from_explicit` is 1-based: 1 means the track adjacent to the explicit grid.
let m = offset_from_explicit % len
let idx = (len - m) % len
auto_tracks[idx]
}
}
let padding = resolve_rect_width_basis(node.style.padding, available_space)
let border = resolve_rect_width_basis(node.style.border, available_space)
let scrollbar_w = node.style.scrollbar_width
let scrollbar_x = match node.style.overflow.x {
OverflowScroll => scrollbar_w
_ => 0.0
}
let scrollbar_y = match node.style.overflow.y {
OverflowScroll => scrollbar_w
_ => 0.0
}
let horiz_non_scroll_inset = padding.left +
padding.right +
border.left +
border.right
let vert_non_scroll_inset = padding.top +
padding.bottom +
border.top +
border.bottom
// Overflow::Scroll reserves scrollbar space inside the border box.
// Horizontal scrollbar consumes cross (vertical) space; vertical scrollbar consumes main (horizontal) space.
let horiz_inset = horiz_non_scroll_inset + scrollbar_y
let vert_inset = vert_non_scroll_inset + scrollbar_x
let style_width = resolve_optional_dimension(
node.style.size.width,
available_space.width,
)
let style_height = resolve_optional_dimension(
node.style.size.height,
available_space.height,
)
let specified_width = match known_dimensions.width {
Some(w) => Some(w)
None => style_width
}
let specified_height = match known_dimensions.height {
Some(h) => Some(h)
None => style_height
}
// If the container size is definite, resolve track sizing against the content box.
// Otherwise fall back to a simplified "fixed only" resolution.
let mut content_width = 0.0
let mut content_height = 0.0
let mut border_box_width = 0.0
let mut border_box_height = 0.0
match specified_width {
Some(w) => {
border_box_width = max_double(
clamp_dimension(
w,
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
),
horiz_non_scroll_inset,
)
content_width = max_double(border_box_width - horiz_inset, 0.0)
}
None => ()
}
match specified_height {
Some(h) => {
border_box_height = max_double(
clamp_dimension(
h,
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
),
vert_non_scroll_inset,
)
content_height = max_double(border_box_height - vert_inset, 0.0)
}
None => ()
}
// Grid gaps: treat `gap.width` as column gap, and `gap.height` as row gap.
// For `percent` gaps, follow CSS semantics and resolve against the container's inline content size (width).
// If that basis is not yet known (intrinsic sizing), percent gaps are deferred and handled after sizing.
let gap_width_percent = match node.style.gap.width {
DimPercent(p) => Some(p)
_ => None
}
let gap_height_percent = match node.style.gap.height {
DimPercent(p) => Some(p)
_ => None
}
let gap_inline_basis = match specified_width {
Some(_) => content_width
None =>
match available_space.width {
AvailDefinite(v) => max_double(v - horiz_inset, 0.0)
_ => 0.0
}
}
let mut col_gap = match gap_width_percent {
Some(p) => gap_inline_basis * p
None => resolve_dimension(node.style.gap.width, available_space.width)
}
let mut row_gap = match gap_height_percent {
Some(p) => gap_inline_basis * p
None => resolve_dimension(node.style.gap.height, available_space.height)
}
// Expand `repeat(...)` in grid templates.
let col_expanded = expand_grid_template_axis(
node.style.grid_template_columns,
content_width,
col_gap,
)
let row_expanded = expand_grid_template_axis(
node.style.grid_template_rows,
content_height,
row_gap,
)
let mut explicit_col_template = col_expanded.0
let mut explicit_row_template = row_expanded.0
let mut non_auto_fit_col_count = col_expanded.1
let has_auto_fit_cols = col_expanded.2
let mut non_auto_fit_row_count = row_expanded.1
let has_auto_fit_rows = row_expanded.2
// If an axis has no explicit tracks, treat it as having a single implicit `auto` track.
// This matches upstream behavior for column-only/row-only templates.
if explicit_col_template.length() == 0 {
explicit_col_template = [DimAuto]
non_auto_fit_col_count = 1
}
if explicit_row_template.length() == 0 {
explicit_row_template = [DimAuto]
non_auto_fit_row_count = 1
}
let explicit_col_count = explicit_col_template.length()
let explicit_row_count = explicit_row_template.length()
// Compute implicit track counts required by definite placements (negative/positive implicit tracks).
let mut min_col_line = 0
let mut max_col_line = 0
let mut min_row_line = 0
let mut max_row_line = 0
for child_id in node.children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
let col_line = merged_placement_line(
child.style.grid_column,
child.style.grid_column_start,
)
let row_line = merged_placement_line(
child.style.grid_row,
child.style.grid_row_start,
)
let col_res = axis_resolve_start_line_and_span(col_line, explicit_col_count)
match col_res.0 {
Some(sl) => {
if sl < min_col_line {
min_col_line = sl
}
let end = sl + col_res.1
if end > max_col_line {
max_col_line = end
}
}
None => ()
}
let row_res = axis_resolve_start_line_and_span(row_line, explicit_row_count)
match row_res.0 {
Some(sl) => {
if sl < min_row_line {
min_row_line = sl
}
let end = sl + row_res.1
if end > max_row_line {
max_row_line = end
}
}
None => ()
}
}
let negative_cols = if min_col_line < 0 { -min_col_line } else { 0 }
let positive_cols = if max_col_line > explicit_col_count {
max_col_line - explicit_col_count
} else {
0
}
let negative_rows = if min_row_line < 0 { -min_row_line } else { 0 }
let positive_rows = if max_row_line > explicit_row_count {
max_row_line - explicit_row_count
} else {
0
}
// Build an initial implicit grid from templates + auto tracks.
let auto_cols = axis_auto_track_list(node.style.grid_auto_columns)
let auto_rows = axis_auto_track_list(node.style.grid_auto_rows)
let mut col_tracks : Array[Dimension] = []
for i in 0.. Array[Array[Bool]] {
let m : Array[Array[Bool]] = []
for _r in 0.. Unit {
while col_tracks.length() <= target_col {
let pos_existing = col_tracks.length() - negative_cols - explicit_cols
let dim = auto_cols[pos_existing % auto_cols.length()]
col_tracks.push(dim)
for r in 0.. Unit {
while row_tracks.length() <= target_row {
let pos_existing = row_tracks.length() - negative_rows - explicit_rows
let dim = auto_rows[pos_existing % auto_rows.length()]
row_tracks.push(dim)
occ.push(Array::make(occ[0].length(), false))
}
}
fn region_is_free(
occ : Array[Array[Bool]],
row : Int,
col : Int,
row_span : Int,
col_span : Int,
) -> Bool {
for r in row..<(row + row_span) {
for c in col..<(col + col_span) {
if occ[r][c] {
return false
}
}
}
true
}
fn mark_placed(
placed : Array[Bool],
placed_row : Array[Int],
placed_col : Array[Int],
placed_row_span : Array[Int],
placed_col_span : Array[Int],
occ : Array[Array[Bool]],
row_tracks : Array[Dimension],
col_tracks : Array[Dimension],
auto_rows : Array[Dimension],
auto_cols : Array[Dimension],
negative_rows : Int,
negative_cols : Int,
explicit_rows : Int,
explicit_cols : Int,
idx : Int,
row : Int,
col : Int,
row_span : Int,
col_span : Int,
) -> Unit {
let row_end = row + row_span - 1
let col_end = col + col_span - 1
ensure_rows(
occ, row_tracks, auto_rows, negative_rows, explicit_rows, row_end,
)
ensure_cols(
occ, col_tracks, auto_cols, negative_cols, explicit_cols, col_end,
)
for r in row..<(row + row_span) {
for c in col..<(col + col_span) {
occ[r][c] = true
}
}
placed[idx] = true
placed_row[idx] = row
placed_col[idx] = col
placed_row_span[idx] = row_span
placed_col_span[idx] = col_span
}
fn axis_start_index_and_span(
placement : Line[GridPlacement],
legacy_start : Int?,
explicit_track_count : Int,
negative_implicit : Int,
) -> (Int?, Int) {
let merged = merged_placement_line(placement, legacy_start)
let resolved = axis_resolve_start_line_and_span(
merged, explicit_track_count,
)
let start = match resolved.0 {
Some(sl) => Some(sl + negative_implicit)
None => None
}
(start, resolved.1)
}
fn child_start_indexes_and_spans(
child_style : Style,
explicit_cols : Int,
explicit_rows : Int,
negative_cols : Int,
negative_rows : Int,
) -> (Int?, Int, Int?, Int) {
let col = axis_start_index_and_span(
child_style.grid_column,
child_style.grid_column_start,
explicit_cols,
negative_cols,
)
let row = axis_start_index_and_span(
child_style.grid_row,
child_style.grid_row_start,
explicit_rows,
negative_rows,
)
(row.0, row.1, col.0, col.1)
}
// Placement algorithm: very small subset of CSS Grid (enough for upstream implicit/auto tracks tests).
let primary_is_col = match node.style.grid_auto_flow {
Row | RowDense => true
Column | ColumnDense => false
}
let dense = match node.style.grid_auto_flow {
RowDense | ColumnDense => true
_ => false
}
// Pass 1: both axes definite.
for i in 0.. c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => ()
_ => {
match child.style.position {
PosAbsolute => continue
PosRelative => ()
}
let def = child_start_indexes_and_spans(
child.style,
explicit_col_count,
explicit_row_count,
negative_cols,
negative_rows,
)
match (def.0, def.2) {
(Some(r), Some(c)) =>
mark_placed(
placed,
placed_row,
placed_col,
placed_row_span,
placed_col_span,
occ,
row_tracks,
col_tracks,
auto_rows,
auto_cols,
negative_rows,
negative_cols,
explicit_row_count,
explicit_col_count,
i,
r,
c,
def.1,
def.3,
)
_ => ()
}
}
}
}
// Pass 2: definite in secondary axis only.
for i in 0.. c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => ()
_ => {
match child.style.position {
PosAbsolute => continue
PosRelative => ()
}
let def = child_start_indexes_and_spans(
child.style,
explicit_col_count,
explicit_row_count,
negative_cols,
negative_rows,
)
if primary_is_col {
// Flow rows: primary axis is columns, secondary axis is rows.
match (def.0, def.2) {
(Some(r), None) => {
let row_span = def.1
let col_span = def.3
let mut c = 0
while true {
ensure_rows(
occ,
row_tracks,
auto_rows,
negative_rows,
explicit_row_count,
r + row_span - 1,
)
ensure_cols(
occ,
col_tracks,
auto_cols,
negative_cols,
explicit_col_count,
c + col_span - 1,
)
if region_is_free(occ, r, c, row_span, col_span) {
mark_placed(
placed, placed_row, placed_col, placed_row_span, placed_col_span,
occ, row_tracks, col_tracks, auto_rows, auto_cols, negative_rows,
negative_cols, explicit_row_count, explicit_col_count, i, r,
c, row_span, col_span,
)
break
}
c = c + 1
}
}
_ => ()
}
} else {
// Flow columns: primary axis is rows, secondary axis is columns.
match (def.0, def.2) {
(None, Some(c)) => {
let row_span = def.1
let col_span = def.3
let mut r = 0
while true {
ensure_rows(
occ,
row_tracks,
auto_rows,
negative_rows,
explicit_row_count,
r + row_span - 1,
)
ensure_cols(
occ,
col_tracks,
auto_cols,
negative_cols,
explicit_col_count,
c + col_span - 1,
)
if region_is_free(occ, r, c, row_span, col_span) {
mark_placed(
placed, placed_row, placed_col, placed_row_span, placed_col_span,
occ, row_tracks, col_tracks, auto_rows, auto_cols, negative_rows,
negative_cols, explicit_row_count, explicit_col_count, i, r,
c, row_span, col_span,
)
break
}
r = r + 1
}
}
_ => ()
}
}
}
}
}
// Pass 3: remaining items (auto-placement in the flow direction).
// Dense: restart the scan for each item, filling earlier holes.
// Sparse: resume placement from the last cursor position.
let mut cursor_sec = 0
let mut cursor_prim = 0
for i in 0.. c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => ()
_ => {
match child.style.position {
PosAbsolute => continue
PosRelative => ()
}
let def = child_start_indexes_and_spans(
child.style,
explicit_col_count,
explicit_row_count,
negative_cols,
negative_rows,
)
let row_span = def.1
let col_span = def.3
let mut sec = if dense { 0 } else { cursor_sec }
while true {
if primary_is_col {
// Auto-flow rows: scan row-by-row
let fixed_col = def.2
match fixed_col {
Some(c0) =>
while true {
if not(dense) && sec == cursor_sec && c0 < cursor_prim {
sec = sec + 1
}
ensure_rows(
occ,
row_tracks,
auto_rows,
negative_rows,
explicit_row_count,
sec + row_span - 1,
)
ensure_cols(
occ,
col_tracks,
auto_cols,
negative_cols,
explicit_col_count,
c0 + col_span - 1,
)
if region_is_free(occ, sec, c0, row_span, col_span) {
mark_placed(
placed, placed_row, placed_col, placed_row_span, placed_col_span,
occ, row_tracks, col_tracks, auto_rows, auto_cols, negative_rows,
negative_cols, explicit_row_count, explicit_col_count, i, sec,
c0, row_span, col_span,
)
if not(dense) {
cursor_sec = sec
cursor_prim = c0 + col_span
if cursor_prim >= col_tracks.length() {
cursor_sec = cursor_sec + 1
cursor_prim = 0
}
}
sec = -1
break
}
sec = sec + 1
}
None => {
ensure_cols(
occ,
col_tracks,
auto_cols,
negative_cols,
explicit_col_count,
col_span - 1,
)
while true {
ensure_rows(
occ,
row_tracks,
auto_rows,
negative_rows,
explicit_row_count,
sec + row_span - 1,
)
let mut prim = if dense || sec != cursor_sec {
0
} else {
cursor_prim
}
let max_prim = col_tracks.length() - col_span + 1
let mut found = false
while prim < max_prim {
if region_is_free(occ, sec, prim, row_span, col_span) {
mark_placed(
placed, placed_row, placed_col, placed_row_span, placed_col_span,
occ, row_tracks, col_tracks, auto_rows, auto_cols, negative_rows,
negative_cols, explicit_row_count, explicit_col_count, i,
sec, prim, row_span, col_span,
)
if not(dense) {
cursor_sec = sec
cursor_prim = prim + col_span
if cursor_prim >= col_tracks.length() {
cursor_sec = cursor_sec + 1
cursor_prim = 0
}
}
found = true
break
}
prim = prim + 1
}
if found {
sec = -1
break
}
sec = sec + 1
}
}
}
if sec == -1 {
break
}
} else {
// Auto-flow columns: scan col-by-col
let fixed_row = def.0
match fixed_row {
Some(r0) =>
while true {
if not(dense) && sec == cursor_sec && r0 < cursor_prim {
sec = sec + 1
}
ensure_cols(
occ,
col_tracks,
auto_cols,
negative_cols,
explicit_col_count,
sec + col_span - 1,
)
ensure_rows(
occ,
row_tracks,
auto_rows,
negative_rows,
explicit_row_count,
r0 + row_span - 1,
)
if region_is_free(occ, r0, sec, row_span, col_span) {
mark_placed(
placed, placed_row, placed_col, placed_row_span, placed_col_span,
occ, row_tracks, col_tracks, auto_rows, auto_cols, negative_rows,
negative_cols, explicit_row_count, explicit_col_count, i, r0,
sec, row_span, col_span,
)
if not(dense) {
cursor_sec = sec
cursor_prim = r0 + row_span
if cursor_prim >= row_tracks.length() {
cursor_sec = cursor_sec + 1
cursor_prim = 0
}
}
sec = -1
break
}
sec = sec + 1
}
None => {
ensure_rows(
occ,
row_tracks,
auto_rows,
negative_rows,
explicit_row_count,
row_span - 1,
)
while true {
ensure_cols(
occ,
col_tracks,
auto_cols,
negative_cols,
explicit_col_count,
sec + col_span - 1,
)
let mut prim = if dense || sec != cursor_sec {
0
} else {
cursor_prim
}
let max_prim = row_tracks.length() - row_span + 1
let mut found = false
while prim < max_prim {
if region_is_free(occ, prim, sec, row_span, col_span) {
mark_placed(
placed, placed_row, placed_col, placed_row_span, placed_col_span,
occ, row_tracks, col_tracks, auto_rows, auto_cols, negative_rows,
negative_cols, explicit_row_count, explicit_col_count, i,
prim, sec, row_span, col_span,
)
if not(dense) {
cursor_sec = sec
cursor_prim = prim + row_span
if cursor_prim >= row_tracks.length() {
cursor_sec = cursor_sec + 1
cursor_prim = 0
}
}
found = true
break
}
prim = prim + 1
}
if found {
sec = -1
break
}
sec = sec + 1
}
}
}
if sec == -1 {
break
}
}
}
}
}
}
let mut col_count = col_tracks.length()
let mut row_count = row_tracks.length()
// Compute min-content and max-content contributions for each track.
let col_min_contrib : Array[Double] = Array::make(col_count, 0.0)
let col_max_contrib : Array[Double] = Array::make(col_count, 0.0)
let row_min_contrib : Array[Double] = Array::make(row_count, 0.0)
let row_max_contrib : Array[Double] = Array::make(row_count, 0.0)
fn fr_track_weight(track : Dimension) -> Double {
match track {
DimFr(w) => if w > 0.0 { w } else { 0.0 }
_ => 0.0
}
}
fn apply_col_contribution(
col_tracks : Array[Dimension],
start : Int,
span : Int,
needed : Double,
contrib : Array[Double],
) -> Unit {
if span <= 1 {
if needed > contrib[start] {
contrib[start] = needed
}
return
}
let mut all_fr = true
let weights : Array[Double] = Array::make(span, 0.0)
let mut positive_weight_sum = 0.0
for offset in 0.. 0.0 {
positive_weight_sum = positive_weight_sum + weight
}
}
if all_fr {
if positive_weight_sum > 0.0 {
for offset in 0.. contrib[idx] {
contrib[idx] = share
}
}
} else {
let share = needed / span.to_double()
for offset in 0.. contrib[idx] {
contrib[idx] = share
}
}
}
} else if positive_weight_sum > 0.0 {
let mut current_sum = 0.0
for offset in 0.. 0.0 {
for offset in 0.. 0.0 {
contrib[idx] = contrib[idx] + deficit * weight / positive_weight_sum
}
}
}
} else {
let share = needed / span.to_double()
for offset in 0.. contrib[idx] {
contrib[idx] = share
}
}
}
}
fn track_definite_len(track : Dimension) -> Double? {
match track {
DimLength(v) => Some(v)
DimMinMax(min_d, max_d) =>
match (min_d, max_d) {
(DimLength(a), DimLength(b)) if a == b => Some(a)
_ => None
}
_ => None
}
}
fn spanned_definite_len(
tracks : Array[Dimension],
start : Int,
span : Int,
gap : Double,
) -> Double? {
if span <= 0 {
return Some(0.0)
}
let mut total = 0.0
for i in start..<(start + span) {
match track_definite_len(tracks[i]) {
Some(v) => total = total + v
None => return None
}
}
if span > 1 {
total = total + gap * (span - 1).to_double()
}
Some(total)
}
fn col_min_sizing_kind(track : Dimension) -> Int {
// 0 = fixed/other, 1 = min-content, 2 = max-content, 3 = auto
match track {
DimMinContent => 1
DimMaxContent => 2
DimAuto => 3
DimFitContent(_) => 3
DimMinMax(min_d, _) =>
match min_d {
DimMinContent => 1
DimMaxContent => 2
DimAuto => 3
DimFitContent(_) => 3
_ => 0
}
_ => 0
}
}
fn compute_intrinsic_bases_max_content(
tracks : Array[Dimension],
col_start : Array[Int],
col_span : Array[Int],
min_needed : Array[Double],
max_needed : Array[Double],
placed : Array[Bool],
percent_basis : Double?,
) -> Array[Double] {
let count = tracks.length()
let bases : Array[Double] = Array::make(count, 0.0)
for i in 0.. bases[i] = v
DimPercent(p) =>
match percent_basis {
Some(b) => bases[i] = b * p
None => bases[i] = 0.0
}
DimMinMax(min_d, max_d) =>
// Handle simple fixed-length/percent minmax cases
match (min_d, max_d, percent_basis) {
(DimLength(a), DimLength(b), _) if a == b => bases[i] = a
(DimPercent(p), DimPercent(q), Some(b)) if p == q =>
bases[i] = b * p
_ => ()
}
_ => ()
}
}
fn distribute(
bases : Array[Double],
tracks : Array[Dimension],
start : Int,
span : Int,
space : Double,
should_affect : (Int, Dimension) -> Bool,
) -> Unit {
if space <= 0.0 || span <= 0 {
return
}
let mut used = 0.0
for j in start..<(start + span) {
used = used + bases[j]
}
let extra = space - used
if extra <= 0.0 {
return
}
let mut count = 0
for j in start..<(start + span) {
if should_affect(j, tracks[j]) {
count = count + 1
}
}
if count <= 0 {
return
}
let share = extra / count.to_double()
for j in start..<(start + span) {
if should_affect(j, tracks[j]) {
bases[j] = bases[j] + share
}
}
}
let mut max_span = 1
for i in 0.. max_span {
max_span = col_span[i]
}
}
// Distribute min-content contributions, then max-content contributions (max-content constraint quirk).
for span in 1..<(max_span + 1) {
for i in 0.. c0
None => raise InvalidNodeId(child_id)
}
let c = placed_col[i]
let col_span = placed_col_span[i]
let r = placed_row[i]
let row_span = placed_row_span[i]
let gap_total = col_gap * (col_span - 1).to_double()
let contrib_available_height = match specified_height {
Some(_) => AvailDefinite(content_height)
None =>
match spanned_definite_len(row_tracks, r, row_span, row_gap) {
Some(v) => AvailDefinite(v)
None => AvailMaxContent
}
}
// Max-content contribution in the column axis
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=None, height=None),
Size::new(width=AvailMaxContent, height=contrib_available_height),
Point::zero(),
measure_function,
false,
)
let max_sz = tree.nodes[child_id].layout.size
let resolved_margin = resolve_rect_width_basis(
child.style.margin,
Size::new(width=AvailMaxContent, height=AvailMaxContent),
)
let max_needed = max_double(
max_sz.width + resolved_margin.left + resolved_margin.right - gap_total,
0.0,
)
col_item_max_needed[i] = max_needed
apply_col_contribution(col_tracks, c, col_span, max_needed, col_max_contrib)
// Min-content contribution in the column axis
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=None, height=None),
Size::new(width=AvailMinContent, height=contrib_available_height),
Point::zero(),
measure_function,
false,
)
let min_col_raw = tree.nodes[child_id].layout.size.width
let min_col = match (child.style.overflow.x, child.style.overflow.y) {
(OverflowVisible, OverflowVisible) => min_col_raw
_ => 0.0
}
let min_needed = max_double(
min_col + resolved_margin.left + resolved_margin.right - gap_total,
0.0,
)
col_item_min_needed[i] = min_needed
apply_col_contribution(col_tracks, c, col_span, min_needed, col_min_contrib)
}
fn track_basis_fit_content(
min_c : Double,
max_c : Double,
limit : Dimension,
available : Double,
) -> Double {
match limit {
DimPercent(_) => max_c
_ => {
let lim = resolve_track_dimension(limit, available)
let preferred = if max_c < lim { max_c } else { lim }
if preferred < min_c {
min_c
} else {
preferred
}
}
}
}
fn axis_min_content_basis(
tracks : Array[Dimension],
min_contrib : Array[Double],
max_contrib : Array[Double],
gap : Double,
) -> Double {
let mut total = 0.0
let mut fr_sum = 0.0
let mut fr_fraction = 0.0
for i in 0..
if w > 0.0 {
fr_sum = fr_sum + w
let ratio = min_c / w
if ratio > fr_fraction {
fr_fraction = ratio
}
} else {
total = total + min_c
}
_ =>
total = total +
(match tracks[i] {
DimLength(v) => v
DimAuto => min_c
DimPercent(_) => min_c
DimMinContent => min_c
DimMaxContent => max_c
DimFitContent(limit) =>
track_basis_fit_content(min_c, max_c, limit, 0.0)
DimMinMax(min_d, _max_d) =>
// Conservative: use the min sizing function where possible.
match min_d {
DimAuto | DimMinContent => min_c
DimMaxContent => max_c
_ => resolve_track_dimension(min_d, 0.0)
}
_ => 0.0
})
}
}
total = total + fr_fraction * fr_sum
if tracks.length() > 1 {
total = total + gap * (tracks.length() - 1).to_double()
}
total
}
fn axis_max_content_basis(
tracks : Array[Dimension],
min_contrib : Array[Double],
max_contrib : Array[Double],
gap : Double,
) -> Double {
let mut total = 0.0
let mut fr_sum = 0.0
let mut fr_fraction = 0.0
for i in 0..
if w > 0.0 {
fr_sum = fr_sum + w
let ratio = max_c / w
if ratio > fr_fraction {
fr_fraction = ratio
}
} else {
total = total + max_c
}
_ =>
total = total +
(match tracks[i] {
DimLength(v) => v
DimAuto => max_c
DimPercent(_) => max_c
DimMinContent => min_c
DimMaxContent => max_c
DimFitContent(limit) =>
track_basis_fit_content(min_c, max_c, limit, 0.0)
DimMinMax(_min_d, max_d) => {
let min_basis = match _min_d {
DimAuto => 0.0
DimMinContent => min_c
DimMaxContent => max_c
DimFitContent(limit) =>
track_basis_fit_content(min_c, max_c, limit, 0.0)
_ => resolve_track_dimension(_min_d, 0.0)
}
let max_basis = match max_d {
DimAuto | DimMaxContent => max_c
DimMinContent => min_c
DimPercent(_) => max_c
DimFr(_) => max_c
DimFitContent(limit) =>
track_basis_fit_content(min_c, max_c, limit, 0.0)
_ => resolve_track_dimension(max_d, 0.0)
}
if max_basis < min_basis {
min_basis
} else {
max_basis
}
}
_ => 0.0
})
}
}
total = total + fr_fraction * fr_sum
if tracks.length() > 1 {
total = total + gap * (tracks.length() - 1).to_double()
}
total
}
fn available_inset_or_basis(
available : AvailableSpace,
inset : Double,
min_basis : Double,
max_basis : Double,
) -> Double {
match available {
AvailDefinite(v) => max_double(v - inset, 0.0)
AvailMinContent => min_basis
AvailMaxContent => max_basis
}
}
let mut col_min_basis = axis_min_content_basis(
col_tracks, col_min_contrib, col_max_contrib, col_gap,
)
let mut col_max_basis = axis_max_content_basis(
col_tracks, col_min_contrib, col_max_contrib, col_gap,
)
let mut col_available = match specified_width {
Some(_) => content_width
None =>
available_inset_or_basis(
available_space.width,
horiz_inset,
col_min_basis,
col_max_basis,
)
}
let mut col_available_base = col_available
let mut col_sizes : Array[Double] = compute_grid_track_sizes_with_contributions(
col_tracks,
col_available,
col_gap,
col_min_contrib,
col_max_contrib,
specified_width is Some(_),
)
let mut has_percent_col = false
let mut has_flex_col = false
let mut has_intrinsic_col = false
for t in col_tracks {
if col_min_sizing_kind(t) != 0 {
has_intrinsic_col = true
}
match t {
DimPercent(_) => has_percent_col = true
DimFr(_) => has_flex_col = true
DimMinMax(_, max_d) =>
match max_d {
DimFr(_) => has_flex_col = true
_ => ()
}
_ => ()
}
}
if specified_width is None &&
available_space.width is AvailMaxContent &&
has_percent_col &&
has_intrinsic_col &&
!has_flex_col {
// Match taffy 0.5 behavior: when sizing under a max-content constraint, percentage tracks
// initially resolve to 0, then are re-resolved against the computed content width and the
// intrinsic sizing pass is rerun once.
let bases1 = compute_intrinsic_bases_max_content(
col_tracks,
placed_col,
placed_col_span,
col_item_min_needed,
col_item_max_needed,
placed,
None,
)
let mut basis1 = 0.0
for v in bases1 {
basis1 = basis1 + v
}
let bases2 = compute_intrinsic_bases_max_content(
col_tracks,
placed_col,
placed_col_span,
col_item_min_needed,
col_item_max_needed,
placed,
Some(basis1),
)
col_sizes = bases2
col_available = basis1
col_available_base = col_available
let basis_with_gaps = basis1 + col_gap * (col_count - 1).to_double()
col_min_basis = basis_with_gaps
col_max_basis = basis_with_gaps
}
let default_justify_items_for_contrib = match node.style.justify_items {
Some(v) => v
None => ItemsStretch
}
// Compute row contributions with the resolved column widths as a constraint.
for i in 0.. c0
None => raise InvalidNodeId(child_id)
}
let c = placed_col[i]
let col_span = placed_col_span[i]
let r = placed_row[i]
let row_span = placed_row_span[i]
let mut col_w = 0.0
for j in c..<(c + col_span) {
col_w = col_w + col_sizes[j]
}
if col_span > 1 {
col_w = col_w + col_gap * (col_span - 1).to_double()
}
let row_gap_total = row_gap * (row_span - 1).to_double()
let resolved_margin = resolve_rect_width_basis(
child.style.margin,
Size::new(width=AvailDefinite(col_w), height=AvailMaxContent),
)
let margin_left_auto = child.style.margin.left is DimAuto
let margin_right_auto = child.style.margin.right is DimAuto
let has_auto_margin_x = margin_left_auto || margin_right_auto
let justify = match child.style.justify_self {
Some(v) => v
None => default_justify_items_for_contrib
}
let available_w_for_item = max_double(
col_w - resolved_margin.left - resolved_margin.right,
0.0,
)
let mut known_w_for_contrib = resolve_optional_dimension(
child.style.size.width,
AvailDefinite(col_w),
)
if known_w_for_contrib is None &&
child.style.size.width is DimAuto &&
justify is ItemsStretch &&
!has_auto_margin_x {
known_w_for_contrib = Some(available_w_for_item)
}
// Max-content contribution in the row axis under the column width constraint.
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=known_w_for_contrib, height=None),
Size::new(width=AvailDefinite(col_w), height=AvailMaxContent),
Point::zero(),
measure_function,
false,
)
let max_h = tree.nodes[child_id].layout.size.height
let max_needed = max_double(
max_h + resolved_margin.top + resolved_margin.bottom - row_gap_total,
0.0,
)
let max_share = max_needed / row_span.to_double()
for j in r..<(r + row_span) {
if max_share > row_max_contrib[j] {
row_max_contrib[j] = max_share
}
}
// Min-content contribution in the row axis under the column width constraint.
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=known_w_for_contrib, height=None),
Size::new(width=AvailDefinite(col_w), height=AvailMinContent),
Point::zero(),
measure_function,
false,
)
let min_h_raw = tree.nodes[child_id].layout.size.height
let min_h = match (child.style.overflow.x, child.style.overflow.y) {
(OverflowVisible, OverflowVisible) => min_h_raw
_ => 0.0
}
let min_needed = max_double(
min_h + resolved_margin.top + resolved_margin.bottom - row_gap_total,
0.0,
)
let min_share = min_needed / row_span.to_double()
for j in r..<(r + row_span) {
if min_share > row_min_contrib[j] {
row_min_contrib[j] = min_share
}
}
}
let row_min_basis = axis_min_content_basis(
row_tracks, row_min_contrib, row_max_contrib, row_gap,
)
let row_max_basis = axis_max_content_basis(
row_tracks, row_min_contrib, row_max_contrib, row_gap,
)
let row_available = match specified_height {
Some(_) => content_height
None =>
available_inset_or_basis(
available_space.height,
vert_inset,
row_min_basis,
row_max_basis,
)
}
let row_available_base = row_available
let mut row_sizes : Array[Double] = compute_grid_track_sizes_with_contributions(
row_tracks,
row_available,
row_gap,
row_min_contrib,
row_max_contrib,
specified_height is Some(_),
)
let mut used_cols = 0.0
for w in col_sizes {
used_cols = used_cols + w
}
if col_count > 1 {
used_cols = used_cols + col_gap * (col_count - 1).to_double()
}
let mut used_rows = 0.0
for h in row_sizes {
used_rows = used_rows + h
}
if row_count > 1 {
used_rows = used_rows + row_gap * (row_count - 1).to_double()
}
// Container sizing (border-box). If width/height are not specified, size to tracks.
if specified_width is None {
// Don't force-fill definite available space for auto-sized containers.
// Stretch behavior should be expressed via `known_dimensions` from the parent.
let intrinsic_cols = match available_space.width {
AvailMaxContent => col_max_basis
AvailMinContent => col_min_basis
AvailDefinite(_) => used_cols
}
border_box_width = max_double(
clamp_dimension(
max_double(intrinsic_cols + horiz_inset, horiz_inset),
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
),
horiz_non_scroll_inset,
)
content_width = max_double(border_box_width - horiz_inset, 0.0)
}
if specified_height is None {
let intrinsic_rows = match available_space.height {
AvailMaxContent => row_max_basis
AvailMinContent => row_min_basis
AvailDefinite(_) => used_rows
}
border_box_height = max_double(
clamp_dimension(
max_double(intrinsic_rows + vert_inset, vert_inset),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
),
vert_non_scroll_inset,
)
content_height = max_double(border_box_height - vert_inset, 0.0)
}
// Deferred percentage gaps: if the container inline size was unknown during intrinsic sizing,
// re-resolve percentage gaps against the computed inline content size and recompute track sizes.
let mut rerun_track_sizing = false
match gap_width_percent {
Some(p) =>
if col_gap == 0.0 && content_width > 0.0 {
col_gap = content_width * p
rerun_track_sizing = true
}
None => ()
}
match gap_height_percent {
Some(p) =>
if row_gap == 0.0 && content_width > 0.0 {
row_gap = content_width * p
rerun_track_sizing = true
}
None => ()
}
if rerun_track_sizing {
let rerun_col_contrib_available_height = match specified_height {
Some(_) => AvailDefinite(content_height)
None => AvailMaxContent
}
// Recompute contributions with the updated gap values.
for i in 0.. c0
None => raise InvalidNodeId(child_id)
}
let c = placed_col[i]
let col_span = placed_col_span[i]
let gap_total = col_gap * (col_span - 1).to_double()
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=None, height=None),
Size::new(
width=AvailMaxContent,
height=rerun_col_contrib_available_height,
),
Point::zero(),
measure_function,
false,
)
let max_sz = tree.nodes[child_id].layout.size
let resolved_margin = resolve_rect_width_basis(
child.style.margin,
Size::new(width=AvailMaxContent, height=AvailMaxContent),
)
let max_needed = max_double(
max_sz.width + resolved_margin.left + resolved_margin.right - gap_total,
0.0,
)
apply_col_contribution(
col_tracks, c, col_span, max_needed, col_max_contrib,
)
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=None, height=None),
Size::new(
width=AvailMinContent,
height=rerun_col_contrib_available_height,
),
Point::zero(),
measure_function,
false,
)
let min_col_raw = tree.nodes[child_id].layout.size.width
let min_col = match (child.style.overflow.x, child.style.overflow.y) {
(OverflowVisible, OverflowVisible) => min_col_raw
_ => 0.0
}
let min_needed = max_double(
min_col + resolved_margin.left + resolved_margin.right - gap_total,
0.0,
)
apply_col_contribution(
col_tracks, c, col_span, min_needed, col_min_contrib,
)
}
col_sizes = compute_grid_track_sizes_with_contributions(
col_tracks,
col_available_base,
col_gap,
col_min_contrib,
col_max_contrib,
specified_width is Some(_),
)
for i in 0.. c0
None => raise InvalidNodeId(child_id)
}
let c = placed_col[i]
let col_span = placed_col_span[i]
let r = placed_row[i]
let row_span = placed_row_span[i]
let mut col_w = 0.0
for j in c..<(c + col_span) {
col_w = col_w + col_sizes[j]
}
if col_span > 1 {
col_w = col_w + col_gap * (col_span - 1).to_double()
}
let row_gap_total = row_gap * (row_span - 1).to_double()
let resolved_margin = resolve_rect_width_basis(
child.style.margin,
Size::new(width=AvailDefinite(col_w), height=AvailMaxContent),
)
let margin_left_auto = child.style.margin.left is DimAuto
let margin_right_auto = child.style.margin.right is DimAuto
let has_auto_margin_x = margin_left_auto || margin_right_auto
let justify = match child.style.justify_self {
Some(v) => v
None => default_justify_items_for_contrib
}
let available_w_for_item = max_double(
col_w - resolved_margin.left - resolved_margin.right,
0.0,
)
let mut known_w_for_contrib = resolve_optional_dimension(
child.style.size.width,
AvailDefinite(col_w),
)
if known_w_for_contrib is None &&
child.style.size.width is DimAuto &&
justify is ItemsStretch &&
!has_auto_margin_x {
known_w_for_contrib = Some(available_w_for_item)
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=known_w_for_contrib, height=None),
Size::new(width=AvailDefinite(col_w), height=AvailMaxContent),
Point::zero(),
measure_function,
false,
)
let max_h = tree.nodes[child_id].layout.size.height
let max_needed = max_double(
max_h + resolved_margin.top + resolved_margin.bottom - row_gap_total,
0.0,
)
let max_share = max_needed / row_span.to_double()
for j in r..<(r + row_span) {
if max_share > row_max_contrib[j] {
row_max_contrib[j] = max_share
}
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=known_w_for_contrib, height=None),
Size::new(width=AvailDefinite(col_w), height=AvailMinContent),
Point::zero(),
measure_function,
false,
)
let min_h_raw = tree.nodes[child_id].layout.size.height
let min_h = match (child.style.overflow.x, child.style.overflow.y) {
(OverflowVisible, OverflowVisible) => min_h_raw
_ => 0.0
}
let min_needed = max_double(
min_h + resolved_margin.top + resolved_margin.bottom - row_gap_total,
0.0,
)
let min_share = min_needed / row_span.to_double()
for j in r..<(r + row_span) {
if min_share > row_min_contrib[j] {
row_min_contrib[j] = min_share
}
}
}
row_sizes = compute_grid_track_sizes_with_contributions(
row_tracks,
row_available_base,
row_gap,
row_min_contrib,
row_max_contrib,
specified_height is Some(_),
)
used_cols = 0.0
for w in col_sizes {
used_cols = used_cols + w
}
if col_count > 1 {
used_cols = used_cols + col_gap * (col_count - 1).to_double()
}
used_rows = 0.0
for h in row_sizes {
used_rows = used_rows + h
}
if row_count > 1 {
used_rows = used_rows + row_gap * (row_count - 1).to_double()
}
}
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::new(width=border_box_width, height=border_box_height),
}
// Track alignment within the container's content box.
let align_content = match node.style.align_content {
Some(v) => v
None => AlignStart
}
let justify_content = match node.style.justify_content {
Some(v) => v
None => AlignStart
}
// `repeat(auto-fit, ...)` collapses empty tracks.
if has_auto_fit_cols || has_auto_fit_rows {
let mut max_row_used = 0
let mut max_col_used = 0
for i in 0.. max_row_used {
max_row_used = row_end
}
if col_end > max_col_used {
max_col_used = col_end
}
}
if has_auto_fit_cols {
let needed = max_col_used + 1
let keep = if needed > non_auto_fit_col_count {
needed
} else {
non_auto_fit_col_count
}
if keep > 0 && keep < col_count {
col_count = keep
let truncated : Array[Double] = Array::make(col_count, 0.0)
for i in 0.. 1 {
used_cols = used_cols + col_gap * (col_count - 1).to_double()
}
}
}
if has_auto_fit_rows {
let needed = max_row_used + 1
let keep = if needed > non_auto_fit_row_count {
needed
} else {
non_auto_fit_row_count
}
if keep > 0 && keep < row_count {
row_count = keep
let truncated : Array[Double] = Array::make(row_count, 0.0)
for i in 0.. 1 {
used_rows = used_rows + row_gap * (row_count - 1).to_double()
}
}
}
}
let leftover_x = content_width - used_cols
let leftover_y = content_height - used_rows
let mut start_x = 0.0
let mut col_gap_effective = col_gap
match justify_content {
AlignCenter => start_x = leftover_x / 2.0
AlignFlexEnd | AlignEnd => start_x = leftover_x
AlignFlexStart | AlignStart => ()
AlignSpaceBetween =>
if col_count > 1 && leftover_x > 0.0 {
col_gap_effective = col_gap_effective +
leftover_x / (col_count - 1).to_double()
}
AlignSpaceAround =>
if col_count > 0 && leftover_x > 0.0 {
let extra = leftover_x / col_count.to_double()
if is_near_int(leftover_x) {
let gap_extra_floor = extra.floor()
let gap_extra = if extra > gap_extra_floor {
gap_extra_floor + 1.0
} else {
gap_extra_floor
}
col_gap_effective = col_gap_effective + gap_extra
start_x = (extra / 2.0).floor()
} else {
col_gap_effective = col_gap_effective + extra
start_x = extra / 2.0
}
}
AlignSpaceEvenly =>
if col_count > 0 && leftover_x > 0.0 {
let spaces = (col_count + 1).to_double()
let extra = leftover_x / spaces
if leftover_x > 0.0 && is_near_int(leftover_x) {
let start_extra = extra.floor()
let gap_extra = if extra > start_extra {
start_extra + 1.0
} else {
start_extra
}
col_gap_effective = col_gap_effective + gap_extra
start_x = start_extra
} else {
col_gap_effective = col_gap_effective + extra
start_x = extra
}
}
AlignStretch =>
if col_count > 0 {
let extra = leftover_x / col_count.to_double()
for i in 0.. 0.0 { v } else { 0.0 }
}
}
}
let mut start_y = 0.0
let mut row_gap_effective = row_gap
match align_content {
AlignCenter => start_y = leftover_y / 2.0
AlignFlexEnd | AlignEnd => start_y = leftover_y
AlignFlexStart | AlignStart => ()
AlignSpaceBetween =>
if row_count > 1 && leftover_y > 0.0 {
row_gap_effective = row_gap_effective +
leftover_y / (row_count - 1).to_double()
}
AlignSpaceAround =>
if row_count > 0 && leftover_y > 0.0 {
let extra = leftover_y / row_count.to_double()
if is_near_int(leftover_y) {
let gap_extra_floor = extra.floor()
let gap_extra = if extra > gap_extra_floor {
gap_extra_floor + 1.0
} else {
gap_extra_floor
}
row_gap_effective = row_gap_effective + gap_extra
start_y = (extra / 2.0).floor()
} else {
row_gap_effective = row_gap_effective + extra
start_y = extra / 2.0
}
}
AlignSpaceEvenly =>
if row_count > 0 && leftover_y > 0.0 {
let spaces = (row_count + 1).to_double()
let extra = leftover_y / spaces
if leftover_y > 0.0 && is_near_int(leftover_y) {
let start_extra = extra.floor()
let gap_extra = if extra > start_extra {
start_extra + 1.0
} else {
start_extra
}
row_gap_effective = row_gap_effective + gap_extra
start_y = start_extra
} else {
row_gap_effective = row_gap_effective + extra
start_y = extra
}
}
AlignStretch =>
if row_count > 0 {
let extra = leftover_y / row_count.to_double()
for i in 0.. 0.0 { v } else { 0.0 }
}
}
}
let base_x = absolute_origin.x + border.left + padding.left
let base_y = absolute_origin.y + border.top + padding.top
let default_align_items = match node.style.align_items {
Some(v) => v
None => ItemsStretch
}
let default_justify_items = match node.style.justify_items {
Some(v) => v
None => ItemsStretch
}
let row_baseline_before_max : Array[Double] = Array::make(row_count, 0.0)
let row_baseline_after_max : Array[Double] = Array::make(row_count, 0.0)
let item_baseline_offsets : Array[Double] = Array::make(placement_count, 0.0)
for i in 0.. c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => continue
_ => ()
}
match child.style.position {
PosAbsolute => continue
PosRelative => ()
}
let row = placed_row[i]
let col = placed_col[i]
let row_span = placed_row_span[i]
let col_span = placed_col_span[i]
let row_end = row + row_span - 1
let col_end = col + col_span - 1
if row < 0 || col < 0 || row_end >= row_count || col_end >= col_count {
continue
}
let align = match child.style.align_self {
Some(v) => v
None => default_align_items
}
if row_span != 1 {
continue
}
match align {
ItemsBaseline => ()
_ => continue
}
let mut cell_w = 0.0
for c in col..<(col + col_span) {
cell_w = cell_w + col_sizes[c]
}
if col_span > 1 {
cell_w = cell_w + col_gap_effective * (col_span - 1).to_double()
}
let mut cell_h = 0.0
for r in row..<(row + row_span) {
cell_h = cell_h + row_sizes[r]
}
if row_span > 1 {
cell_h = cell_h + row_gap_effective * (row_span - 1).to_double()
}
let child_available = Size::new(
width=AvailDefinite(cell_w),
height=AvailDefinite(cell_h),
)
let resolved_margin = resolve_rect_width_basis(
child.style.margin,
child_available,
)
let margin_left_auto = child.style.margin.left is DimAuto
let margin_right_auto = child.style.margin.right is DimAuto
let margin_top_auto = child.style.margin.top is DimAuto
let margin_bottom_auto = child.style.margin.bottom is DimAuto
let has_auto_margin_x = margin_left_auto || margin_right_auto
let has_auto_margin_y = margin_top_auto || margin_bottom_auto
if has_auto_margin_y {
continue
}
let available_w_for_item = max_double(
cell_w - resolved_margin.left - resolved_margin.right,
0.0,
)
let available_h_for_item = max_double(
cell_h - resolved_margin.top - resolved_margin.bottom,
0.0,
)
let item_available = Size::new(
width=AvailDefinite(available_w_for_item),
height=AvailDefinite(available_h_for_item),
)
let justify = match child.style.justify_self {
Some(v) => v
None => default_justify_items
}
let stretch_fit_limit_w = if col_span == 1 {
match child.style.overflow.x {
OverflowVisible => None
_ =>
match col_tracks[col] {
DimFitContent(limit) =>
resolve_optional_dimension(limit, AvailDefinite(content_width))
_ => None
}
}
} else {
None
}
let known_w = match justify {
ItemsStretch =>
if child.style.size.width is DimAuto && !has_auto_margin_x {
match stretch_fit_limit_w {
Some(limit_w) =>
Some(
if limit_w < available_w_for_item {
limit_w
} else {
available_w_for_item
},
)
None => Some(available_w_for_item)
}
} else {
None
}
_ => None
}
// Percent sizes resolve against the containing block size (the grid area), not the
// remaining space after subtracting margins.
let known_w = match (known_w, child.style.size.width) {
(None, DimPercent(p)) => Some(cell_w * p)
_ => known_w
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=known_w, height=None),
item_available,
Point::zero(),
measure_function,
false,
)
let baseline = grid_item_baseline_offset_y(tree, child_id)
let intrinsic = tree.nodes[child_id].layout.size
item_baseline_offsets[i] = baseline
let before = resolved_margin.top + baseline
let after = resolved_margin.bottom +
max_double(intrinsic.height - baseline, 0.0)
row_baseline_before_max[row] = max_double(
row_baseline_before_max[row],
before,
)
row_baseline_after_max[row] = max_double(row_baseline_after_max[row], after)
}
for row in 0..
if baseline_total > row_sizes[row] {
row_sizes[row] = baseline_total
}
_ => ()
}
}
for i in 0.. c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => compute_hidden_layout(tree, child_id, Point::zero())
_ => {
if not(placed[i]) {
continue
} else {
()
}
match child.style.position {
PosAbsolute => continue
PosRelative => ()
}
let row = placed_row[i]
let col = placed_col[i]
let row_span = placed_row_span[i]
let col_span = placed_col_span[i]
let row_end = row + row_span - 1
let col_end = col + col_span - 1
if row < 0 || col < 0 || row_end >= row_count || col_end >= col_count {
continue
}
let mut x = start_x
for c in 0.. 1 {
cell_w = cell_w + col_gap_effective * (col_span - 1).to_double()
}
let mut cell_h = 0.0
for r in row..<(row + row_span) {
cell_h = cell_h + row_sizes[r]
}
if row_span > 1 {
cell_h = cell_h + row_gap_effective * (row_span - 1).to_double()
}
let child_available = Size::new(
width=AvailDefinite(cell_w),
height=AvailDefinite(cell_h),
)
// Grid item margins apply inside the cell and contribute to track sizing.
let resolved_margin = resolve_rect_width_basis(
child.style.margin,
child_available,
)
let margin_left_auto = child.style.margin.left is DimAuto
let margin_right_auto = child.style.margin.right is DimAuto
let margin_top_auto = child.style.margin.top is DimAuto
let margin_bottom_auto = child.style.margin.bottom is DimAuto
let has_auto_margin_x = margin_left_auto || margin_right_auto
let has_auto_margin_y = margin_top_auto || margin_bottom_auto
let available_w_for_item = max_double(
cell_w - resolved_margin.left - resolved_margin.right,
0.0,
)
let available_h_for_item = max_double(
cell_h - resolved_margin.top - resolved_margin.bottom,
0.0,
)
let item_available = Size::new(
width=AvailDefinite(available_w_for_item),
height=AvailDefinite(available_h_for_item),
)
let justify = match child.style.justify_self {
Some(v) => v
None => default_justify_items
}
let align = match child.style.align_self {
Some(v) => v
None => default_align_items
}
let stretch_fit_limit_w = if col_span == 1 {
match child.style.overflow.x {
OverflowVisible => None
_ =>
match col_tracks[col] {
DimFitContent(limit) =>
resolve_optional_dimension(
limit,
AvailDefinite(content_width),
)
_ => None
}
}
} else {
None
}
let mut known_w = match justify {
ItemsStretch =>
if child.style.size.width is DimAuto && !has_auto_margin_x {
match stretch_fit_limit_w {
Some(limit_w) =>
Some(
if limit_w < available_w_for_item {
limit_w
} else {
available_w_for_item
},
)
None => Some(available_w_for_item)
}
} else {
None
}
_ => None
}
let mut known_h = match align {
ItemsStretch =>
if child.style.size.height is DimAuto &&
!has_auto_margin_y &&
child.style.aspect_ratio is None {
Some(available_h_for_item)
} else {
None
}
_ => None
}
// Percent sizes resolve against the containing block size (the grid area), not the
// remaining space after subtracting margins.
match (known_w, child.style.size.width) {
(None, DimPercent(p)) => known_w = Some(cell_w * p)
_ => ()
}
match (known_h, child.style.size.height) {
(None, DimPercent(p)) => known_h = Some(cell_h * p)
_ => ()
}
// First pass: compute the item's intrinsic size within the cell constraints.
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=known_w, height=known_h),
child_available,
Point::zero(),
measure_function,
false,
)
match (align, child.style.aspect_ratio, child.style.size.height) {
(ItemsStretch, Some(ratio), DimAuto) =>
if ratio > 0.0 &&
known_w is Some(_) &&
known_h is None &&
!has_auto_margin_y {
let intrinsic_after_stretch = tree.nodes[child_id].layout.size
let max_height = resolve_optional_dimension(
child.style.max_size.height,
item_available.height,
)
match max_height {
Some(max_h) if intrinsic_after_stretch.height > max_h => {
known_h = Some(max_h)
known_w = Some((max_h * ratio).round())
}
_ => {
let ratio_height = (intrinsic_after_stretch.width / ratio).round()
if intrinsic_after_stretch.height > ratio_height {
known_h = Some(ratio_height)
} else if intrinsic_after_stretch.height < ratio_height {
known_w = Some(
(intrinsic_after_stretch.height * ratio).round(),
)
} else {
()
}
}
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=known_w, height=known_h),
child_available,
Point::zero(),
measure_function,
false,
)
} else {
()
}
_ => ()
}
let intrinsic = tree.nodes[child_id].layout.size
let free_x = max_double(available_w_for_item - intrinsic.width, 0.0)
let free_y = max_double(available_h_for_item - intrinsic.height, 0.0)
let margin_left = if margin_left_auto {
if margin_right_auto {
free_x / 2.0
} else {
free_x
}
} else {
resolved_margin.left
}
let margin_top = if margin_top_auto {
if margin_bottom_auto {
free_y / 2.0
} else {
free_y
}
} else {
resolved_margin.top
}
let offset_x = if has_auto_margin_x {
0.0
} else {
match justify {
ItemsEnd | ItemsFlexEnd => available_w_for_item - intrinsic.width
ItemsCenter => (available_w_for_item - intrinsic.width) / 2.0
_ => 0.0
}
}
let offset_y = if has_auto_margin_y {
0.0
} else {
match align {
ItemsEnd | ItemsFlexEnd => available_h_for_item - intrinsic.height
ItemsCenter => (available_h_for_item - intrinsic.height) / 2.0
ItemsBaseline =>
if row_span == 1 {
row_baseline_before_max[row] -
resolved_margin.top -
item_baseline_offsets[i]
} else {
0.0
}
_ => 0.0
}
}
// Second pass: position the item within the cell.
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=known_w, height=known_h),
child_available,
Point::new(
x=(base_x + x + margin_left + offset_x).round(),
y=(base_y + y + margin_top + offset_y).round(),
),
measure_function,
false,
)
}
}
}
let padding_origin = Point::new(
x=absolute_origin.x + border.left,
y=absolute_origin.y + border.top,
)
let padding_box_width = max_double(
border_box_width - border.left - border.right,
0.0,
)
let padding_box_height = max_double(
border_box_height - border.top - border.bottom,
0.0,
)
let content_start_x = padding.left + start_x
let content_start_y = padding.top + start_y
let explicit_col_lines : Array[Double] = [content_start_x]
let explicit_row_lines : Array[Double] = [content_start_y]
let mut col_line_pos = content_start_x
let mut row_line_pos = content_start_y
for i in 0..= 0 && track_idx < col_sizes.length() {
col_line_pos = col_line_pos + col_sizes[track_idx]
}
if i + 1 < explicit_col_count {
col_line_pos = col_line_pos + col_gap_effective
}
explicit_col_lines.push(col_line_pos)
}
for i in 0..= 0 && track_idx < row_sizes.length() {
row_line_pos = row_line_pos + row_sizes[track_idx]
}
if i + 1 < explicit_row_count {
row_line_pos = row_line_pos + row_gap_effective
}
explicit_row_lines.push(row_line_pos)
}
fn resolve_grid_abs_line_position(
lines : Array[Double],
line : Int,
) -> Double {
let last = lines.length() - 1
if last <= 0 {
lines[0]
} else {
let raw = if line > 0 { line - 1 } else { last + line + 1 }
let idx = if raw < 0 { 0 } else if raw > last { last } else { raw }
lines[idx]
}
}
fn resolve_grid_abs_axis_containing_block(
placement : Line[GridPlacement],
lines : Array[Double],
padding_box_size : Double,
) -> (Double, Double) {
let start_line = match placement.start {
PlaceLine(v) => Some(resolve_grid_abs_line_position(lines, v))
_ => None
}
let end_line = match placement.end {
PlaceLine(v) => Some(resolve_grid_abs_line_position(lines, v))
_ => None
}
let start_pos = match (start_line, end_line) {
(Some(s), _) => s
(None, Some(_)) => 0.0
(None, None) => 0.0
}
let end_pos = match (start_line, end_line) {
(_, Some(e)) => e
(Some(_), None) => padding_box_size
(None, None) => padding_box_size
}
if end_pos >= start_pos {
(start_pos, end_pos)
} else {
(end_pos, start_pos)
}
}
let default_abs_align = match node.style.align_items {
Some(v) => v
None => ItemsStretch
}
let default_abs_justify = match node.style.justify_items {
Some(v) => v
None => ItemsStretch
}
for child_id in node.children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => ()
_ =>
match child.style.position {
PosRelative => ()
PosAbsolute => {
let col_line = merged_placement_line(
child.style.grid_column,
child.style.grid_column_start,
)
let row_line = merged_placement_line(
child.style.grid_row,
child.style.grid_row_start,
)
let cb_x = resolve_grid_abs_axis_containing_block(
col_line, explicit_col_lines, padding_box_width,
)
let cb_y = resolve_grid_abs_axis_containing_block(
row_line, explicit_row_lines, padding_box_height,
)
let containing_x = cb_x.0
let containing_y = cb_y.0
let containing_width = max_double(cb_x.1 - cb_x.0, 0.0)
let containing_height = max_double(cb_y.1 - cb_y.0, 0.0)
let abs_available = Size::new(
width=AvailDefinite(containing_width),
height=AvailDefinite(containing_height),
)
let margin = child.style.margin
let margin_left_auto = margin.left is DimAuto
let margin_right_auto = margin.right is DimAuto
let margin_top_auto = margin.top is DimAuto
let margin_bottom_auto = margin.bottom is DimAuto
let margin_left_fixed = resolve_dimension_width_basis(
margin.left,
containing_width,
)
let margin_right_fixed = resolve_dimension_width_basis(
margin.right,
containing_width,
)
let margin_top_fixed = resolve_dimension_width_basis(
margin.top,
containing_width,
)
let margin_bottom_fixed = resolve_dimension_width_basis(
margin.bottom,
containing_width,
)
let inset = child.style.inset
let left = resolve_optional_dimension(
inset.left,
abs_available.width,
)
let right = resolve_optional_dimension(
inset.right,
abs_available.width,
)
let top = resolve_optional_dimension(
inset.top,
abs_available.height,
)
let bottom = resolve_optional_dimension(
inset.bottom,
abs_available.height,
)
let mut used_width = resolve_optional_dimension(
child.style.size.width,
abs_available.width,
)
let mut used_height = resolve_optional_dimension(
child.style.size.height,
abs_available.height,
)
let width_was_auto = used_width is None
let height_was_auto = used_height is None
let mut width_from_inset = false
let mut height_from_inset = false
match used_width {
Some(_) => ()
None =>
match (left, right) {
(Some(l), Some(r)) =>
used_width = Some(
max_double(
containing_width -
l -
r -
margin_left_fixed -
margin_right_fixed,
0.0,
),
)
_ => ()
}
}
match used_height {
Some(_) => ()
None =>
match (top, bottom) {
(Some(t), Some(b)) =>
used_height = Some(
max_double(
containing_height -
t -
b -
margin_top_fixed -
margin_bottom_fixed,
0.0,
),
)
_ => ()
}
}
match (used_width, used_height) {
(Some(_), _) =>
if width_was_auto {
match (left, right) {
(Some(_), Some(_)) => width_from_inset = true
_ => ()
}
} else {
()
}
_ => ()
}
match (used_width, used_height) {
(_, Some(_)) =>
if height_was_auto {
match (top, bottom) {
(Some(_), Some(_)) => height_from_inset = true
_ => ()
}
} else {
()
}
_ => ()
}
match child.style.aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match (used_width, used_height) {
(Some(w), None) => used_height = Some((w / ratio).round())
(None, Some(h)) => used_width = Some((h * ratio).round())
(Some(w), Some(_h)) =>
if width_was_auto &&
height_was_auto &&
width_from_inset &&
height_from_inset {
used_height = Some((w / ratio).round())
} else {
()
}
_ => ()
}
}
None => ()
}
let mut min_width = resolve_optional_dimension(
child.style.min_size.width,
abs_available.width,
)
let mut min_height = resolve_optional_dimension(
child.style.min_size.height,
abs_available.height,
)
let mut max_width = resolve_optional_dimension(
child.style.max_size.width,
abs_available.width,
)
let mut max_height = resolve_optional_dimension(
child.style.max_size.height,
abs_available.height,
)
match child.style.aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match (min_width, min_height) {
(None, Some(h)) => min_width = Some((h * ratio).round())
(Some(w), None) => min_height = Some((w / ratio).round())
_ => ()
}
match (max_width, max_height) {
(None, Some(h)) => max_width = Some((h * ratio).round())
(Some(w), None) => max_height = Some((w / ratio).round())
_ => ()
}
} else {
()
}
None => ()
}
let intrinsic = match (used_width, used_height) {
(Some(w), Some(h)) => Size::new(width=w, height=h)
_ => {
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=None, height=None),
abs_available,
Point::zero(),
measure_function,
false,
)
tree.nodes[child_id].layout.size
}
}
let final_width = match used_width {
Some(w) => w
None => intrinsic.width
}
let final_height = match used_height {
Some(h) => h
None => intrinsic.height
}
let mut clamped_final_width = final_width
let mut clamped_final_height = final_height
match min_width {
Some(m) =>
clamped_final_width = max_double(clamped_final_width, m)
None => ()
}
match max_width {
Some(m) =>
if clamped_final_width > m {
clamped_final_width = m
} else {
()
}
None => ()
}
match min_height {
Some(m) =>
clamped_final_height = max_double(clamped_final_height, m)
None => ()
}
match max_height {
Some(m) =>
if clamped_final_height > m {
clamped_final_height = m
} else {
()
}
None => ()
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(
width=Some(clamped_final_width),
height=Some(clamped_final_height),
),
abs_available,
Point::zero(),
measure_function,
false,
)
let final_size = tree.nodes[child_id].layout.size
let mut final_margin_left = if margin_left_auto {
0.0
} else {
margin_left_fixed
}
let mut final_margin_right = if margin_right_auto {
0.0
} else {
margin_right_fixed
}
let mut final_margin_top = if margin_top_auto {
0.0
} else {
margin_top_fixed
}
let mut final_margin_bottom = if margin_bottom_auto {
0.0
} else {
margin_bottom_fixed
}
match (left, right) {
(Some(l), Some(r)) => {
let fixed = (if margin_left_auto {
0.0
} else {
final_margin_left
}) +
(if margin_right_auto { 0.0 } else { final_margin_right })
let remaining = containing_width -
l -
r -
final_size.width -
fixed
let auto_count = (if margin_left_auto { 1 } else { 0 }) +
(if margin_right_auto { 1 } else { 0 })
match auto_count {
2 =>
if remaining >= 0.0 {
final_margin_left = remaining / 2.0
final_margin_right = remaining / 2.0
} else {
final_margin_left = 0.0
final_margin_right = 0.0
}
1 =>
if margin_left_auto {
final_margin_left = remaining
} else {
final_margin_right = remaining
}
_ => ()
}
}
_ => ()
}
match (top, bottom) {
(Some(t), Some(b)) => {
let fixed = (if margin_top_auto {
0.0
} else {
final_margin_top
}) +
(if margin_bottom_auto { 0.0 } else { final_margin_bottom })
let remaining = containing_height -
t -
b -
final_size.height -
fixed
let auto_count = (if margin_top_auto { 1 } else { 0 }) +
(if margin_bottom_auto { 1 } else { 0 })
match auto_count {
2 =>
if remaining >= 0.0 {
final_margin_top = remaining / 2.0
final_margin_bottom = remaining / 2.0
} else {
final_margin_top = 0.0
final_margin_bottom = 0.0
}
1 =>
if margin_top_auto {
final_margin_top = remaining
} else {
final_margin_bottom = remaining
}
_ => ()
}
}
_ => ()
}
let justify = match child.style.justify_self {
Some(v) => v
None => default_abs_justify
}
let align = match child.style.align_self {
Some(v) => v
None => default_abs_align
}
let available_width_for_static = max_double(
containing_width - final_margin_left - final_margin_right,
0.0,
)
let available_height_for_static = max_double(
containing_height - final_margin_top - final_margin_bottom,
0.0,
)
let static_x = final_margin_left +
(match justify {
ItemsEnd | ItemsFlexEnd =>
available_width_for_static - final_size.width
ItemsCenter =>
(available_width_for_static - final_size.width) / 2.0
_ => 0.0
})
let static_y = final_margin_top +
(match align {
ItemsEnd | ItemsFlexEnd =>
available_height_for_static - final_size.height
ItemsCenter =>
(available_height_for_static - final_size.height) / 2.0
_ => 0.0
})
let x_in_padding = match left {
Some(v) => containing_x + v + final_margin_left
None =>
match right {
Some(v) =>
containing_x +
containing_width -
v -
final_margin_right -
final_size.width
None => containing_x + static_x
}
}
let y_in_padding = match top {
Some(v) => containing_y + v + final_margin_top
None =>
match bottom {
Some(v) =>
containing_y +
containing_height -
v -
final_margin_bottom -
final_size.height
None => containing_y + static_y
}
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(
width=Some(final_size.width),
height=Some(final_size.height),
),
abs_available,
Point::new(
x=padding_origin.x + x_in_padding,
y=padding_origin.y + y_in_padding,
),
measure_function,
false,
)
}
}
}
}
let resolved_margin = resolve_rect_width_basis(
node.style.margin,
available_space,
)
set_effective_margin_states(
tree,
node_id,
margin_collapse_state_from(resolved_margin.top),
margin_collapse_state_from(resolved_margin.bottom),
)
}
///|
fn[C] compute_block_layout_with_measure(
tree : TaffyTree[C],
node_id : NodeId,
known_dimensions : Size[Double?],
available_space : Size[AvailableSpace],
absolute_origin : Point[Double],
measure_function : (Size[Double?], Size[AvailableSpace], NodeId, C?, Style) -> Size[
Double,
],
is_layout_root : Bool,
) -> Unit raise TaffyError {
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
let padding = resolve_rect_width_basis(node.style.padding, available_space)
let border = resolve_rect_width_basis(node.style.border, available_space)
let scrollbar_w = node.style.scrollbar_width
let scrollbar_x = match node.style.overflow.x {
OverflowScroll => scrollbar_w
_ => 0.0
}
let scrollbar_y = match node.style.overflow.y {
OverflowScroll => scrollbar_w
_ => 0.0
}
let horiz_non_scroll_inset = padding.left +
padding.right +
border.left +
border.right
let vert_non_scroll_inset = padding.top +
padding.bottom +
border.top +
border.bottom
// Overflow::Scroll reserves scrollbar space inside the border box.
// Horizontal scrollbar consumes cross (vertical) space; vertical scrollbar consumes main (horizontal) space.
let horiz_inset = horiz_non_scroll_inset + scrollbar_y
let vert_inset = vert_non_scroll_inset + scrollbar_x
let style_width = resolve_optional_dimension(
node.style.size.width,
available_space.width,
)
let style_height = resolve_optional_dimension(
node.style.size.height,
available_space.height,
)
let specified_width = match known_dimensions.width {
Some(w) => Some(w)
None => style_width
}
let specified_height = match known_dimensions.height {
Some(h) => Some(h)
None => style_height
}
let width_definite = match specified_width {
Some(_) => true
None =>
match available_space.width {
AvailDefinite(_) => true
_ => false
}
}
// Minimal block sizing: border-box.
let raw_width = match specified_width {
Some(w) => w
None =>
match available_space.width {
AvailDefinite(v) => v
_ => horiz_inset
}
}
let raw_height = match specified_height {
Some(h) => h
None =>
match available_space.height {
AvailDefinite(v) => v
_ => vert_inset
}
}
let border_box_width = max_double(
clamp_dimension(
raw_width,
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
),
horiz_non_scroll_inset,
)
let border_box_height = max_double(
clamp_dimension(
raw_height,
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
),
vert_non_scroll_inset,
)
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::new(width=border_box_width, height=border_box_height),
}
// Containing block for in-flow positioning: content box (padding box minus padding).
let padding_origin = Point::new(
x=absolute_origin.x + border.left,
y=absolute_origin.y + border.top,
)
let content_origin = Point::new(
x=padding_origin.x + padding.left,
y=padding_origin.y + padding.top,
)
let content_width = max_double(border_box_width - horiz_inset, 0.0)
let content_height = max_double(border_box_height - vert_inset, 0.0)
let flow_available = Size::new(
width=match width_definite {
true => AvailDefinite(content_width)
false => available_space.width
},
height=match specified_height {
Some(_) => AvailDefinite(content_height)
None => AvailMaxContent
},
)
let margin_basis_width = match available_space.width {
AvailDefinite(v) => v
_ =>
match specified_width {
Some(w) => max_double(w - horiz_inset, 0.0)
None => 0.0
}
}
let margin_basis_available = Size::new(
width=AvailDefinite(margin_basis_width),
height=flow_available.height,
)
fn collapse_state_add(
state : (Double, Double),
margin : Double,
) -> (Double, Double) {
let mut max_pos = state.0
let mut min_neg = state.1
if margin > 0.0 {
if margin > max_pos {
max_pos = margin
} else {
()
}
} else if margin < 0.0 {
if margin < min_neg {
min_neg = margin
} else {
()
}
} else {
()
}
(max_pos, min_neg)
}
fn collapse_state_value(state : (Double, Double)) -> Double {
state.0 + state.1
}
fn collapse_state_from(margin : Double) -> (Double, Double) {
collapse_state_add((0.0, 0.0), margin)
}
fn collapse_state_merge(
a : (Double, Double),
b : (Double, Double),
) -> (Double, Double) {
(if a.0 > b.0 { a.0 } else { b.0 }, if a.1 < b.1 { a.1 } else { b.1 })
}
fn has_in_flow_children(
tree : TaffyTree[C],
children : Array[NodeId],
) -> Bool raise TaffyError {
for child_id in children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => ()
_ =>
match child.style.position {
PosRelative => return true
PosAbsolute => ()
}
}
}
false
}
fn can_collapse_through_child(
tree : TaffyTree[C],
child : Node[C],
child_size : Size[Double],
) -> Bool raise TaffyError {
match child.style.display {
DisplayBlock => ()
_ => return false
}
if child_size.height != 0.0 {
return false
} else {
()
}
match child.context {
Some(_) => return false
None => ()
}
let overflow_visible = match
(child.style.overflow.x, child.style.overflow.y) {
(OverflowVisible, OverflowVisible) => true
_ => false
}
if not(overflow_visible) {
return false
} else {
()
}
if has_in_flow_children(tree, child.children) {
return false
} else {
()
}
true
}
let resolved_own_margin = resolve_rect_width_basis(
node.style.margin,
available_space,
)
let own_margin_top = resolved_own_margin.top
let own_margin_bottom = resolved_own_margin.bottom
let parent_overflow_visible = match
(node.style.overflow.x, node.style.overflow.y) {
(OverflowVisible, OverflowVisible) => true
_ => false
}
let mut first_in_flow_child : NodeId? = None
let mut last_in_flow_child : NodeId? = None
for child_id in node.children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => ()
_ =>
match child.style.position {
PosRelative => {
match first_in_flow_child {
None => first_in_flow_child = Some(child_id)
Some(_) => ()
}
last_in_flow_child = Some(child_id)
}
PosAbsolute => ()
}
}
}
let collapse_with_children = not(is_layout_root) &&
parent_overflow_visible &&
specified_height is None
let first_child_allows_parent_child_collapse = match first_in_flow_child {
Some(child_id) => {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
child.style.display is DisplayBlock
}
None => false
}
let last_child_allows_parent_child_collapse = match last_in_flow_child {
Some(child_id) => {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
child.style.display is DisplayBlock
}
None => false
}
let collapse_top_with_child = collapse_with_children &&
padding.top == 0.0 &&
border.top == 0.0 &&
first_child_allows_parent_child_collapse
let collapse_bottom_with_child = collapse_with_children &&
padding.bottom == 0.0 &&
border.bottom == 0.0 &&
last_child_allows_parent_child_collapse
let abs_child_ids : Array[NodeId] = []
let abs_child_static_y : Array[Double] = []
let mut flow_anchor_border_bottom : Double = 0.0
let mut flow_margin_collapse : (Double, Double) = (0.0, 0.0)
let mut max_flow_child_width = 0.0
let content_width_definite = match flow_available.width {
AvailDefinite(_) => true
_ => false
}
let content_width_for_flow = match flow_available.width {
AvailDefinite(v) => v
_ => 0.0
}
let mut is_first_in_flow = true
for child_id in node.children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => compute_hidden_layout(tree, child_id, Point::zero())
_ =>
match child.style.position {
PosAbsolute => {
abs_child_ids.push(child_id)
abs_child_static_y.push(
flow_anchor_border_bottom +
collapse_state_value(flow_margin_collapse),
)
}
PosRelative => {
// Minimal normal flow: stack children vertically.
let margin = child.style.margin
let margin_left_auto = match margin.left {
DimAuto => true
_ => false
}
let margin_right_auto = match margin.right {
DimAuto => true
_ => false
}
let margin_top_auto = match margin.top {
DimAuto => true
_ => false
}
let resolved_margin = resolve_rect_width_basis(
margin, margin_basis_available,
)
let fixed_margin_left = if margin_left_auto {
0.0
} else {
resolved_margin.left
}
let fixed_margin_right = if margin_right_auto {
0.0
} else {
resolved_margin.right
}
let fixed_margin_top = if margin_top_auto {
0.0
} else {
resolved_margin.top
}
let child_width_auto = match child.style.size.width {
DimAuto => true
_ => false
}
let aspect_ratio = child.style.aspect_ratio
let width_from_aspect = match
(aspect_ratio, child.style.size.width) {
(Some(ratio), DimAuto) =>
match
resolve_optional_dimension(
child.style.size.height,
flow_available.height,
) {
Some(h) if ratio > 0.0 => Some((h * ratio).round())
_ => None
}
_ => None
}
let mut target_width = match width_from_aspect {
Some(w) => Some(w)
None =>
if content_width_definite && child_width_auto {
Some(
max_double(
content_width_for_flow -
fixed_margin_left -
fixed_margin_right,
0.0,
),
)
} else {
None
}
}
match target_width {
Some(w) => {
let mut min_width = resolve_optional_dimension(
child.style.min_size.width,
flow_available.width,
)
let mut max_width = resolve_optional_dimension(
child.style.max_size.width,
flow_available.width,
)
match aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match
(
min_width,
resolve_optional_dimension(
child.style.min_size.height,
flow_available.height,
),
) {
(None, Some(h)) => min_width = Some((h * ratio).round())
_ => ()
}
match
(
max_width,
resolve_optional_dimension(
child.style.max_size.height,
flow_available.height,
),
) {
(None, Some(h)) => max_width = Some((h * ratio).round())
_ => ()
}
} else {
()
}
None => ()
}
let mut clamped = w
match min_width {
Some(m) => clamped = max_double(clamped, m)
None => ()
}
match max_width {
Some(m) => if clamped > m { clamped = m } else { () }
None => ()
}
target_width = Some(clamped)
}
None => ()
}
let is_last_in_flow = match last_in_flow_child {
Some(v) => v == child_id
None => false
}
let top_used_base = if collapse_top_with_child && is_first_in_flow {
0.0
} else {
fixed_margin_top
}
let used_before_base = collapse_state_value(
collapse_state_add(flow_margin_collapse, top_used_base),
)
let mut child_y_rel_static = flow_anchor_border_bottom +
used_before_base
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=target_width, height=None),
margin_basis_available,
Point::new(
x=content_origin.x + fixed_margin_left,
y=content_origin.y + child_y_rel_static,
),
measure_function,
false,
)
let child_size = tree.nodes[child_id].layout.size
let effective_top = tree.nodes[child_id].effective_margin_top
let top_used_effective = if collapse_top_with_child &&
is_first_in_flow {
0.0
} else {
effective_top
}
let used_before_effective = collapse_state_value(
collapse_state_add(flow_margin_collapse, top_used_effective),
)
let delta_y = used_before_effective - used_before_base
if delta_y != 0.0 {
offset_subtree(tree, child_id, 0.0, delta_y)
child_y_rel_static = child_y_rel_static + delta_y
} else {
()
}
let remaining = if content_width_definite {
content_width_for_flow -
fixed_margin_left -
fixed_margin_right -
child_size.width
} else {
0.0
}
let mut auto_left = 0.0
let mut auto_right = 0.0
if content_width_definite {
if margin_left_auto && margin_right_auto {
if remaining > 0.0 {
auto_left = remaining / 2.0
auto_right = remaining / 2.0
} else {
auto_left = 0.0
auto_right = 0.0
}
} else if margin_left_auto {
auto_left = if remaining > 0.0 { remaining } else { 0.0 }
} else if margin_right_auto {
auto_right = if remaining > 0.0 { remaining } else { 0.0 }
} else {
()
}
if auto_left != 0.0 {
offset_subtree(tree, child_id, auto_left, 0.0)
} else {
()
}
} else {
()
}
let effective_bottom_raw = tree.nodes[child_id].effective_margin_bottom
let bottom_used_effective = if collapse_bottom_with_child &&
is_last_in_flow {
0.0
} else {
effective_bottom_raw
}
let through = can_collapse_through_child(tree, child, child_size)
if through {
flow_margin_collapse = collapse_state_add(
collapse_state_add(flow_margin_collapse, top_used_effective),
bottom_used_effective,
)
} else {
flow_anchor_border_bottom = child_y_rel_static + child_size.height
flow_margin_collapse = collapse_state_from(bottom_used_effective)
}
is_first_in_flow = false
let child_outer_width = fixed_margin_left +
auto_left +
child_size.width +
fixed_margin_right +
auto_right
if child_outer_width > max_flow_child_width {
max_flow_child_width = child_outer_width
} else {
()
}
}
}
}
}
// Auto sizing from in-flow children.
let final_border_box_width = match width_definite {
true => border_box_width
false =>
max_double(
clamp_dimension(
horiz_inset + max_flow_child_width,
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
),
horiz_non_scroll_inset,
)
}
let mut final_border_box_height = match specified_height {
Some(_) => border_box_height
None =>
max_double(
clamp_dimension(
vert_inset +
flow_anchor_border_bottom +
collapse_state_value(flow_margin_collapse),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
),
vert_non_scroll_inset,
)
}
// Taffy block layout computes container width first, then performs final in-flow layout with that width.
// When this container's width is auto, do a second layout pass so that in-flow children can stretch
// (e.g. auto-width block children) and so that absolute children's static y positions are correct.
if not(width_definite) {
let final_content_width_for_flow = max_double(
final_border_box_width - horiz_inset,
0.0,
)
let final_content_height_for_flow = max_double(
final_border_box_height - vert_inset,
0.0,
)
let flow_available2 = Size::new(
width=AvailDefinite(final_content_width_for_flow),
height=match specified_height {
Some(_) => AvailDefinite(final_content_height_for_flow)
None => AvailMaxContent
},
)
let margin_basis_available2 = Size::new(
width=AvailDefinite(final_content_width_for_flow),
height=flow_available2.height,
)
abs_child_ids.clear()
abs_child_static_y.clear()
flow_anchor_border_bottom = 0.0
flow_margin_collapse = (0.0, 0.0)
max_flow_child_width = 0.0
let content_width_for_flow2 = final_content_width_for_flow
let mut is_first_in_flow2 = true
for child_id in node.children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => compute_hidden_layout(tree, child_id, Point::zero())
_ =>
match child.style.position {
PosAbsolute => {
abs_child_ids.push(child_id)
abs_child_static_y.push(
flow_anchor_border_bottom +
collapse_state_value(flow_margin_collapse),
)
}
PosRelative => {
let margin = child.style.margin
let margin_left_auto = margin.left is DimAuto
let margin_right_auto = margin.right is DimAuto
let margin_top_auto = margin.top is DimAuto
let resolved_margin = resolve_rect_width_basis(
margin, margin_basis_available2,
)
let fixed_margin_left = if margin_left_auto {
0.0
} else {
resolved_margin.left
}
let fixed_margin_right = if margin_right_auto {
0.0
} else {
resolved_margin.right
}
let fixed_margin_top = if margin_top_auto {
0.0
} else {
resolved_margin.top
}
let child_width_auto = child.style.size.width is DimAuto
let aspect_ratio = child.style.aspect_ratio
let width_from_aspect = match
(aspect_ratio, child.style.size.width) {
(Some(ratio), DimAuto) =>
match
resolve_optional_dimension(
child.style.size.height,
flow_available2.height,
) {
Some(h) if ratio > 0.0 => Some((h * ratio).round())
_ => None
}
_ => None
}
let mut target_width = match width_from_aspect {
Some(w) => Some(w)
None =>
if child_width_auto {
Some(
max_double(
content_width_for_flow2 -
fixed_margin_left -
fixed_margin_right,
0.0,
),
)
} else {
None
}
}
match target_width {
Some(w) => {
let mut min_width = resolve_optional_dimension(
child.style.min_size.width,
flow_available2.width,
)
let mut max_width = resolve_optional_dimension(
child.style.max_size.width,
flow_available2.width,
)
match aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match
(
min_width,
resolve_optional_dimension(
child.style.min_size.height,
flow_available2.height,
),
) {
(None, Some(h)) =>
min_width = Some((h * ratio).round())
_ => ()
}
match
(
max_width,
resolve_optional_dimension(
child.style.max_size.height,
flow_available2.height,
),
) {
(None, Some(h)) =>
max_width = Some((h * ratio).round())
_ => ()
}
} else {
()
}
None => ()
}
let mut clamped = w
match min_width {
Some(m) => clamped = max_double(clamped, m)
None => ()
}
match max_width {
Some(m) => if clamped > m { clamped = m } else { () }
None => ()
}
target_width = Some(clamped)
}
None => ()
}
let is_last_in_flow = match last_in_flow_child {
Some(v) => v == child_id
None => false
}
let top_used_base = if collapse_top_with_child &&
is_first_in_flow2 {
0.0
} else {
fixed_margin_top
}
let used_before_base = collapse_state_value(
collapse_state_add(flow_margin_collapse, top_used_base),
)
let mut child_y_rel_static = flow_anchor_border_bottom +
used_before_base
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=target_width, height=None),
flow_available2,
Point::new(
x=content_origin.x + fixed_margin_left,
y=content_origin.y + child_y_rel_static,
),
measure_function,
false,
)
let child_size = tree.nodes[child_id].layout.size
let effective_top = tree.nodes[child_id].effective_margin_top
let top_used_effective = if collapse_top_with_child &&
is_first_in_flow2 {
0.0
} else {
effective_top
}
let used_before_effective = collapse_state_value(
collapse_state_add(flow_margin_collapse, top_used_effective),
)
let delta_y = used_before_effective - used_before_base
if delta_y != 0.0 {
offset_subtree(tree, child_id, 0.0, delta_y)
child_y_rel_static = child_y_rel_static + delta_y
} else {
()
}
let remaining = content_width_for_flow2 -
fixed_margin_left -
fixed_margin_right -
child_size.width
let mut auto_left = 0.0
let mut auto_right = 0.0
if margin_left_auto && margin_right_auto {
if remaining > 0.0 {
auto_left = remaining / 2.0
auto_right = remaining / 2.0
} else {
auto_left = 0.0
auto_right = 0.0
}
} else if margin_left_auto {
auto_left = if remaining > 0.0 { remaining } else { 0.0 }
} else if margin_right_auto {
auto_right = if remaining > 0.0 { remaining } else { 0.0 }
} else {
()
}
if auto_left != 0.0 {
offset_subtree(tree, child_id, auto_left, 0.0)
} else {
()
}
let effective_bottom_raw = tree.nodes[child_id].effective_margin_bottom
let bottom_used_effective = if collapse_bottom_with_child &&
is_last_in_flow {
0.0
} else {
effective_bottom_raw
}
let through = can_collapse_through_child(tree, child, child_size)
if through {
flow_margin_collapse = collapse_state_add(
collapse_state_add(flow_margin_collapse, top_used_effective),
bottom_used_effective,
)
} else {
flow_anchor_border_bottom = child_y_rel_static +
child_size.height
flow_margin_collapse = collapse_state_from(
bottom_used_effective,
)
}
is_first_in_flow2 = false
let child_outer_width = fixed_margin_left +
auto_left +
child_size.width +
fixed_margin_right +
auto_right
if child_outer_width > max_flow_child_width {
max_flow_child_width = child_outer_width
} else {
()
}
}
}
}
}
// Update auto height after reflow.
final_border_box_height = match specified_height {
Some(_) => final_border_box_height
None =>
max_double(
clamp_dimension(
vert_inset +
flow_anchor_border_bottom +
collapse_state_value(flow_margin_collapse),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
),
vert_non_scroll_inset,
)
}
}
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::new(
width=final_border_box_width,
height=final_border_box_height,
),
}
// Absolute children: positioned relative to the final padding box.
let final_padding_box_width = max_double(
final_border_box_width - border.left - border.right - scrollbar_y,
0.0,
)
let final_padding_box_height = max_double(
final_border_box_height - border.top - border.bottom - scrollbar_x,
0.0,
)
let abs_available = Size::new(
width=AvailDefinite(final_padding_box_width),
height=AvailDefinite(final_padding_box_height),
)
for i in 0.. c
None => raise InvalidNodeId(child_id)
}
let margin = child.style.margin
// CSS compatibility: margin percentages are resolved against the width.
let margin_left_auto = match margin.left {
DimAuto => true
_ => false
}
let margin_right_auto = match margin.right {
DimAuto => true
_ => false
}
let margin_top_auto = match margin.top {
DimAuto => true
_ => false
}
let margin_bottom_auto = match margin.bottom {
DimAuto => true
_ => false
}
let margin_left_fixed = resolve_dimension_width_basis(
margin.left,
final_padding_box_width,
)
let margin_right_fixed = resolve_dimension_width_basis(
margin.right,
final_padding_box_width,
)
let margin_top_fixed = resolve_dimension_width_basis(
margin.top,
final_padding_box_width,
)
let margin_bottom_fixed = resolve_dimension_width_basis(
margin.bottom,
final_padding_box_width,
)
let inset = child.style.inset
let left = resolve_optional_dimension(inset.left, abs_available.width)
let right = resolve_optional_dimension(inset.right, abs_available.width)
let top = resolve_optional_dimension(inset.top, abs_available.height)
let bottom = resolve_optional_dimension(inset.bottom, abs_available.height)
// Compute the used size for absolutely positioned items.
let mut used_width = resolve_optional_dimension(
child.style.size.width,
abs_available.width,
)
let mut used_height = resolve_optional_dimension(
child.style.size.height,
abs_available.height,
)
let width_was_auto = match used_width {
Some(_) => false
None => true
}
let height_was_auto = match used_height {
Some(_) => false
None => true
}
let mut width_from_inset = false
let mut height_from_inset = false
// If size is auto and both insets are definite, the size is determined by the inset constraints.
match used_width {
Some(_) => ()
None =>
match (left, right) {
(Some(l), Some(r)) =>
used_width = Some(
max_double(
final_padding_box_width -
l -
r -
margin_left_fixed -
margin_right_fixed,
0.0,
),
)
_ => ()
}
}
match used_height {
Some(_) => ()
None =>
match (top, bottom) {
(Some(t), Some(b)) =>
used_height = Some(
max_double(
final_padding_box_height -
t -
b -
margin_top_fixed -
margin_bottom_fixed,
0.0,
),
)
_ => ()
}
}
match (used_width, used_height) {
(Some(_), _) =>
if width_was_auto {
match (left, right) {
(Some(_), Some(_)) => width_from_inset = true
_ => ()
}
} else {
()
}
_ => ()
}
match (used_width, used_height) {
(_, Some(_)) =>
if height_was_auto {
match (top, bottom) {
(Some(_), Some(_)) => height_from_inset = true
_ => ()
}
} else {
()
}
_ => ()
}
// Apply aspect ratio when only one axis is known.
match child.style.aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match (used_width, used_height) {
(Some(w), None) => used_height = Some((w / ratio).round())
(None, Some(h)) => used_width = Some((h * ratio).round())
(Some(w), Some(_h)) =>
if width_was_auto &&
height_was_auto &&
width_from_inset &&
height_from_inset {
used_height = Some((w / ratio).round())
} else {
()
}
_ => ()
}
}
None => ()
}
// Apply aspect ratio to min/max constraints (taffy 0.5 behavior).
let mut min_width = resolve_optional_dimension(
child.style.min_size.width,
abs_available.width,
)
let mut min_height = resolve_optional_dimension(
child.style.min_size.height,
abs_available.height,
)
let mut max_width = resolve_optional_dimension(
child.style.max_size.width,
abs_available.width,
)
let mut max_height = resolve_optional_dimension(
child.style.max_size.height,
abs_available.height,
)
match child.style.aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match (min_width, min_height) {
(None, Some(h)) => min_width = Some((h * ratio).round())
(Some(w), None) => min_height = Some((w / ratio).round())
_ => ()
}
match (max_width, max_height) {
(None, Some(h)) => max_width = Some((h * ratio).round())
(Some(w), None) => max_height = Some((w / ratio).round())
_ => ()
}
} else {
()
}
None => ()
}
// First pass: determine intrinsic size under the containing block constraints, if needed.
let intrinsic = match (used_width, used_height) {
(Some(w), Some(h)) => Size::new(width=w, height=h)
_ => {
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=None, height=None),
abs_available,
Point::zero(),
measure_function,
false,
)
tree.nodes[child_id].layout.size
}
}
let final_width = match used_width {
Some(w) => w
None => intrinsic.width
}
let final_height = match used_height {
Some(h) => h
None => intrinsic.height
}
let mut clamped_final_width = final_width
let mut clamped_final_height = final_height
match min_width {
Some(m) => clamped_final_width = max_double(clamped_final_width, m)
None => ()
}
match max_width {
Some(m) =>
if clamped_final_width > m {
clamped_final_width = m
} else {
()
}
None => ()
}
match min_height {
Some(m) => clamped_final_height = max_double(clamped_final_height, m)
None => ()
}
match max_height {
Some(m) =>
if clamped_final_height > m {
clamped_final_height = m
} else {
()
}
None => ()
}
// Second pass: compute final size (may be clamped by min/max).
compute_node_layout_with_measure(
tree,
child_id,
Size::new(
width=Some(clamped_final_width),
height=Some(clamped_final_height),
),
abs_available,
Point::zero(),
measure_function,
false,
)
let final_size = tree.nodes[child_id].layout.size
let mut final_margin_left = if margin_left_auto {
0.0
} else {
margin_left_fixed
}
let mut final_margin_right = if margin_right_auto {
0.0
} else {
margin_right_fixed
}
let mut final_margin_top = if margin_top_auto {
0.0
} else {
margin_top_fixed
}
let mut final_margin_bottom = if margin_bottom_auto {
0.0
} else {
margin_bottom_fixed
}
// Absolute auto margins: distribute remaining space when both inset sides are definite.
match (left, right) {
(Some(l), Some(r)) => {
let fixed = (if margin_left_auto { 0.0 } else { final_margin_left }) +
(if margin_right_auto { 0.0 } else { final_margin_right })
let remaining = final_padding_box_width -
l -
r -
final_size.width -
fixed
let auto_count = (if margin_left_auto { 1 } else { 0 }) +
(if margin_right_auto { 1 } else { 0 })
match auto_count {
2 =>
if remaining >= 0.0 {
final_margin_left = remaining / 2.0
final_margin_right = remaining / 2.0
} else {
final_margin_left = 0.0
final_margin_right = 0.0
}
1 =>
if margin_left_auto {
final_margin_left = remaining
} else {
final_margin_right = remaining
}
_ => ()
}
}
_ => ()
}
match (top, bottom) {
(Some(t), Some(b)) => {
let fixed = (if margin_top_auto { 0.0 } else { final_margin_top }) +
(if margin_bottom_auto { 0.0 } else { final_margin_bottom })
let remaining = final_padding_box_height -
t -
b -
final_size.height -
fixed
let auto_count = (if margin_top_auto { 1 } else { 0 }) +
(if margin_bottom_auto { 1 } else { 0 })
match auto_count {
2 =>
if remaining >= 0.0 {
final_margin_top = remaining / 2.0
final_margin_bottom = remaining / 2.0
} else {
final_margin_top = 0.0
final_margin_bottom = 0.0
}
1 =>
if margin_top_auto {
final_margin_top = remaining
} else {
final_margin_bottom = remaining
}
_ => ()
}
}
_ => ()
}
let x_in_padding = match left {
Some(v) => v + final_margin_left
None =>
match right {
Some(v) =>
final_padding_box_width - v - final_margin_right - final_size.width
None => padding.left + final_margin_left
}
}
let y_in_padding = match top {
Some(v) => v + final_margin_top
None =>
match bottom {
Some(v) =>
final_padding_box_height -
v -
final_margin_bottom -
final_size.height
None => padding.top + static_y + final_margin_top
}
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=Some(final_size.width), height=Some(final_size.height)),
abs_available,
Point::new(
x=padding_origin.x + x_in_padding,
y=padding_origin.y + y_in_padding,
),
measure_function,
false,
)
}
let effective_top_state = if collapse_top_with_child {
let first_id = match first_in_flow_child {
Some(v) => v
None => node_id
}
collapse_state_merge(
collapse_state_from(own_margin_top),
(
tree.nodes[first_id].effective_margin_top_max_pos,
tree.nodes[first_id].effective_margin_top_min_neg,
),
)
} else {
collapse_state_from(own_margin_top)
}
let effective_bottom_state = if collapse_bottom_with_child {
let last_id = match last_in_flow_child {
Some(v) => v
None => node_id
}
collapse_state_merge(
collapse_state_from(own_margin_bottom),
(
tree.nodes[last_id].effective_margin_bottom_max_pos,
tree.nodes[last_id].effective_margin_bottom_min_neg,
),
)
} else {
collapse_state_from(own_margin_bottom)
}
set_effective_margin_states(
tree, node_id, effective_top_state, effective_bottom_state,
)
}
///|
fn[C] baseline_offset_y(
tree : TaffyTree[C],
node_id : NodeId,
) -> Double raise TaffyError {
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
let node_layout = node.layout
match node.style.display {
DisplayBlock | DisplayGrid => node_layout.size.height
_ =>
if node.children.length() == 0 {
node_layout.size.height
} else {
if node.style.display is DisplayFlex &&
!is_column(node.style.flex_direction) {
let default_align = match node.style.align_items {
Some(v) => v
None => ItemsStretch
}
let mut first_line_top : Double? = None
for child_id in node.children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match (child.style.display, child.style.position) {
(DisplayNone, _) | (_, PosAbsolute) => ()
_ => {
let rel_top = child.layout.location.y - node_layout.location.y
first_line_top = match first_line_top {
Some(v) => Some(min_double(v, rel_top))
None => Some(rel_top)
}
}
}
}
match first_line_top {
Some(first_line_top) => {
let mut first_candidate : NodeId? = None
let mut baseline_candidate : NodeId? = None
for child_id in node.children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match (child.style.display, child.style.position) {
(DisplayNone, _) | (_, PosAbsolute) => ()
_ => {
let rel_top = child.layout.location.y -
node_layout.location.y
if abs_double(rel_top - first_line_top) < 0.0001 {
if first_candidate is None {
first_candidate = Some(child_id)
}
let align = match child.style.align_self {
Some(v) => v
None => default_align
}
if baseline_candidate is None && align is ItemsBaseline {
baseline_candidate = Some(child_id)
}
}
}
}
}
let chosen = match baseline_candidate {
Some(id) => Some(id)
None => first_candidate
}
match chosen {
Some(chosen_id) => {
let chosen_layout = tree.nodes[chosen_id].layout
return chosen_layout.location.y -
node_layout.location.y +
baseline_offset_y(tree, chosen_id)
}
None => ()
}
}
None => ()
}
}
for child_id in node.children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => ()
_ =>
match child.style.position {
PosAbsolute => ()
PosRelative => {
let child_layout = child.layout
let child_margin_top = if node.style.display is DisplayFlex &&
is_column(node.style.flex_direction) {
match child.style.margin.top {
DimPercent(_) =>
resolve_dimension_width_basis(
child.style.margin.top,
node_layout.size.width,
)
_ => 0.0
}
} else {
0.0
}
return child_layout.location.y -
node_layout.location.y +
baseline_offset_y(tree, child_id) -
child_margin_top
}
}
}
}
node_layout.size.height
}
}
}
///|
fn[C] grid_item_baseline_offset_y(
tree : TaffyTree[C],
node_id : NodeId,
) -> Double raise TaffyError {
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
let node_layout = node.layout
if node.children.length() == 0 {
node_layout.size.height
} else {
for child_id in node.children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => ()
_ =>
match child.style.position {
PosAbsolute => ()
PosRelative => {
let child_layout = child.layout
return child_layout.location.y -
node_layout.location.y +
grid_item_baseline_offset_y(tree, child_id)
}
}
}
}
node_layout.size.height
}
}
///|
fn[C] resolve_flex_auto_min_main(
tree : TaffyTree[C],
child_id : NodeId,
is_col : Bool,
parent_available_for_children : Size[AvailableSpace],
main_axis_available : AvailableSpace,
measure_function : (Size[Double?], Size[AvailableSpace], NodeId, C?, Style) -> Size[
Double,
],
) -> Double raise TaffyError {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
let main_from_size = if is_col {
resolve_optional_dimension(child.style.size.height, main_axis_available)
} else {
resolve_optional_dimension(child.style.size.width, main_axis_available)
}
let max_from_style = if is_col {
resolve_optional_dimension(child.style.max_size.height, main_axis_available)
} else {
resolve_optional_dimension(child.style.max_size.width, main_axis_available)
}
let original_style = child.style
let original_layout = child.layout
let cleared_size = if is_col {
Size::new(width=original_style.size.width, height=DimAuto)
} else {
Size::new(width=DimAuto, height=original_style.size.height)
}
tree.nodes[child_id].style = { ..original_style, size: cleared_size }
let min_content_space = if is_col {
Size::new(width=parent_available_for_children.width, height=AvailMinContent)
} else {
Size::new(
width=AvailMinContent,
height=parent_available_for_children.height,
)
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=None, height=None),
min_content_space,
Point::zero(),
measure_function,
false,
)
let min_layout = tree.layout(child_id)
let min_content_main = if is_col {
min_layout.size.height
} else {
min_layout.size.width
}
tree.nodes[child_id].style = original_style
tree.nodes[child_id].layout = original_layout
let mut clamped = min_content_main
match main_from_size {
Some(v) => clamped = min_double(clamped, v)
None => ()
}
match max_from_style {
Some(v) => clamped = min_double(clamped, v)
None => ()
}
let resolved_padding = resolve_rect_width_basis(
original_style.padding,
parent_available_for_children,
)
let resolved_border = resolve_rect_width_basis(
original_style.border,
parent_available_for_children,
)
let padding_main_sum = if is_col {
resolved_padding.top + resolved_padding.bottom
} else {
resolved_padding.left + resolved_padding.right
}
let border_main_sum = if is_col {
resolved_border.top + resolved_border.bottom
} else {
resolved_border.left + resolved_border.right
}
max_double(clamped, padding_main_sum + border_main_sum)
}
///|
fn[C] resolve_wrap_line_main_sizes(
tree : TaffyTree[C],
flow_children : Array[NodeId],
indices : Array[Int],
is_col : Bool,
container_main : Double,
gap_main : Double,
base_sizes : Array[Size[Double]],
flex_grow : Array[Double],
flex_shrink : Array[Double],
margin_main_start : Array[Double],
margin_main_end : Array[Double],
min_main_sizes : Array[Double],
max_main_sizes : Array[Double?],
main_available_pre : AvailableSpace,
parent_available_for_children : Size[AvailableSpace],
measure_function : (Size[Double?], Size[AvailableSpace], NodeId, C?, Style) -> Size[
Double,
],
) -> Array[Double] raise TaffyError {
let flow_count = flow_children.length()
let count = indices.length()
let line_main_sizes : Array[Double] = Array::make(flow_count, 0.0)
let line_frozen : Array[Bool] = Array::make(flow_count, false)
let line_violations : Array[Double] = Array::make(flow_count, 0.0)
for idx in indices {
line_main_sizes[idx] = get_main(base_sizes[idx], is_col)
}
let line_gap_main = if count > 1 {
gap_main * (count - 1).to_double()
} else {
0.0
}
let mut line_initial_used = line_gap_main
for idx in indices {
line_initial_used = line_initial_used +
margin_main_start[idx] +
margin_main_end[idx] +
line_main_sizes[idx]
}
let line_initial_free_space = container_main - line_initial_used
while true {
let mut all_line_frozen = true
for idx in indices {
if !line_frozen[idx] {
all_line_frozen = false
}
}
if all_line_frozen {
break
}
let mut used = line_gap_main
for idx in indices {
used = used +
margin_main_start[idx] +
margin_main_end[idx] +
(if line_frozen[idx] {
line_main_sizes[idx]
} else {
get_main(base_sizes[idx], is_col)
})
}
let free_space_raw = container_main - used
let mut sum_grow = 0.0
let mut sum_shrink = 0.0
let mut sum_scaled_shrink = 0.0
for idx in indices {
if !line_frozen[idx] {
sum_grow = sum_grow + flex_grow[idx]
sum_shrink = sum_shrink + flex_shrink[idx]
sum_scaled_shrink = sum_scaled_shrink +
get_main(base_sizes[idx], is_col) * flex_shrink[idx]
}
}
let free_space = if free_space_raw > 0.0 && sum_grow > 0.0 && sum_grow < 1.0 {
let scaled = line_initial_free_space * sum_grow
if abs_double(scaled) < abs_double(free_space_raw) {
scaled
} else {
free_space_raw
}
} else if free_space_raw < 0.0 && sum_shrink > 0.0 && sum_shrink < 1.0 {
let scaled = line_initial_free_space * sum_shrink
if abs_double(scaled) < abs_double(free_space_raw) {
scaled
} else {
free_space_raw
}
} else {
free_space_raw
}
for idx in indices {
if !line_frozen[idx] {
let base_main = get_main(base_sizes[idx], is_col)
if free_space > 0.0 && sum_grow > 0.0 {
line_main_sizes[idx] = base_main +
free_space * (flex_grow[idx] / sum_grow)
} else if free_space < 0.0 &&
sum_shrink > 0.0 &&
sum_scaled_shrink > 0.0 {
let scaled = base_main * flex_shrink[idx]
line_main_sizes[idx] = base_main +
free_space * (scaled / sum_scaled_shrink)
} else {
line_main_sizes[idx] = base_main
}
}
}
for idx in indices {
let child_for_min = match tree.nodes.get(flow_children[idx]) {
Some(c) => c
None => raise InvalidNodeId(flow_children[idx])
}
let has_definite_basis = resolve_optional_dimension(
child_for_min.style.flex_basis,
main_available_pre,
)
is Some(_)
let should_resolve_auto_min = free_space < 0.0 ||
has_definite_basis ||
(
double_approx_equal(free_space, 0.0) &&
child_for_min.context is Some(_) &&
main_available_pre is AvailMaxContent
)
if !line_frozen[idx] &&
min_main_sizes[idx] < 0.0 &&
should_resolve_auto_min {
let auto_min = resolve_flex_auto_min_main(
tree,
flow_children[idx],
is_col,
parent_available_for_children,
main_available_pre,
measure_function,
)
min_main_sizes[idx] = auto_min
}
}
let mut total_violation = 0.0
for idx in indices {
if !line_frozen[idx] {
let unclamped = line_main_sizes[idx]
let min_main = min_main_sizes[idx]
let max_main = max_main_sizes[idx]
let mut clamped = unclamped
if clamped < min_main {
clamped = min_main
}
match max_main {
Some(v) => if clamped > v { clamped = v }
None => ()
}
if clamped < 0.0 {
clamped = 0.0
}
line_violations[idx] = clamped - unclamped
line_main_sizes[idx] = clamped
total_violation = total_violation + line_violations[idx]
}
}
for idx in indices {
if !line_frozen[idx] {
if total_violation > 0.0 {
line_frozen[idx] = line_violations[idx] > 0.0
} else if total_violation < 0.0 {
line_frozen[idx] = line_violations[idx] < 0.0
} else {
line_frozen[idx] = true
}
}
}
}
line_main_sizes
}
///|
fn compute_single_line_auto_intrinsic_main(
flow_count : Int,
gap_main : Double,
base_main_sizes : Array[Double],
preferred_main_sizes : Array[Double?],
min_main_sizes : Array[Double],
max_main_sizes : Array[Double?],
flex_grow : Array[Double],
flex_shrink : Array[Double],
margin_main_start : Array[Double],
margin_main_end : Array[Double],
content_main_sizes : Array[Double],
) -> Double {
let mut intrinsic_main = if flow_count > 1 {
gap_main * (flow_count - 1).to_double()
} else {
0.0
}
for i in 0.. max_double(flex_basis, pref)
None => flex_basis
}
let flex_basis_min = if flex_shrink[i] == 0.0 {
Some(clamping_basis)
} else {
None
}
let flex_basis_max = if flex_grow[i] == 0.0 {
Some(clamping_basis)
} else {
None
}
let min_main = max_double(
match flex_basis_min {
Some(v) => v
None => resolved_min_main
},
resolved_min_main,
)
let max_main = match (max_main_sizes[i], flex_basis_max) {
(Some(a), Some(b)) => Some(min_double(a, b))
(Some(a), None) => Some(a)
(None, Some(b)) => Some(b)
(None, None) => None
}
let contribution = match (preferred_main_sizes[i], max_main) {
(Some(pref), Some(max_v)) if max_v <= min_main || max_v <= pref => {
let mut v = pref
if v > max_v {
v = max_v
}
if v < min_main {
v = min_main
}
v
}
(_, Some(max_v)) if max_v <= min_main => min_main
_ => {
let mut v = content_main_sizes[i]
if v < min_main {
v = min_main
}
match max_main {
Some(max_v) => if v > max_v { v = max_v }
None => ()
}
v
}
}
intrinsic_main = intrinsic_main +
margin_main_start[i] +
contribution +
margin_main_end[i]
}
intrinsic_main
}
///|
fn[C] compute_flex_layout_with_measure(
tree : TaffyTree[C],
node_id : NodeId,
known_dimensions : Size[Double?],
available_space : Size[AvailableSpace],
absolute_origin : Point[Double],
measure_function : (Size[Double?], Size[AvailableSpace], NodeId, C?, Style) -> Size[
Double,
],
) -> Unit raise TaffyError {
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
let padding = resolve_rect_width_basis(node.style.padding, available_space)
let border = resolve_rect_width_basis(node.style.border, available_space)
let horiz_inset = padding.left + padding.right + border.left + border.right
let vert_inset = padding.top + padding.bottom + border.top + border.bottom
let is_col = is_column(node.style.flex_direction)
let style_width = resolve_optional_dimension(
node.style.size.width,
available_space.width,
)
let style_height = resolve_optional_dimension(
node.style.size.height,
available_space.height,
)
let specified_width = match known_dimensions.width {
Some(w) => Some(w)
None => style_width
}
let specified_height = match known_dimensions.height {
Some(h) => Some(h)
None => style_height
}
let width_def_for_percent = known_dimensions.width is Some(_) ||
style_width is Some(_)
let height_def_for_percent = known_dimensions.height is Some(_) ||
style_height is Some(_)
let tentative_border_box_width = match specified_width {
Some(w) => max_double(w, horiz_inset)
None => horiz_inset
}
let tentative_border_box_height = match specified_height {
Some(h) => max_double(h, vert_inset)
None => vert_inset
}
let tentative_content_width = max_double(
tentative_border_box_width - horiz_inset,
0.0,
)
let tentative_content_height = max_double(
tentative_border_box_height - vert_inset,
0.0,
)
let parent_available_for_children = Size::new(
width=if width_def_for_percent {
AvailDefinite(tentative_content_width)
} else {
available_space.width
},
height=if height_def_for_percent {
AvailDefinite(tentative_content_height)
} else {
available_space.height
},
)
let flow_children : Array[NodeId] = []
let abs_children : Array[NodeId] = []
let hidden_children : Array[NodeId] = []
for child_id in node.children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => hidden_children.push(child_id)
_ =>
match child.style.position {
PosAbsolute => abs_children.push(child_id)
PosRelative => flow_children.push(child_id)
}
}
}
let flow_count = flow_children.length()
let default_align_items = match node.style.align_items {
Some(v) => v
None => ItemsStretch
}
let base_sizes : Array[Size[Double]] = Array::make(flow_count, Size::zero())
let cross_is_auto : Array[Bool] = Array::make(flow_count, true)
let flex_grow : Array[Double] = Array::make(flow_count, 0.0)
let flex_shrink : Array[Double] = Array::make(flow_count, 1.0)
let margin_main_start : Array[Double] = Array::make(flow_count, 0.0)
let margin_main_end : Array[Double] = Array::make(flow_count, 0.0)
let margin_cross_start : Array[Double] = Array::make(flow_count, 0.0)
let margin_cross_end : Array[Double] = Array::make(flow_count, 0.0)
let margin_main_start_auto : Array[Bool] = Array::make(flow_count, false)
let margin_main_end_auto : Array[Bool] = Array::make(flow_count, false)
let margin_cross_start_auto : Array[Bool] = Array::make(flow_count, false)
let margin_cross_end_auto : Array[Bool] = Array::make(flow_count, false)
let align_for_flow : Array[AlignItems] = Array::make(
flow_count, default_align_items,
)
let baseline_offsets : Array[Double] = Array::make(flow_count, 0.0)
let base_main_sizes : Array[Double] = Array::make(flow_count, 0.0)
let preferred_main_sizes : Array[Double?] = Array::make(flow_count, None)
let main_size_is_known_constraint : Array[Bool] = Array::make(
flow_count, false,
)
let content_main_sizes : Array[Double] = Array::make(flow_count, 0.0)
let min_main_sizes : Array[Double] = Array::make(flow_count, 0.0)
let max_main_sizes : Array[Double?] = Array::make(flow_count, None)
let mut total_base_main = 0.0
let mut total_base_margin_main = 0.0
let mut max_cross = 0.0
let main_available_pre = if is_col {
parent_available_for_children.height
} else {
parent_available_for_children.width
}
let cross_available_pre = if is_col {
parent_available_for_children.width
} else {
parent_available_for_children.height
}
let gap_main_pre = resolve_gap_main(
node.style.gap,
is_col,
main_available_pre,
)
let gap_cross_pre = resolve_gap_cross(
node.style.gap,
is_col,
cross_available_pre,
)
let total_gap_main_pre = if flow_count > 1 {
gap_main_pre * (flow_count - 1).to_double()
} else {
0.0
}
let total_gap_cross_pre = if flow_count > 1 {
gap_cross_pre * (flow_count - 1).to_double()
} else {
0.0
}
let intrinsic_main_available = match main_available_pre {
AvailMinContent => AvailMinContent
_ => AvailMaxContent
}
let intrinsic_available_for_children = if is_col {
Size::new(
width=parent_available_for_children.width,
height=intrinsic_main_available,
)
} else {
Size::new(
width=intrinsic_main_available,
height=parent_available_for_children.height,
)
}
for i in 0.. c
None => raise InvalidNodeId(child_id)
}
let align = match child.style.align_self {
Some(v) => v
None => default_align_items
}
align_for_flow[i] = align
let resolved_margin = resolve_rect_width_basis(
child.style.margin,
parent_available_for_children,
)
let resolved_padding = resolve_rect_width_basis(
child.style.padding,
parent_available_for_children,
)
let resolved_border = resolve_rect_width_basis(
child.style.border,
parent_available_for_children,
)
let padding_border_main = if is_col {
resolved_padding.top +
resolved_padding.bottom +
resolved_border.top +
resolved_border.bottom
} else {
resolved_padding.left +
resolved_padding.right +
resolved_border.left +
resolved_border.right
}
let main_margin_start_auto = if is_col {
child.style.margin.top is DimAuto
} else {
child.style.margin.left is DimAuto
}
let main_margin_end_auto = if is_col {
child.style.margin.bottom is DimAuto
} else {
child.style.margin.right is DimAuto
}
let cross_margin_start_auto = if is_col {
child.style.margin.left is DimAuto
} else {
child.style.margin.top is DimAuto
}
let cross_margin_end_auto = if is_col {
child.style.margin.right is DimAuto
} else {
child.style.margin.bottom is DimAuto
}
let main_margin_start = if is_col {
resolved_margin.top
} else {
resolved_margin.left
}
let main_margin_end = if is_col {
resolved_margin.bottom
} else {
resolved_margin.right
}
let cross_margin_start = if is_col {
resolved_margin.left
} else {
resolved_margin.top
}
let cross_margin_end = if is_col {
resolved_margin.right
} else {
resolved_margin.bottom
}
let child_intrinsic_available = if is_col {
match (specified_width, child.style.size.width) {
(None, DimPercent(_)) =>
Size::new(
width=match parent_available_for_children.width {
AvailMinContent => AvailMinContent
_ => AvailMaxContent
},
height=intrinsic_available_for_children.height,
)
_ => intrinsic_available_for_children
}
} else {
intrinsic_available_for_children
}
let intrinsic_known_dimensions = if child.context is Some(_) &&
child.children.length() == 0 &&
align is ItemsStretch {
let intrinsic_cross_available = if is_col {
child_intrinsic_available.width
} else {
child_intrinsic_available.height
}
match intrinsic_cross_available {
AvailDefinite(v) => {
let stretched = max_double(
v - cross_margin_start - cross_margin_end,
0.0,
)
if is_col {
Size::new(width=Some(stretched), height=None)
} else {
Size::new(width=None, height=Some(stretched))
}
}
_ => Size::new(width=None, height=None)
}
} else {
Size::new(width=None, height=None)
}
compute_node_layout_with_measure(
tree,
child_id,
intrinsic_known_dimensions,
child_intrinsic_available,
Point::zero(),
measure_function,
false,
)
let child_layout = tree.layout(child_id)
content_main_sizes[i] = if is_col {
child_layout.size.height
} else {
child_layout.size.width
}
let main_axis_available = main_available_pre
let cross_axis_available = cross_available_pre
let basis = resolve_optional_dimension(
child.style.flex_basis,
main_axis_available,
)
let main_from_size = if is_col {
resolve_optional_dimension(child.style.size.height, main_axis_available)
} else {
resolve_optional_dimension(child.style.size.width, main_axis_available)
}
let max_from_style = if is_col {
resolve_optional_dimension(
child.style.max_size.height,
main_axis_available,
)
} else {
resolve_optional_dimension(
child.style.max_size.width,
main_axis_available,
)
}
let mut main = match basis {
Some(v) => v
None =>
match main_from_size {
Some(v) => v
None =>
if is_col {
child_layout.size.height
} else {
child_layout.size.width
}
}
}
if basis is None &&
main_from_size is None &&
child.children.length() == 0 &&
child.context is None &&
child.style.aspect_ratio is None {
main = padding_border_main
}
if main < padding_border_main {
main = padding_border_main
}
main_size_is_known_constraint[i] = basis is Some(_) ||
main_from_size is Some(_)
base_main_sizes[i] = main
preferred_main_sizes[i] = main_from_size
max_main_sizes[i] = max_from_style
let cross_from_size = if is_col {
match child.style.size.width {
DimPercent(_) =>
if specified_width is None {
None
} else {
resolve_optional_dimension(
child.style.size.width,
cross_axis_available,
)
}
_ =>
resolve_optional_dimension(
child.style.size.width,
cross_axis_available,
)
}
} else {
resolve_optional_dimension(child.style.size.height, cross_axis_available)
}
let cross = match cross_from_size {
Some(v) => v
None =>
if is_col {
child_layout.size.width
} else {
child_layout.size.height
}
}
base_sizes[i] = make_size_from_main_cross(main, cross, is_col)
cross_is_auto[i] = match cross_from_size {
Some(_) => false
None => true
}
flex_grow[i] = child.style.flex_grow
flex_shrink[i] = child.style.flex_shrink
margin_main_start[i] = main_margin_start
margin_main_end[i] = main_margin_end
margin_cross_start[i] = cross_margin_start
margin_cross_end[i] = cross_margin_end
margin_main_start_auto[i] = main_margin_start_auto
margin_main_end_auto[i] = main_margin_end_auto
margin_cross_start_auto[i] = cross_margin_start_auto
margin_cross_end_auto[i] = cross_margin_end_auto
total_base_main = total_base_main + main
total_base_margin_main = total_base_margin_main +
main_margin_start +
main_margin_end
max_cross = max_double(
max_cross,
cross + cross_margin_start + cross_margin_end,
)
if !is_col {
match align {
ItemsBaseline => baseline_offsets[i] = baseline_offset_y(tree, child_id)
_ => ()
}
}
let min_from_style = if is_col {
resolve_optional_dimension(
child.style.min_size.height,
main_axis_available,
)
} else {
resolve_optional_dimension(
child.style.min_size.width,
main_axis_available,
)
}
let overflow_main = if is_col {
child.style.overflow.y
} else {
child.style.overflow.x
}
min_main_sizes[i] = match min_from_style {
Some(v) => max_double(v, 0.0)
None =>
match overflow_main {
OverflowVisible => -1.0
_ => 0.0
}
}
}
if !is_col {
let mut baseline_before_max = 0.0
let mut baseline_after_max = 0.0
for i in 0.. {
let cross = get_cross(base_sizes[i], is_col)
let baseline = baseline_offsets[i]
let after_inner = cross - baseline
let after_inner = if after_inner > 0.0 { after_inner } else { 0.0 }
let before = margin_cross_start[i] + baseline
let after = margin_cross_end[i] + after_inner
baseline_before_max = max_double(baseline_before_max, before)
baseline_after_max = max_double(baseline_after_max, after)
}
_ => ()
}
}
max_cross = max_double(max_cross, baseline_before_max + baseline_after_max)
}
let total_base_outer_main = total_base_main + total_base_margin_main
// Container sizing (border-box)
let raw_width = match specified_width {
Some(w) => max_double(w, horiz_inset)
None =>
if is_col {
max_double(max_cross + total_gap_cross_pre + horiz_inset, horiz_inset)
} else {
max_double(
total_base_outer_main + total_gap_main_pre + horiz_inset,
horiz_inset,
)
}
}
let raw_height = match specified_height {
Some(h) => max_double(h, vert_inset)
None =>
if is_col {
max_double(
total_base_outer_main + total_gap_main_pre + vert_inset,
vert_inset,
)
} else {
max_double(max_cross + total_gap_cross_pre + vert_inset, vert_inset)
}
}
let mut border_box_width = clamp_dimension(
raw_width,
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
)
let mut border_box_height = clamp_dimension(
raw_height,
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
)
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::new(width=border_box_width, height=border_box_height),
}
let mut content_width = max_double(border_box_width - horiz_inset, 0.0)
let mut content_height = max_double(border_box_height - vert_inset, 0.0)
// Resolve gaps again now that the container's content size is known
let main_available_final = if flow_count > 1 {
if is_col {
AvailDefinite(content_height)
} else {
AvailDefinite(content_width)
}
} else if is_col {
resolve_available_for_percent(height_def_for_percent, content_height)
} else {
resolve_available_for_percent(width_def_for_percent, content_width)
}
let cross_available_final = if is_col {
resolve_available_for_percent(width_def_for_percent, content_width)
} else {
resolve_available_for_percent(height_def_for_percent, content_height)
}
let gap_main = resolve_gap_main(node.style.gap, is_col, main_available_final)
let gap_cross = resolve_gap_cross(
node.style.gap,
is_col,
cross_available_final,
)
let justify = match node.style.justify_content {
Some(j) => j
None => AlignFlexStart
}
let is_reverse = match node.style.flex_direction {
FlexRowReverse | FlexColumnReverse => true
_ => false
}
let base_x = absolute_origin.x + border.left + padding.left
let base_y = absolute_origin.y + border.top + padding.top
let mut container_main = if is_col { content_height } else { content_width }
let mut container_cross = if is_col { content_width } else { content_height }
let is_wrap = match node.style.flex_wrap {
FlexNoWrap => false
_ => true
}
let is_wrap_reverse = node.style.flex_wrap is FlexWrapReverse
let main_is_definite = if is_col {
specified_height is Some(_)
} else {
specified_width is Some(_)
}
let cross_is_auto_container = if is_col {
specified_width is None
} else {
specified_height is None
}
if is_wrap && main_is_definite && flow_count > 0 {
// Build flex lines (single pass, simplified).
let lines : Array[Array[Int]] = []
let line_crosses : Array[Double] = []
let line_baseline_befores : Array[Double] = []
let mut current_line : Array[Int] = []
let mut current_outer_main = 0.0
let mut current_cross = 0.0
let mut current_baseline_before = 0.0
let mut current_baseline_after = 0.0
for i in 0.. if main > max_main { main = max_main }
None => ()
}
let cross = get_cross(base_sizes[i], is_col)
let outer_main = margin_main_start[i] + main + margin_main_end[i]
let outer_cross = margin_cross_start[i] + cross + margin_cross_end[i]
let mut item_before = 0.0
let mut item_after = 0.0
if !is_col {
match align_for_flow[i] {
ItemsBaseline => {
let baseline = baseline_offsets[i]
let after_inner = cross - baseline
let after_inner = if after_inner > 0.0 { after_inner } else { 0.0 }
item_before = margin_cross_start[i] + baseline
item_after = margin_cross_end[i] + after_inner
}
_ => ()
}
}
if current_line.length() == 0 {
current_line.push(i)
current_outer_main = outer_main
current_cross = outer_cross
current_baseline_before = item_before
current_baseline_after = item_after
current_cross = max_double(
current_cross,
current_baseline_before + current_baseline_after,
)
} else {
let tentative = current_outer_main + gap_main + outer_main
if tentative > container_main {
lines.push(current_line)
line_crosses.push(current_cross)
line_baseline_befores.push(current_baseline_before)
current_line = []
current_line.push(i)
current_outer_main = outer_main
current_cross = outer_cross
current_baseline_before = item_before
current_baseline_after = item_after
current_cross = max_double(
current_cross,
current_baseline_before + current_baseline_after,
)
} else {
current_line.push(i)
current_outer_main = tentative
current_cross = max_double(current_cross, outer_cross)
current_baseline_before = max_double(
current_baseline_before, item_before,
)
current_baseline_after = max_double(
current_baseline_after, item_after,
)
current_cross = max_double(
current_cross,
current_baseline_before + current_baseline_after,
)
}
}
}
if current_line.length() > 0 {
lines.push(current_line)
line_crosses.push(current_cross)
line_baseline_befores.push(current_baseline_before)
}
let line_count = lines.length()
let mut total_lines_cross = 0.0
for c in line_crosses {
total_lines_cross = total_lines_cross + c
}
let total_cross_gaps = if line_count > 1 {
gap_cross * (line_count - 1).to_double()
} else {
0.0
}
total_lines_cross = total_lines_cross + total_cross_gaps
// If the container's cross size is auto, compute it from wrapped line sizes.
if cross_is_auto_container {
if is_col {
border_box_width = clamp_dimension(
max_double(total_lines_cross + horiz_inset, horiz_inset),
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
)
content_width = max_double(border_box_width - horiz_inset, 0.0)
} else {
border_box_height = clamp_dimension(
max_double(total_lines_cross + vert_inset, vert_inset),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
)
content_height = max_double(border_box_height - vert_inset, 0.0)
}
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::new(width=border_box_width, height=border_box_height),
}
container_main = if is_col { content_height } else { content_width }
container_cross = if is_col { content_width } else { content_height }
}
let align_content = match node.style.align_content {
Some(v) => v
None => AlignStretch
}
let leftover_cross = container_cross - total_lines_cross
let distributable_cross = if leftover_cross > 0.0 {
leftover_cross
} else {
0.0
}
let extra_per_line = match align_content {
AlignStretch =>
if line_count > 0 {
distributable_cross / line_count.to_double()
} else {
0.0
}
_ => 0.0
}
let mut start_cross = 0.0
let mut extra_gap_cross = 0.0
match align_content {
AlignStretch => ()
AlignCenter => start_cross = leftover_cross / 2.0
AlignFlexEnd | AlignEnd => start_cross = leftover_cross
AlignSpaceBetween =>
if line_count > 1 {
extra_gap_cross = distributable_cross / (line_count - 1).to_double()
}
AlignSpaceAround =>
if line_count > 0 {
extra_gap_cross = distributable_cross / line_count.to_double()
start_cross = extra_gap_cross / 2.0
}
AlignSpaceEvenly =>
if line_count > 0 {
extra_gap_cross = distributable_cross / (line_count + 1).to_double()
start_cross = extra_gap_cross
}
_ => ()
}
let line_cross_sizes : Array[Double] = Array::make(line_count, 0.0)
for li in 0.. 1 {
line_used = line_used + gap_main * (count - 1).to_double()
}
let line_leftover = container_main - line_used
let auto_main_start_values : Array[Double] = Array::make(flow_count, 0.0)
let auto_main_end_values : Array[Double] = Array::make(flow_count, 0.0)
let mut line_leftover_for_justify = line_leftover
let mut main_auto_count = 0
for idx in indices {
if margin_main_start_auto[idx] {
main_auto_count = main_auto_count + 1
}
if margin_main_end_auto[idx] {
main_auto_count = main_auto_count + 1
}
}
if main_auto_count > 0 && line_leftover > 0.0 {
let share = line_leftover / main_auto_count.to_double()
for idx in indices {
if margin_main_start_auto[idx] {
auto_main_start_values[idx] = share
}
if margin_main_end_auto[idx] {
auto_main_end_values[idx] = share
}
}
line_leftover_for_justify = 0.0
}
let distributable_line_leftover = if line_leftover_for_justify > 0.0 {
line_leftover_for_justify
} else {
0.0
}
let mut extra_between = 0.0
let start_main = match justify {
AlignSpaceBetween =>
if count > 1 {
extra_between = distributable_line_leftover /
(count - 1).to_double()
0.0
} else {
0.0
}
AlignSpaceAround =>
if count > 0 {
extra_between = distributable_line_leftover / count.to_double()
extra_between / 2.0
} else {
0.0
}
AlignSpaceEvenly =>
if count > 0 {
extra_between = distributable_line_leftover /
(count + 1).to_double()
extra_between
} else {
0.0
}
_ =>
resolve_justify_start_main(
justify, line_leftover_for_justify, is_reverse,
)
}
let actual_gap_main = gap_main + extra_between
let mut main_cursor = if is_reverse {
container_main - start_main
} else {
start_main
}
for idx in indices {
let child_id = flow_children[idx]
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
let main_size = line_main_sizes[idx]
let align = match child.style.align_self {
Some(v) => v
None =>
match node.style.align_items {
Some(v) => v
None => ItemsStretch
}
}
let intrinsic_cross = get_cross(base_sizes[idx], is_col)
let available_cross_for_item = line_cross -
margin_cross_start[idx] -
margin_cross_end[idx]
let stretch_cross_available = max_double(available_cross_for_item, 0.0)
let cross_size_is_auto = if is_col {
child.style.size.width is DimAuto
} else {
child.style.size.height is DimAuto
}
let mut cross_size = match align {
ItemsStretch =>
if cross_size_is_auto {
stretch_cross_available
} else {
intrinsic_cross
}
_ =>
if cross_size_is_auto {
let child_wraps = match child.style.flex_wrap {
FlexNoWrap => false
_ => true
}
if child.children.length() > 0 && child_wraps {
min_double(intrinsic_cross, stretch_cross_available)
} else {
intrinsic_cross
}
} else {
intrinsic_cross
}
}
let cross_axis_available = if is_col {
child_available_for_final.width
} else {
child_available_for_final.height
}
let cross_min = if is_col {
resolve_optional_dimension(
child.style.min_size.width,
cross_axis_available,
)
} else {
resolve_optional_dimension(
child.style.min_size.height,
cross_axis_available,
)
}
let cross_max = if is_col {
resolve_optional_dimension(
child.style.max_size.width,
cross_axis_available,
)
} else {
resolve_optional_dimension(
child.style.max_size.height,
cross_axis_available,
)
}
match cross_min {
Some(v) => if cross_size < v { cross_size = v }
None => ()
}
match cross_max {
Some(v) => if cross_size > v { cross_size = v }
None => ()
}
if cross_size < 0.0 {
cross_size = 0.0
}
let mut auto_cross_start = 0.0
let mut auto_cross_count = 0
if margin_cross_start_auto[idx] {
auto_cross_count = auto_cross_count + 1
}
if margin_cross_end_auto[idx] {
auto_cross_count = auto_cross_count + 1
}
if auto_cross_count > 0 {
let remaining_cross = available_cross_for_item - cross_size
if auto_cross_count == 2 {
if remaining_cross > 0.0 {
auto_cross_start = remaining_cross / 2.0
} else {
auto_cross_start = 0.0
}
} else if margin_cross_start_auto[idx] {
auto_cross_start = if remaining_cross > 0.0 {
remaining_cross
} else {
0.0
}
} else {
()
}
}
let cross_pos_in_available = if auto_cross_count > 0 {
auto_cross_start
} else {
match align {
ItemsBaseline =>
if !is_col {
baseline_before_max -
margin_cross_start[idx] -
baseline_offsets[idx]
} else {
0.0
}
ItemsEnd | ItemsFlexEnd => available_cross_for_item - cross_size
ItemsCenter => (available_cross_for_item - cross_size) / 2.0
_ => 0.0
}
}
let cross_pos_in_available = if is_wrap_reverse {
available_cross_for_item - cross_size - cross_pos_in_available
} else {
cross_pos_in_available
}
let cross_offset = line_offset +
margin_cross_start[idx] +
cross_pos_in_available
let item_main_margin_start = margin_main_start[idx] +
auto_main_start_values[idx]
let item_main_margin_end = margin_main_end[idx] +
auto_main_end_values[idx]
let main_pos = if is_reverse {
main_cursor = main_cursor - item_main_margin_end - main_size
let pos = main_cursor + item_main_margin_start
main_cursor = main_cursor - item_main_margin_start - actual_gap_main
pos
} else {
let pos = main_cursor + item_main_margin_start
main_cursor = main_cursor +
item_main_margin_start +
main_size +
item_main_margin_end +
actual_gap_main
pos
}
let has_measure_context = tree.nodes[child_id].context is Some(_)
let known_width = if has_measure_context {
let main_changed = abs_double(main_size - base_main_sizes[idx]) >
0.0001
let main_known = if main_size_is_known_constraint[idx] || main_changed {
Some(main_size)
} else {
None
}
let cross_known = if !cross_is_auto[idx] {
Some(cross_size)
} else {
match align {
ItemsStretch =>
if cross_is_auto_container &&
main_is_definite &&
flow_count == 1 {
None
} else {
Some(cross_size)
}
_ => None
}
}
if is_col {
cross_known
} else {
main_known
}
} else {
let main_changed = abs_double(main_size - base_main_sizes[idx]) >
0.0001
let child_wraps = match child.style.flex_wrap {
FlexNoWrap => false
_ => true
}
let allow_auto_main = child.children.length() > 0 &&
child_wraps &&
!main_size_is_known_constraint[idx] &&
!main_changed
let main_known = if allow_auto_main { None } else { Some(main_size) }
if is_col {
Some(cross_size)
} else {
main_known
}
}
let known_height = if has_measure_context {
let main_changed = abs_double(main_size - base_main_sizes[idx]) >
0.0001
let main_known = if main_size_is_known_constraint[idx] || main_changed {
Some(main_size)
} else {
None
}
let cross_known = if !cross_is_auto[idx] {
Some(cross_size)
} else {
match align {
ItemsStretch =>
if cross_is_auto_container &&
main_is_definite &&
flow_count == 1 {
None
} else {
Some(cross_size)
}
_ => None
}
}
if is_col {
main_known
} else {
cross_known
}
} else {
let main_changed = abs_double(main_size - base_main_sizes[idx]) >
0.0001
let child_wraps = match child.style.flex_wrap {
FlexNoWrap => false
_ => true
}
let allow_auto_main = child.children.length() > 0 &&
child_wraps &&
!main_size_is_known_constraint[idx] &&
!main_changed
let main_known = if allow_auto_main { None } else { Some(main_size) }
if is_col {
main_known
} else {
Some(cross_size)
}
}
let child_origin = if is_col {
Point::new(x=base_x + cross_offset, y=base_y + main_pos)
} else {
Point::new(x=base_x + main_pos, y=base_y + cross_offset)
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=known_width, height=known_height),
child_available_for_final,
child_origin,
measure_function,
false,
)
}
}
} else {
// Single-line flex layout
let total_gap_main = if flow_count > 1 {
gap_main * (flow_count - 1).to_double()
} else {
0.0
}
if flow_count > 1 &&
!main_is_definite &&
!is_wrap &&
(match justify {
AlignFlexStart | AlignStart => true
_ => false
}) &&
!subtree_has_measure_context(tree, node_id) {
let intrinsic_main = compute_single_line_auto_intrinsic_main(
flow_count, gap_main_pre, base_main_sizes, preferred_main_sizes, min_main_sizes,
max_main_sizes, flex_grow, flex_shrink, margin_main_start, margin_main_end,
content_main_sizes,
)
if intrinsic_main > container_main {
if is_col {
border_box_height = clamp_dimension(
max_double(intrinsic_main + vert_inset, vert_inset),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
)
content_height = max_double(border_box_height - vert_inset, 0.0)
container_main = content_height
} else {
border_box_width = clamp_dimension(
max_double(intrinsic_main + horiz_inset, horiz_inset),
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
)
content_width = max_double(border_box_width - horiz_inset, 0.0)
container_main = content_width
}
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::new(width=border_box_width, height=border_box_height),
}
}
}
// Flex grow/shrink (single-line, simplified)
let final_main_sizes : Array[Double] = Array::make(flow_count, 0.0)
let frozen : Array[Bool] = Array::make(flow_count, false)
let violations : Array[Double] = Array::make(flow_count, 0.0)
let mut initial_used = total_gap_main
for i in 0.. 0.0 && sum_grow > 0.0 && sum_grow < 1.0 {
let scaled = initial_free_space * sum_grow
if abs_double(scaled) < abs_double(free_space) {
scaled
} else {
free_space
}
} else if free_space < 0.0 && sum_shrink > 0.0 && sum_shrink < 1.0 {
let scaled = initial_free_space * sum_shrink
if abs_double(scaled) < abs_double(free_space) {
scaled
} else {
free_space
}
} else {
free_space
}
for i in 0.. 0.0 && sum_grow > 0.0 {
final_main_sizes[i] = base_main +
free_space * (flex_grow[i] / sum_grow)
} else if free_space < 0.0 &&
sum_shrink > 0.0 &&
sum_scaled_shrink > 0.0 {
let scaled = base_main * flex_shrink[i]
final_main_sizes[i] = base_main +
free_space * (scaled / sum_scaled_shrink)
} else {
final_main_sizes[i] = base_main
}
}
}
for i in 0.. c
None => raise InvalidNodeId(flow_children[i])
}
let has_definite_basis = resolve_optional_dimension(
child_for_min.style.flex_basis,
main_available_pre,
)
is Some(_)
let should_resolve_auto_min = free_space < 0.0 ||
has_definite_basis ||
(
double_approx_equal(free_space, 0.0) &&
child_for_min.context is Some(_) &&
main_available_pre is AvailMaxContent &&
cross_available_pre is AvailMaxContent
)
if !frozen[i] && min_main_sizes[i] < 0.0 && should_resolve_auto_min {
let auto_min = resolve_flex_auto_min_main(
tree,
flow_children[i],
is_col,
parent_available_for_children,
main_available_pre,
measure_function,
)
min_main_sizes[i] = auto_min
}
}
let mut total_violation = 0.0
for i in 0.. if clamped > v { clamped = v }
None => ()
}
if clamped < 0.0 {
clamped = 0.0
}
violations[i] = clamped - unclamped
final_main_sizes[i] = clamped
total_violation = total_violation + violations[i]
}
}
for i in 0.. 0.0 {
frozen[i] = violations[i] > 0.0
} else if total_violation < 0.0 {
frozen[i] = violations[i] < 0.0
} else {
frozen[i] = true
}
}
}
}
// Justify content (single-line)
let mut used_main = 0.0
for i in 0.. 0 && leftover > 0.0 {
let share = leftover / main_auto_count.to_double()
for i in 0.. 0.0 {
leftover_for_justify
} else {
0.0
}
let mut extra_between = 0.0
let start_main = match justify {
AlignSpaceBetween =>
if flow_count > 1 {
extra_between = distributable_leftover / (flow_count - 1).to_double()
0.0
} else {
0.0
}
AlignSpaceAround =>
if flow_count > 0 {
extra_between = distributable_leftover / flow_count.to_double()
extra_between / 2.0
} else {
0.0
}
AlignSpaceEvenly =>
if flow_count > 0 {
extra_between = distributable_leftover / (flow_count + 1).to_double()
extra_between
} else {
0.0
}
_ => resolve_justify_start_main(justify, leftover_for_justify, is_reverse)
}
let actual_gap_main = gap_main + extra_between
let child_available_for_final = Size::new(
width=AvailDefinite(content_width),
height=AvailDefinite(content_height),
)
let container_cross_for_placement = {
let s = tree.nodes[node_id].layout.size
if is_col {
max_double(s.width - horiz_inset, 0.0)
} else {
max_double(s.height - vert_inset, 0.0)
}
}
let mut baseline_before_max = 0.0
if !is_col {
let mut baseline_after_max = 0.0
for i in 0.. {
let cross = get_cross(base_sizes[i], is_col)
let baseline = baseline_offsets[i]
let after_inner = cross - baseline
let after_inner = if after_inner > 0.0 { after_inner } else { 0.0 }
let before = margin_cross_start[i] + baseline
let after = margin_cross_end[i] + after_inner
baseline_before_max = max_double(baseline_before_max, before)
baseline_after_max = max_double(baseline_after_max, after)
}
_ => ()
}
}
}
let mut cursor = if is_reverse {
container_main - start_main
} else {
start_main
}
for i in 0.. c
None => raise InvalidNodeId(child_id)
}
let main_size = final_main_sizes[i]
let align = match child.style.align_self {
Some(v) => v
None =>
match node.style.align_items {
Some(v) => v
None => ItemsStretch
}
}
let intrinsic_cross = get_cross(base_sizes[i], is_col)
let available_cross_for_item = container_cross_for_placement -
margin_cross_start[i] -
margin_cross_end[i]
let stretch_cross_available = max_double(available_cross_for_item, 0.0)
let cross_size_is_auto = if is_col {
child.style.size.width is DimAuto
} else {
child.style.size.height is DimAuto
}
let mut cross_size = match align {
ItemsStretch =>
if cross_size_is_auto {
stretch_cross_available
} else {
intrinsic_cross
}
_ =>
if cross_size_is_auto {
let child_wraps = match child.style.flex_wrap {
FlexNoWrap => false
_ => true
}
if child.children.length() > 0 && child_wraps {
min_double(intrinsic_cross, stretch_cross_available)
} else {
intrinsic_cross
}
} else {
intrinsic_cross
}
}
let cross_axis_available = if is_col {
child_available_for_final.width
} else {
child_available_for_final.height
}
let cross_min = if is_col {
resolve_optional_dimension(
child.style.min_size.width,
cross_axis_available,
)
} else {
resolve_optional_dimension(
child.style.min_size.height,
cross_axis_available,
)
}
let cross_max = if is_col {
resolve_optional_dimension(
child.style.max_size.width,
cross_axis_available,
)
} else {
resolve_optional_dimension(
child.style.max_size.height,
cross_axis_available,
)
}
match cross_min {
Some(v) => if cross_size < v { cross_size = v }
None => ()
}
match cross_max {
Some(v) => if cross_size > v { cross_size = v }
None => ()
}
if cross_size < 0.0 {
cross_size = 0.0
}
let mut auto_cross_start = 0.0
let mut auto_cross_count = 0
if margin_cross_start_auto[i] {
auto_cross_count = auto_cross_count + 1
}
if margin_cross_end_auto[i] {
auto_cross_count = auto_cross_count + 1
}
if auto_cross_count > 0 {
let remaining_cross = available_cross_for_item - cross_size
if auto_cross_count == 2 {
if remaining_cross > 0.0 {
auto_cross_start = remaining_cross / 2.0
} else {
auto_cross_start = 0.0
}
} else if margin_cross_start_auto[i] {
auto_cross_start = if remaining_cross > 0.0 {
remaining_cross
} else {
0.0
}
} else {
()
}
}
let cross_pos_in_available = if auto_cross_count > 0 {
auto_cross_start
} else {
match align {
ItemsBaseline =>
if !is_col {
baseline_before_max - margin_cross_start[i] - baseline_offsets[i]
} else {
0.0
}
ItemsEnd | ItemsFlexEnd => available_cross_for_item - cross_size
ItemsCenter => (available_cross_for_item - cross_size) / 2.0
_ => 0.0
}
}
let cross_pos_in_available = if is_wrap_reverse {
available_cross_for_item - cross_size - cross_pos_in_available
} else {
cross_pos_in_available
}
let cross_offset = margin_cross_start[i] + cross_pos_in_available
let item_main_margin_start = margin_main_start[i] +
auto_main_start_values[i]
let item_main_margin_end = margin_main_end[i] + auto_main_end_values[i]
let main_pos = if is_reverse {
cursor = cursor - item_main_margin_end - main_size
let pos = cursor + item_main_margin_start
cursor = cursor - item_main_margin_start - actual_gap_main
pos
} else {
let pos = cursor + item_main_margin_start
cursor = cursor +
item_main_margin_start +
main_size +
item_main_margin_end +
actual_gap_main
pos
}
let has_measure_context = tree.nodes[child_id].context is Some(_)
let known_width = if has_measure_context {
let main_changed = abs_double(main_size - base_main_sizes[i]) > 0.0001
let main_known = if main_size_is_known_constraint[i] || main_changed {
Some(main_size)
} else {
None
}
let cross_known = if !cross_is_auto[i] {
Some(cross_size)
} else {
match align {
ItemsStretch =>
if cross_is_auto_container && main_is_definite && flow_count == 1 {
None
} else {
Some(cross_size)
}
_ => None
}
}
if is_col {
cross_known
} else {
main_known
}
} else {
let main_changed = abs_double(main_size - base_main_sizes[i]) > 0.0001
let child_wraps = match child.style.flex_wrap {
FlexNoWrap => false
_ => true
}
let allow_auto_main = child.children.length() > 0 &&
child_wraps &&
!main_size_is_known_constraint[i] &&
!main_changed
let main_known = if allow_auto_main { None } else { Some(main_size) }
let allow_auto_cross = child.children.length() > 0 &&
child_wraps &&
cross_is_auto[i]
let cross_known = if !cross_is_auto[i] {
Some(cross_size)
} else {
match align {
ItemsStretch =>
if cross_is_auto_container && allow_auto_cross {
None
} else {
Some(cross_size)
}
_ => Some(cross_size)
}
}
if is_col {
cross_known
} else {
main_known
}
}
let known_height = if has_measure_context {
let main_changed = abs_double(main_size - base_main_sizes[i]) > 0.0001
let main_known = if main_size_is_known_constraint[i] || main_changed {
Some(main_size)
} else {
None
}
let cross_known = if !cross_is_auto[i] {
Some(cross_size)
} else {
match align {
ItemsStretch =>
if cross_is_auto_container && main_is_definite && flow_count == 1 {
None
} else {
Some(cross_size)
}
_ => None
}
}
if is_col {
main_known
} else {
cross_known
}
} else {
let main_changed = abs_double(main_size - base_main_sizes[i]) > 0.0001
let child_wraps = match child.style.flex_wrap {
FlexNoWrap => false
_ => true
}
let allow_auto_main = child.children.length() > 0 &&
child_wraps &&
!main_size_is_known_constraint[i] &&
!main_changed
let main_known = if allow_auto_main { None } else { Some(main_size) }
let allow_auto_cross = child.children.length() > 0 &&
child_wraps &&
cross_is_auto[i]
let cross_known = if !cross_is_auto[i] {
Some(cross_size)
} else {
match align {
ItemsStretch =>
if cross_is_auto_container && allow_auto_cross {
None
} else {
Some(cross_size)
}
_ => Some(cross_size)
}
}
if is_col {
main_known
} else {
cross_known
}
}
let child_origin = if is_col {
Point::new(x=base_x + cross_offset, y=base_y + main_pos)
} else {
Point::new(x=base_x + main_pos, y=base_y + cross_offset)
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=known_width, height=known_height),
child_available_for_final,
child_origin,
measure_function,
false,
)
}
if cross_is_auto_container &&
flow_count > 1 &&
!subtree_has_measure_context(tree, node_id) {
let mut needed_cross = 0.0
for i in 0.. needed_cross {
needed_cross = end
}
}
container_cross = needed_cross
if is_col {
border_box_width = clamp_dimension(
max_double(container_cross + horiz_inset, horiz_inset),
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
)
content_width = max_double(border_box_width - horiz_inset, 0.0)
} else {
border_box_height = clamp_dimension(
max_double(container_cross + vert_inset, vert_inset),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
)
content_height = max_double(border_box_height - vert_inset, 0.0)
}
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::new(width=border_box_width, height=border_box_height),
}
}
}
let justify_is_startish = match justify {
AlignFlexStart | AlignStart => true
_ => false
}
let align_items_for_resize = match node.style.align_items {
Some(v) => v
None => ItemsStretch
}
let cross_align_is_stretch = align_items_for_resize is ItemsStretch
let allow_auto_resize = !subtree_has_measure_context(tree, node_id)
if flow_count == 1 &&
(!main_is_definite || cross_is_auto_container) &&
justify_is_startish &&
cross_align_is_stretch {
let only_child = flow_children[0]
let child = match tree.nodes.get(only_child) {
Some(c) => c
None => raise InvalidNodeId(only_child)
}
let child_layout = tree.layout(only_child)
let child_main_size = if is_col {
child_layout.size.height
} else {
child_layout.size.width
}
let child_cross_size = if is_col {
child_layout.size.width
} else {
child_layout.size.height
}
let mut child_outer_main = margin_main_start[0] +
child_main_size +
margin_main_end[0]
let child_outer_cross = margin_cross_start[0] +
child_cross_size +
margin_cross_end[0]
let cross_axis_available_for_min = if is_col {
AvailDefinite(content_width)
} else {
AvailDefinite(content_height)
}
let cross_min_for_resize = if is_col {
resolve_optional_dimension(
child.style.min_size.width,
cross_axis_available_for_min,
)
} else {
resolve_optional_dimension(
child.style.min_size.height,
cross_axis_available_for_min,
)
}
let needs_main_resize_for_cross_min = match cross_min_for_resize {
Some(v) => v > container_cross + 0.0001
None => false
}
let needs_main_resize_for_measured_margin_reflow = child.context is Some(_) &&
(
margin_main_start[0] > 0.0 ||
margin_main_end[0] > 0.0 ||
margin_cross_start[0] > 0.0 ||
margin_cross_end[0] > 0.0
) &&
abs_double(child_main_size - base_main_sizes[0]) > 0.0001
let child_basis_is_definite = resolve_optional_dimension(
child.style.flex_basis,
main_available_pre,
)
is Some(_)
let force_basisless_leaf_main = !is_col &&
child.children.length() == 0 &&
child.style.size.width is DimAuto &&
child.context is None &&
child_basis_is_definite &&
main_available_pre is AvailMaxContent
if force_basisless_leaf_main {
child_outer_main = margin_main_start[0] +
content_main_sizes[0] +
margin_main_end[0]
}
let mut resized = false
if !main_is_definite &&
(
allow_auto_resize ||
needs_main_resize_for_cross_min ||
needs_main_resize_for_measured_margin_reflow
) {
container_main = child_outer_main
if is_col {
border_box_height = clamp_dimension(
max_double(container_main + vert_inset, vert_inset),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
)
content_height = max_double(border_box_height - vert_inset, 0.0)
} else {
border_box_width = clamp_dimension(
max_double(container_main + horiz_inset, horiz_inset),
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
)
content_width = max_double(border_box_width - horiz_inset, 0.0)
}
resized = true
}
if cross_is_auto_container {
container_cross = child_outer_cross
if is_col {
border_box_width = clamp_dimension(
max_double(container_cross + horiz_inset, horiz_inset),
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
)
content_width = max_double(border_box_width - horiz_inset, 0.0)
} else {
border_box_height = clamp_dimension(
max_double(container_cross + vert_inset, vert_inset),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
)
content_height = max_double(border_box_height - vert_inset, 0.0)
}
resized = true
}
if resized {
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::new(width=border_box_width, height=border_box_height),
}
if force_basisless_leaf_main {
let child_available_for_final = Size::new(
width=AvailDefinite(content_width),
height=AvailDefinite(content_height),
)
let child_origin = Point::new(
x=base_x + margin_main_start[0],
y=base_y + margin_cross_start[0],
)
compute_node_layout_with_measure(
tree,
only_child,
Size::new(
width=Some(content_main_sizes[0]),
height=Some(child_cross_size),
),
child_available_for_final,
child_origin,
measure_function,
false,
)
}
}
}
if flow_count > 1 &&
!main_is_definite &&
!is_wrap &&
abs_double(gap_main - gap_main_pre) < 0.0001 &&
justify_is_startish &&
allow_auto_resize {
let mut needed_main = 0.0
for i in 0.. needed_main {
needed_main = end
}
}
let mut intrinsic_needed_main = if flow_count > 1 {
gap_main_pre * (flow_count - 1).to_double()
} else {
0.0
}
for i in 0.. max_double(flex_basis, pref)
None => flex_basis
}
let flex_basis_min = if flex_shrink[i] == 0.0 {
Some(clamping_basis)
} else {
None
}
let flex_basis_max = if flex_grow[i] == 0.0 {
Some(clamping_basis)
} else {
None
}
let min_main = max_double(
match flex_basis_min {
Some(v) => v
None => resolved_min_main
},
resolved_min_main,
)
let max_main = match (max_main_sizes[i], flex_basis_max) {
(Some(a), Some(b)) => Some(min_double(a, b))
(Some(a), None) => Some(a)
(None, Some(b)) => Some(b)
(None, None) => None
}
let content_contribution = match (preferred_main_sizes[i], max_main) {
(Some(pref), Some(max_v)) if max_v <= min_main || max_v <= pref => {
let mut v = pref
if v > max_v {
v = max_v
}
if v < min_main {
v = min_main
}
v
}
(_, Some(max_v)) if max_v <= min_main => min_main
_ => {
let mut v = content_main_sizes[i]
if v < min_main {
v = min_main
}
match max_main {
Some(max_v) => if v > max_v { v = max_v }
None => ()
}
v
}
}
intrinsic_needed_main = intrinsic_needed_main +
margin_main_start[i] +
content_contribution +
margin_main_end[i]
}
if intrinsic_needed_main > needed_main {
needed_main = intrinsic_needed_main
}
if is_col {
border_box_height = clamp_dimension(
max_double(needed_main + vert_inset, vert_inset),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
)
content_height = max_double(border_box_height - vert_inset, 0.0)
container_main = content_height
} else {
border_box_width = clamp_dimension(
max_double(needed_main + horiz_inset, horiz_inset),
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
)
content_width = max_double(border_box_width - horiz_inset, 0.0)
container_main = content_width
}
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::new(width=border_box_width, height=border_box_height),
}
if cross_is_auto_container {
let mut needed_cross = 0.0
for i in 0.. needed_cross {
needed_cross = end
}
}
if is_col {
border_box_width = clamp_dimension(
max_double(needed_cross + horiz_inset, horiz_inset),
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
)
content_width = max_double(border_box_width - horiz_inset, 0.0)
container_cross = content_width
} else {
border_box_height = clamp_dimension(
max_double(needed_cross + vert_inset, vert_inset),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
)
content_height = max_double(border_box_height - vert_inset, 0.0)
container_cross = content_height
}
tree.nodes[node_id].layout = Layout::{
location: absolute_origin,
size: Size::new(width=border_box_width, height=border_box_height),
}
}
}
// Absolute positioned children: compute after container size is known, and do not affect flow layout.
// Follow the same sizing rules as the block layout absolute-positioning pass:
// inset constraints can determine auto sizes, and aspect-ratio participates in sizing.
let padding_origin = Point::new(
x=absolute_origin.x + border.left,
y=absolute_origin.y + border.top,
)
let padding_box_width = max_double(
border_box_width - border.left - border.right,
0.0,
)
let padding_box_height = max_double(
border_box_height - border.top - border.bottom,
0.0,
)
let abs_available = Size::new(
width=AvailDefinite(padding_box_width),
height=AvailDefinite(padding_box_height),
)
for child_id in abs_children {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
let margin = child.style.margin
// CSS compatibility: margin percentages are resolved against the width.
let margin_left_auto = margin.left is DimAuto
let margin_right_auto = margin.right is DimAuto
let margin_top_auto = margin.top is DimAuto
let margin_bottom_auto = margin.bottom is DimAuto
let margin_left_fixed = resolve_dimension_width_basis(
margin.left,
padding_box_width,
)
let margin_right_fixed = resolve_dimension_width_basis(
margin.right,
padding_box_width,
)
let margin_top_fixed = resolve_dimension_width_basis(
margin.top,
padding_box_width,
)
let margin_bottom_fixed = resolve_dimension_width_basis(
margin.bottom,
padding_box_width,
)
let inset = child.style.inset
let left = resolve_optional_dimension(inset.left, abs_available.width)
let right = resolve_optional_dimension(inset.right, abs_available.width)
let top = resolve_optional_dimension(inset.top, abs_available.height)
let bottom = resolve_optional_dimension(inset.bottom, abs_available.height)
// Compute the used size for absolutely positioned items.
let mut used_width = resolve_optional_dimension(
child.style.size.width,
abs_available.width,
)
let mut used_height = resolve_optional_dimension(
child.style.size.height,
abs_available.height,
)
let width_was_auto = used_width is None
let height_was_auto = used_height is None
let mut width_from_inset = false
let mut height_from_inset = false
// If size is auto and both insets are definite, the size is determined by the inset constraints.
match used_width {
Some(_) => ()
None =>
match (left, right) {
(Some(l), Some(r)) =>
used_width = Some(
max_double(
padding_box_width -
l -
r -
margin_left_fixed -
margin_right_fixed,
0.0,
),
)
_ => ()
}
}
match used_height {
Some(_) => ()
None =>
match (top, bottom) {
(Some(t), Some(b)) =>
used_height = Some(
max_double(
padding_box_height -
t -
b -
margin_top_fixed -
margin_bottom_fixed,
0.0,
),
)
_ => ()
}
}
match (used_width, used_height) {
(Some(_), _) =>
if width_was_auto {
match (left, right) {
(Some(_), Some(_)) => width_from_inset = true
_ => ()
}
} else {
()
}
_ => ()
}
match (used_width, used_height) {
(_, Some(_)) =>
if height_was_auto {
match (top, bottom) {
(Some(_), Some(_)) => height_from_inset = true
_ => ()
}
} else {
()
}
_ => ()
}
// Apply aspect ratio when only one axis is known.
match child.style.aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match (used_width, used_height) {
(Some(w), None) => used_height = Some((w / ratio).round())
(None, Some(h)) => used_width = Some((h * ratio).round())
(Some(w), Some(_h)) =>
if width_was_auto &&
height_was_auto &&
width_from_inset &&
height_from_inset {
used_height = Some((w / ratio).round())
} else {
()
}
_ => ()
}
}
None => ()
}
// Apply aspect ratio to min/max constraints (taffy 0.5 behavior).
let mut min_width = resolve_optional_dimension(
child.style.min_size.width,
abs_available.width,
)
let mut min_height = resolve_optional_dimension(
child.style.min_size.height,
abs_available.height,
)
let mut max_width = resolve_optional_dimension(
child.style.max_size.width,
abs_available.width,
)
let mut max_height = resolve_optional_dimension(
child.style.max_size.height,
abs_available.height,
)
match child.style.aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match (min_width, min_height) {
(None, Some(h)) => min_width = Some((h * ratio).round())
(Some(w), None) => min_height = Some((w / ratio).round())
_ => ()
}
match (max_width, max_height) {
(None, Some(h)) => max_width = Some((h * ratio).round())
(Some(w), None) => max_height = Some((w / ratio).round())
_ => ()
}
} else {
()
}
None => ()
}
// First pass: determine intrinsic size under the containing block constraints, if needed.
let intrinsic = match (used_width, used_height) {
(Some(w), Some(h)) => Size::new(width=w, height=h)
_ => {
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=None, height=None),
abs_available,
Point::zero(),
measure_function,
false,
)
tree.nodes[child_id].layout.size
}
}
let final_width = match used_width {
Some(w) => w
None => intrinsic.width
}
let final_height = match used_height {
Some(h) => h
None => intrinsic.height
}
let mut clamped_final_width = final_width
let mut clamped_final_height = final_height
match min_width {
Some(m) => clamped_final_width = max_double(clamped_final_width, m)
None => ()
}
match max_width {
Some(m) =>
if clamped_final_width > m {
clamped_final_width = m
} else {
()
}
None => ()
}
match min_height {
Some(m) => clamped_final_height = max_double(clamped_final_height, m)
None => ()
}
match max_height {
Some(m) =>
if clamped_final_height > m {
clamped_final_height = m
} else {
()
}
None => ()
}
// Second pass: compute final size (may be clamped by min/max).
compute_node_layout_with_measure(
tree,
child_id,
Size::new(
width=Some(clamped_final_width),
height=Some(clamped_final_height),
),
abs_available,
Point::zero(),
measure_function,
false,
)
let final_size = tree.nodes[child_id].layout.size
let mut final_margin_left = if margin_left_auto {
0.0
} else {
margin_left_fixed
}
let mut final_margin_right = if margin_right_auto {
0.0
} else {
margin_right_fixed
}
let mut final_margin_top = if margin_top_auto {
0.0
} else {
margin_top_fixed
}
let mut final_margin_bottom = if margin_bottom_auto {
0.0
} else {
margin_bottom_fixed
}
// Absolute auto margins: distribute remaining space when both inset sides are definite.
match (left, right) {
(Some(l), Some(r)) => {
let fixed = (if margin_left_auto { 0.0 } else { final_margin_left }) +
(if margin_right_auto { 0.0 } else { final_margin_right })
let remaining = padding_box_width - l - r - final_size.width - fixed
let auto_count = (if margin_left_auto { 1 } else { 0 }) +
(if margin_right_auto { 1 } else { 0 })
match auto_count {
2 =>
if remaining >= 0.0 {
final_margin_left = remaining / 2.0
final_margin_right = remaining / 2.0
} else {
final_margin_left = 0.0
final_margin_right = 0.0
}
1 =>
if margin_left_auto {
final_margin_left = remaining
} else {
final_margin_right = remaining
}
_ => ()
}
}
_ => ()
}
match (top, bottom) {
(Some(t), Some(b)) => {
let fixed = (if margin_top_auto { 0.0 } else { final_margin_top }) +
(if margin_bottom_auto { 0.0 } else { final_margin_bottom })
let remaining = padding_box_height - t - b - final_size.height - fixed
let auto_count = (if margin_top_auto { 1 } else { 0 }) +
(if margin_bottom_auto { 1 } else { 0 })
match auto_count {
2 =>
if remaining >= 0.0 {
final_margin_top = remaining / 2.0
final_margin_bottom = remaining / 2.0
} else {
final_margin_top = 0.0
final_margin_bottom = 0.0
}
1 =>
if margin_top_auto {
final_margin_top = remaining
} else {
final_margin_bottom = remaining
}
_ => ()
}
}
_ => ()
}
// Static position fallback when no inset is specified on an axis.
let main_margin_start = if is_col {
final_margin_top
} else {
final_margin_left
}
let main_margin_end = if is_col {
final_margin_bottom
} else {
final_margin_right
}
let cross_margin_start = if is_col {
final_margin_left
} else {
final_margin_top
}
let cross_margin_end = if is_col {
final_margin_right
} else {
final_margin_bottom
}
let main_size = if is_col { final_size.height } else { final_size.width }
let cross_size = if is_col { final_size.width } else { final_size.height }
let outer_main = main_margin_start + main_size + main_margin_end
let leftover_main = container_main - outer_main
let leftover_main = if leftover_main > 0.0 { leftover_main } else { 0.0 }
let static_start_main = resolve_justify_start_main(
justify, leftover_main, is_reverse,
)
let static_main = static_start_main + main_margin_start
let align = match child.style.align_self {
Some(v) => v
None =>
match node.style.align_items {
Some(v) => v
None => ItemsStretch
}
}
let available_cross_for_item = max_double(
container_cross - cross_margin_start - cross_margin_end,
0.0,
)
let cross_pos_in_available = match align {
ItemsEnd | ItemsFlexEnd => available_cross_for_item - cross_size
ItemsCenter => (available_cross_for_item - cross_size) / 2.0
_ => 0.0
}
let cross_pos_in_available = if is_wrap_reverse {
available_cross_for_item - cross_size - cross_pos_in_available
} else {
cross_pos_in_available
}
let static_cross = cross_margin_start + cross_pos_in_available
let x_in_padding = match left {
Some(v) => v + final_margin_left
None =>
match right {
Some(v) =>
padding_box_width - v - final_margin_right - final_size.width
None =>
padding.left + (if is_col { static_cross } else { static_main })
}
}
let y_in_padding = match top {
Some(v) => v + final_margin_top
None =>
match bottom {
Some(v) =>
padding_box_height - v - final_margin_bottom - final_size.height
None =>
padding.top + (if is_col { static_main } else { static_cross })
}
}
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=Some(final_size.width), height=Some(final_size.height)),
abs_available,
Point::new(
x=padding_origin.x + x_in_padding,
y=padding_origin.y + y_in_padding,
),
measure_function,
false,
)
}
// Ensure `display: none` children get laid out (to zero) as well.
for child_id in hidden_children {
compute_node_layout_with_measure(
tree,
child_id,
Size::new(width=None, height=None),
abs_available,
Point::zero(),
measure_function,
false,
)
}
let resolved_margin = resolve_rect_width_basis(
node.style.margin,
available_space,
)
set_effective_margin_states(
tree,
node_id,
margin_collapse_state_from(resolved_margin.top),
margin_collapse_state_from(resolved_margin.bottom),
)
}