use std::cell::RefCell;
use std::sync::Arc;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::scroll::{ScrollAlignment, ScrollRequest};
use crate::state::{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,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct IntrinsicCacheKey {
computed_id: String,
available_width_bits: Option<u32>,
}
#[derive(Default)]
struct IntrinsicCache {
measurements: FxHashMap<IntrinsicCacheKey, (f32, f32)>,
stats: LayoutIntrinsicCacheStats,
prune: LayoutPruneStats,
}
thread_local! {
static INTRINSIC_CACHE: RefCell<Option<IntrinsicCache>> = const { RefCell::new(None) };
static LAST_INTRINSIC_CACHE_STATS: RefCell<LayoutIntrinsicCacheStats> =
const { RefCell::new(LayoutIntrinsicCacheStats { hits: 0, misses: 0 }) };
static LAST_PRUNE_STATS: RefCell<LayoutPruneStats> =
const { RefCell::new(LayoutPruneStats { subtrees: 0, nodes: 0 }) };
}
struct IntrinsicCacheGuard {
previous: Option<IntrinsicCache>,
}
impl Drop for IntrinsicCacheGuard {
fn drop(&mut self) {
INTRINSIC_CACHE.with(|cell| {
cell.replace(self.previous.take());
});
}
}
fn with_intrinsic_cache(f: impl FnOnce()) {
let previous = INTRINSIC_CACHE.with(|cell| cell.replace(Some(IntrinsicCache::default())));
let mut guard = IntrinsicCacheGuard { previous };
f();
let finished = INTRINSIC_CACHE.with(|cell| cell.replace(guard.previous.take()));
if let Some(cache) = finished {
LAST_INTRINSIC_CACHE_STATS.with(|stats| {
*stats.borrow_mut() = cache.stats;
});
LAST_PRUNE_STATS.with(|stats| {
*stats.borrow_mut() = cache.prune;
});
}
std::mem::forget(guard);
}
pub fn take_intrinsic_cache_stats() -> LayoutIntrinsicCacheStats {
LAST_INTRINSIC_CACHE_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 },
}
#[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,
},
anchor_policy: VirtualAnchorPolicy::default(),
row_key: Arc::new(row_key),
build_row: Arc::new(build_row),
}
}
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_id(root, "root");
}
layout_post_assign(root, ui_state, viewport);
}
pub fn layout_post_assign(root: &mut El, ui_state: &mut UiState, viewport: Rect) {
with_intrinsic_cache(|| {
{
crate::profile_span!("layout::root_setup");
ui_state
.layout
.computed_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();
}
crate::profile_span!("layout::children");
layout_children(root, viewport, ui_state);
});
}
pub fn assign_id_appended(parent_id: &str, child: &mut El, child_index: usize) {
let role = role_token(&child.kind);
let suffix = match (&child.key, role) {
(Some(k), r) => format!("{r}[{k}]"),
(None, r) => format!("{r}.{child_index}"),
};
assign_id(child, &format!("{parent_id}.{suffix}"));
}
fn rebuild_key_index(root: &El, ui_state: &mut UiState) {
ui_state.layout.key_index.clear();
fn visit(node: &El, index: &mut rustc_hash::FxHashMap<String, String>) {
if let Some(key) = &node.key {
index
.entry(key.clone())
.or_insert_with(|| node.computed_id.clone());
}
for c in &node.children {
visit(c, index);
}
}
visit(root, &mut ui_state.layout.key_index);
}
pub fn assign_ids(root: &mut El) {
assign_id(root, "root");
}
fn assign_id(node: &mut El, path: &str) {
node.computed_id = path.to_string();
for (i, c) in node.children.iter_mut().enumerate() {
let role = role_token(&c.kind);
let suffix = match (&c.key, role) {
(Some(k), r) => format!("{r}[{k}]"),
(None, r) => format!("{r}.{i}"),
};
let child_path = format!("{path}.{suffix}");
assign_id(c, &child_path);
}
}
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::Custom(name) => name,
}
}
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 {
ui_state.layout.computed_rects.insert(
c.computed_id.clone(),
Rect::new(node_rect.x, node_rect.y, 0.0, 0.0),
);
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.clone() {
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);
}
return;
}
match node.axis {
Axis::Overlay => {
let inner = node_rect.inset(node.padding);
for c in &mut node.children {
let c_rect = overlay_rect(c, inner, node.align, node.justify);
ui_state
.layout
.computed_rects
.insert(c.computed_id.clone(), c_rect);
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);
}
}
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| intrinsic(c);
let key_index = &ui_state.layout.key_index;
let computed_rects = &ui_state.layout.computed_rects;
let rect_of_key = |key: &str| -> Option<Rect> {
let id = key_index.get(key)?;
computed_rects.get(id).copied()
};
let rect_of_id = |id: &str| -> Option<Rect> { computed_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) {
ui_state
.layout
.computed_rects
.insert(c.computed_id.clone(), c_rect);
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,
} => layout_virtual_dynamic(
node,
inner,
items.count,
estimated_row_height,
DynamicVirtualFns {
anchor_policy: items.anchor_policy,
row_key: items.row_key,
build_row: items.build_row,
},
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.clone(), 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_end(node, stored, max_offset, ui_state);
let offset = stored.clamp(0.0, max_offset);
ui_state
.scroll
.offsets
.insert(node.computed_id.clone(), 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.clone(),
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 suffix = match (&child.key, role) {
(Some(k), r) => format!("{r}[{k}]"),
(None, r) => format!("{r}.{global_i}"),
};
assign_id(child, &format!("{parent_id}.{suffix}"));
}
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();
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);
let row_y = inner.y + row_top - offset;
let c_rect = Rect::new(inner.x, row_y, inner.w, row_height);
ui_state
.layout
.computed_rects
.insert(child.computed_id.clone(), c_rect);
layout_children(&mut child, c_rect, ui_state);
realized.push(child);
}
node.children = realized;
}
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_end_would_be_active(node, stored, max_offset, ui_state).unwrap_or(false);
let provisional_offset = if pin_active {
max_offset
} 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,
row_keys: &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_end(node, stored, max_offset, ui_state);
let pin_active = node.pin_end
&& 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.clone(), 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,
row_keys: &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 {
max_offset
} 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 &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.clone(), offset);
}
if node.pin_end {
ui_state
.scroll
.pin_prev_max
.insert(node.computed_id.clone(), 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.clone(), 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,
row_keys: &'a [String],
width_bucket: u32,
build_row: &'a Arc<dyn Fn(usize) -> El + Send + Sync>,
}
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, key) in ctx.row_keys.iter().enumerate().take(end).skip(start) {
let child = (ctx.build_row)(idx);
let actual_h = measure_dynamic_row(node, idx, ctx.inner.w, &child);
new_measurements.push((key.clone(), 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::Hug => intrinsic_constrained(child, Some(width)).1.max(0.0),
Size::Fill(_) => panic!(
"virtual_list_dyn row {idx} on {:?} must size with Size::Fixed or Size::Hug; \
Size::Fill would absorb the viewport's height and break virtualization",
node.computed_id,
),
}
}
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.clone())
.or_default();
for (row_key, h) in measurements {
entry.entry(row_key).or_default().insert(width_bucket, h);
}
}
#[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, key) in ctx.row_keys.iter().enumerate().take(end).skip(start) {
let mut child = (ctx.build_row)(idx);
assign_virtual_row_id(&mut child, &node.computed_id, idx);
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);
ui_state
.layout
.computed_rects
.insert(child.computed_id.clone(), c_rect);
layout_children(&mut child, c_rect, ui_state);
realized_rows.push(DynamicRealizedRow {
index: idx,
key: key.clone(),
rect: c_rect,
});
realized.push(child);
cursor_y += actual_h + gap;
}
store_dynamic_measurements(node, ctx.width_bucket, new_measurements, ui_state);
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: &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.clone(), 0.0);
ui_state.scroll.metrics.insert(
node.computed_id.clone(),
crate::state::ScrollMetrics {
viewport_h: inner.h,
content_h: 0.0,
max_offset: 0.0,
},
);
return;
}
let content_bottom = node
.children
.iter()
.map(|c| ui_state.rect(&c.computed_id).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);
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_end(node, stored, max_offset, ui_state);
let clamped = stored.clamp(0.0, max_offset);
if clamped > 0.0 {
for c in &node.children {
shift_subtree_y(c, -clamped, ui_state);
}
}
ui_state
.scroll
.offsets
.insert(node.computed_id.clone(), clamped);
ui_state.scroll.metrics.insert(
node.computed_id.clone(),
crate::state::ScrollMetrics {
viewport_h: inner.h,
content_h,
max_offset,
},
);
write_thumb_rect(node, inner, content_h, max_offset, clamped, ui_state);
}
const PIN_END_EPSILON: f32 = 0.5;
fn pin_end_would_be_active(
node: &El,
stored: f32,
_max_offset: f32,
ui_state: &UiState,
) -> Option<bool> {
if !node.pin_end {
return None;
}
let prev_max = ui_state.scroll.pin_prev_max.get(&node.computed_id).copied();
let prev_active = ui_state.scroll.pin_active.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_END_EPSILON {
false
} else if !prev && prev_max > 0.0 && stored >= prev_max - PIN_END_EPSILON {
true
} else {
prev
}
}
})
}
fn resolve_pin_end(node: &El, stored: f32, max_offset: f32, ui_state: &mut UiState) -> f32 {
if !node.pin_end {
ui_state.scroll.pin_active.remove(&node.computed_id);
ui_state.scroll.pin_prev_max.remove(&node.computed_id);
return stored;
}
let active = pin_end_would_be_active(node, stored, max_offset, ui_state).unwrap_or(false);
ui_state
.scroll
.pin_active
.insert(node.computed_id.clone(), active);
ui_state
.scroll
.pin_prev_max
.insert(node.computed_id.clone(), max_offset);
if active { max_offset } else { stored }
}
fn resolve_ensure_visible_for_scroll(
node: &El,
inner: Rect,
content_h: f32,
ui_state: &mut UiState,
) {
if ui_state.scroll.pending_requests.is_empty() {
return;
}
let pending = std::mem::take(&mut ui_state.scroll.pending_requests);
let mut remaining: Vec<ScrollRequest> = Vec::with_capacity(pending.len());
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_str())
.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.clone(), new_offset);
}
ui_state.scroll.pending_requests = remaining;
}
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 thumb_x = inner.right() - thumb_w - track_inset;
let track_x = inner.right() - track_w - track_inset;
ui_state.scroll.thumb_rects.insert(
node.computed_id.clone(),
Rect::new(thumb_x, thumb_y, thumb_w, thumb_h),
);
ui_state.scroll.thumb_tracks.insert(
node.computed_id.clone(),
Rect::new(track_x, inner.y, track_w, inner.h),
);
}
fn shift_subtree_y(node: &El, dy: f32, ui_state: &mut UiState) {
if let Some(rect) = ui_state.layout.computed_rects.get_mut(&node.computed_id) {
rect.y += dy;
}
for c in &node.children {
shift_subtree_y(c, dy, ui_state);
}
}
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 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 intrinsics: Vec<(f32, f32)> = {
crate::profile_span!("layout::axis::intrinsics");
node.children
.iter()
.map(|c| child_intrinsic(c, vertical, cross_extent, node.align))
.collect()
};
let mut consumed = 0.0;
let mut fill_weight_total = 0.0;
for (c, (iw, ih)) in node.children.iter().zip(intrinsics.iter()) {
match main_size_of(c, *iw, *ih, vertical) {
MainSize::Resolved(v) => consumed += v,
MainSize::Fill(w) => fill_weight_total += w.max(0.001),
}
}
let remaining = (main_extent - consumed - total_gap).max(0.0);
let free_after_used = if fill_weight_total == 0.0 {
remaining
} else {
0.0
};
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 && fill_weight_total == 0.0 {
remaining / (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, (iw, ih))) in node.children.iter_mut().zip(intrinsics).enumerate() {
let main_size = match main_size_of(c, iw, ih, vertical) {
MainSize::Resolved(v) => v,
MainSize::Fill(w) => remaining * w.max(0.001) / fill_weight_total.max(0.001),
};
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::Hug | Size::Fill(_) => match node.align {
Align::Stretch => cross_extent,
Align::Start | Align::Center | Align::End => cross_intrinsic,
},
};
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)
};
ui_state
.layout
.computed_rects
.insert(c.computed_id.clone(), c_rect);
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 {
layout_children(c, c_rect, ui_state);
}
cursor += main_size + node.gap + if i + 1 < n { between_extra } else { 0.0 };
}
}
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 || node.pin_end {
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)
}
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: &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 &node.children {
ui_state
.layout
.computed_rects
.insert(child.computed_id.clone(), zero);
count += 1 + zero_descendant_rects(child, zero, ui_state);
}
count
}
fn record_pruned_subtree(nodes: u64) {
INTRINSIC_CACHE.with(|cell| {
if let Some(cache) = cell.borrow_mut().as_mut() {
cache.prune.subtrees += 1;
cache.prune.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 };
match s {
Size::Fixed(v) => MainSize::Resolved(v),
Size::Hug => MainSize::Resolved(intr),
Size::Fill(w) => MainSize::Fill(w),
}
}
fn child_intrinsic(
c: &El,
vertical: bool,
parent_cross_extent: f32,
parent_align: Align,
) -> (f32, f32) {
if !vertical {
return intrinsic(c);
}
let available_width = match c.width {
Size::Fixed(v) => Some(v),
Size::Fill(_) => Some(parent_cross_extent),
Size::Hug => match parent_align {
Align::Stretch => Some(parent_cross_extent),
Align::Start | Align::Center | Align::End => Some(parent_cross_extent),
},
};
intrinsic_constrained(c, available_width)
}
fn overlay_rect(c: &El, parent: Rect, align: Align, justify: Justify) -> Rect {
let constrained_width = match c.width {
Size::Fixed(v) => Some(v),
Size::Fill(_) | Size::Hug => Some(parent.w),
};
let (iw, ih) = intrinsic_constrained(c, constrained_width);
let w = match c.width {
Size::Fixed(v) => v,
Size::Hug => iw.min(parent.w),
Size::Fill(_) => parent.w,
};
let h = match c.height {
Size::Fixed(v) => v,
Size::Hug => ih.min(parent.h),
Size::Fill(_) => parent.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 key = intrinsic_cache_key(c, available_width);
if let Some(key) = &key
&& let Some(cached) = INTRINSIC_CACHE.with(|cell| {
let mut slot = cell.borrow_mut();
let cache = slot.as_mut()?;
let cached = cache.measurements.get(key).copied();
if cached.is_some() {
cache.stats.hits += 1;
}
cached
})
{
return cached;
}
if key.is_some() {
INTRINSIC_CACHE.with(|cell| {
if let Some(cache) = cell.borrow_mut().as_mut() {
cache.stats.misses += 1;
}
});
}
let measured = intrinsic_constrained_uncached(c, available_width);
if let Some(key) = key {
INTRINSIC_CACHE.with(|cell| {
if let Some(cache) = cell.borrow_mut().as_mut() {
cache.measurements.insert(key, measured);
}
});
}
measured
}
fn intrinsic_cache_key(c: &El, available_width: Option<f32>) -> Option<IntrinsicCacheKey> {
if INTRINSIC_CACHE.with(|cell| cell.borrow().is_none()) {
return None;
}
if c.computed_id.is_empty() {
return None;
}
Some(IntrinsicCacheKey {
computed_id: c.computed_id.clone(),
available_width_bits: available_width.map(f32::to_bits),
})
}
fn intrinsic_constrained_uncached(c: &El, available_width: Option<f32>) -> (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 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 apply_min(c, 0.0, 0.0);
}
if matches!(c.kind, Kind::Inlines) {
return inline_paragraph_intrinsic(c, available_width);
}
if matches!(c.kind, Kind::HardBreak) {
return 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 apply_min(
c,
layout.width + c.padding.left + c.padding.right,
layout.height() + c.padding.top + c.padding.bottom,
);
}
return apply_min(c, 0.0, 0.0);
}
if c.icon.is_some() {
return 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 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::Fill(_) | Size::Hug => 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_wrap,
content_available,
);
let w = match (content_available, c.width) {
(Some(available), Size::Hug) => {
let unwrapped = text_metrics::layout_text_with_family(
text,
c.font_size,
c.font_family,
c.font_weight,
c.font_mono,
TextWrap::NoWrap,
None,
);
unwrapped.width.min(available) + c.padding.left + c.padding.right
}
(Some(available), Size::Fixed(_) | Size::Fill(_)) => {
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 apply_min(c, w, h);
}
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::Fill(_) | Size::Hug => 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_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,
)
} 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;
}
(w, h)
}
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::Fill(_) | Size::Hug => 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,
node.text_wrap,
content_available,
);
let w = match (content_available, node.width) {
(Some(available), Size::Hug) => {
let unwrapped = text_metrics::layout_text_with_line_height_and_family(
&concat,
size,
line_height,
node.font_family,
FontWeight::Regular,
false,
TextWrap::NoWrap,
None,
);
unwrapped.width.min(available) + node.padding.left + node.padding.right
}
(Some(available), Size::Fixed(_) | Size::Fill(_)) => {
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::Fill(_) | Size::Hug => None,
}),
TextWrap::NoWrap => None,
}
.map(|w| (w - node.padding.left - node.padding.right).max(1.0));
let mut breaker = crate::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,
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 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 = state.rect(&root.children[0].computed_id);
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 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 = state.rect(&root.children[0].computed_id);
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 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 = state.rect(&root.children[0].computed_id);
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 = state.rect(&root.children[0].computed_id);
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 = state.rect(&root.children[0].computed_id).y;
let y1 = state.rect(&root.children[1].computed_id).y;
let y2 = state.rect(&root.children[2].computed_id).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 = state.rect(&root.children[0].computed_id).h;
let small_h = state.rect(&root.children[1].computed_id).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 = state.rect(&root.children[0].computed_id);
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 = state.rect(&root.children[0].computed_id);
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(), 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 = state.rect(&root.children[0].computed_id);
assert!(
(c0.y - (-80.0)).abs() < 0.01,
"child 0 y = {} (expected -80)",
c0.y
);
state
.scroll
.offsets
.insert(root.computed_id.clone(), 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(), 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(&root, "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 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(), 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 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 = state.rect(&root.children[0].computed_id);
let r1 = state.rect(&root.children[1].computed_id);
let r2 = state.rect(&root.children[2].computed_id);
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_id = &popover_layer.children[0].computed_id;
let panel_rect = state.rect(panel_id);
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 = state.rect(&root.children[0].computed_id);
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 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(), 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 = state.rect(&root.children[0].computed_id);
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!(
(state.rect(&row_1.computed_id).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(), 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(), 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(), 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 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| state.rect(&c.computed_id).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!(
(state.rect(&row_1.computed_id).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_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(), 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| state.rect(&child.computed_id).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())
.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| state.rect(&child.computed_id).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(), 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}"
);
}
#[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 = state.rect(&root.children[0].computed_id);
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 = state.rect(&root.children[0].computed_id);
assert_eq!(panel_rect.w, PANEL_W, "panel keeps its Fixed width");
let para_rect = state.rect(&root.children[0].children[0].computed_id);
let button_rect = state.rect(&root.children[0].children[1].computed_id);
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 = state.rect(&root.children[0].computed_id);
let row1_rect = state.rect(&root.children[1].computed_id);
let para0_rect = state.rect(&root.children[0].children[1].computed_id);
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,
);
}
}