#![warn(missing_docs)]
use std::cell::RefCell;
use std::sync::Arc;
use rustc_hash::FxHashSet;
use crate::scroll::{ScrollAlignment, ScrollRequest};
use crate::state::dyn_height::DynHeightIndex;
use crate::state::{ScrollAnchor, UiState, VirtualAnchor};
use crate::text::metrics as text_metrics;
use crate::tree::*;
#[derive(Clone)]
pub struct LayoutFn(pub Arc<dyn Fn(LayoutCtx) -> Vec<Rect> + Send + Sync>);
impl LayoutFn {
pub fn new<F>(f: F) -> Self
where
F: Fn(LayoutCtx) -> Vec<Rect> + Send + Sync + 'static,
{
LayoutFn(Arc::new(f))
}
}
impl std::fmt::Debug for LayoutFn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("LayoutFn(<fn>)")
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct LayoutIntrinsicCacheStats {
pub hits: u64,
pub misses: u64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct LayoutPruneStats {
pub subtrees: u64,
pub nodes: u64,
}
thread_local! {
static SIZING_VISITS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
static LAST_SIZING_STATS: RefCell<LayoutIntrinsicCacheStats> =
const { RefCell::new(LayoutIntrinsicCacheStats { hits: 0, misses: 0 }) };
static PRUNE_STATS: RefCell<LayoutPruneStats> =
const { RefCell::new(LayoutPruneStats { subtrees: 0, nodes: 0 }) };
static LAST_PRUNE_STATS: RefCell<LayoutPruneStats> =
const { RefCell::new(LayoutPruneStats { subtrees: 0, nodes: 0 }) };
}
pub fn take_intrinsic_cache_stats() -> LayoutIntrinsicCacheStats {
LAST_SIZING_STATS.with(|stats| std::mem::take(&mut *stats.borrow_mut()))
}
pub fn take_prune_stats() -> LayoutPruneStats {
LAST_PRUNE_STATS.with(|stats| std::mem::take(&mut *stats.borrow_mut()))
}
#[derive(Clone, Debug)]
pub enum VirtualMode {
Fixed {
row_height: f32,
},
Dynamic {
estimated_row_height: f32,
append_only: bool,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum VirtualAnchorPolicy {
ViewportFraction {
y_fraction: f32,
},
FirstVisible,
LastVisible,
}
impl Default for VirtualAnchorPolicy {
fn default() -> Self {
Self::ViewportFraction { y_fraction: 0.25 }
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct VirtualItems {
pub count: usize,
pub mode: VirtualMode,
pub anchor_policy: VirtualAnchorPolicy,
pub row_key: Arc<dyn Fn(usize) -> String + Send + Sync>,
pub build_row: Arc<dyn Fn(usize) -> El + Send + Sync>,
}
impl VirtualItems {
pub fn new<F>(count: usize, row_height: f32, build_row: F) -> Self
where
F: Fn(usize) -> El + Send + Sync + 'static,
{
assert!(
row_height > 0.0,
"VirtualItems::new requires row_height > 0.0 (got {row_height})"
);
VirtualItems {
count,
mode: VirtualMode::Fixed { row_height },
anchor_policy: VirtualAnchorPolicy::default(),
row_key: Arc::new(|i| i.to_string()),
build_row: Arc::new(build_row),
}
}
pub fn new_dyn<K, F>(count: usize, estimated_row_height: f32, row_key: K, build_row: F) -> Self
where
K: Fn(usize) -> String + Send + Sync + 'static,
F: Fn(usize) -> El + Send + Sync + 'static,
{
assert!(
estimated_row_height > 0.0,
"VirtualItems::new_dyn requires estimated_row_height > 0.0 (got {estimated_row_height})"
);
VirtualItems {
count,
mode: VirtualMode::Dynamic {
estimated_row_height,
append_only: false,
},
anchor_policy: VirtualAnchorPolicy::default(),
row_key: Arc::new(row_key),
build_row: Arc::new(build_row),
}
}
pub fn append_only(mut self) -> Self {
if let VirtualMode::Dynamic {
ref mut append_only,
..
} = self.mode
{
*append_only = true;
}
self
}
pub fn anchor_policy(mut self, policy: VirtualAnchorPolicy) -> Self {
self.anchor_policy = policy;
self
}
}
impl std::fmt::Debug for VirtualItems {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VirtualItems")
.field("count", &self.count)
.field("mode", &self.mode)
.field("anchor_policy", &self.anchor_policy)
.field("row_key", &"<fn>")
.field("build_row", &"<fn>")
.finish()
}
}
#[non_exhaustive]
pub struct LayoutCtx<'a> {
pub container: Rect,
pub children: &'a [El],
pub measure: &'a dyn Fn(&El) -> (f32, f32),
pub rect_of_key: &'a dyn Fn(&str) -> Option<Rect>,
pub rect_of_id: &'a dyn Fn(&str) -> Option<Rect>,
}
pub fn layout(root: &mut El, ui_state: &mut UiState, viewport: Rect) {
{
crate::profile_span!("layout::assign_ids");
assign_ids(root);
}
layout_post_assign(root, ui_state, viewport);
}
pub fn layout_post_assign(root: &mut El, ui_state: &mut UiState, viewport: Rect) {
SIZING_VISITS.with(|c| c.set(0));
PRUNE_STATS.with(|s| *s.borrow_mut() = LayoutPruneStats::default());
{
crate::profile_span!("layout::root_setup");
root.computed_rect = viewport;
ui_state
.layout
.keyed_rects
.insert(root.computed_id.clone(), viewport);
rebuild_key_index(root, ui_state);
ui_state.scroll.metrics.clear();
ui_state.scroll.thumb_rects.clear();
ui_state.scroll.thumb_tracks.clear();
ui_state.scroll.visible_ranges.clear();
ui_state.resize.bands.clear();
ui_state.viewport.metrics.clear();
apply_size_rewrites(root, ui_state, None);
}
{
crate::profile_span!("layout::size");
size_tree(root, Some(viewport.w));
}
{
crate::profile_span!("layout::children");
layout_children(root, viewport, ui_state);
}
publish_resize_bands(root, ui_state);
LAST_SIZING_STATS.with(|s| {
*s.borrow_mut() = LayoutIntrinsicCacheStats {
hits: 0,
misses: SIZING_VISITS.with(|c| c.get()),
};
});
LAST_PRUNE_STATS.with(|last| *last.borrow_mut() = PRUNE_STATS.with(|s| *s.borrow()));
}
fn resize_clamp(child: &El, axis: Axis) -> (f32, f32) {
let (min, max) = match axis {
Axis::Column => (child.min_height, child.max_height),
_ => (child.min_width, child.max_width),
};
let min = min.unwrap_or(0.0).max(0.0);
(min, max.unwrap_or(f32::INFINITY).max(min))
}
fn apply_size_rewrites(node: &mut El, ui_state: &UiState, parent_axis: Option<Axis>) {
if node.user_resizable
&& let Some(axis) = parent_axis
&& !matches!(axis, Axis::Overlay)
{
let id = node.key.as_deref().unwrap_or(&node.computed_id);
if let Some(&px) = ui_state.resize.overrides.get(id) {
let (min, max) = resize_clamp(node, axis);
let px = px.clamp(min, max);
match axis {
Axis::Column => node.height = Size::Fixed(px),
_ => node.width = Size::Fixed(px),
}
}
}
if let Size::Ch(n) = node.width {
node.width = Size::Fixed((n * ch_unit(node)).max(0.0));
}
if let Size::Ch(n) = node.height {
node.height = Size::Fixed((n * ch_unit(node)).max(0.0));
}
let axis = node.axis;
for child in &mut node.children {
apply_size_rewrites(child, ui_state, Some(axis));
}
}
fn ch_unit(node: &El) -> f32 {
text_metrics::layout_text_with_family(
"0",
node.font_size,
node.font_family,
node.font_weight,
node.font_mono,
node.text_tabular_numerals,
TextWrap::NoWrap,
None,
)
.width
.max(0.0)
}
fn publish_resize_bands(node: &El, ui_state: &mut UiState) {
use crate::state::resize::{RESIZE_BAND_THICKNESS as T, ResizeBand};
let axis = node.axis;
if !matches!(axis, Axis::Overlay) && node.children.iter().any(|c| c.user_resizable) {
let parent_rect = node.computed_rect;
let inner_main = match axis {
Axis::Column => parent_rect.h - node.padding.top - node.padding.bottom,
_ => parent_rect.w - node.padding.left - node.padding.right,
};
let count = node.children.len();
for (idx, child) in node.children.iter().enumerate() {
if !child.user_resizable {
continue;
}
let rect = child.computed_rect;
let trailing = idx + 1 < count || count == 1;
let band = match (axis, trailing) {
(Axis::Column, true) => Rect::new(rect.x, rect.y + rect.h - T / 2.0, rect.w, T),
(Axis::Column, false) => Rect::new(rect.x, rect.y - T / 2.0, rect.w, T),
(_, true) => Rect::new(rect.x + rect.w - T / 2.0, rect.y, T, rect.h),
(_, false) => Rect::new(rect.x - T / 2.0, rect.y, T, rect.h),
};
let (min, max) = resize_clamp(child, axis);
let max = max.min(inner_main.max(min));
ui_state.resize.bands.push(ResizeBand {
id: child
.key
.clone()
.unwrap_or_else(|| child.computed_id.to_string()),
key: child.key.clone(),
container_id: node.computed_id.to_string(),
band,
axis,
sign: if trailing { 1.0 } else { -1.0 },
current: match axis {
Axis::Column => rect.h,
_ => rect.w,
},
min,
max,
});
}
}
for child in &node.children {
publish_resize_bands(child, ui_state);
}
}
pub fn assign_id_appended(parent_id: &str, child: &mut El, child_index: usize) {
let mut path = String::with_capacity(parent_id.len() + 24);
path.push_str(parent_id);
push_id_suffix(&mut path, child, child_index);
assign_id(child, &mut path);
}
fn rebuild_key_index(root: &El, ui_state: &mut UiState) {
ui_state.layout.key_index.clear();
let mut id_counts: rustc_hash::FxHashMap<&str, u32> = Default::default();
fn visit<'a>(
node: &'a El,
index: &mut rustc_hash::FxHashMap<String, std::sync::Arc<str>>,
id_counts: &mut rustc_hash::FxHashMap<&'a str, u32>,
) {
if let Some(key) = &node.key {
index
.entry(key.clone())
.or_insert_with(|| node.computed_id.clone());
}
*id_counts.entry(node.computed_id.as_ref()).or_insert(0) += 1;
for c in &node.children {
visit(c, index, id_counts);
}
}
visit(root, &mut ui_state.layout.key_index, &mut id_counts);
warn_duplicate_ids(&id_counts, &mut ui_state.layout.warned_duplicate_ids);
}
fn warn_duplicate_ids(
id_counts: &rustc_hash::FxHashMap<&str, u32>,
warned: &mut rustc_hash::FxHashSet<String>,
) {
for (&id, &n) in id_counts {
if n > 1 && warned.insert(id.to_string()) {
log::warn!(
"DuplicateId: {n} nodes share id {id} — duplicate sibling key; \
their rects and intrinsic-cache entries collide silently (issue #64)"
);
}
}
}
pub fn assign_ids(root: &mut El) {
let mut path = String::with_capacity(128);
path.push_str("root");
assign_id(root, &mut path);
}
fn push_id_suffix(path: &mut String, child: &El, child_index: usize) {
use std::fmt::Write;
path.push('.');
path.push_str(role_token(&child.kind));
match &child.key {
Some(k) => {
path.push('[');
path.push_str(k);
path.push(']');
}
None => {
let _ = write!(path, ".{child_index}");
}
}
}
fn assign_id(node: &mut El, path: &mut String) {
node.computed_id = std::sync::Arc::from(path.as_str());
for (i, c) in node.children.iter_mut().enumerate() {
let len = path.len();
push_id_suffix(path, c, i);
assign_id(c, path);
path.truncate(len);
}
}
fn role_token(k: &Kind) -> &'static str {
match k {
Kind::Group => "group",
Kind::Card => "card",
Kind::Button => "button",
Kind::Badge => "badge",
Kind::Text => "text",
Kind::Heading => "heading",
Kind::Spacer => "spacer",
Kind::Divider => "divider",
Kind::Overlay => "overlay",
Kind::Scrim => "scrim",
Kind::Modal => "modal",
Kind::Scroll => "scroll",
Kind::VirtualList => "virtual_list",
Kind::Inlines => "inlines",
Kind::HardBreak => "hard_break",
Kind::Math => "math",
Kind::Image => "image",
Kind::Surface => "surface",
Kind::Vector => "vector",
Kind::Scene3D => "scene3d",
Kind::Plot => "plot",
Kind::Viewport => "viewport",
Kind::Custom(name) => name,
}
}
#[inline]
fn set_rect(node: &mut El, rect: Rect, ui_state: &mut UiState) {
node.computed_rect = rect;
if node.key.is_some() {
ui_state
.layout
.keyed_rects
.insert(node.computed_id.clone(), rect);
}
}
fn find_descendant_rect(node: &El, target_id: &str) -> Option<Rect> {
for c in &node.children {
if &*c.computed_id == target_id {
return Some(c.computed_rect);
}
if target_id
.strip_prefix(&*c.computed_id)
.is_some_and(|rest| rest.starts_with('.'))
{
return find_descendant_rect(c, target_id);
}
}
None
}
fn layout_children(node: &mut El, node_rect: Rect, ui_state: &mut UiState) {
if matches!(node.kind, Kind::Inlines) {
for c in &mut node.children {
set_rect(c, Rect::new(node_rect.x, node_rect.y, 0.0, 0.0), ui_state);
layout_children(c, Rect::new(node_rect.x, node_rect.y, 0.0, 0.0), ui_state);
}
return;
}
if let Some(items) = node.virtual_items.as_deref().cloned() {
layout_virtual(node, node_rect, items, ui_state);
return;
}
if let Some(layout_fn) = node.layout_override.clone() {
layout_custom(node, node_rect, layout_fn, ui_state);
if node.scrollable {
apply_scroll_offset(node, node_rect, ui_state);
}
if node.viewport.is_some() {
apply_viewport_transform(node, node_rect, ui_state);
}
return;
}
match node.axis {
Axis::Overlay => {
let inner = node_rect.inset(node.padding);
let clamp_to_parent = node.viewport.is_none();
for c in &mut node.children {
let c_rect = overlay_rect(c, inner, node.align, node.justify, clamp_to_parent);
resize_if_width_diverged(c, c_rect.w);
set_rect(c, c_rect, ui_state);
layout_children(c, c_rect, ui_state);
}
}
Axis::Column => layout_axis(node, node_rect, true, ui_state),
Axis::Row => layout_axis(node, node_rect, false, ui_state),
}
if node.scrollable {
apply_scroll_offset(node, node_rect, ui_state);
}
if node.viewport.is_some() {
apply_viewport_transform(node, node_rect, ui_state);
}
}
fn layout_custom(node: &mut El, node_rect: Rect, layout_fn: LayoutFn, ui_state: &mut UiState) {
let inner = node_rect.inset(node.padding);
let measure = |c: &El| c.measured_size;
let key_index = &ui_state.layout.key_index;
let keyed_rects = &ui_state.layout.keyed_rects;
let rect_of_key = |key: &str| -> Option<Rect> {
let id = key_index.get(key)?;
keyed_rects.get(id).copied()
};
let rect_of_id = |id: &str| -> Option<Rect> { keyed_rects.get(id).copied() };
let rects = (layout_fn.0)(LayoutCtx {
container: inner,
children: &node.children,
measure: &measure,
rect_of_key: &rect_of_key,
rect_of_id: &rect_of_id,
});
assert_eq!(
rects.len(),
node.children.len(),
"LayoutFn for {:?} returned {} rects for {} children",
node.computed_id,
rects.len(),
node.children.len(),
);
for (c, c_rect) in node.children.iter_mut().zip(rects) {
resize_if_width_diverged(c, c_rect.w);
set_rect(c, c_rect, ui_state);
layout_children(c, c_rect, ui_state);
}
}
fn layout_virtual(node: &mut El, node_rect: Rect, items: VirtualItems, ui_state: &mut UiState) {
let inner = node_rect.inset(node.padding);
match items.mode {
VirtualMode::Fixed { row_height } => layout_virtual_fixed(
node,
inner,
items.count,
row_height,
items.build_row,
ui_state,
),
VirtualMode::Dynamic {
estimated_row_height,
append_only,
} => {
let fns = DynamicVirtualFns {
anchor_policy: items.anchor_policy,
row_key: items.row_key,
build_row: items.build_row,
};
if append_only {
layout_virtual_dynamic_incremental(
node,
inner,
items.count,
estimated_row_height,
fns,
ui_state,
);
} else {
layout_virtual_dynamic(
node,
inner,
items.count,
estimated_row_height,
fns,
ui_state,
);
}
}
}
}
fn resolve_scroll_requests<F, K>(
node: &El,
inner: Rect,
count: usize,
row_extent: F,
row_for_key: K,
ui_state: &mut UiState,
) -> bool
where
F: Fn(usize) -> (f32, f32),
K: Fn(&str) -> Option<usize>,
{
if ui_state.scroll.pending_requests.is_empty() {
return false;
}
let Some(key) = node.key.as_deref() else {
return false;
};
let pending = std::mem::take(&mut ui_state.scroll.pending_requests);
let (matched, remaining): (Vec<ScrollRequest>, Vec<ScrollRequest>) =
pending.into_iter().partition(|req| match req {
ScrollRequest::ToRow { list_key, .. } => list_key == key,
ScrollRequest::ToRowKey { list_key, .. } => list_key == key,
ScrollRequest::EnsureVisible { .. } => false,
});
ui_state.scroll.pending_requests = remaining;
let mut wrote = false;
for req in matched {
let (row, align) = match req {
ScrollRequest::ToRow { row, align, .. } => (row, align),
ScrollRequest::ToRowKey { row_key, align, .. } => {
let Some(row) = row_for_key(&row_key) else {
continue;
};
(row, align)
}
ScrollRequest::EnsureVisible { .. } => continue,
};
if row >= count {
continue;
}
let (row_top, row_h) = row_extent(row);
let row_bottom = row_top + row_h;
let viewport_h = inner.h;
let current = ui_state
.scroll
.offsets
.get(&*node.computed_id)
.copied()
.unwrap_or(0.0);
let new_offset = match align {
ScrollAlignment::Start => row_top,
ScrollAlignment::End => row_bottom - viewport_h,
ScrollAlignment::Center => row_top + (row_h - viewport_h) / 2.0,
ScrollAlignment::Visible => {
if row_top < current {
row_top
} else if row_bottom > current + viewport_h {
row_bottom - viewport_h
} else {
continue;
}
}
};
ui_state
.scroll
.offsets
.insert(node.computed_id.to_string(), new_offset);
wrote = true;
}
wrote
}
fn write_virtual_scroll_state(node: &El, inner: Rect, total_h: f32, ui_state: &mut UiState) -> f32 {
let max_offset = (total_h - inner.h).max(0.0);
let stored = ui_state
.scroll
.offsets
.get(&*node.computed_id)
.copied()
.unwrap_or(0.0);
let stored = resolve_pin(node, stored, max_offset, ui_state);
let offset = stored.clamp(0.0, max_offset);
ui_state
.scroll
.offsets
.insert(node.computed_id.to_string(), offset);
write_virtual_scroll_metrics(node, inner, total_h, max_offset, offset, ui_state);
offset
}
fn write_virtual_scroll_metrics(
node: &El,
inner: Rect,
total_h: f32,
max_offset: f32,
offset: f32,
ui_state: &mut UiState,
) {
ui_state.scroll.metrics.insert(
node.computed_id.to_string(),
crate::state::ScrollMetrics {
viewport_h: inner.h,
content_h: total_h,
max_offset,
},
);
write_thumb_rect(node, inner, total_h, max_offset, offset, ui_state);
}
fn assign_virtual_row_id(child: &mut El, parent_id: &str, global_i: usize) {
let role = role_token(&child.kind);
let mut path = String::with_capacity(parent_id.len() + 24);
path.push_str(parent_id);
path.push('.');
path.push_str(role);
match &child.key {
Some(k) => {
path.push('[');
path.push_str(k);
path.push(']');
}
None => {
use std::fmt::Write;
let _ = write!(path, ".{global_i}");
}
}
assign_id(child, &mut path);
}
fn layout_virtual_fixed(
node: &mut El,
inner: Rect,
count: usize,
row_height: f32,
build_row: Arc<dyn Fn(usize) -> El + Send + Sync>,
ui_state: &mut UiState,
) {
let gap = node.gap.max(0.0);
let pitch = row_height + gap;
let total_h = virtual_total_height(count, count as f32 * row_height, gap);
resolve_scroll_requests(
node,
inner,
count,
|i| (i as f32 * pitch, row_height),
|row_key| row_key.parse::<usize>().ok().filter(|row| *row < count),
ui_state,
);
let offset = write_virtual_scroll_state(node, inner, total_h, ui_state);
if count == 0 {
node.children.clear();
return;
}
let start = (offset / pitch).floor() as usize;
let end = ((((offset + inner.h) / pitch).ceil() as usize) + 1).min(count);
let mut realized: Vec<El> = Vec::new();
let mut realized_range: Option<(usize, usize)> = None;
for global_i in start..end {
let row_top = global_i as f32 * pitch;
if row_top >= offset + inner.h || row_top + row_height <= offset {
continue;
}
let mut child = (build_row)(global_i);
assign_virtual_row_id(&mut child, &node.computed_id, global_i);
size_tree(&mut child, Some(inner.w));
let row_y = inner.y + row_top - offset;
let c_rect = Rect::new(inner.x, row_y, inner.w, row_height);
set_rect(&mut child, c_rect, ui_state);
layout_children(&mut child, c_rect, ui_state);
realized.push(child);
realized_range = Some(match realized_range {
None => (global_i, global_i + 1),
Some((s, _)) => (s, global_i + 1),
});
}
if let Some((s, e)) = realized_range {
ui_state
.scroll
.visible_ranges
.insert(node.computed_id.to_string(), (s, e));
}
node.children = realized;
}
fn layout_virtual_dynamic_incremental(
node: &mut El,
inner: Rect,
count: usize,
estimated_row_height: f32,
fns: DynamicVirtualFns,
ui_state: &mut UiState,
) {
let gap = node.gap.max(0.0);
let width_bucket = virtual_width_bucket(inner.w);
if count == 0 {
ui_state.scroll.virtual_anchors.remove(&*node.computed_id);
ui_state.scroll.dyn_height_index.remove(&*node.computed_id);
let offset = write_virtual_scroll_state(node, inner, 0.0, ui_state);
debug_assert_eq!(offset, 0.0);
node.children.clear();
return;
}
let existing = ui_state.scroll.dyn_height_index.remove(&*node.computed_id);
let head_key = (fns.row_key)(0);
let mut trimmed_keys: Vec<String> = Vec::new();
let reconciled = existing.and_then(|mut ix| {
let ok = ix.reconcile(
width_bucket,
estimated_row_height,
count,
&head_key,
|i| (fns.row_key)(i),
|_i, key| {
cached_row_height(
ui_state,
&node.computed_id,
key,
width_bucket,
estimated_row_height,
)
},
&mut trimmed_keys,
);
ok.then_some(ix)
});
let mut index = match reconciled {
Some(ix) => ix,
None => DynHeightIndex::build(width_bucket, estimated_row_height, count, |i| {
let key = (fns.row_key)(i);
let h = cached_row_height(
ui_state,
&node.computed_id,
&key,
width_bucket,
estimated_row_height,
);
(key, h)
}),
};
if !trimmed_keys.is_empty()
&& let Some(measured) = ui_state
.scroll
.measured_row_heights
.get_mut(&*node.computed_id)
{
for key in &trimmed_keys {
measured.remove(key);
}
if measured.is_empty() {
ui_state
.scroll
.measured_row_heights
.remove(&*node.computed_id);
}
}
let has_request = node.key.as_deref().is_some_and(|k| {
ui_state.scroll.pending_requests.iter().any(|r| match r {
ScrollRequest::ToRow { list_key, .. } => list_key == k,
ScrollRequest::ToRowKey { list_key, .. } => list_key == k,
ScrollRequest::EnsureVisible { .. } => false,
})
});
let mut request_wrote = false;
if has_request {
request_wrote = resolve_scroll_requests(
node,
inner,
count,
|target| (index.row_top(target, gap), index.height(target)),
|row_key| index.index_for_key(row_key),
ui_state,
);
}
let total_h = virtual_total_height(count, index.heights_sum(), gap);
let max_offset = (total_h - inner.h).max(0.0);
let stored = ui_state
.scroll
.offsets
.get(&*node.computed_id)
.copied()
.unwrap_or(0.0);
let pin_active = pin_would_be_active(node, stored, max_offset, ui_state).unwrap_or(false);
let provisional_offset = if pin_active {
match node.pin_policy {
crate::tree::PinPolicy::End => max_offset,
crate::tree::PinPolicy::Start => 0.0,
crate::tree::PinPolicy::None => unreachable!(),
}
} else if request_wrote {
stored
} else {
dynamic_anchor_offset_indexed(node, &index, gap, stored, ui_state).unwrap_or(stored)
}
.clamp(0.0, max_offset);
let (measure_start, _, measure_end) = index.visible_range(gap, provisional_offset, inner.h);
measure_dynamic_range(
node,
DynamicRangeCtx {
inner,
keys: KeySource::Func(&*fns.row_key),
width_bucket,
build_row: &fns.build_row,
},
measure_start,
measure_end,
ui_state,
);
refresh_index_range(
&mut index,
&node.computed_id,
width_bucket,
measure_start,
measure_end,
&*fns.row_key,
estimated_row_height,
ui_state,
);
let total_h = virtual_total_height(count, index.heights_sum(), gap);
let max_offset = (total_h - inner.h).max(0.0);
let stored = ui_state
.scroll
.offsets
.get(&*node.computed_id)
.copied()
.unwrap_or(0.0);
let pin_resolved = resolve_pin(node, stored, max_offset, ui_state);
let pin_active = !matches!(node.pin_policy, crate::tree::PinPolicy::None)
&& ui_state
.scroll
.pin_active
.get(&*node.computed_id)
.copied()
.unwrap_or(false);
let mut offset = if pin_active {
pin_resolved
} else if request_wrote {
stored
} else {
dynamic_anchor_offset_indexed(node, &index, gap, stored, ui_state).unwrap_or(stored)
}
.clamp(0.0, max_offset);
ui_state
.scroll
.offsets
.insert(node.computed_id.to_string(), offset);
let (start, start_y, end) = index.visible_range(gap, offset, inner.h);
let mut realized_rows = layout_dynamic_range(
node,
DynamicRangeCtx {
inner,
keys: KeySource::Func(&*fns.row_key),
width_bucket,
build_row: &fns.build_row,
},
offset,
start,
start_y,
end,
ui_state,
);
refresh_index_range(
&mut index,
&node.computed_id,
width_bucket,
start,
end,
&*fns.row_key,
estimated_row_height,
ui_state,
);
let total_h = virtual_total_height(count, index.heights_sum(), gap);
let max_offset = (total_h - inner.h).max(0.0);
let corrected_offset = if pin_active {
match node.pin_policy {
crate::tree::PinPolicy::End => max_offset,
crate::tree::PinPolicy::Start => 0.0,
crate::tree::PinPolicy::None => unreachable!(),
}
} else if request_wrote {
offset
} else {
dynamic_anchor_offset_indexed(node, &index, gap, stored, ui_state).unwrap_or(offset)
}
.clamp(0.0, max_offset);
if (corrected_offset - offset).abs() > 0.01 {
let dy = offset - corrected_offset;
for child in &mut node.children {
shift_subtree_y(child, dy, ui_state);
}
for row in &mut realized_rows {
row.rect.y += dy;
}
offset = corrected_offset;
ui_state
.scroll
.offsets
.insert(node.computed_id.to_string(), offset);
}
if matches!(node.pin_policy, crate::tree::PinPolicy::End) {
ui_state
.scroll
.pin_prev_max
.insert(node.computed_id.to_string(), max_offset);
}
write_virtual_scroll_metrics(node, inner, total_h, max_offset, offset, ui_state);
if let Some(anchor) = choose_dynamic_anchor(fns.anchor_policy, inner, offset, &realized_rows) {
ui_state
.scroll
.virtual_anchors
.insert(node.computed_id.to_string(), anchor);
} else {
ui_state.scroll.virtual_anchors.remove(&*node.computed_id);
}
ui_state
.scroll
.dyn_height_index
.insert(node.computed_id.to_string(), index);
}
fn cached_row_height(
ui_state: &UiState,
id: &str,
key: &str,
width_bucket: u32,
estimated_row_height: f32,
) -> f32 {
ui_state
.scroll
.measured_row_heights
.get(id)
.and_then(|m| m.get(key))
.and_then(|by_width| by_width.get(&width_bucket))
.copied()
.unwrap_or(estimated_row_height)
}
#[allow(clippy::too_many_arguments)]
fn refresh_index_range(
index: &mut DynHeightIndex,
id: &str,
width_bucket: u32,
start: usize,
end: usize,
row_key: &(dyn Fn(usize) -> String + Send + Sync),
estimated_row_height: f32,
ui_state: &UiState,
) {
for idx in start..end {
let key = row_key(idx);
let h = cached_row_height(ui_state, id, &key, width_bucket, estimated_row_height);
index.set_height(idx, h);
}
}
fn dynamic_anchor_offset_indexed(
node: &El,
index: &DynHeightIndex,
gap: f32,
stored: f32,
ui_state: &UiState,
) -> Option<f32> {
let anchor = ui_state.scroll.virtual_anchors.get(&*node.computed_id)?;
let idx = index.index_for_key(&anchor.row_key)?;
let row_h = index.height(idx).max(0.0);
let row_point = row_h * anchor.row_fraction.clamp(0.0, 1.0);
let scroll_delta = stored - anchor.resolved_offset;
let viewport_y = anchor.viewport_y - scroll_delta;
Some(index.row_top(idx, gap) + row_point - viewport_y)
}
fn layout_virtual_dynamic(
node: &mut El,
inner: Rect,
count: usize,
estimated_row_height: f32,
fns: DynamicVirtualFns,
ui_state: &mut UiState,
) {
let gap = node.gap.max(0.0);
let width_bucket = virtual_width_bucket(inner.w);
let row_keys = (0..count).map(|i| (fns.row_key)(i)).collect::<Vec<_>>();
prune_dynamic_measurements(node, &row_keys, ui_state);
if count == 0 {
ui_state.scroll.virtual_anchors.remove(&*node.computed_id);
let offset = write_virtual_scroll_state(node, inner, 0.0, ui_state);
debug_assert_eq!(offset, 0.0);
node.children.clear();
return;
}
let mut row_heights = dynamic_row_heights(
node,
&row_keys,
width_bucket,
estimated_row_height,
ui_state,
);
let has_request = node.key.as_deref().is_some_and(|k| {
ui_state.scroll.pending_requests.iter().any(|r| match r {
ScrollRequest::ToRow { list_key, .. } => list_key == k,
ScrollRequest::ToRowKey { list_key, .. } => list_key == k,
ScrollRequest::EnsureVisible { .. } => false,
})
});
let mut request_wrote = false;
if has_request {
request_wrote = resolve_scroll_requests(
node,
inner,
count,
|target| {
(
dynamic_row_top(&row_heights, gap, target),
row_heights[target],
)
},
|row_key| row_keys.iter().position(|key| key == row_key),
ui_state,
);
}
let total_h = virtual_total_height(count, row_heights.iter().sum(), gap);
let max_offset = (total_h - inner.h).max(0.0);
let stored = ui_state
.scroll
.offsets
.get(&*node.computed_id)
.copied()
.unwrap_or(0.0);
let pin_active = pin_would_be_active(node, stored, max_offset, ui_state).unwrap_or(false);
let provisional_offset = if pin_active {
match node.pin_policy {
crate::tree::PinPolicy::End => max_offset,
crate::tree::PinPolicy::Start => 0.0,
crate::tree::PinPolicy::None => unreachable!(),
}
} else if request_wrote {
stored
} else {
dynamic_anchor_offset(node, &row_keys, &row_heights, gap, stored, ui_state)
.unwrap_or(stored)
}
.clamp(0.0, max_offset);
let (measure_start, _, measure_end) =
dynamic_visible_range(&row_heights, gap, provisional_offset, inner.h);
measure_dynamic_range(
node,
DynamicRangeCtx {
inner,
keys: KeySource::Slice(&row_keys),
width_bucket,
build_row: &fns.build_row,
},
measure_start,
measure_end,
ui_state,
);
row_heights = dynamic_row_heights(
node,
&row_keys,
width_bucket,
estimated_row_height,
ui_state,
);
let total_h = virtual_total_height(count, row_heights.iter().sum(), gap);
let max_offset = (total_h - inner.h).max(0.0);
let stored = ui_state
.scroll
.offsets
.get(&*node.computed_id)
.copied()
.unwrap_or(0.0);
let pin_resolved = resolve_pin(node, stored, max_offset, ui_state);
let pin_active = !matches!(node.pin_policy, crate::tree::PinPolicy::None)
&& ui_state
.scroll
.pin_active
.get(&*node.computed_id)
.copied()
.unwrap_or(false);
let mut offset = if pin_active {
pin_resolved
} else if request_wrote {
stored
} else {
dynamic_anchor_offset(node, &row_keys, &row_heights, gap, stored, ui_state)
.unwrap_or(stored)
}
.clamp(0.0, max_offset);
ui_state
.scroll
.offsets
.insert(node.computed_id.to_string(), offset);
let (start, start_y, end) = dynamic_visible_range(&row_heights, gap, offset, inner.h);
let mut realized_rows = layout_dynamic_range(
node,
DynamicRangeCtx {
inner,
keys: KeySource::Slice(&row_keys),
width_bucket,
build_row: &fns.build_row,
},
offset,
start,
start_y,
end,
ui_state,
);
row_heights = dynamic_row_heights(
node,
&row_keys,
width_bucket,
estimated_row_height,
ui_state,
);
let total_h = virtual_total_height(count, row_heights.iter().sum(), gap);
let max_offset = (total_h - inner.h).max(0.0);
let corrected_offset = if pin_active {
match node.pin_policy {
crate::tree::PinPolicy::End => max_offset,
crate::tree::PinPolicy::Start => 0.0,
crate::tree::PinPolicy::None => unreachable!(),
}
} else if request_wrote {
offset
} else {
dynamic_anchor_offset(node, &row_keys, &row_heights, gap, stored, ui_state)
.unwrap_or(offset)
}
.clamp(0.0, max_offset);
if (corrected_offset - offset).abs() > 0.01 {
let dy = offset - corrected_offset;
for child in &mut node.children {
shift_subtree_y(child, dy, ui_state);
}
for row in &mut realized_rows {
row.rect.y += dy;
}
offset = corrected_offset;
ui_state
.scroll
.offsets
.insert(node.computed_id.to_string(), offset);
}
if matches!(node.pin_policy, crate::tree::PinPolicy::End) {
ui_state
.scroll
.pin_prev_max
.insert(node.computed_id.to_string(), max_offset);
}
write_virtual_scroll_metrics(node, inner, total_h, max_offset, offset, ui_state);
if let Some(anchor) = choose_dynamic_anchor(fns.anchor_policy, inner, offset, &realized_rows) {
ui_state
.scroll
.virtual_anchors
.insert(node.computed_id.to_string(), anchor);
} else {
ui_state.scroll.virtual_anchors.remove(&*node.computed_id);
}
}
struct DynamicVirtualFns {
anchor_policy: VirtualAnchorPolicy,
row_key: Arc<dyn Fn(usize) -> String + Send + Sync>,
build_row: Arc<dyn Fn(usize) -> El + Send + Sync>,
}
#[derive(Clone, Copy)]
struct DynamicRangeCtx<'a> {
inner: Rect,
keys: KeySource<'a>,
width_bucket: u32,
build_row: &'a Arc<dyn Fn(usize) -> El + Send + Sync>,
}
#[derive(Clone, Copy)]
enum KeySource<'a> {
Slice(&'a [String]),
Func(&'a (dyn Fn(usize) -> String + Send + Sync)),
}
impl KeySource<'_> {
fn key(&self, idx: usize) -> String {
match self {
KeySource::Slice(keys) => keys[idx].clone(),
KeySource::Func(f) => f(idx),
}
}
}
fn virtual_width_bucket(width: f32) -> u32 {
width.max(0.0).round().min(u32::MAX as f32) as u32
}
fn prune_dynamic_measurements(node: &El, row_keys: &[String], ui_state: &mut UiState) {
let Some(measurements) = ui_state
.scroll
.measured_row_heights
.get_mut(&*node.computed_id)
else {
return;
};
let live_keys = row_keys
.iter()
.map(String::as_str)
.collect::<FxHashSet<_>>();
measurements.retain(|key, widths| {
let live = live_keys.contains(key.as_str());
if live {
widths.retain(|_, h| h.is_finite() && *h >= 0.0);
}
live && !widths.is_empty()
});
if measurements.is_empty() {
ui_state
.scroll
.measured_row_heights
.remove(&*node.computed_id);
}
}
fn dynamic_row_heights(
node: &El,
row_keys: &[String],
width_bucket: u32,
estimated_row_height: f32,
ui_state: &UiState,
) -> Vec<f32> {
let measurements = ui_state.scroll.measured_row_heights.get(&*node.computed_id);
row_keys
.iter()
.map(|key| {
measurements
.and_then(|m| m.get(key))
.and_then(|by_width| by_width.get(&width_bucket))
.copied()
.unwrap_or(estimated_row_height)
})
.collect()
}
fn dynamic_row_top(row_heights: &[f32], gap: f32, target: usize) -> f32 {
row_heights
.iter()
.take(target)
.fold(0.0, |y, h| y + *h + gap)
}
fn dynamic_visible_range(
row_heights: &[f32],
gap: f32,
offset: f32,
viewport_h: f32,
) -> (usize, f32, usize) {
let count = row_heights.len();
let mut start = 0;
let mut y = 0.0_f32;
while start < count {
let h = row_heights[start];
if y + h > offset {
break;
}
y += h + gap;
start += 1;
}
let mut end = start;
let mut cursor = y;
let viewport_bottom = offset + viewport_h;
while end < count && cursor < viewport_bottom {
let h = row_heights[end];
end += 1;
cursor += h + gap;
}
(start, y, end)
}
fn dynamic_anchor_offset(
node: &El,
row_keys: &[String],
row_heights: &[f32],
gap: f32,
stored: f32,
ui_state: &UiState,
) -> Option<f32> {
let anchor = ui_state.scroll.virtual_anchors.get(&*node.computed_id)?;
let idx = if anchor.row_index < row_keys.len() && row_keys[anchor.row_index] == anchor.row_key {
anchor.row_index
} else {
row_keys.iter().position(|key| key == &anchor.row_key)?
};
let row_h = row_heights.get(idx).copied().unwrap_or(0.0).max(0.0);
let row_point = row_h * anchor.row_fraction.clamp(0.0, 1.0);
let scroll_delta = stored - anchor.resolved_offset;
let viewport_y = anchor.viewport_y - scroll_delta;
Some(dynamic_row_top(row_heights, gap, idx) + row_point - viewport_y)
}
fn measure_dynamic_range(
node: &El,
ctx: DynamicRangeCtx<'_>,
start: usize,
end: usize,
ui_state: &mut UiState,
) {
if start >= end {
return;
}
let mut new_measurements = Vec::new();
for idx in start..end {
let key = ctx.keys.key(idx);
let mut child = (ctx.build_row)(idx);
assign_virtual_row_id(&mut child, &node.computed_id, idx);
size_tree(&mut child, Some(ctx.inner.w));
let actual_h = measure_dynamic_row(node, idx, ctx.inner.w, &child);
new_measurements.push((key, actual_h));
}
store_dynamic_measurements(node, ctx.width_bucket, new_measurements, ui_state);
}
fn measure_dynamic_row(node: &El, idx: usize, width: f32, child: &El) -> f32 {
match child.height {
Size::Fixed(v) => v.max(0.0),
Size::Ch(n) => (n * ch_unit(child)).max(0.0),
Size::Hug => child.measured_size.1.max(0.0),
Size::Aspect(r) => (width * r).max(0.0),
Size::Fill(_) => panic!(
"virtual_list_dyn row {idx} on {:?} must size with Size::Fixed, Size::Hug, \
or Size::Aspect; Size::Fill would absorb the viewport's height and break \
virtualization",
node.computed_id,
),
}
}
const MAX_WIDTH_BUCKETS_PER_ROW: usize = 8;
fn store_dynamic_measurements(
node: &El,
width_bucket: u32,
measurements: Vec<(String, f32)>,
ui_state: &mut UiState,
) {
if measurements.is_empty() {
return;
}
let entry = ui_state
.scroll
.measured_row_heights
.entry(node.computed_id.to_string())
.or_default();
for (row_key, h) in measurements {
let buckets = entry.entry(row_key).or_default();
buckets.insert(width_bucket, h);
if buckets.len() > MAX_WIDTH_BUCKETS_PER_ROW {
let mut widths: Vec<u32> = buckets.keys().copied().collect();
widths.sort_unstable_by_key(|w| (i64::from(*w) - i64::from(width_bucket)).abs());
for w in widths.drain(MAX_WIDTH_BUCKETS_PER_ROW..) {
buckets.remove(&w);
}
}
}
}
#[derive(Clone, Debug)]
struct DynamicRealizedRow {
index: usize,
key: String,
rect: Rect,
}
fn layout_dynamic_range(
node: &mut El,
ctx: DynamicRangeCtx<'_>,
offset: f32,
start: usize,
start_y: f32,
end: usize,
ui_state: &mut UiState,
) -> Vec<DynamicRealizedRow> {
let gap = node.gap.max(0.0);
let mut cursor_y = start_y;
let mut realized = Vec::new();
let mut realized_rows = Vec::new();
let mut new_measurements = Vec::new();
for idx in start..end {
let key = ctx.keys.key(idx);
let mut child = (ctx.build_row)(idx);
assign_virtual_row_id(&mut child, &node.computed_id, idx);
size_tree(&mut child, Some(ctx.inner.w));
let actual_h = measure_dynamic_row(node, idx, ctx.inner.w, &child);
new_measurements.push((key.clone(), actual_h));
let row_y = ctx.inner.y + cursor_y - offset;
let c_rect = Rect::new(ctx.inner.x, row_y, ctx.inner.w, actual_h);
set_rect(&mut child, c_rect, ui_state);
layout_children(&mut child, c_rect, ui_state);
realized_rows.push(DynamicRealizedRow {
index: idx,
key,
rect: c_rect,
});
realized.push(child);
cursor_y += actual_h + gap;
}
store_dynamic_measurements(node, ctx.width_bucket, new_measurements, ui_state);
if let (Some(first), Some(last)) = (realized_rows.first(), realized_rows.last()) {
ui_state
.scroll
.visible_ranges
.insert(node.computed_id.to_string(), (first.index, last.index + 1));
}
node.children = realized;
realized_rows
}
fn choose_dynamic_anchor(
policy: VirtualAnchorPolicy,
inner: Rect,
offset: f32,
rows: &[DynamicRealizedRow],
) -> Option<VirtualAnchor> {
let visible = rows
.iter()
.filter(|row| row.rect.bottom() > inner.y && row.rect.y < inner.bottom())
.collect::<Vec<_>>();
if visible.is_empty() {
return None;
}
let chosen = match policy {
VirtualAnchorPolicy::ViewportFraction { y_fraction } => {
let target_y = inner.y + inner.h * y_fraction.clamp(0.0, 1.0);
visible
.iter()
.min_by(|a, b| {
let ad = distance_to_interval(target_y, a.rect.y, a.rect.bottom());
let bd = distance_to_interval(target_y, b.rect.y, b.rect.bottom());
ad.total_cmp(&bd)
})
.copied()
.map(|row| {
let anchor_y = target_y.clamp(row.rect.y, row.rect.bottom());
(row.clone(), anchor_y)
})
}
VirtualAnchorPolicy::FirstVisible => {
let row = visible
.iter()
.find(|row| row.rect.y >= inner.y && row.rect.bottom() <= inner.bottom())
.or_else(|| visible.first())
.copied()?;
let anchor_y = row.rect.y.max(inner.y);
Some((row.clone(), anchor_y))
}
VirtualAnchorPolicy::LastVisible => {
let row = visible
.iter()
.rev()
.find(|row| row.rect.y >= inner.y && row.rect.bottom() <= inner.bottom())
.or_else(|| visible.last())
.copied()?;
let anchor_y = row.rect.bottom().min(inner.bottom());
Some((row.clone(), anchor_y))
}
}?;
let (row, anchor_y) = chosen;
let row_h = row.rect.h.max(0.0);
let row_fraction = if row_h > 0.0 {
((anchor_y - row.rect.y) / row_h).clamp(0.0, 1.0)
} else {
0.0
};
Some(VirtualAnchor {
row_key: row.key.clone(),
row_index: row.index,
row_fraction,
viewport_y: anchor_y - inner.y,
resolved_offset: offset,
})
}
fn distance_to_interval(y: f32, top: f32, bottom: f32) -> f32 {
if y < top {
top - y
} else if y > bottom {
y - bottom
} else {
0.0
}
}
fn virtual_total_height(count: usize, row_sum: f32, gap: f32) -> f32 {
if count == 0 {
0.0
} else {
row_sum + gap * count.saturating_sub(1) as f32
}
}
fn apply_scroll_offset(node: &mut El, node_rect: Rect, ui_state: &mut UiState) {
let inner = node_rect.inset(node.padding);
if node.children.is_empty() {
ui_state
.scroll
.offsets
.insert(node.computed_id.to_string(), 0.0);
ui_state.scroll.scroll_anchors.remove(&*node.computed_id);
ui_state.scroll.metrics.insert(
node.computed_id.to_string(),
crate::state::ScrollMetrics {
viewport_h: inner.h,
content_h: 0.0,
max_offset: 0.0,
},
);
return;
}
let content_bottom = node
.children
.iter()
.map(|c| c.computed_rect.bottom())
.fold(f32::NEG_INFINITY, f32::max);
let content_h = (content_bottom - inner.y).max(0.0);
let max_offset = (content_h - inner.h).max(0.0);
let request_wrote = resolve_ensure_visible_for_scroll(node, inner, content_h, ui_state);
let stored = ui_state
.scroll
.offsets
.get(&*node.computed_id)
.copied()
.unwrap_or(0.0);
let stored = resolve_pin(node, stored, max_offset, ui_state);
let pin_active = !matches!(node.pin_policy, crate::tree::PinPolicy::None)
&& ui_state
.scroll
.pin_active
.get(&*node.computed_id)
.copied()
.unwrap_or(false);
let stored = if pin_active || request_wrote {
stored
} else {
scroll_anchor_offset(node, inner, stored, ui_state).unwrap_or(stored)
};
let clamped = stored.clamp(0.0, max_offset);
if clamped > 0.0 {
for c in &mut node.children {
shift_subtree_y(c, -clamped, ui_state);
}
}
ui_state
.scroll
.offsets
.insert(node.computed_id.to_string(), clamped);
ui_state.scroll.metrics.insert(
node.computed_id.to_string(),
crate::state::ScrollMetrics {
viewport_h: inner.h,
content_h,
max_offset,
},
);
write_thumb_rect(node, inner, content_h, max_offset, clamped, ui_state);
if let Some(anchor) = choose_scroll_anchor(node, inner, clamped) {
ui_state
.scroll
.scroll_anchors
.insert(node.computed_id.to_string(), anchor);
} else {
ui_state.scroll.scroll_anchors.remove(&*node.computed_id);
}
}
fn apply_viewport_transform(node: &mut El, node_rect: Rect, ui_state: &mut UiState) {
use crate::viewport::ViewportView;
let cfg = node
.viewport
.as_deref()
.copied()
.expect("apply_viewport_transform called on a non-viewport node");
let inner = node_rect.inset(node.padding);
let origin = (inner.x, inner.y);
let content = viewport_content_bbox(node);
let mut view = ui_state
.viewport
.views
.get(&*node.computed_id)
.copied()
.unwrap_or_default();
if let Some(key) = node.key.as_deref() {
let mut i = 0;
while i < ui_state.viewport.pending_requests.len() {
if ui_state.viewport.pending_requests[i].key() == key {
let req = ui_state.viewport.pending_requests.remove(i);
let mut target = apply_viewport_request(&req, cfg, inner, origin, content, view);
if let crate::viewport::FitPolicy::Contain { padding } = cfg.fit
&& matches!(
req,
crate::viewport::ViewportRequest::FitContent { .. }
| crate::viewport::ViewportRequest::ResetView { .. }
)
{
target = viewport_fit_view(cfg, inner, origin, content, target, padding);
}
let fly = req.behavior() == crate::viewport::ViewportBehavior::Smooth
&& ui_state.animation.mode == crate::state::AnimationMode::Live
&& !matches!(cfg.fit, crate::viewport::FitPolicy::Lock { .. })
&& inner.w > 0.0
&& inner.h > 0.0
&& target != view;
let rearm = matches!(
req,
crate::viewport::ViewportRequest::FitContent { .. }
| crate::viewport::ViewportRequest::ResetView { .. }
);
if rearm && !fly {
ui_state.viewport.taken_over.remove(&*node.computed_id);
} else {
ui_state
.viewport
.taken_over
.insert(node.computed_id.to_string());
}
if fly {
let path = crate::viewport::ZoomPath::new(
view_framing(view, inner, origin),
view_framing(target, inner, origin),
);
let ms = (f64::from(path.length()) * VIEWPORT_FLIGHT_MS_PER_UNIT)
.clamp(VIEWPORT_FLIGHT_MS_MIN, VIEWPORT_FLIGHT_MS_MAX);
ui_state.viewport.flights.insert(
node.computed_id.to_string(),
crate::state::ViewportFlight {
path,
started: viewport_clock(ui_state),
duration: std::time::Duration::from_secs_f64(ms / 1000.0),
rearm_on_arrival: rearm,
},
);
} else {
ui_state.viewport.flights.remove(&*node.computed_id);
view = target;
}
} else {
i += 1;
}
}
}
match cfg.fit {
crate::viewport::FitPolicy::Manual => {}
crate::viewport::FitPolicy::Contain { padding } => {
if !ui_state.viewport.taken_over.contains(&*node.computed_id) {
view = viewport_fit_view(cfg, inner, origin, content, view, padding);
}
}
crate::viewport::FitPolicy::Lock { padding } => {
ui_state.viewport.taken_over.remove(&*node.computed_id);
view = viewport_fit_view(cfg, inner, origin, content, view, padding);
}
}
if matches!(cfg.fit, crate::viewport::FitPolicy::Lock { .. }) {
ui_state.viewport.flights.remove(&*node.computed_id);
} else if let Some(flight) = ui_state.viewport.flights.get(&*node.computed_id).copied() {
let t = if flight.duration.is_zero()
|| ui_state.animation.mode == crate::state::AnimationMode::Settled
{
1.0
} else {
(viewport_clock(ui_state)
.saturating_duration_since(flight.started)
.as_secs_f32()
/ flight.duration.as_secs_f32())
.min(1.0)
};
let (cx, cy, w) = flight.path.sample(ease_in_out_cubic(t));
let w = w.clamp(
inner.w / cfg.max_zoom.max(1e-6),
inner.w / cfg.min_zoom.max(1e-6),
);
view = framing_view((cx, cy, w), inner, origin);
if t >= 1.0 {
ui_state.viewport.flights.remove(&*node.computed_id);
if flight.rearm_on_arrival {
ui_state.viewport.taken_over.remove(&*node.computed_id);
if let crate::viewport::FitPolicy::Contain { padding } = cfg.fit {
view = viewport_fit_view(cfg, inner, origin, content, view, padding);
}
}
}
}
view.zoom = view.zoom.clamp(cfg.min_zoom, cfg.max_zoom);
if let Some(c) = content {
clamp_viewport_pan(&mut view, cfg.pan_bounds, inner, origin, c);
}
if view != ViewportView::default() {
transform_viewport_subtree(node, view, origin, ui_state);
}
ui_state
.viewport
.views
.insert(node.computed_id.to_string(), view);
ui_state.viewport.metrics.insert(
node.computed_id.to_string(),
crate::state::ViewportMetrics {
inner,
content,
cfg,
},
);
}
fn viewport_content_bbox(node: &El) -> Option<Rect> {
let mut acc: Option<Rect> = None;
for c in &node.children {
let r = c.computed_rect;
acc = Some(acc.map_or(r, |a| union_rect(a, r)));
if let Some(bb) = viewport_content_bbox(c) {
acc = Some(acc.map_or(bb, |a| union_rect(a, bb)));
}
}
acc
}
fn union_rect(a: Rect, b: Rect) -> Rect {
let x = a.x.min(b.x);
let y = a.y.min(b.y);
let r = a.right().max(b.right());
let bot = a.bottom().max(b.bottom());
Rect::new(x, y, r - x, bot - y)
}
fn transform_viewport_subtree(
node: &mut El,
view: crate::viewport::ViewportView,
origin: (f32, f32),
ui_state: &mut UiState,
) {
for c in &mut node.children {
let rect = c.computed_rect;
let (nx, ny) = view.project((rect.x, rect.y), origin);
c.computed_rect = Rect::new(nx, ny, rect.w * view.zoom, rect.h * view.zoom);
if c.key.is_some()
&& let Some(r) = ui_state.layout.keyed_rects.get_mut(&c.computed_id)
{
*r = c.computed_rect;
}
transform_viewport_subtree(c, view, origin, ui_state);
}
}
const VIEWPORT_FLIGHT_MS_PER_UNIT: f64 = 350.0;
const VIEWPORT_FLIGHT_MS_MIN: f64 = 200.0;
const VIEWPORT_FLIGHT_MS_MAX: f64 = 800.0;
fn viewport_clock(ui_state: &UiState) -> web_time::Instant {
ui_state
.viewport
.clock_override
.unwrap_or_else(web_time::Instant::now)
}
fn ease_in_out_cubic(t: f32) -> f32 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
fn view_framing(
view: crate::viewport::ViewportView,
inner: Rect,
origin: (f32, f32),
) -> (f32, f32, f32) {
let c = view.unproject((inner.center_x(), inner.center_y()), origin);
(c.0, c.1, inner.w / view.zoom.max(1e-6))
}
fn framing_view(
(cx, cy, w): (f32, f32, f32),
inner: Rect,
origin: (f32, f32),
) -> crate::viewport::ViewportView {
viewport_center_on(inner, origin, inner.w / w.max(1e-6), (cx, cy))
}
fn apply_viewport_request(
req: &crate::viewport::ViewportRequest,
cfg: crate::viewport::ViewportConfig,
inner: Rect,
origin: (f32, f32),
content: Option<Rect>,
current: crate::viewport::ViewportView,
) -> crate::viewport::ViewportView {
use crate::viewport::{ViewportRequest, ViewportView};
match req {
ViewportRequest::ResetView { .. } => ViewportView::default(),
ViewportRequest::CenterOn { point, .. } => {
viewport_center_on(inner, origin, current.zoom, *point)
}
ViewportRequest::FitContent { padding, .. } => {
viewport_fit_view(cfg, inner, origin, content, current, *padding)
}
ViewportRequest::FrameRect { rect, padding, .. } => {
viewport_fit_rect(cfg, inner, origin, *rect, current, *padding)
}
}
}
fn viewport_fit_view(
cfg: crate::viewport::ViewportConfig,
inner: Rect,
origin: (f32, f32),
content: Option<Rect>,
current: crate::viewport::ViewportView,
padding: f32,
) -> crate::viewport::ViewportView {
match content {
Some(c) if c.w > 0.0 || c.h > 0.0 => {
viewport_fit_rect(cfg, inner, origin, c, current, padding)
}
_ => current,
}
}
fn viewport_fit_rect(
cfg: crate::viewport::ViewportConfig,
inner: Rect,
origin: (f32, f32),
rect: Rect,
current: crate::viewport::ViewportView,
padding: f32,
) -> crate::viewport::ViewportView {
if rect.w <= 0.0 && rect.h <= 0.0 {
return viewport_center_on(inner, origin, current.zoom, (rect.x, rect.y));
}
let avail_w = (inner.w - 2.0 * padding).max(1.0);
let avail_h = (inner.h - 2.0 * padding).max(1.0);
let mut zoom = f32::INFINITY;
if rect.w > 0.0 {
zoom = zoom.min(avail_w / rect.w);
}
if rect.h > 0.0 {
zoom = zoom.min(avail_h / rect.h);
}
if !zoom.is_finite() {
return current;
}
let zoom = zoom.clamp(cfg.min_zoom, cfg.max_zoom);
viewport_center_on(inner, origin, zoom, (rect.center_x(), rect.center_y()))
}
fn viewport_center_on(
inner: Rect,
origin: (f32, f32),
zoom: f32,
point: (f32, f32),
) -> crate::viewport::ViewportView {
crate::viewport::ViewportView {
pan: (
inner.center_x() - origin.0 - zoom * (point.0 - origin.0),
inner.center_y() - origin.1 - zoom * (point.1 - origin.1),
),
zoom,
}
}
fn clamp_viewport_pan(
view: &mut crate::viewport::ViewportView,
bounds: crate::viewport::PanBounds,
inner: Rect,
origin: (f32, f32),
content: Rect,
) {
if matches!(bounds, crate::viewport::PanBounds::Free) {
return;
}
let (lx, ty) = view.project((content.x, content.y), origin);
let w = content.w * view.zoom;
let h = content.h * view.zoom;
view.pan.0 += clamp_axis_delta(bounds, lx, lx + w, inner.x, inner.right(), w, inner.w);
view.pan.1 += clamp_axis_delta(bounds, ty, ty + h, inner.y, inner.bottom(), h, inner.h);
}
fn clamp_axis_delta(
bounds: crate::viewport::PanBounds,
lo: f32,
hi: f32,
vlo: f32,
vhi: f32,
size: f32,
vsize: f32,
) -> f32 {
use crate::viewport::PanBounds;
match bounds {
PanBounds::Free => 0.0,
PanBounds::Center => {
let vc = 0.5 * (vlo + vhi);
if lo > vc {
vc - lo
} else if hi < vc {
vc - hi
} else {
0.0
}
}
PanBounds::Contain => {
if size <= vsize {
if lo < vlo {
vlo - lo
} else if hi > vhi {
vhi - hi
} else {
0.0
}
} else {
if lo > vlo {
vlo - lo
} else if hi < vhi {
vhi - hi
} else {
0.0
}
}
}
}
}
fn scroll_anchor_offset(node: &El, inner: Rect, stored: f32, ui_state: &UiState) -> Option<f32> {
let anchor = ui_state.scroll.scroll_anchors.get(&*node.computed_id)?;
let rect = &find_descendant_rect(node, anchor.node_id.as_str())?;
if rect.h <= 0.0 {
return None;
}
let rect_point = rect.h * anchor.rect_fraction.clamp(0.0, 1.0);
let scroll_delta = stored - anchor.resolved_offset;
let viewport_y = anchor.viewport_y - scroll_delta;
Some(rect.y - inner.y + rect_point - viewport_y)
}
fn choose_scroll_anchor(node: &El, inner: Rect, offset: f32) -> Option<ScrollAnchor> {
if inner.h <= 0.0 {
return None;
}
let target_y = inner.y + inner.h * 0.25;
let mut best = None;
for child in &node.children {
choose_scroll_anchor_in_subtree(child, inner, target_y, 1, &mut best);
}
let candidate = best?;
let anchor_y = target_y.clamp(candidate.rect.y, candidate.rect.bottom());
let rect_fraction = if candidate.rect.h > 0.0 {
((anchor_y - candidate.rect.y) / candidate.rect.h).clamp(0.0, 1.0)
} else {
0.0
};
Some(ScrollAnchor {
node_id: candidate.node_id,
rect_fraction,
viewport_y: anchor_y - inner.y,
resolved_offset: offset,
})
}
#[derive(Clone, Debug)]
struct ScrollAnchorCandidate {
node_id: String,
rect: Rect,
distance: f32,
depth: usize,
}
fn choose_scroll_anchor_in_subtree(
node: &El,
inner: Rect,
target_y: f32,
depth: usize,
best: &mut Option<ScrollAnchorCandidate>,
) {
let rect = node.computed_rect;
if rect.w > 0.0 && rect.h > 0.0 && rect.bottom() > inner.y && rect.y < inner.bottom() {
let distance = distance_to_interval(target_y, rect.y, rect.bottom());
let candidate = ScrollAnchorCandidate {
node_id: node.computed_id.clone().to_string(),
rect,
distance,
depth,
};
let replace = best.as_ref().is_none_or(|current| {
candidate.distance < current.distance
|| (candidate.distance == current.distance && candidate.depth > current.depth)
|| (candidate.distance == current.distance
&& candidate.depth == current.depth
&& candidate.rect.h < current.rect.h)
});
if replace {
*best = Some(candidate);
}
}
if node.scrollable {
return;
}
for child in &node.children {
choose_scroll_anchor_in_subtree(child, inner, target_y, depth + 1, best);
}
}
const PIN_EPSILON: f32 = 0.5;
fn pin_would_be_active(
node: &El,
stored: f32,
_max_offset: f32,
ui_state: &UiState,
) -> Option<bool> {
let prev_active = ui_state.scroll.pin_active.get(&*node.computed_id).copied();
match node.pin_policy {
crate::tree::PinPolicy::None => None,
crate::tree::PinPolicy::End => {
let prev_max = ui_state
.scroll
.pin_prev_max
.get(&*node.computed_id)
.copied();
Some(match prev_active {
None => true,
Some(prev) => {
let prev_max = prev_max.unwrap_or(0.0);
if prev && stored < prev_max - PIN_EPSILON {
false
} else if !prev && prev_max > 0.0 && stored >= prev_max - PIN_EPSILON {
true
} else {
prev
}
}
})
}
crate::tree::PinPolicy::Start => Some(match prev_active {
None => true,
Some(prev) => {
if prev && stored > PIN_EPSILON {
false
} else if !prev && stored <= PIN_EPSILON {
true
} else {
prev
}
}
}),
}
}
fn resolve_pin(node: &El, stored: f32, max_offset: f32, ui_state: &mut UiState) -> f32 {
if matches!(node.pin_policy, crate::tree::PinPolicy::None) {
ui_state.scroll.pin_active.remove(&*node.computed_id);
ui_state.scroll.pin_prev_max.remove(&*node.computed_id);
return stored;
}
let active = pin_would_be_active(node, stored, max_offset, ui_state).unwrap_or(false);
ui_state
.scroll
.pin_active
.insert(node.computed_id.to_string(), active);
match node.pin_policy {
crate::tree::PinPolicy::End => {
ui_state
.scroll
.pin_prev_max
.insert(node.computed_id.to_string(), max_offset);
if active { max_offset } else { stored }
}
crate::tree::PinPolicy::Start => {
ui_state.scroll.pin_prev_max.remove(&*node.computed_id);
if active { 0.0 } else { stored }
}
crate::tree::PinPolicy::None => unreachable!(),
}
}
fn resolve_ensure_visible_for_scroll(
node: &El,
inner: Rect,
content_h: f32,
ui_state: &mut UiState,
) -> bool {
if ui_state.scroll.pending_requests.is_empty() {
return false;
}
let pending = std::mem::take(&mut ui_state.scroll.pending_requests);
let mut remaining: Vec<ScrollRequest> = Vec::with_capacity(pending.len());
let mut wrote = false;
for req in pending {
let ScrollRequest::EnsureVisible {
container_key,
y,
h,
} = &req
else {
remaining.push(req);
continue;
};
let Some(ancestor_id) = ui_state.layout.key_index.get(container_key) else {
remaining.push(req);
continue;
};
let inside = node.computed_id == *ancestor_id
|| node
.computed_id
.strip_prefix(ancestor_id.as_ref())
.is_some_and(|rest| rest.starts_with('.'));
if !inside {
remaining.push(req);
continue;
}
let current = ui_state
.scroll
.offsets
.get(&*node.computed_id)
.copied()
.unwrap_or(0.0);
let target_top = *y;
let target_bottom = *y + *h;
let viewport_h = inner.h;
let new_offset = if target_top < current {
target_top
} else if target_bottom > current + viewport_h {
target_bottom - viewport_h
} else {
continue;
};
let max = (content_h - viewport_h).max(0.0);
let new_offset = new_offset.clamp(0.0, max);
ui_state
.scroll
.offsets
.insert(node.computed_id.to_string(), new_offset);
wrote = true;
}
ui_state.scroll.pending_requests = remaining;
wrote
}
fn write_thumb_rect(
node: &El,
inner: Rect,
content_h: f32,
max_offset: f32,
offset: f32,
ui_state: &mut UiState,
) {
if !node.scrollbar || max_offset <= 0.0 || inner.h <= 0.0 || content_h <= 0.0 {
return;
}
let thumb_w = crate::tokens::SCROLLBAR_THUMB_WIDTH;
let track_w = crate::tokens::SCROLLBAR_HITBOX_WIDTH;
let track_inset = crate::tokens::SCROLLBAR_TRACK_INSET;
let min_thumb_h = crate::tokens::SCROLLBAR_THUMB_MIN_H;
let thumb_h = ((inner.h * inner.h / content_h).max(min_thumb_h)).min(inner.h);
let track_remaining = (inner.h - thumb_h).max(0.0);
let thumb_y = inner.y + track_remaining * (offset / max_offset);
let edge = if node.scrollbar_gutter {
inner.right() + crate::tokens::SCROLLBAR_GUTTER
} else {
inner.right()
};
let thumb_x = edge - thumb_w - track_inset;
let track_x = edge - track_w - track_inset;
ui_state.scroll.thumb_rects.insert(
node.computed_id.to_string(),
Rect::new(thumb_x, thumb_y, thumb_w, thumb_h),
);
ui_state.scroll.thumb_tracks.insert(
node.computed_id.to_string(),
Rect::new(track_x, inner.y, track_w, inner.h),
);
}
fn shift_subtree_y(node: &mut El, dy: f32, ui_state: &mut UiState) {
node.computed_rect.y += dy;
if node.key.is_some()
&& let Some(rect) = ui_state.layout.keyed_rects.get_mut(&node.computed_id)
{
rect.y += dy;
}
if node.scrollbar {
if let Some(thumb) = ui_state.scroll.thumb_rects.get_mut(&*node.computed_id) {
thumb.y += dy;
}
if let Some(track) = ui_state.scroll.thumb_tracks.get_mut(&*node.computed_id) {
track.y += dy;
}
}
for c in &mut node.children {
shift_subtree_y(c, dy, ui_state);
}
}
#[derive(Default)]
struct AxisScratch {
main_sizes: Vec<f32>,
fill_weights: Vec<Option<f32>>,
row_slots: Vec<Option<(f32, f32)>>,
}
impl AxisScratch {
fn take() -> Self {
AXIS_SCRATCH_POOL
.with_borrow_mut(|pool| pool.pop())
.unwrap_or_default()
}
fn release(mut self) {
self.main_sizes.clear();
self.fill_weights.clear();
self.row_slots.clear();
AXIS_SCRATCH_POOL.with_borrow_mut(|pool| {
if pool.len() < 256 {
pool.push(self);
}
});
}
}
thread_local! {
static AXIS_SCRATCH_POOL: std::cell::RefCell<Vec<AxisScratch>> =
const { std::cell::RefCell::new(Vec::new()) };
}
fn layout_axis(node: &mut El, node_rect: Rect, vertical: bool, ui_state: &mut UiState) {
let inner = node_rect.inset(node.padding);
let n = node.children.len();
if n == 0 {
return;
}
let mut scratch = AxisScratch::take();
let total_gap = node.gap * n.saturating_sub(1) as f32;
let main_extent = if vertical { inner.h } else { inner.w };
let cross_extent = if vertical { inner.w } else { inner.h };
let resolve_main = |c: &El, iw: f32, ih: f32| -> MainSize {
let main_intent = if vertical { c.height } else { c.width };
if let Size::Aspect(r) = main_intent {
let cross_intent = if vertical { c.width } else { c.height };
if !matches!(cross_intent, Size::Aspect(_)) {
let cross_intrinsic = if vertical { iw } else { ih };
let cross_size = match cross_intent {
Size::Fixed(v) => v,
Size::Ch(n) => n * ch_unit(c),
Size::Hug | Size::Fill(_) => match node.align {
Align::Stretch => cross_extent,
Align::Start | Align::Center | Align::End => cross_intrinsic,
},
Size::Aspect(_) => unreachable!(),
};
let cross_size = if vertical {
clamp_w(c, cross_size)
} else {
clamp_h(c, cross_size)
};
let main = cross_size * r.max(0.0);
let clamped = if vertical {
clamp_h(c, main)
} else {
clamp_w(c, main)
};
return MainSize::Resolved(clamped);
}
}
main_size_of(c, iw, ih, vertical)
};
let AxisScratch {
main_sizes,
fill_weights,
..
} = &mut scratch;
let mut consumed = 0.0;
for c in node.children.iter() {
let (iw, ih) = c.measured_size;
match resolve_main(c, iw, ih) {
MainSize::Resolved(v) => {
consumed += v;
main_sizes.push(v);
fill_weights.push(None);
}
MainSize::Fill(w) => {
main_sizes.push(0.0);
fill_weights.push(Some(w.max(0.001)));
}
}
}
let mut remaining = (main_extent - consumed - total_gap).max(0.0);
loop {
let flexible_weight: f32 = fill_weights.iter().flatten().sum();
if flexible_weight == 0.0 {
break;
}
let mut frozen_any = false;
let mut newly_frozen = 0.0;
for (i, c) in node.children.iter().enumerate() {
let Some(w) = fill_weights[i] else { continue };
let raw = remaining * w / flexible_weight;
let clamped = if vertical {
clamp_h(c, raw)
} else {
clamp_w(c, raw)
};
main_sizes[i] = clamped;
if clamped != raw {
fill_weights[i] = None;
frozen_any = true;
newly_frozen += clamped;
}
}
if !frozen_any {
remaining = 0.0;
break;
}
remaining = (remaining - newly_frozen).max(0.0);
}
let free_after_used = remaining;
let mut cursor = match node.justify {
Justify::Start => 0.0,
Justify::Center => free_after_used * 0.5,
Justify::End => free_after_used,
Justify::SpaceBetween => 0.0,
};
let between_extra = if matches!(node.justify, Justify::SpaceBetween) && n > 1 {
free_after_used / (n - 1) as f32
} else {
0.0
};
let scroll_visible = scroll_visible_content_rect(node, inner, vertical, ui_state);
crate::profile_span!("layout::axis::place");
for (i, c) in node.children.iter_mut().enumerate() {
let (iw, ih) = c.measured_size;
let main_size = main_sizes[i];
let cross_intent = if vertical { c.width } else { c.height };
let cross_intrinsic = if vertical { iw } else { ih };
let cross_size = match cross_intent {
Size::Fixed(v) => v,
Size::Ch(n) => n * ch_unit(c),
Size::Aspect(r) => main_size * r,
Size::Hug | Size::Fill(_) => match node.align {
Align::Stretch => cross_extent,
Align::Start | Align::Center | Align::End => cross_intrinsic,
},
};
let cross_size = if vertical {
clamp_w(c, cross_size)
} else {
clamp_h(c, cross_size)
};
let cross_off = match node.align {
Align::Start | Align::Stretch => 0.0,
Align::Center => (cross_extent - cross_size) * 0.5,
Align::End => cross_extent - cross_size,
};
let c_rect = if vertical {
Rect::new(inner.x + cross_off, inner.y + cursor, cross_size, main_size)
} else {
Rect::new(inner.x + cursor, inner.y + cross_off, main_size, cross_size)
};
set_rect(c, c_rect, ui_state);
if can_prune_scroll_child(c, c_rect, scroll_visible) {
let nodes = zero_descendant_rects(c, c_rect, ui_state);
record_pruned_subtree(nodes);
} else {
resize_if_width_diverged(c, c_rect.w);
layout_children(c, c_rect, ui_state);
}
cursor += main_size + node.gap + if i + 1 < n { between_extra } else { 0.0 };
}
scratch.release();
}
const SCROLL_LAYOUT_PRUNE_OVERSCAN: f32 = 256.0;
fn scroll_visible_content_rect(
node: &El,
inner: Rect,
vertical: bool,
ui_state: &UiState,
) -> Option<Rect> {
if !vertical || !node.scrollable || !matches!(node.pin_policy, crate::tree::PinPolicy::None) {
return None;
}
let offset = ui_state
.scroll
.offsets
.get(&*node.computed_id)
.copied()
.unwrap_or(0.0)
.max(0.0);
Some(Rect::new(
inner.x,
inner.y + offset - SCROLL_LAYOUT_PRUNE_OVERSCAN,
inner.w,
inner.h + 2.0 * SCROLL_LAYOUT_PRUNE_OVERSCAN,
))
}
fn can_prune_scroll_child(child: &El, child_rect: Rect, visible: Option<Rect>) -> bool {
let Some(visible) = visible else {
return false;
};
child_rect.intersect(visible).is_none() && subtree_is_layout_confined(child)
}
pub(crate) fn subtree_is_layout_confined(node: &El) -> bool {
if node.translate != (0.0, 0.0)
|| node.scale != 1.0
|| node.shadow > 0.0
|| node.paint_overflow != Sides::zero()
|| node.hit_overflow != Sides::zero()
|| node.layout_override.is_some()
|| node.virtual_items.is_some()
{
return false;
}
node.children.iter().all(subtree_is_layout_confined)
}
fn zero_descendant_rects(node: &mut El, rect: Rect, ui_state: &mut UiState) -> u64 {
let mut count = 0;
let zero = Rect::new(rect.x, rect.y, 0.0, 0.0);
for child in &mut node.children {
set_rect(child, zero, ui_state);
count += 1 + zero_descendant_rects(child, zero, ui_state);
}
count
}
fn record_pruned_subtree(nodes: u64) {
PRUNE_STATS.with(|stats| {
let mut stats = stats.borrow_mut();
stats.subtrees += 1;
stats.nodes += nodes;
});
}
enum MainSize {
Resolved(f32),
Fill(f32),
}
fn main_size_of(c: &El, iw: f32, ih: f32, vertical: bool) -> MainSize {
let s = if vertical { c.height } else { c.width };
let intr = if vertical { ih } else { iw };
let clamp = |v: f32| {
if vertical {
clamp_h(c, v)
} else {
clamp_w(c, v)
}
};
match s {
Size::Fixed(v) => MainSize::Resolved(clamp(v)),
Size::Ch(n) => MainSize::Resolved(clamp(n * ch_unit(c))),
Size::Hug => MainSize::Resolved(clamp(intr)),
Size::Fill(w) => MainSize::Fill(w),
Size::Aspect(_) => MainSize::Resolved(clamp(intr)),
}
}
fn overlay_rect(
c: &El,
parent: Rect,
align: Align,
justify: Justify,
clamp_to_parent: bool,
) -> Rect {
let hug_w = |iw: f32| {
if clamp_to_parent {
iw.min(parent.w)
} else {
iw
}
};
let hug_h = |ih: f32| {
if clamp_to_parent {
ih.min(parent.h)
} else {
ih
}
};
let (iw, ih) = c.measured_size;
let (w, h) = match (c.width, c.height) {
(Size::Aspect(_), Size::Aspect(_)) => (hug_w(iw), hug_h(ih)),
(Size::Aspect(r), _) => {
let h = match c.height {
Size::Fixed(v) => v,
Size::Ch(n) => n * ch_unit(c),
Size::Hug => hug_h(ih),
Size::Fill(_) => parent.h,
Size::Aspect(_) => unreachable!(),
};
(h * r, h)
}
(_, Size::Aspect(r)) => {
let w = match c.width {
Size::Fixed(v) => v,
Size::Ch(n) => n * ch_unit(c),
Size::Hug => hug_w(iw),
Size::Fill(_) => parent.w,
Size::Aspect(_) => unreachable!(),
};
(w, w * r)
}
_ => {
let w = match c.width {
Size::Fixed(v) => v,
Size::Ch(n) => n * ch_unit(c),
Size::Hug => hug_w(iw),
Size::Fill(_) => parent.w,
Size::Aspect(_) => unreachable!(),
};
let h = match c.height {
Size::Fixed(v) => v,
Size::Ch(n) => n * ch_unit(c),
Size::Hug => hug_h(ih),
Size::Fill(_) => parent.h,
Size::Aspect(_) => unreachable!(),
};
(w, h)
}
};
let w = clamp_w(c, w);
let h = clamp_h(c, h);
let x = match align {
Align::Start | Align::Stretch => parent.x,
Align::Center => parent.x + (parent.w - w) * 0.5,
Align::End => parent.right() - w,
};
let y = match justify {
Justify::Start | Justify::SpaceBetween => parent.y,
Justify::Center => parent.y + (parent.h - h) * 0.5,
Justify::End => parent.bottom() - h,
};
Rect::new(x, y, w, h)
}
pub fn intrinsic(c: &El) -> (f32, f32) {
intrinsic_constrained(c, None)
}
fn intrinsic_constrained(c: &El, available_width: Option<f32>) -> (f32, f32) {
let available_width = match c.width {
Size::Fixed(v) => Some(v),
_ => available_width,
};
apply_aspect(
c,
available_width,
intrinsic_constrained_uncached(c, available_width),
)
}
fn size_tree(node: &mut El, available_width: Option<f32>) -> (f32, f32) {
SIZING_VISITS.with(|c| c.set(c.get() + 1));
let available_width = match node.width {
Size::Fixed(v) => Some(v),
Size::Ch(n) => Some(n * ch_unit(node)),
_ => available_width,
};
let inner = size_tree_inner(node, available_width);
let size = apply_aspect(node, available_width, inner);
node.measured_size = size;
node.sized_at_width = available_width.unwrap_or(size.0);
node.width_sensitive = (node.text.is_some() && matches!(node.text_wrap, TextWrap::Wrap))
|| matches!(node.kind, Kind::Inlines)
|| node.layout_override.is_some()
|| node.virtual_items.is_some()
|| matches!(node.width, Size::Aspect(_))
|| matches!(node.height, Size::Aspect(_))
|| node.children.iter().any(|c| c.width_sensitive);
size
}
#[inline]
fn resize_if_width_diverged(c: &mut El, final_w: f32) {
if c.width_sensitive && !c.children.is_empty() && (final_w - c.sized_at_width).abs() > 0.5 {
size_tree(c, Some(final_w));
}
}
fn size_tree_inner(node: &mut El, available_width: Option<f32>) -> (f32, f32) {
if let Some(size) = leaf_intrinsic(node, available_width) {
if node.layout_override.is_some() {
for ch in &mut node.children {
size_tree(ch, None);
}
} else if !node.children.is_empty()
&& node.virtual_items.is_none()
&& !matches!(node.kind, Kind::Inlines)
{
size_tree_children(node, Some(size.0));
}
return size;
}
size_tree_children(node, available_width)
}
fn size_tree_children(node: &mut El, available_width: Option<f32>) -> (f32, f32) {
match node.axis {
Axis::Overlay => {
let child_available =
available_width.map(|w| (w - node.padding.left - node.padding.right).max(0.0));
let mut w: f32 = 0.0;
let mut h: f32 = 0.0;
for ch in &mut node.children {
let ca = if matches!(ch.width, Size::Aspect(_)) {
None
} else {
child_available
};
let (cw, chh) = size_tree(ch, ca);
w = w.max(cw);
h = h.max(chh);
}
apply_min(
node,
w + node.padding.left + node.padding.right,
h + node.padding.top + node.padding.bottom,
)
}
Axis::Column => {
let mut w: f32 = 0.0;
let mut h: f32 = node.padding.top + node.padding.bottom;
let n = node.children.len();
let child_available =
available_width.map(|w| (w - node.padding.left - node.padding.right).max(0.0));
for (i, ch) in node.children.iter_mut().enumerate() {
let (cw, chh) = size_tree(ch, child_available);
w = w.max(cw);
h += chh;
if i + 1 < n {
h += node.gap;
}
}
apply_min(node, w + node.padding.left + node.padding.right, h)
}
Axis::Row => {
let n = node.children.len();
let total_gap = node.gap * n.saturating_sub(1) as f32;
let inner_available = available_width
.map(|w| (w - node.padding.left - node.padding.right - total_gap).max(0.0));
let mut scratch = AxisScratch::take();
let mut consumed: f32 = 0.0;
let mut fill_weight_total: f32 = 0.0;
for ch in &mut node.children {
match ch.width {
Size::Fill(w) => {
fill_weight_total += w.max(0.001);
scratch.row_slots.push(None);
}
_ => {
let (cw, chh) = size_tree(ch, None);
consumed += cw;
scratch.row_slots.push(Some((cw, chh)));
}
}
}
let fill_remaining = inner_available.map(|av| (av - consumed).max(0.0));
let mut w_total: f32 = node.padding.left + node.padding.right;
let mut h_max: f32 = 0.0;
for (i, ch) in node.children.iter_mut().enumerate() {
let (cw, chh) = match scratch.row_slots[i] {
Some(rc) => rc,
None => match (fill_remaining, fill_weight_total > 0.0) {
(Some(av), true) => {
let weight = match ch.width {
Size::Fill(w) => w.max(0.001),
_ => 1.0,
};
size_tree(ch, Some(av * weight / fill_weight_total))
}
_ => size_tree(ch, None),
},
};
w_total += cw;
if i + 1 < n {
w_total += node.gap;
}
h_max = h_max.max(chh);
}
scratch.release();
apply_min(
node,
w_total,
h_max + node.padding.top + node.padding.bottom,
)
}
}
}
fn apply_aspect(c: &El, available_width: Option<f32>, (iw, ih): (f32, f32)) -> (f32, f32) {
match (c.width, c.height) {
(Size::Aspect(_), Size::Aspect(_)) => (iw, ih),
(Size::Aspect(r), _) => {
(clamp_w(c, ih * r.max(0.0)), ih)
}
(_, Size::Aspect(r)) => {
let raw_basis = match c.width {
Size::Fixed(v) => v,
Size::Ch(n) => n * ch_unit(c),
Size::Fill(_) => available_width.unwrap_or(iw),
Size::Hug | Size::Aspect(_) => iw,
};
let basis = clamp_w(c, raw_basis);
(iw, clamp_h(c, basis * r.max(0.0)))
}
_ => (iw, ih),
}
}
fn intrinsic_constrained_uncached(c: &El, available_width: Option<f32>) -> (f32, f32) {
if let Some(size) = leaf_intrinsic(c, available_width) {
return size;
}
container_intrinsic(c, available_width)
}
fn leaf_intrinsic(c: &El, available_width: Option<f32>) -> Option<(f32, f32)> {
if c.layout_override.is_some() {
if matches!(c.width, Size::Hug) || matches!(c.height, Size::Hug) {
panic!(
"layout_override on {:?} requires Size::Fixed or Size::Fill on both axes; \
Size::Hug is not supported for custom layouts",
c.computed_id,
);
}
return Some(apply_min(c, 0.0, 0.0));
}
if c.virtual_items.is_some() {
if matches!(c.width, Size::Hug) || matches!(c.height, Size::Hug) {
panic!(
"virtual_list on {:?} requires Size::Fixed or Size::Fill on both axes; \
Size::Hug would defeat virtualization",
c.computed_id,
);
}
return Some(apply_min(c, 0.0, 0.0));
}
if matches!(c.kind, Kind::Inlines) {
return Some(inline_paragraph_intrinsic(c, available_width));
}
if matches!(c.kind, Kind::HardBreak) {
return Some(apply_min(c, 0.0, 0.0));
}
if matches!(c.kind, Kind::Math) {
if let Some(expr) = &c.math {
let layout = crate::math::layout_math(expr, c.font_size, c.math_display);
return Some(apply_min(
c,
layout.width + c.padding.left + c.padding.right,
layout.height() + c.padding.top + c.padding.bottom,
));
}
return Some(apply_min(c, 0.0, 0.0));
}
if c.icon.is_some() {
return Some(apply_min(
c,
c.font_size + c.padding.left + c.padding.right,
c.font_size + c.padding.top + c.padding.bottom,
));
}
if let Some(img) = &c.image {
let w = img.width() as f32 + c.padding.left + c.padding.right;
let h = img.height() as f32 + c.padding.top + c.padding.bottom;
return Some(apply_min(c, w, h));
}
if let Some(text) = &c.text {
let content_available = match c.text_wrap {
TextWrap::NoWrap => None,
TextWrap::Wrap => available_width
.or(match c.width {
Size::Fixed(v) => Some(v),
Size::Ch(n) => Some(n * ch_unit(c)),
Size::Fill(_) | Size::Hug | Size::Aspect(_) => None,
})
.map(|w| (w - c.padding.left - c.padding.right).max(1.0)),
};
let display = display_text_for_measure(c, text, content_available);
let layout = text_metrics::layout_text_with_line_height_and_family(
&display,
c.font_size,
c.line_height,
c.font_family,
c.font_weight,
c.font_mono,
c.text_tabular_numerals,
c.text_letter_spacing,
c.text_wrap,
content_available,
);
let w = match (content_available, c.width) {
(Some(available), Size::Hug | Size::Aspect(_)) => {
let unwrapped = text_metrics::layout_text_with_family(
text,
c.font_size,
c.font_family,
c.font_weight,
c.font_mono,
c.text_tabular_numerals,
TextWrap::NoWrap,
None,
);
unwrapped.width.min(available) + c.padding.left + c.padding.right
}
(Some(available), Size::Fixed(_) | Size::Fill(_) | Size::Ch(_)) => {
available + c.padding.left + c.padding.right
}
(None, _) => layout.width + c.padding.left + c.padding.right,
};
let h = layout.height + c.padding.top + c.padding.bottom;
return Some(apply_min(c, w, h));
}
None
}
fn container_intrinsic(c: &El, available_width: Option<f32>) -> (f32, f32) {
match c.axis {
Axis::Overlay => {
let mut w: f32 = 0.0;
let mut h: f32 = 0.0;
for ch in &c.children {
let child_available =
available_width.map(|w| (w - c.padding.left - c.padding.right).max(0.0));
let (cw, chh) = intrinsic_constrained(ch, child_available);
w = w.max(cw);
h = h.max(chh);
}
apply_min(
c,
w + c.padding.left + c.padding.right,
h + c.padding.top + c.padding.bottom,
)
}
Axis::Column => {
let mut w: f32 = 0.0;
let mut h: f32 = c.padding.top + c.padding.bottom;
let n = c.children.len();
let child_available =
available_width.map(|w| (w - c.padding.left - c.padding.right).max(0.0));
for (i, ch) in c.children.iter().enumerate() {
let (cw, chh) = intrinsic_constrained(ch, child_available);
w = w.max(cw);
h += chh;
if i + 1 < n {
h += c.gap;
}
}
apply_min(c, w + c.padding.left + c.padding.right, h)
}
Axis::Row => {
let n = c.children.len();
let total_gap = c.gap * n.saturating_sub(1) as f32;
let inner_available = available_width
.map(|w| (w - c.padding.left - c.padding.right - total_gap).max(0.0));
let mut consumed: f32 = 0.0;
let mut fill_weight_total: f32 = 0.0;
let mut sizes: Vec<Option<(f32, f32)>> = Vec::with_capacity(n);
for ch in &c.children {
match ch.width {
Size::Fill(w) => {
fill_weight_total += w.max(0.001);
sizes.push(None);
}
_ => {
let (cw, chh) = intrinsic(ch);
consumed += cw;
sizes.push(Some((cw, chh)));
}
}
}
let fill_remaining = inner_available.map(|av| (av - consumed).max(0.0));
let mut w_total: f32 = c.padding.left + c.padding.right;
let mut h_max: f32 = 0.0;
for (i, (ch, slot)) in c.children.iter().zip(sizes).enumerate() {
let (cw, chh) = match slot {
Some(rc) => rc,
None => match (fill_remaining, fill_weight_total > 0.0) {
(Some(av), true) => {
let weight = match ch.width {
Size::Fill(w) => w.max(0.001),
_ => 1.0,
};
intrinsic_constrained(ch, Some(av * weight / fill_weight_total))
}
_ => intrinsic(ch),
},
};
w_total += cw;
if i + 1 < n {
w_total += c.gap;
}
h_max = h_max.max(chh);
}
apply_min(c, w_total, h_max + c.padding.top + c.padding.bottom)
}
}
}
pub(crate) fn text_layout(
c: &El,
available_width: Option<f32>,
) -> Option<text_metrics::TextLayout> {
let text = c.text.as_ref()?;
let content_available = match c.text_wrap {
TextWrap::NoWrap => None,
TextWrap::Wrap => available_width
.or(match c.width {
Size::Fixed(v) => Some(v),
Size::Ch(n) => Some(n * ch_unit(c)),
Size::Fill(_) | Size::Hug | Size::Aspect(_) => None,
})
.map(|w| (w - c.padding.left - c.padding.right).max(1.0)),
};
let display = display_text_for_measure(c, text, content_available);
Some(text_metrics::layout_text_with_line_height_and_family(
&display,
c.font_size,
c.line_height,
c.font_family,
c.font_weight,
c.font_mono,
c.text_tabular_numerals,
c.text_letter_spacing,
c.text_wrap,
content_available,
))
}
fn display_text_for_measure(c: &El, text: &str, available_width: Option<f32>) -> String {
if let (TextWrap::Wrap, Some(max_lines), Some(width)) =
(c.text_wrap, c.text_max_lines, available_width)
{
text_metrics::clamp_text_to_lines_with_family(
text,
c.font_size,
c.font_family,
c.font_weight,
c.font_mono,
width,
max_lines.get() as usize,
)
} else {
text.to_string()
}
}
fn apply_min(c: &El, mut w: f32, mut h: f32) -> (f32, f32) {
if let Size::Fixed(v) = c.width {
w = v;
}
if let Size::Fixed(v) = c.height {
h = v;
}
(clamp_w(c, w), clamp_h(c, h))
}
pub(crate) fn clamp_w(c: &El, mut w: f32) -> f32 {
if let Some(max_w) = c.max_width {
w = w.min(max_w);
}
if let Some(min_w) = c.min_width {
w = w.max(min_w);
}
w.max(0.0)
}
pub(crate) fn clamp_h(c: &El, mut h: f32) -> f32 {
if let Some(max_h) = c.max_height {
h = h.min(max_h);
}
if let Some(min_h) = c.min_height {
h = h.max(min_h);
}
h.max(0.0)
}
fn inline_paragraph_intrinsic(node: &El, available_width: Option<f32>) -> (f32, f32) {
if node.children.iter().any(|c| matches!(c.kind, Kind::Math)) {
return inline_mixed_intrinsic(node, available_width);
}
let concat = concat_inline_text(&node.children);
let size = inline_paragraph_size(node);
let line_height = inline_paragraph_line_height(node);
let content_available = match node.text_wrap {
TextWrap::NoWrap => None,
TextWrap::Wrap => available_width
.or(match node.width {
Size::Fixed(v) => Some(v),
Size::Ch(n) => Some(n * ch_unit(node)),
Size::Fill(_) | Size::Hug | Size::Aspect(_) => None,
})
.map(|w| (w - node.padding.left - node.padding.right).max(1.0)),
};
let layout = text_metrics::layout_text_with_line_height_and_family(
&concat,
size,
line_height,
node.font_family,
FontWeight::Regular,
false,
false,
0.0,
node.text_wrap,
content_available,
);
let w = match (content_available, node.width) {
(Some(available), Size::Hug | Size::Aspect(_)) => {
let unwrapped = text_metrics::layout_text_with_line_height_and_family(
&concat,
size,
line_height,
node.font_family,
FontWeight::Regular,
false,
false,
0.0,
TextWrap::NoWrap,
None,
);
unwrapped.width.min(available) + node.padding.left + node.padding.right
}
(Some(available), Size::Fixed(_) | Size::Fill(_) | Size::Ch(_)) => {
available + node.padding.left + node.padding.right
}
(None, _) => layout.width + node.padding.left + node.padding.right,
};
let h = layout.height + node.padding.top + node.padding.bottom;
apply_min(node, w, h)
}
fn inline_mixed_intrinsic(node: &El, available_width: Option<f32>) -> (f32, f32) {
let wrap_width = match node.text_wrap {
TextWrap::Wrap => available_width.or(match node.width {
Size::Fixed(v) => Some(v),
Size::Ch(n) => Some(n * ch_unit(node)),
Size::Fill(_) | Size::Hug | Size::Aspect(_) => None,
}),
TextWrap::NoWrap => None,
}
.map(|w| (w - node.padding.left - node.padding.right).max(1.0));
let mut breaker = crate::text::inline_mixed::MixedInlineBreaker::new(
node.text_wrap,
wrap_width,
node.font_size * 0.82,
node.font_size * 0.22,
node.line_height,
);
for child in &node.children {
match child.kind {
Kind::HardBreak => {
breaker.finish_line();
continue;
}
Kind::Text => {
let text = child.text.as_deref().unwrap_or("");
for chunk in inline_text_chunks(text) {
let is_space = chunk.chars().all(char::is_whitespace);
if breaker.skips_leading_space(is_space) {
continue;
}
let (w, ascent, descent) = inline_text_chunk_metrics(child, chunk);
if breaker.wraps_before(is_space, w) {
breaker.finish_line();
}
if breaker.skips_overflowing_space(is_space, w) {
continue;
}
breaker.push(w, ascent, descent);
}
continue;
}
_ => {}
}
let (w, ascent, descent) = inline_child_metrics(child);
if breaker.wraps_before(false, w) {
breaker.finish_line();
}
breaker.push(w, ascent, descent);
}
let measurement = breaker.finish();
let w = measurement.width + node.padding.left + node.padding.right;
let h = measurement.height + node.padding.top + node.padding.bottom;
apply_min(node, w, h)
}
fn inline_text_chunks(text: &str) -> Vec<&str> {
let mut chunks = Vec::new();
let mut start = 0;
let mut last_space = None;
for (i, ch) in text.char_indices() {
let is_space = ch.is_whitespace();
match last_space {
None => last_space = Some(is_space),
Some(prev) if prev != is_space => {
chunks.push(&text[start..i]);
start = i;
last_space = Some(is_space);
}
_ => {}
}
}
if start < text.len() {
chunks.push(&text[start..]);
}
chunks
}
fn inline_text_chunk_metrics(child: &El, text: &str) -> (f32, f32, f32) {
let layout = text_metrics::layout_text_with_line_height_and_family(
text,
child.font_size,
child.line_height,
child.font_family,
child.font_weight,
child.font_mono,
child.text_tabular_numerals,
child.text_letter_spacing,
TextWrap::NoWrap,
None,
);
(layout.width, child.font_size * 0.82, child.font_size * 0.22)
}
fn inline_child_metrics(child: &El) -> (f32, f32, f32) {
match child.kind {
Kind::Text => inline_text_chunk_metrics(child, child.text.as_deref().unwrap_or("")),
Kind::Math => {
if let Some(expr) = &child.math {
let layout = crate::math::layout_math(expr, child.font_size, child.math_display);
(layout.width, layout.ascent, layout.descent)
} else {
(0.0, 0.0, 0.0)
}
}
_ => (0.0, 0.0, 0.0),
}
}
fn concat_inline_text(children: &[El]) -> String {
let mut s = String::new();
for c in children {
match c.kind {
Kind::Text => {
if let Some(t) = &c.text {
s.push_str(t);
}
}
Kind::HardBreak => s.push('\n'),
_ => {}
}
}
s
}
fn inline_paragraph_size(node: &El) -> f32 {
let mut size: f32 = node.font_size;
for c in &node.children {
if matches!(c.kind, Kind::Text) {
size = size.max(c.font_size);
}
}
size
}
fn inline_paragraph_line_height(node: &El) -> f32 {
let mut line_height: f32 = node.line_height;
let mut max_size: f32 = node.font_size;
for c in &node.children {
if matches!(c.kind, Kind::Text) && c.font_size >= max_size {
max_size = c.font_size;
line_height = c.line_height;
}
}
line_height
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::UiState;
#[test]
fn duplicate_sibling_keys_flagged_once() {
let mut root = column([
crate::widgets::text::text("a").key("dup"),
crate::widgets::text::text("b").key("dup"),
]);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 100.0, 100.0));
assert_eq!(root.children[0].computed_id, root.children[1].computed_id);
let id = root.children[0].computed_id.clone();
assert!(state.layout.warned_duplicate_ids.contains(&*id));
let n_before = state.layout.warned_duplicate_ids.len();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 100.0, 100.0));
assert_eq!(state.layout.warned_duplicate_ids.len(), n_before);
}
#[test]
fn distinct_sibling_keys_not_flagged() {
let mut root = column([
crate::widgets::text::text("a").key("one"),
crate::widgets::text::text("b").key("two"),
]);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 100.0, 100.0));
assert!(state.layout.warned_duplicate_ids.is_empty());
}
#[test]
fn align_center_shrinks_fill_child_to_intrinsic() {
let mut root = column([crate::row([crate::widgets::text::text("hi")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))])])
.align(Align::Center)
.width(Size::Fixed(200.0))
.height(Size::Fixed(100.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 100.0));
let row_rect = root.children[0].computed_rect;
assert!(
(row_rect.x - 80.0).abs() < 0.5,
"expected x≈80 (centered), got {}",
row_rect.x
);
assert!(
(row_rect.w - 40.0).abs() < 0.5,
"expected w≈40 (shrunk to intrinsic), got {}",
row_rect.w
);
}
#[test]
fn max_clamped_fill_frees_space_to_sibling_fills() {
let mut root = crate::row([
El::new(Kind::Group).width(Size::Fill(1.0)).max_width(50.0),
El::new(Kind::Group).width(Size::Fill(1.0)),
])
.width(Size::Fixed(400.0))
.height(Size::Fixed(100.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 100.0));
let a = root.children[0].computed_rect;
let b = root.children[1].computed_rect;
assert!((a.w - 50.0).abs() < 0.5, "capped fill w={}", a.w);
assert!(
(b.w - 350.0).abs() < 0.5,
"sibling fill should absorb the freed 150px, got w={}",
b.w
);
}
#[test]
fn min_clamped_fill_steals_space_from_sibling_fills() {
let mut root = crate::row([
El::new(Kind::Group).width(Size::Fill(1.0)).min_width(300.0),
El::new(Kind::Group).width(Size::Fill(1.0)),
])
.width(Size::Fixed(400.0))
.height(Size::Fixed(100.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 100.0));
let a = root.children[0].computed_rect;
let b = root.children[1].computed_rect;
assert!((a.w - 300.0).abs() < 0.5, "floored fill w={}", a.w);
assert!(
(b.w - 100.0).abs() < 0.5,
"sibling fill should shrink to the remaining 100px, got w={}",
b.w
);
}
#[test]
fn justify_distributes_space_left_by_fully_capped_fills() {
let mut root = crate::row([El::new(Kind::Group).width(Size::Fill(1.0)).max_width(100.0)])
.justify(Justify::Center)
.width(Size::Fixed(400.0))
.height(Size::Fixed(100.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 100.0));
let a = root.children[0].computed_rect;
assert!((a.w - 100.0).abs() < 0.5, "capped fill w={}", a.w);
assert!(
(a.x - 150.0).abs() < 0.5,
"capped fill should be centered (x≈150), got x={}",
a.x
);
}
#[test]
fn align_stretch_preserves_fill_stretch() {
let mut root = column([crate::row([crate::widgets::text::text("hi")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))])])
.align(Align::Stretch)
.width(Size::Fixed(200.0))
.height(Size::Fixed(100.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 100.0));
let row_rect = root.children[0].computed_rect;
assert!(
(row_rect.x - 0.0).abs() < 0.5 && (row_rect.w - 200.0).abs() < 0.5,
"expected stretched (x=0, w=200), got x={} w={}",
row_rect.x,
row_rect.w
);
}
#[test]
fn children_of_text_bearing_node_still_get_sized() {
let mut root = column([El::new(Kind::Group)
.text("label".to_string())
.child(
crate::widgets::text::text("nested child")
.width(Size::Hug)
.height(Size::Hug),
)
.width(Size::Fixed(300.0))
.height(Size::Fixed(100.0))]);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 100.0));
let child = root.children[0].children[0].computed_rect;
assert!(
child.w > 10.0 && child.h > 5.0,
"hug child of a text-bearing parent must size from its own \
content, got {child:?}"
);
}
#[test]
fn clamped_fill_resizes_wrap_text_descendants_at_final_width() {
let long = "words that will definitely wrap at two hundred pixels \
but not at three hundred, repeated for effect and \
measure, wrapping across several lines";
let make = |max_w: Option<f32>| {
let mut fill = column([crate::widgets::text::text(long).wrap_text()])
.width(Size::Fill(1.0))
.height(Size::Hug);
if let Some(m) = max_w {
fill.max_width = Some(m);
}
crate::row([
crate::tree::spacer()
.width(Size::Fixed(100.0))
.height(Size::Fixed(10.0)),
fill,
])
.width(Size::Fixed(400.0))
.height(Size::Fixed(400.0))
};
let mut clamped = make(Some(200.0));
let mut state = UiState::new();
layout(&mut clamped, &mut state, Rect::new(0.0, 0.0, 400.0, 400.0));
let col = &clamped.children[1];
let col_rect = col.computed_rect;
assert!(
(col_rect.w - 200.0).abs() < 0.5,
"fill should clamp to 200, got {}",
col_rect.w
);
let para = col.children[0].computed_rect;
let (_, expected_h) = intrinsic_constrained(&col.children[0], Some(200.0));
assert!(
(para.h - expected_h).abs() < 0.5,
"paragraph should reserve wrap-at-200 height {expected_h}, got {}",
para.h
);
}
#[test]
fn hug_ancestor_measures_wrap_text_at_its_own_fixed_width() {
let long = "The quick brown fox jumps over the lazy dog, then \
does it again and again until the line is long \
enough to wrap several times.";
let mut root = column([column([column([crate::widgets::text::text(long)
.wrap_text()
.width(Size::Fixed(200.0))])
.width(Size::Fixed(240.0))])
.width(Size::Fill(1.0))]);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 600.0));
let card = root.children[0].computed_rect;
let text_rect = root.children[0].children[0].children[0].computed_rect;
assert!(
text_rect.h > 25.0,
"text should wrap to multiple lines at 200px, got h={}",
text_rect.h
);
assert!(
(card.h - text_rect.h).abs() < 0.5,
"Hug card height {} must match wrapped text height {}",
card.h,
text_rect.h
);
}
#[test]
fn justify_center_centers_hug_children() {
let mut root = column([crate::widgets::text::text("hi")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))])
.justify(Justify::Center)
.height(Size::Fill(1.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 100.0, 100.0));
let child_rect = root.children[0].computed_rect;
assert!(
(child_rect.y - 40.0).abs() < 0.5,
"expected y≈40, got {}",
child_rect.y
);
}
#[test]
fn justify_end_pushes_to_bottom() {
let mut root = column([crate::widgets::text::text("hi")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))])
.justify(Justify::End)
.height(Size::Fill(1.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 100.0, 100.0));
let child_rect = root.children[0].computed_rect;
assert!(
(child_rect.y - 80.0).abs() < 0.5,
"expected y≈80, got {}",
child_rect.y
);
}
#[test]
fn justify_space_between_distributes_evenly() {
let row_child = || {
crate::widgets::text::text("x")
.width(Size::Fixed(20.0))
.height(Size::Fixed(20.0))
};
let mut root = column([row_child(), row_child(), row_child()])
.justify(Justify::SpaceBetween)
.height(Size::Fixed(200.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 100.0, 200.0));
let y0 = root.children[0].computed_rect.y;
let y1 = root.children[1].computed_rect.y;
let y2 = root.children[2].computed_rect.y;
assert!(
y0.abs() < 0.5,
"first child should be flush at y=0, got {y0}"
);
assert!(
(y1 - 90.0).abs() < 0.5,
"middle child should be at y≈90, got {y1}"
);
assert!(
(y2 - 180.0).abs() < 0.5,
"last child should be flush at y≈180, got {y2}"
);
}
#[test]
fn fill_weight_distributes_proportionally() {
let big = crate::widgets::text::text("big")
.width(Size::Fixed(40.0))
.height(Size::Fill(2.0));
let small = crate::widgets::text::text("small")
.width(Size::Fixed(40.0))
.height(Size::Fill(1.0));
let mut root = column([big, small]).height(Size::Fixed(300.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 100.0, 300.0));
let big_h = root.children[0].computed_rect.h;
let small_h = root.children[1].computed_rect.h;
assert!(
(big_h - 200.0).abs() < 0.5,
"Fill(2.0) should claim 2/3 of 300 ≈ 200, got {big_h}"
);
assert!(
(small_h - 100.0).abs() < 0.5,
"Fill(1.0) should claim 1/3 of 300 ≈ 100, got {small_h}"
);
}
#[test]
fn padding_on_hug_includes_in_intrinsic() {
let root = column([crate::widgets::text::text("x")
.width(Size::Fixed(40.0))
.height(Size::Fixed(40.0))])
.padding(Sides::all(20.0));
let (w, h) = intrinsic(&root);
assert!((w - 80.0).abs() < 0.5, "expected intrinsic w≈80, got {w}");
assert!((h - 80.0).abs() < 0.5, "expected intrinsic h≈80, got {h}");
}
#[test]
fn align_end_pins_to_cross_axis_far_edge() {
let mut root = crate::row([crate::widgets::text::text("hi")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))])
.align(Align::End)
.width(Size::Fixed(200.0))
.height(Size::Fixed(100.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 100.0));
let child_rect = root.children[0].computed_rect;
assert!(
(child_rect.y - 80.0).abs() < 0.5,
"expected y≈80 (pinned to bottom), got {}",
child_rect.y
);
}
#[test]
fn overlay_can_center_hug_child() {
let mut root = stack([crate::titled_card("Dialog", [crate::text("Body")])
.width(Size::Fixed(200.0))
.height(Size::Hug)])
.align(Align::Center)
.justify(Justify::Center);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 600.0, 400.0));
let child_rect = root.children[0].computed_rect;
assert!(
(child_rect.x - 200.0).abs() < 0.5,
"expected x≈200, got {}",
child_rect.x
);
assert!(
child_rect.y > 100.0 && child_rect.y < 200.0,
"expected centered y, got {}",
child_rect.y
);
}
#[test]
fn scroll_offset_translates_children_and_clamps_to_content() {
let mut root = scroll(
(0..6)
.map(|i| crate::widgets::text::text(format!("row {i}")).height(Size::Fixed(50.0))),
)
.key("list")
.gap(12.0)
.height(Size::Fixed(200.0));
let mut state = UiState::new();
assign_ids(&mut root);
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), 80.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let stored = state
.scroll
.offsets
.get(&*root.computed_id)
.copied()
.unwrap_or(0.0);
assert!(
(stored - 80.0).abs() < 0.01,
"offset clamped unexpectedly: {stored}"
);
let c0 = root.children[0].computed_rect;
assert!(
(c0.y - (-80.0)).abs() < 0.01,
"child 0 y = {} (expected -80)",
c0.y
);
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), 9999.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let stored = state
.scroll
.offsets
.get(&*root.computed_id)
.copied()
.unwrap_or(0.0);
assert!(
(stored - 160.0).abs() < 0.01,
"overshoot clamped to {stored}"
);
let mut tiny =
scroll([crate::widgets::text::text("just one row").height(Size::Fixed(20.0))])
.height(Size::Fixed(200.0));
let mut tiny_state = UiState::new();
assign_ids(&mut tiny);
tiny_state
.scroll
.offsets
.insert(tiny.computed_id.clone().to_string(), 50.0);
layout(
&mut tiny,
&mut tiny_state,
Rect::new(0.0, 0.0, 300.0, 200.0),
);
assert_eq!(
tiny_state
.scroll
.offsets
.get(&*tiny.computed_id)
.copied()
.unwrap_or(0.0),
0.0
);
}
#[test]
fn scroll_layout_prunes_far_offscreen_descendants() {
let far = column([crate::widgets::text::text("far row body").key("far-text")])
.height(Size::Fixed(40.0));
let mut root = scroll([
column([crate::widgets::text::text("near row body")]).height(Size::Fixed(40.0)),
crate::tree::spacer().height(Size::Fixed(400.0)),
far,
])
.key("list")
.height(Size::Fixed(80.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 80.0));
let stats = take_prune_stats();
assert!(
stats.subtrees >= 1,
"expected at least one far scroll child to be pruned, got {stats:?}"
);
assert!(
stats.nodes >= 1,
"expected pruned descendants to be zeroed, got {stats:?}"
);
let far_text = state
.rect_of_key("far-text")
.expect("far text keeps a zero rect while pruned");
assert_eq!(far_text.w, 0.0);
assert_eq!(far_text.h, 0.0);
}
#[test]
fn plain_scroll_preserves_visible_anchor_when_width_reflows_content() {
let make_root = || {
let paragraph_text = "Variable width text wraps into a different number of lines when \
the viewport narrows, which used to make a plain scroll box lose \
the item the user was reading.";
scroll([column((0..30).map(|i| {
crate::widgets::text::paragraph(format!("{i}: {paragraph_text}"))
.key(format!("paragraph-{i}"))
}))
.gap(8.0)])
.key("article")
.height(Size::Fixed(180.0))
};
let mut root = make_root();
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 320.0, 180.0));
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), 520.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 320.0, 180.0));
let anchor = state
.scroll
.scroll_anchors
.get(&*root.computed_id)
.cloned()
.expect("plain scroll should store a visible descendant anchor");
let before_rect = find_descendant_rect(&root, &anchor.node_id).expect("anchor rect before");
let before_anchor_y = before_rect.y + before_rect.h * anchor.rect_fraction;
let before_offset = state.scroll_offset(&root.computed_id);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 180.0));
let after_rect = find_descendant_rect(&root, &anchor.node_id).expect("anchor rect after");
let after_anchor_y = after_rect.y + after_rect.h * anchor.rect_fraction;
let after_offset = state.scroll_offset(&root.computed_id);
assert!(
(after_anchor_y - before_anchor_y).abs() < 0.5,
"anchor point should stay at y={before_anchor_y}, got {after_anchor_y}"
);
assert!(
(after_offset - before_offset).abs() > 20.0,
"offset should absorb height changes above the anchor"
);
}
#[test]
fn scrollbar_thumb_size_and_position_track_overflow() {
let mut root = scroll(
(0..6)
.map(|i| crate::widgets::text::text(format!("row {i}")).height(Size::Fixed(50.0))),
)
.gap(12.0)
.height(Size::Fixed(200.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let metrics = state
.scroll
.metrics
.get(&*root.computed_id)
.copied()
.expect("scrollable should have metrics");
assert!((metrics.viewport_h - 200.0).abs() < 0.01);
assert!((metrics.content_h - 360.0).abs() < 0.01);
assert!((metrics.max_offset - 160.0).abs() < 0.01);
let thumb = state
.scroll
.thumb_rects
.get(&*root.computed_id)
.copied()
.expect("scrollable with scrollbar() and overflow gets a thumb");
assert!((thumb.h - 111.111).abs() < 0.5, "thumb h = {}", thumb.h);
assert!((thumb.w - crate::tokens::SCROLLBAR_THUMB_WIDTH).abs() < 0.01);
assert!(thumb.y.abs() < 0.01);
assert!(
(thumb.x + thumb.w + crate::tokens::SCROLLBAR_TRACK_INSET - 300.0).abs() < 0.01,
"thumb anchored at {} (expected {})",
thumb.x,
300.0 - thumb.w - crate::tokens::SCROLLBAR_TRACK_INSET
);
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), 80.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let thumb = state
.scroll
.thumb_rects
.get(&*root.computed_id)
.copied()
.unwrap();
let track_remaining = 200.0 - thumb.h;
let expected_y = track_remaining * (80.0 / 160.0);
assert!(
(thumb.y - expected_y).abs() < 0.5,
"thumb at half-scroll y = {} (expected {expected_y})",
thumb.y,
);
}
#[test]
fn scrollbar_track_is_wider_than_thumb_and_full_height() {
let mut root = scroll(
(0..6)
.map(|i| crate::widgets::text::text(format!("row {i}")).height(Size::Fixed(50.0))),
)
.gap(12.0)
.height(Size::Fixed(200.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let thumb = state
.scroll
.thumb_rects
.get(&*root.computed_id)
.copied()
.unwrap();
let track = state
.scroll
.thumb_tracks
.get(&*root.computed_id)
.copied()
.unwrap();
assert!(track.w > thumb.w, "track.w {} thumb.w {}", track.w, thumb.w);
assert!(
(track.right() - thumb.right()).abs() < 0.01,
"track and thumb must share the right edge",
);
assert!(
(track.h - 200.0).abs() < 0.01,
"track height = {} (expected 200)",
track.h,
);
}
#[test]
fn scrollbar_thumb_absent_when_disabled_or_no_overflow() {
let mut suppressed = scroll(
(0..6)
.map(|i| crate::widgets::text::text(format!("row {i}")).height(Size::Fixed(50.0))),
)
.no_scrollbar()
.height(Size::Fixed(200.0));
let mut state = UiState::new();
layout(
&mut suppressed,
&mut state,
Rect::new(0.0, 0.0, 300.0, 200.0),
);
assert!(
!state
.scroll
.thumb_rects
.contains_key(&*suppressed.computed_id)
);
let mut tiny = scroll([crate::widgets::text::text("one row").height(Size::Fixed(20.0))])
.height(Size::Fixed(200.0));
let mut tiny_state = UiState::new();
layout(
&mut tiny,
&mut tiny_state,
Rect::new(0.0, 0.0, 300.0, 200.0),
);
assert!(
!tiny_state
.scroll
.thumb_rects
.contains_key(&*tiny.computed_id)
);
}
#[test]
fn nested_scrollbar_thumb_moves_with_outer_scroll_content() {
let make_root = || {
scroll([
crate::tree::spacer().height(Size::Fixed(80.0)),
scroll((0..6).map(|i| {
crate::widgets::text::text(format!("inner row {i}")).height(Size::Fixed(50.0))
}))
.key("inner")
.height(Size::Fixed(120.0)),
crate::tree::spacer().height(Size::Fixed(260.0)),
])
.key("outer")
.height(Size::Fixed(220.0))
};
let mut root = make_root();
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 220.0));
let inner = root
.children
.iter()
.find(|child| child.key.as_deref() == Some("inner"))
.expect("inner scroll");
let inner_id = inner.computed_id.clone();
let inner_rect = state.rect(&inner_id);
let thumb = state
.scroll
.thumb_rects
.get(&*inner_id)
.copied()
.expect("inner scroll should have a thumb");
let track = state
.scroll
.thumb_tracks
.get(&*inner_id)
.copied()
.expect("inner scroll should have a track");
let thumb_rel_y = thumb.y - inner_rect.y;
let track_rel_y = track.y - inner_rect.y;
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), 60.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 220.0));
let inner_rect_after = state.rect(&inner_id);
let thumb_after = state.scroll.thumb_rects.get(&*inner_id).copied().unwrap();
let track_after = state.scroll.thumb_tracks.get(&*inner_id).copied().unwrap();
assert!(
(inner_rect_after.y - (inner_rect.y - 60.0)).abs() < 0.5,
"outer scroll should shift the inner viewport"
);
assert!(
(thumb_after.y - inner_rect_after.y - thumb_rel_y).abs() < 0.5,
"inner thumb should stay fixed relative to its viewport"
);
assert!(
(track_after.y - inner_rect_after.y - track_rel_y).abs() < 0.5,
"inner track should stay fixed relative to its viewport"
);
}
#[test]
fn layout_override_places_children_at_returned_rects() {
let mut root = column((0..3).map(|i| {
crate::widgets::text::text(format!("dot {i}"))
.width(Size::Fixed(20.0))
.height(Size::Fixed(20.0))
}))
.width(Size::Fixed(200.0))
.height(Size::Fixed(200.0))
.layout(|ctx| {
ctx.children
.iter()
.enumerate()
.map(|(i, _)| {
let off = i as f32 * 30.0;
Rect::new(ctx.container.x + off, ctx.container.y + off, 20.0, 20.0)
})
.collect()
});
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 200.0));
let r0 = root.children[0].computed_rect;
let r1 = root.children[1].computed_rect;
let r2 = root.children[2].computed_rect;
assert_eq!((r0.x, r0.y), (0.0, 0.0));
assert_eq!((r1.x, r1.y), (30.0, 30.0));
assert_eq!((r2.x, r2.y), (60.0, 60.0));
}
#[test]
fn layout_override_rect_of_key_resolves_earlier_sibling() {
use crate::tree::stack;
let trigger_x = 40.0;
let trigger_y = 20.0;
let trigger_w = 60.0;
let trigger_h = 30.0;
let mut root = stack([
crate::widgets::button::button("Open")
.key("trig")
.width(Size::Fixed(trigger_w))
.height(Size::Fixed(trigger_h)),
stack([crate::widgets::text::text("popover")
.width(Size::Fixed(80.0))
.height(Size::Fixed(20.0))])
.width(Size::Fill(1.0))
.height(Size::Fill(1.0))
.layout(|ctx| {
let trig = (ctx.rect_of_key)("trig").expect("trigger laid out");
vec![Rect::new(trig.x, trig.bottom() + 4.0, 80.0, 20.0)]
}),
])
.padding(Sides::xy(trigger_x, trigger_y));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let popover_layer = &root.children[1];
let panel_rect = popover_layer.children[0].computed_rect;
assert!(
(panel_rect.x - trigger_x).abs() < 0.01,
"popover x = {} (expected {trigger_x})",
panel_rect.x,
);
assert!(
(panel_rect.y - (trigger_y + trigger_h + 4.0)).abs() < 0.01,
"popover y = {} (expected {})",
panel_rect.y,
trigger_y + trigger_h + 4.0,
);
}
#[test]
fn layout_override_rect_of_key_returns_none_for_missing_key() {
let mut root = column([crate::widgets::text::text("inner")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))])
.width(Size::Fixed(200.0))
.height(Size::Fixed(200.0))
.layout(|ctx| {
assert!((ctx.rect_of_key)("nope").is_none());
vec![Rect::new(ctx.container.x, ctx.container.y, 40.0, 20.0)]
});
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 200.0));
}
#[test]
fn layout_override_rect_of_key_returns_none_for_later_sibling() {
use crate::tree::stack;
let mut root = stack([
stack([crate::widgets::text::text("panel")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))])
.width(Size::Fill(1.0))
.height(Size::Fill(1.0))
.layout(|ctx| {
assert!(
(ctx.rect_of_key)("later").is_none(),
"later sibling's rect must not be available yet"
);
vec![Rect::new(ctx.container.x, ctx.container.y, 40.0, 20.0)]
}),
crate::widgets::button::button("after").key("later"),
]);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
}
#[test]
fn layout_override_measure_returns_intrinsic() {
let mut root = column([crate::widgets::text::text("hi")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))])
.width(Size::Fixed(200.0))
.height(Size::Fixed(200.0))
.layout(|ctx| {
let (w, h) = (ctx.measure)(&ctx.children[0]);
assert!((w - 40.0).abs() < 0.01, "measured width {w}");
assert!((h - 20.0).abs() < 0.01, "measured height {h}");
vec![Rect::new(ctx.container.x, ctx.container.y, w, h)]
});
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 200.0));
let r = root.children[0].computed_rect;
assert_eq!((r.w, r.h), (40.0, 20.0));
}
#[test]
#[should_panic(expected = "returned 1 rects for 2 children")]
fn layout_override_length_mismatch_panics() {
let mut root = column([
crate::widgets::text::text("a")
.width(Size::Fixed(10.0))
.height(Size::Fixed(10.0)),
crate::widgets::text::text("b")
.width(Size::Fixed(10.0))
.height(Size::Fixed(10.0)),
])
.width(Size::Fixed(200.0))
.height(Size::Fixed(200.0))
.layout(|ctx| vec![Rect::new(ctx.container.x, ctx.container.y, 10.0, 10.0)]);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 200.0));
}
#[test]
#[should_panic(expected = "Size::Hug is not supported for custom layouts")]
fn layout_override_hug_panics() {
let mut root = column([column([crate::widgets::text::text("c")])
.width(Size::Hug)
.height(Size::Fixed(200.0))
.layout(|ctx| vec![Rect::new(ctx.container.x, ctx.container.y, 10.0, 10.0)])])
.width(Size::Fixed(200.0))
.height(Size::Fixed(200.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 200.0));
}
#[test]
fn fit_contain_letterboxes_and_centers() {
let child = crate::tree::column([crate::widgets::text::text("c")]).key("fitted");
let mut root = crate::tree::fit_contain(child, 16.0 / 9.0)
.width(Size::Fixed(400.0))
.height(Size::Fixed(400.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 400.0));
let r = state.rect_of_key("fitted").expect("fitted rect");
assert_eq!((r.w, r.h), (400.0, 225.0));
assert_eq!(r.x, 0.0);
assert!((r.y - 87.5).abs() < 0.01, "centered: y = {}", r.y);
let child = crate::tree::column([crate::widgets::text::text("c")]).key("fitted2");
let mut root = crate::tree::fit_contain(child, 16.0 / 9.0)
.width(Size::Fixed(400.0))
.height(Size::Fixed(100.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 100.0));
let r = state.rect_of_key("fitted2").expect("fitted2 rect");
assert!((r.w - 100.0 * 16.0 / 9.0).abs() < 0.01);
assert_eq!(r.h, 100.0);
assert!((r.x - (400.0 - r.w) / 2.0).abs() < 0.01);
}
#[test]
fn fit_cover_overflows_the_slack_axis_and_clips() {
let child = crate::tree::column([crate::widgets::text::text("c")]).key("covered");
let mut root = crate::tree::fit_cover(child, 1.0)
.width(Size::Fixed(400.0))
.height(Size::Fixed(200.0));
assert!(root.clip, "fit_cover must clip its overflow");
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
let r = state.rect_of_key("covered").expect("covered rect");
assert_eq!((r.w, r.h), (400.0, 400.0));
assert!(
(r.y - -100.0).abs() < 0.01,
"centered overflow: y = {}",
r.y
);
}
#[test]
fn fit_contain_intrinsic_uses_the_child_measure() {
let child = crate::tree::column::<Vec<El>, El>(vec![])
.width(Size::Fixed(100.0))
.height(Size::Fixed(50.0))
.key("intrinsic");
let mut root = crate::tree::fit_contain_intrinsic(child)
.width(Size::Fixed(400.0))
.height(Size::Fixed(400.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 400.0));
let r = state.rect_of_key("intrinsic").expect("intrinsic rect");
assert_eq!((r.w, r.h), (400.0, 200.0));
}
#[test]
fn virtual_list_realizes_only_visible_rows() {
let mut root = crate::tree::virtual_list(100, 50.0, |i| {
crate::widgets::text::text(format!("row {i}")).key(format!("row-{i}"))
});
let mut state = UiState::new();
assign_ids(&mut root);
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), 120.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
assert_eq!(
root.children.len(),
5,
"expected 5 realized rows, got {}",
root.children.len()
);
assert_eq!(root.children[0].key.as_deref(), Some("row-2"));
assert_eq!(root.children[4].key.as_deref(), Some("row-6"));
let r0 = root.children[0].computed_rect;
assert!(
(r0.y - (-20.0)).abs() < 0.5,
"row 2 expected y≈-20, got {}",
r0.y
);
}
#[test]
fn virtual_list_gap_contributes_to_row_positions_and_content_height() {
let mut root = crate::tree::virtual_list(10, 40.0, |i| {
crate::widgets::text::text(format!("row {i}")).key(format!("row-{i}"))
})
.gap(10.0);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 120.0));
assert_eq!(
root.children.len(),
3,
"rows 0, 1, and 2 should intersect a 120px viewport with 40px rows and 10px gaps"
);
let row_1 = root
.children
.iter()
.find(|c| c.key.as_deref() == Some("row-1"))
.expect("row 1 should be realized");
assert!(
(row_1.computed_rect.y - 50.0).abs() < 0.5,
"gap should place row 1 at y=50"
);
let metrics = state
.scroll
.metrics
.get(&*root.computed_id)
.expect("virtual list writes scroll metrics");
assert!(
(metrics.content_h - 490.0).abs() < 0.5,
"10 rows x 40 plus 9 gaps x 10 should be 490, got {}",
metrics.content_h
);
}
#[test]
fn virtual_list_keyed_rows_have_stable_computed_id_across_scroll() {
let make_root = || {
crate::tree::virtual_list(50, 50.0, |i| {
crate::widgets::text::text(format!("row {i}")).key(format!("row-{i}"))
})
};
let mut state = UiState::new();
let mut root_a = make_root();
assign_ids(&mut root_a);
state
.scroll
.offsets
.insert(root_a.computed_id.clone().to_string(), 250.0);
layout(&mut root_a, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let id_at_offset_a = root_a
.children
.iter()
.find(|c| c.key.as_deref() == Some("row-5"))
.unwrap()
.computed_id
.clone();
let mut root_b = make_root();
assign_ids(&mut root_b);
state
.scroll
.offsets
.insert(root_b.computed_id.clone().to_string(), 200.0);
layout(&mut root_b, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let id_at_offset_b = root_b
.children
.iter()
.find(|c| c.key.as_deref() == Some("row-5"))
.unwrap()
.computed_id
.clone();
assert_eq!(
id_at_offset_a, id_at_offset_b,
"row-5's computed_id changed when scroll offset moved"
);
}
#[test]
fn virtual_list_clamps_overshoot_offset() {
let mut root =
crate::tree::virtual_list(10, 50.0, |i| crate::widgets::text::text(format!("r{i}")));
let mut state = UiState::new();
assign_ids(&mut root);
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), 9999.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let stored = state
.scroll
.offsets
.get(&*root.computed_id)
.copied()
.unwrap_or(0.0);
assert!(
(stored - 300.0).abs() < 0.01,
"expected clamp to 300, got {stored}"
);
}
#[test]
fn virtual_list_empty_count_realizes_no_children() {
let mut root =
crate::tree::virtual_list(0, 50.0, |i| crate::widgets::text::text(format!("r{i}")));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
assert_eq!(root.children.len(), 0);
}
#[test]
#[should_panic(expected = "row_height > 0.0")]
fn virtual_list_zero_row_height_panics() {
let _ = crate::tree::virtual_list(10, 0.0, |i| crate::widgets::text::text(format!("r{i}")));
}
#[test]
#[should_panic(expected = "Size::Hug would defeat virtualization")]
fn virtual_list_hug_panics() {
let mut root = column([crate::tree::virtual_list(10, 50.0, |i| {
crate::widgets::text::text(format!("r{i}"))
})
.height(Size::Hug)])
.width(Size::Fixed(300.0))
.height(Size::Fixed(200.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
}
#[test]
fn grid_packs_rows_and_aligns_the_partial_tail() {
let cell = |i: usize| {
crate::tree::column::<Vec<El>, El>(vec![])
.key(format!("cell-{i}"))
.height(Size::Fixed(50.0))
};
let mut root = crate::tree::grid(2, 10.0, (0..3).map(cell))
.width(Size::Fixed(210.0))
.height(Size::Fixed(300.0));
assert_eq!(root.arrow_nav, Some(crate::tree::ArrowNav::Grid));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 210.0, 300.0));
let r0 = state.rect_of_key("cell-0").unwrap();
let r1 = state.rect_of_key("cell-1").unwrap();
let r2 = state.rect_of_key("cell-2").unwrap();
assert_eq!((r0.x, r0.w), (0.0, 100.0));
assert_eq!((r1.x, r1.w), (110.0, 100.0));
assert_eq!((r2.x, r2.w), (0.0, 100.0));
assert_eq!(r2.y, 60.0);
}
#[test]
fn virtual_grid_realizes_rows_of_packed_cells() {
let mut root = crate::tree::virtual_grid(10, 3, 50.0, 0.0, |i| {
crate::widgets::text::text(format!("item {i}")).key(format!("item-{i}"))
})
.key("vgrid");
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 120.0));
assert_eq!(root.arrow_nav, Some(crate::tree::ArrowNav::Grid));
let rect_of = |row: usize, col: usize| root.children[row].children[col].computed_rect;
let i0 = rect_of(0, 0);
let i2 = rect_of(0, 2);
assert_eq!(i0.w, 100.0);
assert_eq!(i2.x, 200.0);
let i3 = rect_of(1, 0);
assert_eq!((i3.x, i3.y), (0.0, 50.0));
assert!(state.visible_range("vgrid").is_some());
assert_eq!(root.children[0].children[0].key.as_deref(), Some("item-0"));
}
#[test]
fn visible_range_tracks_realized_rows() {
let mut root = crate::tree::virtual_list(100, 50.0, |i| {
crate::widgets::text::text(format!("row {i}")).key(format!("row-{i}"))
})
.key("list");
let mut state = UiState::new();
assign_ids(&mut root);
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), 120.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
assert_eq!(state.visible_range("list"), Some(2..7));
assert_eq!(state.visible_range("not-a-list"), None);
let mut dyn_root = crate::tree::virtual_list_dyn(
20,
50.0,
|i| format!("row-{i}"),
|i| {
let h = if i % 2 == 0 { 40.0 } else { 80.0 };
crate::tree::column([crate::widgets::text::text(format!("r{i}"))])
.key(format!("row-{i}"))
.height(Size::Fixed(h))
},
)
.key("dyn-list");
let mut dyn_state = UiState::new();
layout(
&mut dyn_root,
&mut dyn_state,
Rect::new(0.0, 0.0, 300.0, 200.0),
);
assert_eq!(dyn_state.visible_range("dyn-list"), Some(0..4));
let mut empty =
crate::tree::virtual_list(0, 50.0, |_| crate::widgets::text::text("never")).key("list");
layout(&mut empty, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
assert_eq!(state.visible_range("list"), None);
}
#[test]
fn virtual_list_dyn_respects_per_row_fixed_heights() {
let mut root = crate::tree::virtual_list_dyn(
20,
50.0,
|i| format!("row-{i}"),
|i| {
let h = if i % 2 == 0 { 40.0 } else { 80.0 };
crate::tree::column([crate::widgets::text::text(format!("r{i}"))])
.key(format!("row-{i}"))
.height(Size::Fixed(h))
},
);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
assert_eq!(
root.children.len(),
4,
"expected 4 realized rows, got {}",
root.children.len()
);
let ys: Vec<f32> = root.children.iter().map(|c| c.computed_rect.y).collect();
assert!(
(ys[0] - 0.0).abs() < 0.5,
"row 0 expected y≈0, got {}",
ys[0]
);
assert!(
(ys[1] - 40.0).abs() < 0.5,
"row 1 expected y≈40, got {}",
ys[1]
);
assert!(
(ys[2] - 120.0).abs() < 0.5,
"row 2 expected y≈120, got {}",
ys[2]
);
assert!(
(ys[3] - 160.0).abs() < 0.5,
"row 3 expected y≈160, got {}",
ys[3]
);
}
#[test]
fn virtual_list_dyn_gap_contributes_to_row_positions_and_content_height() {
let mut root = crate::tree::virtual_list_dyn(
10,
40.0,
|i| format!("row-{i}"),
|i| {
crate::tree::column([crate::widgets::text::text(format!("row {i}"))])
.key(format!("row-{i}"))
.height(Size::Fixed(40.0))
},
)
.gap(10.0);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 120.0));
assert_eq!(
root.children.len(),
3,
"rows 0, 1, and 2 should intersect a 120px viewport with 40px rows and 10px gaps"
);
let row_1 = root
.children
.iter()
.find(|c| c.key.as_deref() == Some("row-1"))
.expect("row 1 should be realized");
assert!(
(row_1.computed_rect.y - 50.0).abs() < 0.5,
"gap should place row 1 at y=50"
);
let metrics = state
.scroll
.metrics
.get(&*root.computed_id)
.expect("virtual list writes scroll metrics");
assert!(
(metrics.content_h - 490.0).abs() < 0.5,
"10 rows x 40 plus 9 gaps x 10 should be 490, got {}",
metrics.content_h
);
}
#[test]
fn virtual_list_dyn_caches_measured_heights() {
let mut root = crate::tree::virtual_list_dyn(
50,
50.0,
|i| format!("row-{i}"),
|i| {
crate::tree::column([crate::widgets::text::text(format!("r{i}"))])
.key(format!("row-{i}"))
.height(Size::Fixed(30.0))
},
);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let measured = state
.scroll
.measured_row_heights
.get(&*root.computed_id)
.expect("dynamic virtual list should populate the height cache");
assert!(
measured.len() >= 6,
"expected ≥ 6 cached row heights, got {}",
measured.len()
);
for by_width in measured.values() {
let h = by_width
.get(&300)
.copied()
.expect("measurement should be keyed at the 300px width bucket");
assert!(
(h - 30.0).abs() < 0.5,
"expected cached height ≈ 30, got {h}"
);
}
}
#[test]
fn virtual_list_dyn_realized_rows_place_at_their_measured_heights() {
let mut root = crate::tree::virtual_list_dyn(
50,
20.0,
|i| format!("row-{i}"),
|i| {
crate::tree::column([crate::widgets::text::text(format!("row body {i}"))])
.key(format!("row-{i}"))
.height(Size::Hug)
},
);
let mut state = UiState::new();
let _ = take_intrinsic_cache_stats();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
assert!(!root.children.is_empty(), "test requires realized rows");
let stored = state
.scroll
.measured_row_heights
.get(&*root.computed_id)
.expect("dynamic list stores per-row heights");
let bucket = virtual_width_bucket(300.0);
for row in &root.children {
let key = row.key.as_deref().expect("rows are keyed");
let h = stored
.get(key)
.and_then(|by_width| by_width.get(&bucket))
.copied()
.expect("realized row height stored at the current width bucket");
assert!(
(row.computed_rect.h - h).abs() < 0.01,
"row {key} placed at h={} but measured h={h}",
row.computed_rect.h,
);
}
let stats = take_intrinsic_cache_stats();
assert_eq!(stats.hits, 0);
assert!(stats.misses > 0, "sizing visits should be reported");
}
#[test]
fn virtual_list_dyn_preserves_visible_anchor_when_above_measurement_changes() {
let make_root = || {
crate::tree::virtual_list_dyn(
100,
40.0,
|i| format!("row-{i}"),
|i| {
crate::tree::column([crate::widgets::text::text(format!("r{i}"))])
.key(format!("row-{i}"))
.height(Size::Fixed(40.0))
},
)
};
let mut root = make_root();
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), 400.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let anchor = state
.scroll
.virtual_anchors
.get(&*root.computed_id)
.cloned()
.expect("dynamic list should store a visible anchor");
let before_y = root
.children
.iter()
.find(|child| child.key.as_deref() == Some(anchor.row_key.as_str()))
.map(|child| child.computed_rect.y)
.expect("anchor row should be realized");
let before_offset = state.scroll_offset(&root.computed_id);
state
.scroll
.measured_row_heights
.entry(root.computed_id.clone().to_string())
.or_default()
.entry("row-0".to_string())
.or_default()
.insert(300, 120.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let after_y = root
.children
.iter()
.find(|child| child.key.as_deref() == Some(anchor.row_key.as_str()))
.map(|child| child.computed_rect.y)
.expect("anchor row should remain realized");
let after_offset = state.scroll_offset(&root.computed_id);
assert!(
(after_y - before_y).abs() < 0.5,
"anchor row should stay at y={before_y}, got {after_y}"
);
assert!(
(after_offset - (before_offset + 80.0)).abs() < 0.5,
"offset should absorb the 80px measurement delta above anchor"
);
}
#[test]
fn virtual_list_dyn_height_cache_is_width_bucketed() {
let mut root = crate::tree::virtual_list_dyn(
20,
50.0,
|i| format!("row-{i}"),
|i| {
crate::tree::column([crate::widgets::text::text(format!("r{i}"))])
.key(format!("row-{i}"))
.height(Size::Fixed(30.0))
},
);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 240.0, 200.0));
let row_0 = state
.scroll
.measured_row_heights
.get(&*root.computed_id)
.and_then(|m| m.get("row-0"))
.expect("row 0 should be measured");
assert!(
row_0.contains_key(&300) && row_0.contains_key(&240),
"expected width buckets 300 and 240, got {:?}",
row_0.keys().collect::<Vec<_>>()
);
}
#[test]
fn virtual_list_dyn_total_height_uses_measured_plus_estimate() {
let make_root = || {
crate::tree::virtual_list_dyn(
20,
50.0,
|i| format!("row-{i}"),
|i| {
crate::tree::column([crate::widgets::text::text(format!("r{i}"))])
.key(format!("row-{i}"))
.height(Size::Fixed(30.0))
},
)
};
let mut state = UiState::new();
let mut root = make_root();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), 9999.0);
let mut root2 = make_root();
layout(&mut root2, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
let measured = state
.scroll
.measured_row_heights
.get(&*root2.computed_id)
.expect("dynamic virtual list should populate the height cache");
let measured_sum = measured
.values()
.filter_map(|by_width| by_width.get(&300))
.sum::<f32>();
let measured_count = measured
.values()
.filter(|by_width| by_width.contains_key(&300))
.count();
let expected_total = measured_sum + (20 - measured_count) as f32 * 50.0;
let expected_max_offset = expected_total - 200.0;
let stored = state
.scroll
.offsets
.get(&*root2.computed_id)
.copied()
.unwrap_or(0.0);
assert!(
(stored - expected_max_offset).abs() < 0.5,
"expected offset clamped to {expected_max_offset}, got {stored}"
);
}
fn parity_list(first: usize, count: usize, append_only: bool, pin: bool) -> El {
let mut el = crate::tree::virtual_list_dyn(
count,
20.0,
move |i| format!("m{}", first + i),
move |i| {
let id = first + i;
let h = 30.0 + (id % 7) as f32 * 10.0;
crate::tree::column([crate::widgets::text::text(format!("m{id}"))])
.key(format!("m{id}"))
.height(Size::Fixed(h))
},
)
.key("chat")
.gap(4.0);
if pin {
el = el.pin_end();
}
if append_only {
el = el.append_only();
}
el
}
fn parity_rows(root: &El) -> Vec<(String, Rect)> {
root.children
.iter()
.map(|c| (c.key.clone().unwrap_or_default(), c.computed_rect))
.collect()
}
struct Frame {
first: usize,
count: usize,
seed_offset: Option<f32>,
request: Option<ScrollRequest>,
}
fn run_parity(frames: &[Frame], pin: bool) {
let mut sg = UiState::new(); let mut si = UiState::new();
for (n, f) in frames.iter().enumerate() {
let viewport = Rect::new(0.0, 0.0, 300.0, 200.0);
let step = |state: &mut UiState, append_only: bool| {
let mut root = parity_list(f.first, f.count, append_only, pin);
assign_ids(&mut root);
if let Some(o) = f.seed_offset {
state
.scroll
.offsets
.insert(root.computed_id.clone().to_string(), o);
}
if let Some(req) = &f.request {
state.push_scroll_requests(vec![req.clone()]);
}
layout(&mut root, state, viewport);
root
};
let root_g = step(&mut sg, false);
let root_i = step(&mut si, true);
let rows_g = parity_rows(&root_g);
let rows_i = parity_rows(&root_i);
assert_eq!(
rows_g.len(),
rows_i.len(),
"frame {n}: realized row count differs (general {} vs incremental {})",
rows_g.len(),
rows_i.len()
);
for ((kg, rg), (ki, ri)) in rows_g.iter().zip(&rows_i) {
assert_eq!(kg, ki, "frame {n}: realized key order differs");
assert!(
(rg.x - ri.x).abs() < 1e-2
&& (rg.y - ri.y).abs() < 1e-2
&& (rg.w - ri.w).abs() < 1e-2
&& (rg.h - ri.h).abs() < 1e-2,
"frame {n}: rect for {kg} differs: general {rg:?} vs incremental {ri:?}"
);
}
let off_g = sg.scroll.offsets.get(&*root_g.computed_id).copied();
let off_i = si.scroll.offsets.get(&*root_i.computed_id).copied();
assert!(
(off_g.unwrap_or(0.0) - off_i.unwrap_or(0.0)).abs() < 1e-2,
"frame {n}: stored offset differs: general {off_g:?} vs incremental {off_i:?}"
);
assert_eq!(
sg.visible_range("chat"),
si.visible_range("chat"),
"frame {n}: visible range differs"
);
}
}
#[test]
fn append_only_matches_general_top_append_scroll_trim() {
run_parity(
&[
Frame {
first: 0,
count: 30,
seed_offset: Some(0.0),
request: None,
},
Frame {
first: 0,
count: 42,
seed_offset: None,
request: None,
},
Frame {
first: 0,
count: 42,
seed_offset: Some(400.0),
request: None,
},
Frame {
first: 8,
count: 50,
seed_offset: None,
request: None,
},
Frame {
first: 8,
count: 62,
seed_offset: None,
request: None,
},
Frame {
first: 20,
count: 62,
seed_offset: None,
request: None,
},
],
false,
);
}
#[test]
fn append_only_matches_general_to_row_key_request() {
run_parity(
&[
Frame {
first: 0,
count: 40,
seed_offset: Some(0.0),
request: None,
},
Frame {
first: 0,
count: 40,
seed_offset: None,
request: Some(ScrollRequest::ToRowKey {
list_key: "chat".into(),
row_key: "m30".into(),
align: ScrollAlignment::Start,
}),
},
Frame {
first: 12,
count: 55,
seed_offset: None,
request: None,
},
],
false,
);
}
#[test]
fn append_only_matches_general_across_width_change() {
let mut sg = UiState::new();
let mut si = UiState::new();
let script = [
(0usize, 30usize, 300.0f32),
(0, 40, 300.0),
(0, 40, 240.0), (6, 52, 240.0), (6, 52, 360.0), ];
for (n, &(first, count, w)) in script.iter().enumerate() {
let viewport = Rect::new(0.0, 0.0, w, 200.0);
let step = |state: &mut UiState, append_only: bool| {
let mut root = parity_list(first, count, append_only, false);
assign_ids(&mut root);
layout(&mut root, state, viewport);
root
};
let rg = step(&mut sg, false);
let ri = step(&mut si, true);
let rows_g = parity_rows(&rg);
let rows_i = parity_rows(&ri);
assert_eq!(rows_g.len(), rows_i.len(), "frame {n}: row count differs");
for ((kg, a), (ki, b)) in rows_g.iter().zip(&rows_i) {
assert_eq!(kg, ki, "frame {n}: key order differs");
assert!(
(a.x - b.x).abs() < 1e-2
&& (a.y - b.y).abs() < 1e-2
&& (a.w - b.w).abs() < 1e-2
&& (a.h - b.h).abs() < 1e-2,
"frame {n}: rect for {kg} differs: {a:?} vs {b:?}"
);
}
assert_eq!(
sg.visible_range("chat"),
si.visible_range("chat"),
"frame {n}: visible range differs"
);
}
}
#[test]
fn append_only_matches_general_pin_end_stick_to_bottom() {
run_parity(
&[
Frame {
first: 0,
count: 20,
seed_offset: None,
request: None,
},
Frame {
first: 0,
count: 35,
seed_offset: None,
request: None,
},
Frame {
first: 10,
count: 50,
seed_offset: None,
request: None,
},
Frame {
first: 25,
count: 70,
seed_offset: None,
request: None,
},
],
true,
);
}
#[test]
fn virtual_list_dyn_empty_count_realizes_no_children() {
let mut root = crate::tree::virtual_list_dyn(
0,
50.0,
|i| format!("row-{i}"),
|i| crate::widgets::text::text(format!("r{i}")),
);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 300.0, 200.0));
assert_eq!(root.children.len(), 0);
}
#[test]
#[should_panic(expected = "estimated_row_height > 0.0")]
fn virtual_list_dyn_zero_estimate_panics() {
let _ = crate::tree::virtual_list_dyn(
10,
0.0,
|i| format!("row-{i}"),
|i| crate::widgets::text::text(format!("r{i}")),
);
}
#[test]
fn text_runs_constructor_shape_smoke() {
let el = crate::tree::text_runs([
crate::widgets::text::text("Hello, "),
crate::widgets::text::text("world").bold(),
crate::tree::hard_break(),
crate::widgets::text::text("of text").italic(),
]);
assert_eq!(el.kind, Kind::Inlines);
assert_eq!(el.children.len(), 4);
assert!(matches!(
el.children[1].font_weight,
FontWeight::Bold | FontWeight::Semibold
));
assert_eq!(el.children[2].kind, Kind::HardBreak);
assert!(el.children[3].text_italic);
}
#[test]
fn wrapped_text_hugs_multiline_height_from_available_width() {
let mut root = column([crate::paragraph(
"A longer sentence should wrap into multiple measured lines.",
)])
.width(Size::Fill(1.0))
.height(Size::Hug);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 180.0, 200.0));
let child_rect = root.children[0].computed_rect;
assert_eq!(child_rect.w, 180.0);
assert!(
child_rect.h > crate::tokens::TEXT_SM.size * 1.4,
"expected multiline paragraph height, got {}",
child_rect.h
);
}
#[test]
fn overlay_child_with_wrapped_text_measures_against_its_resolved_width() {
const PANEL_W: f32 = 240.0;
const PADDING: f32 = 18.0;
const GAP: f32 = 12.0;
let panel = column([
crate::paragraph(
"A long enough warning paragraph that it has to wrap onto a second line \
inside this narrow panel.",
),
crate::widgets::button::button("OK").key("ok"),
])
.width(Size::Fixed(PANEL_W))
.height(Size::Hug)
.padding(Sides::all(PADDING))
.gap(GAP)
.align(Align::Stretch);
let mut root = crate::stack([panel])
.width(Size::Fill(1.0))
.height(Size::Fill(1.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 600.0));
let panel_rect = root.children[0].computed_rect;
assert_eq!(panel_rect.w, PANEL_W, "panel keeps its Fixed width");
let para_rect = root.children[0].children[0].computed_rect;
let button_rect = root.children[0].children[1].computed_rect;
assert!(
para_rect.h > crate::tokens::TEXT_SM.size * 1.4,
"paragraph should wrap to multiple lines inside the Fixed-width panel; \
got h={}",
para_rect.h
);
let bottom_padding = (panel_rect.y + panel_rect.h) - (button_rect.y + button_rect.h);
assert!(
(bottom_padding - PADDING).abs() < 0.5,
"expected {PADDING}px between button and panel bottom, got {bottom_padding}",
);
}
#[test]
fn row_with_fill_paragraph_propagates_height_to_parent_column() {
const COL_W: f32 = 600.0;
const GUTTER_W: f32 = 3.0;
let long = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
sed do eiusmod tempor incididunt ut labore et dolore magna \
aliqua. Ut enim ad minim veniam, quis nostrud exercitation \
ullamco laboris nisi ut aliquip ex ea commodo consequat.";
let make_row = || {
let gutter = El::new(Kind::Custom("gutter"))
.width(Size::Fixed(GUTTER_W))
.height(Size::Fill(1.0));
let body = crate::paragraph(long).width(Size::Fill(1.0));
crate::row([gutter, body]).width(Size::Fill(1.0))
};
let mut root = column([make_row(), make_row()])
.width(Size::Fixed(COL_W))
.height(Size::Hug)
.align(Align::Stretch);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, COL_W, 2000.0));
let row0_rect = root.children[0].computed_rect;
let row1_rect = root.children[1].computed_rect;
let para0_rect = root.children[0].children[1].computed_rect;
let line_height = crate::tokens::TEXT_SM.line_height;
assert!(
para0_rect.h > line_height * 1.5,
"paragraph should wrap to multiple lines at ~597px wide; \
got h={} (line_height={})",
para0_rect.h,
line_height,
);
assert!(
row0_rect.h > line_height * 1.5,
"row 0 should accommodate the wrapped paragraph height; \
got h={} (line_height={})",
row0_rect.h,
line_height,
);
assert!(
row1_rect.y >= row0_rect.y + row0_rect.h - 0.5,
"row 1 starts at y={} but row 0 occupies y={}..{}",
row1_rect.y,
row0_rect.y,
row0_rect.y + row0_rect.h,
);
}
#[test]
fn min_width_floors_resolved_cross_axis_size() {
let mut root = column([crate::widgets::text::text("hi")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))
.min_width(120.0)])
.align(Align::Start)
.width(Size::Fixed(500.0))
.height(Size::Fixed(200.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 500.0, 200.0));
let child_rect = root.children[0].computed_rect;
assert!(
(child_rect.w - 120.0).abs() < 0.5,
"expected child clamped up to 120 (intrinsic 40 < min 120), got w={}",
child_rect.w,
);
}
#[test]
fn max_width_caps_fill_child() {
let mut root = crate::row([crate::widgets::text::text("body")
.width(Size::Fill(1.0))
.height(Size::Fixed(20.0))
.max_width(160.0)])
.width(Size::Fixed(800.0))
.height(Size::Fixed(40.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 40.0));
let child_rect = root.children[0].computed_rect;
assert!(
(child_rect.w - 160.0).abs() < 0.5,
"expected Fill child capped at 160, got w={}",
child_rect.w,
);
}
#[test]
fn ch_unit_reserves_constant_digit_width() {
use crate::widgets::text::text;
let mut root = column([
text("8")
.tabular_numerals()
.width(Size::Ch(4.0))
.height(Size::Fixed(20.0)),
text("123456")
.tabular_numerals()
.width(Size::Ch(4.0))
.height(Size::Fixed(20.0)),
text("8")
.tabular_numerals()
.width(Size::Ch(2.0))
.height(Size::Fixed(20.0)),
]);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 500.0, 200.0));
let four_a = root.children[0].computed_rect.w;
let four_b = root.children[1].computed_rect.w;
let two = root.children[2].computed_rect.w;
assert!(four_a > 0.0);
assert!(
(four_a - four_b).abs() < 0.01,
"Ch(4) width must not depend on the text: {four_a} vs {four_b}"
);
assert!(
(four_a - 2.0 * two).abs() < 0.5,
"Ch(4) should be twice Ch(2): {four_a} vs 2×{two}"
);
}
#[test]
fn min_width_wins_over_max_width_when_conflicting() {
let mut root = column([crate::widgets::text::text("x")
.width(Size::Fixed(50.0))
.height(Size::Fixed(20.0))
.max_width(80.0)
.min_width(120.0)]);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 500.0, 200.0));
let child_rect = root.children[0].computed_rect;
assert!(
(child_rect.w - 120.0).abs() < 0.5,
"expected min_width (120) to win over max_width (80), got w={}",
child_rect.w,
);
}
#[test]
fn min_height_floors_hug_column_inside_fixed_parent() {
let inner = column([crate::widgets::text::text("a")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))])
.width(Size::Fixed(80.0))
.height(Size::Hug)
.min_height(200.0);
let mut root = column([inner])
.align(Align::Start)
.width(Size::Fixed(800.0))
.height(Size::Fixed(600.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 600.0));
let inner_rect = root.children[0].computed_rect;
assert!(
(inner_rect.h - 200.0).abs() < 0.5,
"expected inner column floored to min_height=200 (intrinsic ~20), got h={}",
inner_rect.h,
);
}
#[test]
fn row_passes_allocated_width_to_hug_column_with_wrap_text_child() {
let mut root = crate::row([
column([crate::widgets::text::paragraph(
"A long enough description that must wrap to two lines at 148px",
)])
.width(Size::Fill(1.0)),
crate::widgets::text::text("ok")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0)),
])
.gap(12.0)
.align(Align::Center)
.width(Size::Fixed(200.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 600.0));
let col_rect = root.children[0].computed_rect;
let para_rect = root.children[0].children[0].computed_rect;
assert!(
(col_rect.h - para_rect.h).abs() < 0.5,
"column height ({}) should track its wrapped child's height ({})",
col_rect.h,
para_rect.h,
);
}
#[test]
fn aspect_on_column_main_axis_derives_from_cross() {
let mut root = column([El::new(Kind::Group)
.width(Size::Fill(1.0))
.height(Size::Aspect(0.5))])
.width(Size::Fixed(200.0))
.height(Size::Fixed(400.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 400.0));
let r = root.children[0].computed_rect;
assert!(
(r.w - 200.0).abs() < 0.5,
"expected w≈200 (Fill), got {}",
r.w,
);
assert!(
(r.h - 100.0).abs() < 0.5,
"expected h≈100 (Aspect 0.5 of 200), got {}",
r.h,
);
}
#[test]
fn aspect_height_pushes_siblings_in_column() {
let mut root = column([
El::new(Kind::Group)
.width(Size::Fill(1.0))
.height(Size::Aspect(0.25)),
crate::widgets::text::text("caption")
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0)),
])
.width(Size::Fixed(400.0))
.height(Size::Fixed(500.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 500.0));
let img = root.children[0].computed_rect;
let cap = root.children[1].computed_rect;
assert!(
(img.h - 100.0).abs() < 0.5,
"expected aspect-derived height ≈100, got {}",
img.h,
);
assert!(
(cap.y - 100.0).abs() < 0.5,
"caption should sit immediately below the aspect-sized El (y≈100), got y={}",
cap.y,
);
}
#[test]
fn aspect_on_row_cross_axis_derives_from_main() {
let mut root = crate::row([El::new(Kind::Group)
.height(Size::Fill(1.0))
.width(Size::Aspect(2.0))])
.width(Size::Fixed(800.0))
.height(Size::Fixed(200.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 200.0));
let r = root.children[0].computed_rect;
assert!(
(r.h - 200.0).abs() < 0.5,
"expected h≈200 (Fill), got {}",
r.h,
);
assert!(
(r.w - 400.0).abs() < 0.5,
"expected w≈400 (Aspect 2.0 of 200), got {}",
r.w,
);
}
#[test]
fn aspect_on_both_axes_falls_back_to_intrinsic() {
let mut root = column([crate::widgets::text::text("hi")
.width(Size::Aspect(1.0))
.height(Size::Aspect(1.0))])
.width(Size::Fixed(200.0))
.height(Size::Fixed(200.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 200.0, 200.0));
let r = root.children[0].computed_rect;
assert!(
r.w > 0.0 && r.h > 0.0,
"expected finite size for both-Aspect fallback, got {}x{}",
r.w,
r.h,
);
}
#[test]
fn aspect_respects_min_and_max_on_derived_axis() {
let mut root = column([column([El::new(Kind::Group)
.width(Size::Fill(1.0))
.height(Size::Aspect(1.0))
.max_height(120.0)])
.width(Size::Hug)
.height(Size::Hug)])
.width(Size::Fixed(400.0))
.height(Size::Fixed(600.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 600.0));
let panel = root.children[0].computed_rect;
let img = root.children[0].children[0].computed_rect;
assert!(
(img.h - 120.0).abs() < 0.5,
"max_height should clamp aspect-derived height to 120, got {}",
img.h,
);
assert!(
(panel.h - 120.0).abs() < 0.5,
"hugging panel should match clamped child (120), got {}",
panel.h,
);
let mut root = column([column([El::new(Kind::Group)
.width(Size::Fill(1.0))
.height(Size::Aspect(0.1))
.min_height(200.0)])
.width(Size::Hug)
.height(Size::Hug)])
.width(Size::Fixed(400.0))
.height(Size::Fixed(600.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 600.0));
let panel = root.children[0].computed_rect;
let img = root.children[0].children[0].computed_rect;
assert!(
(img.h - 200.0).abs() < 0.5,
"min_height should bump aspect-derived height to 200, got {}",
img.h,
);
assert!(
(panel.h - 200.0).abs() < 0.5,
"hugging panel should match bumped child (200), got {}",
panel.h,
);
}
#[test]
fn aspect_basis_is_clamped_before_deriving() {
let mut root = column([El::new(Kind::Group)
.width(Size::Fill(1.0))
.height(Size::Aspect(0.5))
.max_width(100.0)])
.width(Size::Fixed(400.0))
.height(Size::Fixed(400.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 400.0));
let img = root.children[0].computed_rect;
assert!(
(img.w - 100.0).abs() < 0.5,
"max_width should cap Fill width at 100, got {}",
img.w,
);
assert!(
(img.h - 50.0).abs() < 0.5,
"aspect-derived height should follow clamped width (100 * 0.5 = 50), got {}",
img.h,
);
}
#[test]
fn hug_column_around_fill_aspect_child_does_not_overflow() {
let mut root = column([column([El::new(Kind::Group)
.width(Size::Fill(1.0))
.height(Size::Aspect(0.5))])
.width(Size::Hug)
.height(Size::Hug)])
.width(Size::Fixed(400.0))
.height(Size::Fixed(400.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 400.0));
let panel = root.children[0].computed_rect;
let img = root.children[0].children[0].computed_rect;
assert!(
(panel.h - 200.0).abs() < 0.5,
"hugging panel should hug to aspect-derived height 200, got {}",
panel.h,
);
assert!(
(img.h - 200.0).abs() < 0.5,
"image should layout to height 200, got {}",
img.h,
);
assert!(
img.bottom() <= panel.bottom() + 0.5,
"image (bottom={}) must fit within hugging panel (bottom={})",
img.bottom(),
panel.bottom(),
);
}
#[test]
fn hugging_parent_sees_aspect_corrected_intrinsic() {
let mut root = column([column([El::new(Kind::Group)
.width(Size::Fixed(80.0))
.height(Size::Aspect(0.5))])
.width(Size::Hug)
.height(Size::Hug)])
.width(Size::Fixed(400.0))
.height(Size::Fixed(400.0))
.align(Align::Start);
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 400.0, 400.0));
let hugger = root.children[0].computed_rect;
assert!(
(hugger.w - 80.0).abs() < 0.5 && (hugger.h - 40.0).abs() < 0.5,
"hugging parent should be 80x40 (matching aspect-corrected intrinsic), got {}x{}",
hugger.w,
hugger.h,
);
}
#[test]
fn max_height_caps_overlay_child_below_intrinsic() {
let mut root = crate::tree::stack([column([crate::widgets::text::text("tall")
.width(Size::Fixed(40.0))
.height(Size::Fixed(300.0))])
.width(Size::Hug)
.height(Size::Hug)
.max_height(100.0)])
.width(Size::Fixed(600.0))
.height(Size::Fixed(600.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 600.0, 600.0));
let child_rect = root.children[0].computed_rect;
assert!(
(child_rect.h - 100.0).abs() < 0.5,
"expected child height capped at 100, got h={}",
child_rect.h,
);
}
#[test]
fn user_resizable_publishes_trailing_edge_band() {
use crate::state::resize::RESIZE_BAND_THICKNESS as T;
let mut root = crate::tree::row([
column(Vec::<El>::new())
.key("nav")
.user_resizable()
.width(Size::Fixed(200.0))
.min_width(120.0)
.max_width(420.0),
column(Vec::<El>::new()).width(Size::Fill(1.0)),
])
.height(Size::Fixed(400.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 400.0));
assert_eq!(state.resize.bands.len(), 1);
let band = &state.resize.bands[0];
assert_eq!(band.id, "nav");
assert_eq!(band.key.as_deref(), Some("nav"));
assert_eq!(band.sign, 1.0, "trailing edge: drag-right grows");
assert!((band.current - 200.0).abs() < 0.5);
assert_eq!((band.min, band.max), (120.0, 420.0));
assert!((band.band.x - (200.0 - T / 2.0)).abs() < 0.5);
assert!((band.band.w - T).abs() < 0.5);
assert!((band.band.h - 400.0).abs() < 0.5);
}
#[test]
fn user_resizable_last_child_gets_leading_edge_band() {
use crate::state::resize::RESIZE_BAND_THICKNESS as T;
let mut root = crate::tree::row([
column(Vec::<El>::new()).width(Size::Fill(1.0)),
column(Vec::<El>::new())
.key("inspector")
.user_resizable()
.width(Size::Fixed(240.0)),
])
.height(Size::Fixed(400.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 400.0));
assert_eq!(state.resize.bands.len(), 1);
let band = &state.resize.bands[0];
assert_eq!(band.sign, -1.0, "leading edge: drag-left grows");
assert!((band.band.x - (560.0 - T / 2.0)).abs() < 0.5);
assert_eq!(band.min, 0.0);
assert!(
(band.max - 800.0).abs() < 0.5,
"no max_width → capped at the parent's inner extent, got {}",
band.max,
);
}
#[test]
fn user_resizable_override_applies_and_clamps() {
let build = || {
crate::tree::row([
column(Vec::<El>::new())
.key("nav")
.user_resizable()
.width(Size::Fixed(200.0))
.min_width(120.0)
.max_width(420.0),
column(Vec::<El>::new()).key("main").width(Size::Fill(1.0)),
])
.height(Size::Fixed(400.0))
};
let mut state = UiState::new();
state.set_user_size("nav", 300.0);
let mut root = build();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 400.0));
let nav = state.rect_of_key("nav").unwrap();
assert!((nav.w - 300.0).abs() < 0.5, "override wins, got {}", nav.w);
let main = state.rect_of_key("main").unwrap();
assert!(
(main.w - 500.0).abs() < 0.5,
"the Fill sibling absorbs the change, got {}",
main.w,
);
state.set_user_size("nav", 9999.0);
let mut root = build();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 400.0));
assert!((state.rect_of_key("nav").unwrap().w - 420.0).abs() < 0.5);
state.clear_user_size("nav");
let mut root = build();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 400.0));
assert!((state.rect_of_key("nav").unwrap().w - 200.0).abs() < 0.5);
}
#[test]
fn user_resizable_in_column_resizes_height() {
use crate::state::resize::RESIZE_BAND_THICKNESS as T;
let mut root = column([
crate::tree::row(Vec::<El>::new())
.key("topbar")
.user_resizable()
.height(Size::Fixed(100.0))
.min_height(40.0),
crate::tree::row(Vec::<El>::new()).height(Size::Fill(1.0)),
])
.width(Size::Fixed(800.0));
let mut state = UiState::new();
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 600.0));
assert_eq!(state.resize.bands.len(), 1);
let band = &state.resize.bands[0];
assert!((band.band.y - (100.0 - T / 2.0)).abs() < 0.5);
assert!((band.band.w - 800.0).abs() < 0.5);
assert_eq!(band.min, 40.0);
assert!((band.current - 100.0).abs() < 0.5);
state.set_user_size("topbar", 250.0);
layout(&mut root, &mut state, Rect::new(0.0, 0.0, 800.0, 600.0));
assert!((state.rect_of_key("topbar").unwrap().h - 250.0).abs() < 0.5);
}
}