use fret_core::time::Instant;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use fret_core::{FrameId, NodeId, Point, Px, Rect, Size};
use rustc_hash::FxHashMap;
use serde_json::{Value, json};
use slotmap::SecondaryMap;
use taffy::{TaffyTree, prelude::NodeId as TaffyNodeId};
use crate::layout_constraints::{AvailableSpace, LayoutConstraints, LayoutSize};
use crate::runtime_config::{LayoutEngineSweepPolicy, ui_runtime_config};
mod flow;
pub(crate) use flow::{build_viewport_flow_subtree, layout_children_from_engine_if_solved};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct NodeContext {
node: NodeId,
measured: bool,
min_content_width_as_max: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LayoutId(TaffyNodeId);
#[derive(Debug, Clone, Copy, Default)]
pub struct LayoutEngineMeasureHotspot {
pub node: NodeId,
pub total_time: Duration,
pub calls: u64,
pub cache_hits: u64,
}
pub struct TaffyLayoutEngine {
tree: TaffyTree<NodeContext>,
node_to_layout: SecondaryMap<NodeId, LayoutId>,
layout_to_node: FxHashMap<LayoutId, NodeId>,
styles: SecondaryMap<NodeId, taffy::Style>,
children: SecondaryMap<NodeId, Vec<NodeId>>,
parent: SecondaryMap<NodeId, NodeId>,
seen_generation: u32,
seen_stamp: SecondaryMap<NodeId, u32>,
seen_count: usize,
child_nodes_scratch: Vec<TaffyNodeId>,
child_unique_scratch: Vec<NodeId>,
child_dedupe_set_scratch: HashSet<NodeId>,
mark_seen_stack_scratch: Vec<NodeId>,
mark_solved_stack_scratch: Vec<NodeId>,
clear_solved_stack_scratch: Vec<NodeId>,
solve_generation: u64,
node_solved_stamp: SecondaryMap<NodeId, SolvedStamp>,
root_solve_stamp: SecondaryMap<NodeId, RootSolveStamp>,
measure_cache_scratch: FxHashMap<LayoutMeasureKey, taffy::geometry::Size<f32>>,
batch_root_scratch: Option<TaffyNodeId>,
batch_root_children_scratch: Vec<TaffyNodeId>,
batch_root_style_size_bits: Option<(u32, u32)>,
solve_scale_factor: f32,
frame_id: Option<FrameId>,
last_solve_time: Duration,
last_solve_root: Option<NodeId>,
last_solve_elapsed: Duration,
last_solve_measure_calls: u64,
last_solve_measure_cache_hits: u64,
measure_profiling_enabled: bool,
last_solve_measure_time: Duration,
last_solve_measure_hotspots: Vec<LayoutEngineMeasureHotspot>,
}
#[derive(Debug, Default, Clone)]
pub struct DebugDumpNodeInfo {
pub label: Option<String>,
pub debug: Option<Value>,
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub(crate) struct ChildLayoutRectSolvedStampDebug {
pub(crate) frame_id: FrameId,
pub(crate) solve_generation: u64,
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub(crate) struct ChildLayoutRectMissDebug {
pub(crate) solve_generation: u64,
pub(crate) engine_frame_id: Option<FrameId>,
pub(crate) parent_stamp: Option<ChildLayoutRectSolvedStampDebug>,
pub(crate) child_stamp: Option<ChildLayoutRectSolvedStampDebug>,
pub(crate) parent_seen: bool,
pub(crate) child_seen: bool,
pub(crate) child_engine_parent: Option<NodeId>,
pub(crate) child_layout_id_present: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct RootSolveKey {
width_bits: u64,
height_bits: u64,
scale_bits: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct LayoutMeasureKey {
node: NodeId,
known_w: Option<u32>,
known_h: Option<u32>,
avail_w: (u8, u32),
avail_h: (u8, u32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SolvedStamp {
frame_id: FrameId,
solve_generation: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RootSolveStamp {
frame_id: FrameId,
key: RootSolveKey,
}
impl Default for TaffyLayoutEngine {
fn default() -> Self {
let mut tree = TaffyTree::new();
tree.enable_rounding();
Self {
tree,
node_to_layout: SecondaryMap::new(),
layout_to_node: FxHashMap::default(),
styles: SecondaryMap::new(),
children: SecondaryMap::new(),
parent: SecondaryMap::new(),
seen_generation: 1,
seen_stamp: SecondaryMap::new(),
seen_count: 0,
child_nodes_scratch: Vec::new(),
child_unique_scratch: Vec::new(),
child_dedupe_set_scratch: HashSet::new(),
mark_seen_stack_scratch: Vec::new(),
mark_solved_stack_scratch: Vec::new(),
clear_solved_stack_scratch: Vec::new(),
solve_generation: 0,
node_solved_stamp: SecondaryMap::new(),
root_solve_stamp: SecondaryMap::new(),
measure_cache_scratch: FxHashMap::default(),
batch_root_scratch: None,
batch_root_children_scratch: Vec::new(),
batch_root_style_size_bits: None,
solve_scale_factor: 1.0,
frame_id: None,
last_solve_time: Duration::default(),
last_solve_root: None,
last_solve_elapsed: Duration::default(),
last_solve_measure_calls: 0,
last_solve_measure_cache_hits: 0,
measure_profiling_enabled: false,
last_solve_measure_time: Duration::default(),
last_solve_measure_hotspots: Vec::new(),
}
}
}
impl TaffyLayoutEngine {
fn warn_taffy_error_once(op: &'static str, err: taffy::TaffyError) {
static SEEN: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
if crate::strict_runtime::strict_runtime_enabled() {
panic!("taffy {op} failed: {err:?}");
}
let key = format!("{op}:{err:?}");
let seen = SEEN.get_or_init(|| Mutex::new(HashSet::new()));
let first = match seen.lock() {
Ok(mut guard) => guard.insert(key),
Err(_) => true,
};
if first {
tracing::warn!(
"taffy {op} failed; layout engine results may be missing for affected nodes (falling back to widget layout): {err:?}"
);
}
}
#[inline]
fn mark_seen(&mut self, node: NodeId) {
let prev = self.seen_stamp.insert(node, self.seen_generation);
if prev != Some(self.seen_generation) {
self.seen_count = self.seen_count.saturating_add(1);
}
}
#[inline]
fn is_seen(&self, node: NodeId) -> bool {
self.seen_stamp.get(node).copied() == Some(self.seen_generation)
}
fn invalidate_solved_ancestors(&mut self, mut node: NodeId) {
while let Some(parent) = self.parent.get(node).copied() {
self.node_solved_stamp.remove(parent);
self.root_solve_stamp.remove(parent);
node = parent;
}
}
fn ensure_batch_root_scratch(&mut self) -> Option<TaffyNodeId> {
if let Some(id) = self.batch_root_scratch {
return Some(id);
}
let id = match self.tree.new_leaf_with_context(
Default::default(),
NodeContext {
node: NodeId::default(),
measured: false,
min_content_width_as_max: false,
},
) {
Ok(id) => id,
Err(err) => {
Self::warn_taffy_error_once("new_leaf_with_context(batch_root)", err);
return None;
}
};
self.batch_root_scratch = Some(id);
Some(id)
}
pub fn begin_frame(&mut self, frame_id: FrameId) {
if self.frame_id != Some(frame_id) {
self.frame_id = Some(frame_id);
self.seen_generation = self.seen_generation.wrapping_add(1);
if self.seen_generation == 0 {
self.seen_generation = 1;
self.seen_stamp.clear();
}
self.seen_count = 0;
self.solve_generation = 0;
self.solve_scale_factor = 1.0;
self.last_solve_time = Duration::default();
self.last_solve_root = None;
self.last_solve_elapsed = Duration::default();
self.last_solve_measure_calls = 0;
self.last_solve_measure_cache_hits = 0;
self.last_solve_measure_time = Duration::default();
self.last_solve_measure_hotspots.clear();
}
}
pub fn end_frame(&mut self) {
if ui_runtime_config().layout_engine_sweep_policy == LayoutEngineSweepPolicy::OnDemand
&& self.seen_count == self.node_to_layout.len()
{
return;
}
let stale: Vec<NodeId> = self
.node_to_layout
.iter()
.filter_map(|(node, _)| (!self.is_seen(node)).then_some(node))
.collect();
for node in stale {
let Some(layout_id) = self.node_to_layout.remove(node) else {
continue;
};
self.layout_to_node.remove(&layout_id);
self.styles.remove(node);
self.seen_stamp.remove(node);
let stale_parent = self.parent.get(node).copied();
if let Some(parent) = stale_parent
&& let Some(parent_children) = self.children.get_mut(parent)
{
let old_len = parent_children.len();
parent_children.retain(|&child| child != node);
if parent_children.len() != old_len {
self.node_solved_stamp.remove(parent);
self.root_solve_stamp.remove(parent);
self.invalidate_solved_ancestors(parent);
}
}
if let Some(children) = self.children.remove(node) {
for child in children {
if self.parent.get(child) == Some(&node) {
self.parent.remove(child);
}
}
}
self.parent.remove(node);
self.node_solved_stamp.remove(node);
self.root_solve_stamp.remove(node);
let _ = self.tree.remove(layout_id.0);
}
}
pub fn layout_id_for_node(&self, node: NodeId) -> Option<LayoutId> {
self.node_to_layout.get(node).copied()
}
pub(crate) fn mark_seen_if_present(&mut self, node: NodeId) {
if self.node_to_layout.contains_key(node) {
self.mark_seen(node);
}
}
pub fn node_for_layout_id(&self, id: LayoutId) -> Option<NodeId> {
self.layout_to_node.get(&id).copied()
}
pub fn solve_count(&self) -> u64 {
self.solve_generation
}
pub fn last_solve_time(&self) -> Duration {
self.last_solve_time
}
pub fn last_solve_root(&self) -> Option<NodeId> {
self.last_solve_root
}
pub fn last_solve_elapsed(&self) -> Duration {
self.last_solve_elapsed
}
pub fn last_solve_measure_calls(&self) -> u64 {
self.last_solve_measure_calls
}
pub fn last_solve_measure_cache_hits(&self) -> u64 {
self.last_solve_measure_cache_hits
}
pub fn last_solve_measure_time(&self) -> Duration {
self.last_solve_measure_time
}
pub fn last_solve_measure_hotspots(&self) -> &[LayoutEngineMeasureHotspot] {
self.last_solve_measure_hotspots.as_slice()
}
pub fn set_measure_profiling_enabled(&mut self, enabled: bool) {
self.measure_profiling_enabled = enabled;
}
pub fn child_layout_rect_if_solved(&self, parent: NodeId, child: NodeId) -> Option<Rect> {
if self.solve_generation == 0 {
return None;
}
let frame_id = self.frame_id?;
let parent_stamp = self.node_solved_stamp.get(parent).copied()?;
let child_stamp = self.node_solved_stamp.get(child).copied()?;
if parent_stamp.frame_id != frame_id || child_stamp.frame_id != frame_id {
return None;
}
if parent_stamp.solve_generation == 0
|| child_stamp.solve_generation == 0
|| parent_stamp.solve_generation != child_stamp.solve_generation
{
return None;
}
if !self.is_seen(parent) || !self.is_seen(child) {
return None;
}
if self.parent.get(child) != Some(&parent) {
return None;
}
self.layout_id_for_node(child)
.map(|id| self.layout_rect(id))
}
pub(crate) fn debug_child_layout_rect_miss(
&self,
parent: NodeId,
child: NodeId,
) -> ChildLayoutRectMissDebug {
fn stamp_debug(stamp: Option<SolvedStamp>) -> Option<ChildLayoutRectSolvedStampDebug> {
stamp.map(|s| ChildLayoutRectSolvedStampDebug {
frame_id: s.frame_id,
solve_generation: s.solve_generation,
})
}
ChildLayoutRectMissDebug {
solve_generation: self.solve_generation,
engine_frame_id: self.frame_id,
parent_stamp: stamp_debug(self.node_solved_stamp.get(parent).copied()),
child_stamp: stamp_debug(self.node_solved_stamp.get(child).copied()),
parent_seen: self.is_seen(parent),
child_seen: self.is_seen(child),
child_engine_parent: self.parent.get(child).copied(),
child_layout_id_present: self.layout_id_for_node(child).is_some(),
}
}
pub fn root_is_solved_for(
&self,
root: NodeId,
available: LayoutSize<AvailableSpace>,
scale_factor: f32,
) -> bool {
if !self.is_seen(root) {
return false;
}
let Some(frame_id) = self.frame_id else {
return false;
};
let solved = self
.node_solved_stamp
.get(root)
.copied()
.is_some_and(|s| s.frame_id == frame_id && s.solve_generation != 0);
if !solved {
return false;
}
fn key_bits(axis: AvailableSpace) -> u64 {
match axis {
AvailableSpace::Definite(px) => px.0.to_bits() as u64,
AvailableSpace::MinContent => 1u64 << 32,
AvailableSpace::MaxContent => 2u64 << 32,
}
}
let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
scale_factor
} else {
1.0
};
let key = RootSolveKey {
width_bits: key_bits(available.width),
height_bits: key_bits(available.height),
scale_bits: sf.to_bits(),
};
self.root_solve_stamp
.get(root)
.copied()
.is_some_and(|s| s.frame_id == frame_id && s.key == key)
}
pub fn compute_root_for_node_with_measure_if_needed(
&mut self,
root: NodeId,
available: LayoutSize<AvailableSpace>,
scale_factor: f32,
measure: impl FnMut(NodeId, LayoutConstraints) -> Size,
) -> Option<LayoutId> {
let root_id = self.layout_id_for_node(root)?;
if !self.root_is_solved_for(root, available, scale_factor) {
self.compute_root_with_measure(root_id, available, scale_factor, measure);
}
Some(root_id)
}
pub(crate) fn compute_independent_roots_with_measure_if_needed(
&mut self,
roots: &[(NodeId, LayoutSize<AvailableSpace>)],
scale_factor: f32,
mut measure: impl FnMut(NodeId, LayoutConstraints) -> Size,
) {
let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
scale_factor
} else {
1.0
};
fn compute_individual<F: FnMut(NodeId, LayoutConstraints) -> Size>(
engine: &mut TaffyLayoutEngine,
roots: &[(NodeId, LayoutSize<AvailableSpace>)],
scale_factor: f32,
measure: &mut F,
) {
for &(root, available) in roots {
let _ = engine.compute_root_for_node_with_measure_if_needed(
root,
available,
scale_factor,
&mut *measure,
);
}
}
let mut batchable: Vec<(NodeId, LayoutId, LayoutSize<AvailableSpace>)> =
Vec::with_capacity(roots.len());
let mut fallback: Vec<(NodeId, LayoutSize<AvailableSpace>)> = Vec::new();
for &(root, available) in roots {
let Some(layout_id) = self.layout_id_for_node(root) else {
continue;
};
if self.root_is_solved_for(root, available, sf) {
continue;
}
if let Some(parent) = self.parent.get(root).copied() {
let linked = self
.children
.get(parent)
.is_some_and(|children| children.as_slice().contains(&root));
if linked {
fallback.push((root, available));
continue;
}
self.parent.remove(root);
}
if !matches!(available.width, AvailableSpace::Definite(_))
|| !matches!(available.height, AvailableSpace::Definite(_))
{
fallback.push((root, available));
continue;
}
batchable.push((root, layout_id, available));
}
if batchable.is_empty() {
compute_individual(self, &fallback, sf, &mut measure);
return;
}
if batchable.len() == 1 {
let (root, _layout_id, available) = batchable[0];
let _ = self.compute_root_for_node_with_measure_if_needed(
root,
available,
sf,
&mut measure,
);
compute_individual(self, &fallback, sf, &mut measure);
return;
}
let mut scratch_w_dp: f32 = 0.0;
let mut scratch_h_dp: f32 = 0.0;
for &(_root, _id, available) in &batchable {
let (AvailableSpace::Definite(w), AvailableSpace::Definite(h)) =
(available.width, available.height)
else {
debug_assert!(
false,
"pending roots must be definite in both axes to batch"
);
return;
};
scratch_w_dp = scratch_w_dp.max(w.0.max(0.0) * sf);
scratch_h_dp += h.0.max(0.0) * sf;
}
scratch_w_dp = scratch_w_dp.max(1.0);
scratch_h_dp = scratch_h_dp.max(1.0);
let Some(scratch_id) = self.ensure_batch_root_scratch() else {
for (root, _layout_id, available) in batchable {
let _ = self.compute_root_for_node_with_measure_if_needed(
root,
available,
sf,
&mut measure,
);
}
compute_individual(self, &fallback, sf, &mut measure);
return;
};
let scratch_style = taffy::Style {
display: taffy::style::Display::Block,
size: taffy::geometry::Size {
width: taffy::style::Dimension::length(scratch_w_dp),
height: taffy::style::Dimension::length(scratch_h_dp),
},
max_size: taffy::geometry::Size {
width: taffy::style::Dimension::length(scratch_w_dp),
height: taffy::style::Dimension::length(scratch_h_dp),
},
..Default::default()
};
let scratch_bits = (scratch_w_dp.to_bits(), scratch_h_dp.to_bits());
if self.batch_root_style_size_bits != Some(scratch_bits)
&& let Err(err) = self.tree.set_style(scratch_id, scratch_style)
{
Self::warn_taffy_error_once("set_style(batch_root)", err);
let _ = self.tree.set_children(scratch_id, &[]);
for (root, _layout_id, available) in batchable {
let _ = self.compute_root_for_node_with_measure_if_needed(
root,
available,
sf,
&mut measure,
);
}
compute_individual(self, &fallback, sf, &mut measure);
return;
}
self.batch_root_style_size_bits = Some(scratch_bits);
self.batch_root_children_scratch.clear();
self.batch_root_children_scratch.reserve(batchable.len());
for (_root, id, _available) in &batchable {
self.batch_root_children_scratch.push(id.0);
}
if let Err(err) = self
.tree
.set_children(scratch_id, &self.batch_root_children_scratch)
{
Self::warn_taffy_error_once("set_children(batch_root)", err);
let _ = self.tree.set_children(scratch_id, &[]);
for (root, _layout_id, available) in batchable {
let _ = self.compute_root_for_node_with_measure_if_needed(
root,
available,
sf,
&mut measure,
);
}
compute_individual(self, &fallback, sf, &mut measure);
return;
}
let started = Instant::now();
self.solve_scale_factor = sf;
let span = if tracing::enabled!(tracing::Level::TRACE) {
tracing::trace_span!(
"fret.ui.layout_engine.solve",
root = tracing::field::Empty,
frame_id = self.frame_id.map(|f| f.0).unwrap_or(0),
scale_factor = sf,
elapsed_us = tracing::field::Empty,
measure_calls = tracing::field::Empty,
measure_cache_hits = tracing::field::Empty,
measure_us = tracing::field::Empty,
)
} else {
tracing::Span::none()
};
let _span_guard = span.enter();
let taffy_available = taffy::geometry::Size {
width: taffy::style::AvailableSpace::Definite(scratch_w_dp),
height: taffy::style::AvailableSpace::Definite(scratch_h_dp),
};
let mut measure_calls: u64 = 0;
let mut measure_cache_hits: u64 = 0;
self.measure_cache_scratch.clear();
let measure_cache = &mut self.measure_cache_scratch;
let enable_profile = self.measure_profiling_enabled;
let mut measure_time = Duration::default();
#[derive(Debug, Clone, Copy, Default)]
struct MeasureNodeProfile {
total_time: Duration,
calls: u64,
cache_hits: u64,
}
let mut by_node: Option<SecondaryMap<NodeId, MeasureNodeProfile>> =
enable_profile.then(SecondaryMap::new);
let result = self.tree.compute_layout_with_measure(
scratch_id,
taffy_available,
|known, avail, _id, ctx, _style| {
let Some(ctx) = ctx else {
return taffy::geometry::Size::default();
};
if !ctx.measured {
return taffy::geometry::Size::default();
}
measure_calls = measure_calls.saturating_add(1);
fn quantize_size_key_bits(value: f32) -> u32 {
if !value.is_finite() || value <= 0.0 {
return 0;
}
let quantum = 64.0f32;
let quantized = (value * quantum).round() / quantum;
quantized.to_bits()
}
fn avail_key(
avail: taffy::style::AvailableSpace,
min_content_as_max: bool,
) -> (u8, u32) {
match avail {
taffy::style::AvailableSpace::Definite(v) => (0, quantize_size_key_bits(v)),
taffy::style::AvailableSpace::MinContent => {
if min_content_as_max {
(2, 0)
} else {
(1, 0)
}
}
taffy::style::AvailableSpace::MaxContent => (2, 0),
}
}
let key = LayoutMeasureKey {
node: ctx.node,
known_w: known.width.map(quantize_size_key_bits),
known_h: known.height.map(quantize_size_key_bits),
avail_w: avail_key(avail.width, ctx.min_content_width_as_max),
avail_h: avail_key(avail.height, false),
};
if let Some(size) = measure_cache.get(&key) {
measure_cache_hits = measure_cache_hits.saturating_add(1);
if enable_profile && let Some(by_node) = by_node.as_mut() {
if by_node.get(ctx.node).is_none() {
by_node.insert(ctx.node, MeasureNodeProfile::default());
}
if let Some(profile) = by_node.get_mut(ctx.node) {
profile.cache_hits = profile.cache_hits.saturating_add(1);
}
}
return *size;
}
let constraints = LayoutConstraints::new(
LayoutSize::new(
known.width.map(|w| Px(w / sf)),
known.height.map(|h| Px(h / sf)),
),
LayoutSize::new(
match avail.width {
taffy::style::AvailableSpace::Definite(w) => {
AvailableSpace::Definite(Px(w / sf))
}
taffy::style::AvailableSpace::MinContent => {
if ctx.min_content_width_as_max {
AvailableSpace::MaxContent
} else {
AvailableSpace::MinContent
}
}
taffy::style::AvailableSpace::MaxContent => AvailableSpace::MaxContent,
},
match avail.height {
taffy::style::AvailableSpace::Definite(h) => {
AvailableSpace::Definite(Px(h / sf))
}
taffy::style::AvailableSpace::MinContent => AvailableSpace::MinContent,
taffy::style::AvailableSpace::MaxContent => AvailableSpace::MaxContent,
},
),
);
let (s, elapsed) = if enable_profile {
let measure_started = Instant::now();
let size = measure(ctx.node, constraints);
(size, measure_started.elapsed())
} else {
(measure(ctx.node, constraints), Duration::default())
};
if enable_profile {
measure_time += elapsed;
if let Some(by_node) = by_node.as_mut() {
if by_node.get(ctx.node).is_none() {
by_node.insert(ctx.node, MeasureNodeProfile::default());
}
if let Some(profile) = by_node.get_mut(ctx.node) {
profile.total_time += elapsed;
profile.calls = profile.calls.saturating_add(1);
} else {
debug_assert!(
false,
"layout engine profiling: expected node profile to exist after insert"
);
}
}
}
let out = taffy::geometry::Size {
width: s.width.0 * sf,
height: s.height.0 * sf,
};
measure_cache.insert(key, out);
out
},
);
self.last_solve_measure_calls = measure_calls;
self.last_solve_measure_cache_hits = measure_cache_hits;
self.last_solve_measure_time = measure_time;
if enable_profile {
const MAX_HOTSPOTS: usize = 8;
let mut hotspots: Vec<LayoutEngineMeasureHotspot> = by_node
.unwrap_or_default()
.into_iter()
.map(|(node, p)| LayoutEngineMeasureHotspot {
node,
total_time: p.total_time,
calls: p.calls,
cache_hits: p.cache_hits,
})
.collect();
hotspots.sort_by_key(|h| std::cmp::Reverse(h.total_time));
hotspots.truncate(MAX_HOTSPOTS);
self.last_solve_measure_hotspots = hotspots;
} else {
self.last_solve_measure_hotspots.clear();
}
if let Err(err) = result {
Self::warn_taffy_error_once("compute_layout_with_measure(batch_root)", err);
let _ = self.tree.set_children(scratch_id, &[]);
self.last_solve_root = None;
self.last_solve_elapsed = started.elapsed();
span.record("elapsed_us", self.last_solve_elapsed.as_micros() as u64);
span.record("measure_calls", measure_calls);
span.record("measure_cache_hits", measure_cache_hits);
span.record("measure_us", measure_time.as_micros() as u64);
self.last_solve_time += self.last_solve_elapsed;
compute_individual(self, &fallback, sf, &mut measure);
return;
}
self.solve_generation = self.solve_generation.saturating_add(1);
let stamp_root = batchable[0].0;
span.record("root", tracing::field::debug(stamp_root));
self.last_solve_root = Some(stamp_root);
fn key_bits(axis: AvailableSpace) -> u64 {
match axis {
AvailableSpace::Definite(px) => px.0.to_bits() as u64,
AvailableSpace::MinContent => 1u64 << 32,
AvailableSpace::MaxContent => 2u64 << 32,
}
}
if let Some(frame_id) = self.frame_id {
for &(root, _id, available) in &batchable {
self.mark_solved_subtree(root);
self.root_solve_stamp.insert(
root,
RootSolveStamp {
frame_id,
key: RootSolveKey {
width_bits: key_bits(available.width),
height_bits: key_bits(available.height),
scale_bits: self.solve_scale_factor.to_bits(),
},
},
);
}
}
self.last_solve_elapsed = started.elapsed();
span.record("elapsed_us", self.last_solve_elapsed.as_micros() as u64);
span.record("measure_calls", measure_calls);
span.record("measure_cache_hits", measure_cache_hits);
span.record("measure_us", measure_time.as_micros() as u64);
self.last_solve_time += self.last_solve_elapsed;
let _ = self.tree.set_children(scratch_id, &[]);
compute_individual(self, &fallback, sf, &mut measure);
}
pub(crate) fn set_viewport_root_override_size(
&mut self,
root: NodeId,
viewport_size: Size,
scale_factor: f32,
) {
let Some(mut style) = self.styles.get(root).cloned() else {
return;
};
let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
scale_factor
} else {
1.0
};
let w = viewport_size.width.0.max(0.0) * sf;
let h = viewport_size.height.0.max(0.0) * sf;
style.size.width = taffy::style::Dimension::length(w);
style.size.height = taffy::style::Dimension::length(h);
style.max_size.width = taffy::style::Dimension::length(w);
style.max_size.height = taffy::style::Dimension::length(h);
self.set_style(root, style);
}
pub fn request_layout_node(&mut self, node: NodeId) -> Option<LayoutId> {
if let Some(id) = self.node_to_layout.get(node).copied() {
self.mark_seen(node);
return Some(id);
}
let taffy_id = match self.tree.new_leaf_with_context(
Default::default(),
NodeContext {
node,
measured: false,
min_content_width_as_max: false,
},
) {
Ok(id) => id,
Err(err) => {
Self::warn_taffy_error_once("new_leaf_with_context", err);
return None;
}
};
let id = LayoutId(taffy_id);
self.node_to_layout.insert(node, id);
self.layout_to_node.insert(id, node);
self.mark_seen(node);
Some(id)
}
pub fn set_measured(&mut self, node: NodeId, measured: bool) {
let Some(id) = self.request_layout_node(node).map(|id| id.0) else {
return;
};
let ctx = self
.tree
.get_node_context(id)
.copied()
.unwrap_or(NodeContext {
node,
measured,
min_content_width_as_max: false,
});
if ctx.node == node && ctx.measured == measured {
return;
}
self.node_solved_stamp.remove(node);
self.root_solve_stamp.remove(node);
self.invalidate_solved_ancestors(node);
if let Err(err) = self.tree.set_node_context(
id,
Some(NodeContext {
node,
measured,
min_content_width_as_max: ctx.min_content_width_as_max,
}),
) {
Self::warn_taffy_error_once("set_node_context", err);
}
if let Err(err) = self.tree.mark_dirty(id) {
Self::warn_taffy_error_once("mark_dirty", err);
}
}
pub fn set_measure_min_content_width_as_max(&mut self, node: NodeId, enabled: bool) {
let Some(id) = self.request_layout_node(node).map(|id| id.0) else {
return;
};
let ctx = self
.tree
.get_node_context(id)
.copied()
.unwrap_or(NodeContext {
node,
measured: false,
min_content_width_as_max: enabled,
});
if ctx.node == node && ctx.min_content_width_as_max == enabled {
return;
}
self.node_solved_stamp.remove(node);
self.root_solve_stamp.remove(node);
self.invalidate_solved_ancestors(node);
if let Err(err) = self.tree.set_node_context(
id,
Some(NodeContext {
node,
measured: ctx.measured,
min_content_width_as_max: enabled,
}),
) {
Self::warn_taffy_error_once("set_node_context", err);
}
if let Err(err) = self.tree.mark_dirty(id) {
Self::warn_taffy_error_once("mark_dirty", err);
}
}
pub fn set_style(&mut self, node: NodeId, style: taffy::Style) {
let Some(id) = self.request_layout_node(node).map(|id| id.0) else {
return;
};
if self.styles.get(node) == Some(&style) {
return;
}
self.node_solved_stamp.remove(node);
self.root_solve_stamp.remove(node);
self.invalidate_solved_ancestors(node);
match self.tree.set_style(id, style.clone()) {
Ok(()) => {
self.styles.insert(node, style);
if let Err(err) = self.tree.mark_dirty(id) {
Self::warn_taffy_error_once("mark_dirty", err);
}
}
Err(err) => {
Self::warn_taffy_error_once("set_style", err);
}
}
}
pub fn set_children(&mut self, node: NodeId, children: &[NodeId]) {
if self
.children
.get(node)
.is_some_and(|prev| prev.as_slice() == children)
{
return;
}
let original_len = children.len();
let mut child_unique_scratch = std::mem::take(&mut self.child_unique_scratch);
let mut child_dedupe_set_scratch = std::mem::take(&mut self.child_dedupe_set_scratch);
let mut had_dupes = false;
let children = if children.len() <= 1 {
children
} else if children.len() <= 32 {
child_unique_scratch.clear();
child_unique_scratch.reserve(children.len());
for &child in children {
if child_unique_scratch.contains(&child) {
had_dupes = true;
continue;
}
child_unique_scratch.push(child);
}
if had_dupes {
child_unique_scratch.as_slice()
} else {
children
}
} else {
child_unique_scratch.clear();
child_dedupe_set_scratch.clear();
child_unique_scratch.reserve(children.len());
for &child in children {
if !child_dedupe_set_scratch.insert(child) {
had_dupes = true;
continue;
}
child_unique_scratch.push(child);
}
if had_dupes {
child_unique_scratch.as_slice()
} else {
children
}
};
if had_dupes {
tracing::warn!(
parent = ?node,
children_len = original_len,
unique_len = children.len(),
"layout engine set_children received duplicate children; deduping to avoid taffy panic"
);
}
let Some(parent) = self.request_layout_node(node).map(|id| id.0) else {
self.child_unique_scratch = child_unique_scratch;
self.child_dedupe_set_scratch = child_dedupe_set_scratch;
return;
};
let prev_children = self.children.get(node).cloned();
self.node_solved_stamp.remove(node);
self.root_solve_stamp.remove(node);
self.invalidate_solved_ancestors(node);
self.child_nodes_scratch.clear();
self.child_nodes_scratch.reserve(children.len());
for &child in children {
let Some(child_id) = self.request_layout_node(child).map(|id| id.0) else {
self.child_unique_scratch = child_unique_scratch;
self.child_dedupe_set_scratch = child_dedupe_set_scratch;
return;
};
self.child_nodes_scratch.push(child_id);
}
match self.tree.set_children(parent, &self.child_nodes_scratch) {
Ok(()) => {
if let Some(prev_children) = prev_children.as_ref() {
for &child in prev_children.iter() {
if self.parent.get(child) == Some(&node) {
self.parent.remove(child);
}
}
}
for &child in children {
self.parent.insert(child, node);
}
self.children.insert(node, children.to_vec());
if let Err(err) = self.tree.mark_dirty(parent) {
Self::warn_taffy_error_once("mark_dirty", err);
}
}
Err(err) => {
Self::warn_taffy_error_once("set_children", err);
}
}
self.child_unique_scratch = child_unique_scratch;
self.child_dedupe_set_scratch = child_dedupe_set_scratch;
}
#[cfg(test)]
pub(crate) fn debug_style_for_node(&self, node: NodeId) -> Option<&taffy::Style> {
self.styles.get(node)
}
fn apply_flex_wrap_intrinsic_main_min_size_patches(
&mut self,
root_node: NodeId,
sf: f32,
measure: &mut impl FnMut(NodeId, LayoutConstraints) -> Size,
) {
use taffy::style::{
AvailableSpace as TaffyAvailableSpace, Dimension, Display, FlexDirection, FlexWrap,
};
#[derive(Clone, Copy)]
enum MainAxis {
Row,
Column,
}
let mut stack: Vec<NodeId> = vec![root_node];
while let Some(node) = stack.pop() {
let children = self.children.get(node).cloned().unwrap_or_default();
if let Some(style) = self.styles.get(node).cloned()
&& style.display == Display::Flex
&& style.flex_wrap == FlexWrap::Wrap
{
let main_axis = match style.flex_direction {
FlexDirection::Row | FlexDirection::RowReverse => MainAxis::Row,
FlexDirection::Column | FlexDirection::ColumnReverse => MainAxis::Column,
};
let compute_intrinsic_main_size =
|engine: &mut Self,
child_id: LayoutId,
main_axis: MainAxis,
use_min_content: bool,
sf: f32,
measure: &mut dyn FnMut(NodeId, LayoutConstraints) -> Size|
-> Option<f32> {
let avail = match (main_axis, use_min_content) {
(MainAxis::Row, true) => taffy::geometry::Size {
width: TaffyAvailableSpace::MinContent,
height: TaffyAvailableSpace::MaxContent,
},
(MainAxis::Row, false) => taffy::geometry::Size {
width: TaffyAvailableSpace::MaxContent,
height: TaffyAvailableSpace::MaxContent,
},
(MainAxis::Column, true) => taffy::geometry::Size {
width: TaffyAvailableSpace::MaxContent,
height: TaffyAvailableSpace::MinContent,
},
(MainAxis::Column, false) => taffy::geometry::Size {
width: TaffyAvailableSpace::MaxContent,
height: TaffyAvailableSpace::MaxContent,
},
};
let result = engine.tree.compute_layout_with_measure(
child_id.0,
avail,
|known, avail, _id, ctx, _style| {
let Some(ctx) = ctx else {
return taffy::geometry::Size::default();
};
if !ctx.measured {
return taffy::geometry::Size::default();
}
let constraints = LayoutConstraints::new(
LayoutSize::new(
known.width.map(|w| Px(w / sf)),
known.height.map(|h| Px(h / sf)),
),
LayoutSize::new(
match avail.width {
TaffyAvailableSpace::Definite(w) => {
AvailableSpace::Definite(Px(w / sf))
}
TaffyAvailableSpace::MinContent => {
if ctx.min_content_width_as_max {
AvailableSpace::MaxContent
} else {
AvailableSpace::MinContent
}
}
TaffyAvailableSpace::MaxContent => {
AvailableSpace::MaxContent
}
},
match avail.height {
TaffyAvailableSpace::Definite(h) => {
AvailableSpace::Definite(Px(h / sf))
}
TaffyAvailableSpace::MinContent => {
AvailableSpace::MinContent
}
TaffyAvailableSpace::MaxContent => {
AvailableSpace::MaxContent
}
},
),
);
let s = measure(ctx.node, constraints);
taffy::geometry::Size {
width: s.width.0 * sf,
height: s.height.0 * sf,
}
},
);
if result.is_err() {
return None;
}
let layout = engine.tree.layout(child_id.0).ok()?;
let main_dp = match main_axis {
MainAxis::Row => layout.size.width,
MainAxis::Column => layout.size.height,
};
if main_dp.is_finite() && main_dp > 0.0 {
Some(main_dp)
} else {
None
}
};
for child in children.iter().copied() {
let Some(mut child_style) = self.styles.get(child).cloned() else {
continue;
};
let overflow_visible_in_main = match main_axis {
MainAxis::Row => child_style.overflow.x == taffy::style::Overflow::Visible,
MainAxis::Column => {
child_style.overflow.y == taffy::style::Overflow::Visible
}
};
if !overflow_visible_in_main {
continue;
}
let (main_size_is_auto, main_min_is_auto) = match main_axis {
MainAxis::Row => (
child_style.size.width.is_auto(),
child_style.min_size.width.is_auto(),
),
MainAxis::Column => (
child_style.size.height.is_auto(),
child_style.min_size.height.is_auto(),
),
};
if !main_size_is_auto || !main_min_is_auto {
continue;
}
let Some(child_id) = self.layout_id_for_node(child) else {
continue;
};
let main_dp =
compute_intrinsic_main_size(self, child_id, main_axis, true, sf, measure)
.or_else(|| {
compute_intrinsic_main_size(
self, child_id, main_axis, false, sf, measure,
)
});
let Some(main_dp) = main_dp else {
continue;
};
match main_axis {
MainAxis::Row => child_style.min_size.width = Dimension::length(main_dp),
MainAxis::Column => {
child_style.min_size.height = Dimension::length(main_dp)
}
}
self.set_style(child, child_style);
}
}
stack.extend(children);
}
}
#[stacksafe::stacksafe]
pub fn compute_root_with_measure(
&mut self,
root: LayoutId,
available: LayoutSize<AvailableSpace>,
scale_factor: f32,
mut measure: impl FnMut(NodeId, LayoutConstraints) -> Size,
) {
fn quantize_size_key_bits(value: f32) -> u32 {
if !value.is_finite() || value <= 0.0 {
return 0;
}
let quantum = 64.0f32;
let quantized = (value * quantum).round() / quantum;
quantized.to_bits()
}
fn avail_key(avail: taffy::style::AvailableSpace, min_content_as_max: bool) -> (u8, u32) {
match avail {
taffy::style::AvailableSpace::Definite(v) => (0, quantize_size_key_bits(v)),
taffy::style::AvailableSpace::MinContent => {
if min_content_as_max {
(2, 0)
} else {
(1, 0)
}
}
taffy::style::AvailableSpace::MaxContent => (2, 0),
}
}
let started = Instant::now();
let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
scale_factor
} else {
1.0
};
self.solve_scale_factor = sf;
let span = if tracing::enabled!(tracing::Level::TRACE) {
tracing::trace_span!(
"fret.ui.layout_engine.solve",
root = tracing::field::Empty,
frame_id = self.frame_id.map(|f| f.0).unwrap_or(0),
scale_factor = sf,
elapsed_us = tracing::field::Empty,
measure_calls = tracing::field::Empty,
measure_cache_hits = tracing::field::Empty,
measure_us = tracing::field::Empty,
)
} else {
tracing::Span::none()
};
let _span_guard = span.enter();
let root_node = self.node_for_layout_id(root);
if let Some(root_node) = root_node {
self.apply_flex_wrap_intrinsic_main_min_size_patches(root_node, sf, &mut measure);
}
let taffy_available = taffy::geometry::Size {
width: match available.width {
AvailableSpace::Definite(px) => taffy::style::AvailableSpace::Definite(px.0 * sf),
AvailableSpace::MinContent => taffy::style::AvailableSpace::MinContent,
AvailableSpace::MaxContent => taffy::style::AvailableSpace::MaxContent,
},
height: match available.height {
AvailableSpace::Definite(px) => taffy::style::AvailableSpace::Definite(px.0 * sf),
AvailableSpace::MinContent => taffy::style::AvailableSpace::MinContent,
AvailableSpace::MaxContent => taffy::style::AvailableSpace::MaxContent,
},
};
let mut measure_calls: u64 = 0;
let mut measure_cache_hits: u64 = 0;
self.measure_cache_scratch.clear();
let measure_cache = &mut self.measure_cache_scratch;
let enable_profile = self.measure_profiling_enabled;
let mut measure_time = Duration::default();
#[derive(Debug, Clone, Copy, Default)]
struct MeasureNodeProfile {
total_time: Duration,
calls: u64,
cache_hits: u64,
}
let mut by_node: Option<SecondaryMap<NodeId, MeasureNodeProfile>> =
enable_profile.then(SecondaryMap::new);
let result = self.tree.compute_layout_with_measure(
root.0,
taffy_available,
|known, avail, _id, ctx, _style| {
let Some(ctx) = ctx else {
return taffy::geometry::Size::default();
};
if !ctx.measured {
return taffy::geometry::Size::default();
}
measure_calls = measure_calls.saturating_add(1);
let key = LayoutMeasureKey {
node: ctx.node,
known_w: known.width.map(quantize_size_key_bits),
known_h: known.height.map(quantize_size_key_bits),
avail_w: avail_key(avail.width, ctx.min_content_width_as_max),
avail_h: avail_key(avail.height, false),
};
if let Some(size) = measure_cache.get(&key) {
measure_cache_hits = measure_cache_hits.saturating_add(1);
if enable_profile && let Some(by_node) = by_node.as_mut() {
if by_node.get(ctx.node).is_none() {
by_node.insert(ctx.node, MeasureNodeProfile::default());
}
if let Some(profile) = by_node.get_mut(ctx.node) {
profile.cache_hits = profile.cache_hits.saturating_add(1);
} else {
debug_assert!(
false,
"layout engine profiling: expected node profile to exist after insert"
);
}
}
return *size;
}
let constraints = LayoutConstraints::new(
LayoutSize::new(
known.width.map(|w| Px(w / sf)),
known.height.map(|h| Px(h / sf)),
),
LayoutSize::new(
match avail.width {
taffy::style::AvailableSpace::Definite(w) => {
AvailableSpace::Definite(Px(w / sf))
}
taffy::style::AvailableSpace::MinContent => {
if ctx.min_content_width_as_max {
AvailableSpace::MaxContent
} else {
AvailableSpace::MinContent
}
}
taffy::style::AvailableSpace::MaxContent => AvailableSpace::MaxContent,
},
match avail.height {
taffy::style::AvailableSpace::Definite(h) => {
AvailableSpace::Definite(Px(h / sf))
}
taffy::style::AvailableSpace::MinContent => AvailableSpace::MinContent,
taffy::style::AvailableSpace::MaxContent => AvailableSpace::MaxContent,
},
),
);
let (s, elapsed) = if enable_profile {
let measure_started = Instant::now();
let size = measure(ctx.node, constraints);
(size, measure_started.elapsed())
} else {
(measure(ctx.node, constraints), Duration::default())
};
if enable_profile {
measure_time += elapsed;
if let Some(by_node) = by_node.as_mut() {
if by_node.get(ctx.node).is_none() {
by_node.insert(ctx.node, MeasureNodeProfile::default());
}
if let Some(profile) = by_node.get_mut(ctx.node) {
profile.total_time += elapsed;
profile.calls = profile.calls.saturating_add(1);
} else {
debug_assert!(
false,
"layout engine profiling: expected node profile to exist after insert"
);
}
}
}
let out = taffy::geometry::Size {
width: s.width.0 * sf,
height: s.height.0 * sf,
};
measure_cache.insert(key, out);
out
},
);
self.last_solve_measure_calls = measure_calls;
self.last_solve_measure_cache_hits = measure_cache_hits;
self.last_solve_measure_time = measure_time;
if enable_profile {
const MAX_HOTSPOTS: usize = 8;
let mut hotspots: Vec<LayoutEngineMeasureHotspot> = by_node
.unwrap_or_default()
.into_iter()
.map(|(node, p)| LayoutEngineMeasureHotspot {
node,
total_time: p.total_time,
calls: p.calls,
cache_hits: p.cache_hits,
})
.collect();
hotspots.sort_by_key(|h| std::cmp::Reverse(h.total_time));
hotspots.truncate(MAX_HOTSPOTS);
self.last_solve_measure_hotspots = hotspots;
} else {
self.last_solve_measure_hotspots.clear();
}
if let Err(err) = result {
Self::warn_taffy_error_once("compute_layout_with_measure", err);
if let Some(root_node) = root_node {
self.clear_solved_subtree(root_node);
self.root_solve_stamp.remove(root_node);
}
self.last_solve_root = None;
self.last_solve_elapsed = started.elapsed();
span.record("elapsed_us", self.last_solve_elapsed.as_micros() as u64);
span.record("measure_calls", measure_calls);
span.record("measure_cache_hits", measure_cache_hits);
span.record("measure_us", measure_time.as_micros() as u64);
self.last_solve_time += self.last_solve_elapsed;
return;
}
self.solve_generation = self.solve_generation.saturating_add(1);
if let Some(root_node) = root_node {
span.record("root", tracing::field::debug(root_node));
self.last_solve_root = Some(root_node);
self.mark_solved_subtree(root_node);
fn key_bits(axis: AvailableSpace) -> u64 {
match axis {
AvailableSpace::Definite(px) => px.0.to_bits() as u64,
AvailableSpace::MinContent => 1u64 << 32,
AvailableSpace::MaxContent => 2u64 << 32,
}
}
if let Some(frame_id) = self.frame_id {
self.root_solve_stamp.insert(
root_node,
RootSolveStamp {
frame_id,
key: RootSolveKey {
width_bits: key_bits(available.width),
height_bits: key_bits(available.height),
scale_bits: self.solve_scale_factor.to_bits(),
},
},
);
}
} else {
self.last_solve_root = None;
}
self.last_solve_elapsed = started.elapsed();
span.record("elapsed_us", self.last_solve_elapsed.as_micros() as u64);
span.record("measure_calls", measure_calls);
span.record("measure_cache_hits", measure_cache_hits);
span.record("measure_us", measure_time.as_micros() as u64);
self.last_solve_time += self.last_solve_elapsed;
}
pub fn compute_root(
&mut self,
root: LayoutId,
available: LayoutSize<AvailableSpace>,
scale_factor: f32,
) {
self.compute_root_with_measure(root, available, scale_factor, |_node, _constraints| {
Size::default()
});
}
pub fn layout_rect(&self, id: LayoutId) -> Rect {
let Ok(layout) = self.tree.layout(id.0) else {
return Rect::new(Point::new(Px(0.0), Px(0.0)), Size::default());
};
let sf = if self.solve_scale_factor.is_finite() && self.solve_scale_factor > 0.0 {
self.solve_scale_factor
} else {
1.0
};
let min_x_dp = layout.location.x;
let min_y_dp = layout.location.y;
let max_x_dp = layout.location.x + layout.size.width;
let max_y_dp = layout.location.y + layout.size.height;
let snap_edge_dp = |v: f32| if v.is_finite() { v.round() } else { 0.0 };
let snapped_min_x_dp = snap_edge_dp(min_x_dp);
let snapped_min_y_dp = snap_edge_dp(min_y_dp);
let snapped_max_x_dp = snap_edge_dp(max_x_dp);
let snapped_max_y_dp = snap_edge_dp(max_y_dp);
Rect::new(
Point::new(Px(snapped_min_x_dp / sf), Px(snapped_min_y_dp / sf)),
Size::new(
Px(((snapped_max_x_dp - snapped_min_x_dp) / sf).max(0.0)),
Px(((snapped_max_y_dp - snapped_min_y_dp) / sf).max(0.0)),
),
)
}
pub fn debug_dump_subtree_json(
&self,
root: NodeId,
mut label_for_node: impl FnMut(NodeId) -> Option<String>,
) -> serde_json::Value {
self.debug_dump_subtree_json_with_info(root, |node| DebugDumpNodeInfo {
label: label_for_node(node),
debug: None,
})
}
pub fn debug_dump_subtree_json_with_info(
&self,
root: NodeId,
mut info_for_node: impl FnMut(NodeId) -> DebugDumpNodeInfo,
) -> serde_json::Value {
fn sanitize_for_filename(s: &str) -> String {
s.chars()
.map(|ch| match ch {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
_ => '_',
})
.collect()
}
fn abs_rect_for_node(
engine: &TaffyLayoutEngine,
node: NodeId,
abs_cache: &mut FxHashMap<NodeId, Rect>,
) -> Rect {
if let Some(rect) = abs_cache.get(&node).copied() {
return rect;
}
let local = engine
.layout_id_for_node(node)
.map(|id| engine.layout_rect(id))
.unwrap_or_else(|| Rect::new(Point::new(Px(0.0), Px(0.0)), Size::default()));
let abs = if let Some(parent) = engine.parent.get(node).copied() {
let parent_abs = abs_rect_for_node(engine, parent, abs_cache);
Rect::new(
Point::new(
Px(parent_abs.origin.x.0 + local.origin.x.0),
Px(parent_abs.origin.y.0 + local.origin.y.0),
),
local.size,
)
} else {
local
};
abs_cache.insert(node, abs);
abs
}
let root_layout_id = self.layout_id_for_node(root);
let mut stack: Vec<NodeId> = vec![root];
let mut visited: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
let mut abs_cache: FxHashMap<NodeId, Rect> = FxHashMap::default();
let mut nodes: Vec<serde_json::Value> = Vec::new();
while let Some(node) = stack.pop() {
if !visited.insert(node) {
continue;
}
let children = self.children.get(node).cloned().unwrap_or_default();
for &child in children.iter().rev() {
stack.push(child);
}
let layout_id = self.layout_id_for_node(node);
let local = layout_id
.map(|id| self.layout_rect(id))
.unwrap_or_else(|| Rect::new(Point::new(Px(0.0), Px(0.0)), Size::default()));
let abs = abs_rect_for_node(self, node, &mut abs_cache);
let measured = layout_id
.and_then(|id| self.tree.get_node_context(id.0).copied())
.map(|ctx| ctx.measured)
.unwrap_or(false);
let style_dbg = self
.styles
.get(node)
.map(|s| format!("{s:?}"))
.unwrap_or_else(|| "<missing>".to_string());
let info = info_for_node(node);
let label_dbg = info.label.unwrap_or_else(|| "<unknown>".to_string());
let mut node_json = json!({
"node": format!("{node:?}"),
"label": label_dbg,
"measured": measured,
"parent": self.parent.get(node).map(|p| format!("{p:?}")),
"children": children.iter().map(|c| format!("{c:?}")).collect::<Vec<_>>(),
"layout_id": layout_id.map(|id| format!("{:?}", id.0)).unwrap_or_else(|| "<missing>".to_string()),
"local_rect": {
"x": local.origin.x.0,
"y": local.origin.y.0,
"w": local.size.width.0,
"h": local.size.height.0,
},
"abs_rect": {
"x": abs.origin.x.0,
"y": abs.origin.y.0,
"w": abs.size.width.0,
"h": abs.size.height.0,
},
"style": style_dbg,
});
if let Some(debug) = info.debug
&& let Some(obj) = node_json.as_object_mut()
{
obj.insert("debug".to_string(), debug);
}
nodes.push(node_json);
}
let filename = sanitize_for_filename(&format!("{root:?}"));
json!({
"meta": {
"root": format!("{root:?}"),
"root_layout_id": root_layout_id.map(|id| format!("{:?}", id.0)),
"frame_id": self.frame_id.map(|id| id.0),
"solve_generation": self.solve_generation,
"solve_scale_factor": self.solve_scale_factor,
"last_solve_time_ms": self.last_solve_time.as_millis(),
"suggested_filename": format!("taffy_{filename}.json"),
},
"nodes": nodes,
})
}
pub fn debug_independent_root_nodes(&self) -> Vec<NodeId> {
self.node_to_layout
.iter()
.filter_map(|(node, _)| self.parent.get(node).is_none().then_some(node))
.collect()
}
pub fn debug_write_subtree_json(
&self,
root: NodeId,
dir: impl AsRef<Path>,
filename: impl AsRef<Path>,
label_for_node: impl FnMut(NodeId) -> Option<String>,
) -> std::io::Result<PathBuf> {
let dir = dir.as_ref();
std::fs::create_dir_all(dir)?;
let path = dir.join(filename);
let dump = self.debug_dump_subtree_json(root, label_for_node);
let bytes = match serde_json::to_vec_pretty(&dump) {
Ok(bytes) => bytes,
Err(err) => {
if crate::strict_runtime::strict_runtime_enabled() {
panic!("serialize taffy debug json failed: {err:?}");
}
tracing::warn!(?err, ?root, "serialize taffy debug json failed");
let fallback = json!({
"error": "serialize taffy debug json failed",
"detail": err.to_string(),
"root": format!("{root:?}"),
});
serde_json::to_vec_pretty(&fallback).unwrap_or_else(|_| {
b"{\"error\":\"serialize taffy debug json failed\"}\n".to_vec()
})
}
};
std::fs::write(&path, bytes)?;
Ok(path)
}
fn mark_solved_subtree(&mut self, root: NodeId) {
let Some(frame_id) = self.frame_id else {
return;
};
if !self.is_seen(root) {
return;
}
self.mark_solved_stack_scratch.clear();
self.mark_solved_stack_scratch.push(root);
while let Some(node) = self.mark_solved_stack_scratch.pop() {
if !self.is_seen(node) {
continue;
}
self.node_solved_stamp.insert(
node,
SolvedStamp {
frame_id,
solve_generation: self.solve_generation,
},
);
if let Some(children) = self.children.get(node) {
for &child in children {
if self.is_seen(child) {
self.mark_solved_stack_scratch.push(child);
}
}
}
}
}
fn clear_solved_subtree(&mut self, root: NodeId) {
self.clear_solved_stack_scratch.clear();
self.clear_solved_stack_scratch.push(root);
while let Some(node) = self.clear_solved_stack_scratch.pop() {
self.node_solved_stamp.remove(node);
if let Some(children) = self.children.get(node) {
self.clear_solved_stack_scratch
.extend(children.iter().copied());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use slotmap::SlotMap;
fn fresh_node_ids(count: usize) -> Vec<NodeId> {
let mut map: SlotMap<NodeId, ()> = SlotMap::with_key();
(0..count).map(|_| map.insert(())).collect()
}
#[test]
fn multiple_roots_do_not_couple_layout_results() {
let [root_a, root_b, child_a, child_b] = fresh_node_ids(4).try_into().unwrap();
let mut engine = TaffyLayoutEngine::default();
engine.begin_frame(FrameId(1));
engine.set_children(root_a, &[child_a]);
engine.set_children(root_b, &[child_b]);
engine.set_style(
root_a,
taffy::Style {
display: taffy::style::Display::Block,
size: taffy::geometry::Size {
width: taffy::style::Dimension::length(100.0),
height: taffy::style::Dimension::length(10.0),
},
..Default::default()
},
);
engine.set_style(
root_b,
taffy::Style {
display: taffy::style::Display::Block,
size: taffy::geometry::Size {
width: taffy::style::Dimension::length(200.0),
height: taffy::style::Dimension::length(10.0),
},
..Default::default()
},
);
let fill = taffy::Style {
display: taffy::style::Display::Block,
size: taffy::geometry::Size {
width: taffy::style::Dimension::percent(1.0),
height: taffy::style::Dimension::percent(1.0),
},
..Default::default()
};
engine.set_style(child_a, fill.clone());
engine.set_style(child_b, fill);
let root_a_id = engine.layout_id_for_node(root_a).unwrap();
let root_b_id = engine.layout_id_for_node(root_b).unwrap();
engine.compute_root(
root_a_id,
LayoutSize::new(
AvailableSpace::Definite(Px(100.0)),
AvailableSpace::Definite(Px(10.0)),
),
1.0,
);
let child_a_id = engine.layout_id_for_node(child_a).unwrap();
let a_before = engine.layout_rect(child_a_id);
assert!((a_before.size.width.0 - 100.0).abs() < 0.01);
assert!((a_before.size.height.0 - 10.0).abs() < 0.01);
engine.compute_root(
root_b_id,
LayoutSize::new(
AvailableSpace::Definite(Px(200.0)),
AvailableSpace::Definite(Px(10.0)),
),
1.0,
);
let child_b_id = engine.layout_id_for_node(child_b).unwrap();
let b = engine.layout_rect(child_b_id);
assert!((b.size.width.0 - 200.0).abs() < 0.01);
assert!((b.size.height.0 - 10.0).abs() < 0.01);
let a_after = engine.layout_rect(child_a_id);
assert_eq!(a_before, a_after);
assert_eq!(
engine.child_layout_rect_if_solved(root_a, child_a),
Some(a_after),
"solved subtree rects should remain readable after solving an unrelated root"
);
}
#[test]
fn barrier_batch_solve_stamps_multiple_roots_in_one_generation() {
let [root_a, root_b, child_a, child_b] = fresh_node_ids(4).try_into().unwrap();
let mut engine = TaffyLayoutEngine::default();
engine.begin_frame(FrameId(1));
engine.set_children(root_a, &[child_a]);
engine.set_children(root_b, &[child_b]);
engine.set_style(
root_a,
taffy::Style {
display: taffy::style::Display::Block,
size: taffy::geometry::Size {
width: taffy::style::Dimension::length(100.0),
height: taffy::style::Dimension::length(10.0),
},
..Default::default()
},
);
engine.set_style(
root_b,
taffy::Style {
display: taffy::style::Display::Block,
size: taffy::geometry::Size {
width: taffy::style::Dimension::length(200.0),
height: taffy::style::Dimension::length(20.0),
},
..Default::default()
},
);
let fill = taffy::Style {
display: taffy::style::Display::Block,
size: taffy::geometry::Size {
width: taffy::style::Dimension::percent(1.0),
height: taffy::style::Dimension::percent(1.0),
},
..Default::default()
};
engine.set_style(child_a, fill.clone());
engine.set_style(child_b, fill);
let roots = [
(
root_a,
LayoutSize::new(
AvailableSpace::Definite(Px(100.0)),
AvailableSpace::Definite(Px(10.0)),
),
),
(
root_b,
LayoutSize::new(
AvailableSpace::Definite(Px(200.0)),
AvailableSpace::Definite(Px(20.0)),
),
),
];
engine.compute_independent_roots_with_measure_if_needed(&roots, 1.0, |_node, _c| {
Size::default()
});
assert_eq!(engine.solve_count(), 1);
let a = engine.layout_rect(engine.layout_id_for_node(child_a).unwrap());
assert!((a.size.width.0 - 100.0).abs() < 0.01);
assert!((a.size.height.0 - 10.0).abs() < 0.01);
let b = engine.layout_rect(engine.layout_id_for_node(child_b).unwrap());
assert!((b.size.width.0 - 200.0).abs() < 0.01);
assert!((b.size.height.0 - 20.0).abs() < 0.01);
engine.compute_independent_roots_with_measure_if_needed(&roots, 1.0, |_node, _c| {
Size::default()
});
assert_eq!(engine.solve_count(), 1);
}
#[test]
fn layout_rect_snaps_edges_not_location_and_size() {
let [root, child] = fresh_node_ids(2).try_into().unwrap();
let mut engine = TaffyLayoutEngine::default();
engine.begin_frame(FrameId(1));
engine.set_children(root, &[child]);
engine.set_style(
root,
taffy::Style {
display: taffy::style::Display::Block,
..Default::default()
},
);
engine.set_style(
child,
taffy::Style {
display: taffy::style::Display::Block,
size: taffy::geometry::Size {
width: taffy::style::Dimension::length(0.5),
height: taffy::style::Dimension::length(0.5),
},
margin: taffy::geometry::Rect {
left: taffy::style::LengthPercentageAuto::length(0.5),
right: taffy::style::LengthPercentageAuto::auto(),
top: taffy::style::LengthPercentageAuto::auto(),
bottom: taffy::style::LengthPercentageAuto::auto(),
},
..Default::default()
},
);
let root_id = engine.layout_id_for_node(root).unwrap();
engine.compute_root(
root_id,
LayoutSize::new(
AvailableSpace::Definite(Px(10.0)),
AvailableSpace::Definite(Px(10.0)),
),
2.0,
);
let child_id = engine.layout_id_for_node(child).unwrap();
let rect = engine.layout_rect(child_id);
assert!((rect.origin.x.0 - 0.5).abs() < 0.0001);
assert!((rect.size.width.0 - 0.0).abs() < 0.0001);
}
#[test]
fn end_frame_prunes_stale_children_from_live_parent_edges() {
let [root, live_child, stale_child] = fresh_node_ids(3).try_into().unwrap();
let mut engine = TaffyLayoutEngine::default();
engine.begin_frame(FrameId(1));
engine.set_children(root, &[live_child, stale_child]);
let block = taffy::Style {
display: taffy::style::Display::Block,
size: taffy::geometry::Size {
width: taffy::style::Dimension::length(100.0),
height: taffy::style::Dimension::length(20.0),
},
..Default::default()
};
engine.set_style(root, block.clone());
engine.set_style(live_child, block.clone());
engine.set_style(stale_child, block);
engine.end_frame();
engine.begin_frame(FrameId(2));
engine.mark_seen(root);
engine.mark_seen(live_child);
engine.end_frame();
assert!(
engine.layout_id_for_node(stale_child).is_none(),
"stale child layout node should be swept at end of frame"
);
assert_eq!(
engine.children.get(root).map(Vec::as_slice),
Some([live_child].as_slice()),
"sweeping a stale child must also sever the live parent's child edge"
);
assert_eq!(
engine.parent.get(live_child).copied(),
Some(root),
"live sibling parent pointers must survive stale-child pruning"
);
let dump = engine.debug_dump_subtree_json(root, |node| {
Some(match node {
n if n == root => "root".to_string(),
n if n == live_child => "live".to_string(),
n if n == stale_child => "stale".to_string(),
_ => format!("{node:?}"),
})
});
let dump_text = dump.to_string();
assert!(
!dump_text.contains("stale"),
"debug dump should not retain swept stale children under a live parent: {dump_text}"
);
assert!(
!dump_text.contains("<unknown>"),
"debug dump should not expose placeholder unknown nodes after stale-child pruning: {dump_text}"
);
}
}