use crate::core::element::{Align, BoxStyle, Element, ElementKind};
use crate::core::runtime::{
ActionId, FocusId, LayoutNode, NodeContent, ScrollId, find_action, find_focus, first_text,
};
use crate::geometry::{PhysicalSize, Point, Rect, Size, Vec2};
use crate::layout::{
Axis, FlexItem, LayoutDirection, Span, Track, TrackItem, break_lines, clamp_len, place_cells,
solve_main_axis, solve_tracks, span_extent,
};
use crate::platform::native::NativeState;
use crate::render::{Color, Font, Scene, TextDirection, TextLayout};
const VIEWPORT_PLACEHOLDER: Color = Color::rgb(0x20, 0x24, 0x2c);
pub fn measure(el: &Element, avail: Size, font: Option<&Font>) -> Size {
let pad = el.layout.padding;
let (min, max) = (min_size(el), max_size(el));
let outer = Size::new(
clamp_len(
el.layout.size.width.unwrap_or(avail.width),
min.width,
max.width,
),
clamp_len(
el.layout.size.height.unwrap_or(avail.height),
min.height,
max.height,
),
);
let inner = outer.deflate(pad);
let content = match &el.kind {
ElementKind::Leaf | ElementKind::Viewport { .. } => Size::ZERO,
ElementKind::Image { source, .. } => {
let natural = source.size();
let fixed = |v: Option<f64>, p: f64| v.map(|v| (v - p).max(0.0));
let w = fixed(el.layout.size.width, pad.horizontal());
let h = fixed(el.layout.size.height, pad.vertical());
match (w, h) {
(Some(w), None) if natural.width > 0.0 => {
Size::new(w, w * natural.height / natural.width)
}
(None, Some(h)) if natural.height > 0.0 => {
Size::new(h * natural.width / natural.height, h)
}
_ => natural,
}
}
ElementKind::Text { text, size, .. } => match font {
Some(f) if el.wrap && inner.width.is_finite() => {
let lines = f.wrap(text, *size, inner.width);
let w = lines
.iter()
.map(|l| f.measure(l, *size).width)
.fold(0.0_f64, f64::max);
Size::new(w, f.line_height(*size) * lines.len() as f64)
}
Some(f) => f.measure(text, *size),
None => Size::ZERO,
},
ElementKind::Stack {
axis,
gap,
children,
..
} => {
let sizes: Vec<Size> = children.iter().map(|c| measure(c, inner, font)).collect();
let lines = if el.wrap {
let mains: Vec<f64> = sizes.iter().map(|s| axis.main(*s)).collect();
break_lines(axis.main(inner), *gap, &mains)
} else {
std::iter::once(0..children.len()).collect()
};
let mut main: f64 = 0.0;
let mut cross = 0.0;
for (k, line) in lines.iter().enumerate() {
let n = line.len() as f64;
let line_main: f64 = sizes[line.clone()].iter().map(|s| axis.main(*s)).sum();
main = main.max(line_main + gap * (n - 1.0).max(0.0));
cross += sizes[line.clone()]
.iter()
.map(|s| axis.cross(*s))
.fold(0.0_f64, f64::max);
if k > 0 {
cross += gap;
}
}
axis.size(main, cross)
}
ElementKind::Grid {
columns,
rows,
column_gap,
row_gap,
children,
..
} => {
let g = grid_geometry(
columns,
rows,
*column_gap,
*row_gap,
children,
inner,
false,
font,
);
Size::new(tracks_end(&g.columns), tracks_end(&g.rows))
}
};
let w = el
.layout
.size
.width
.unwrap_or(content.width + pad.horizontal());
let h = el
.layout
.size
.height
.unwrap_or(content.height + pad.vertical());
Size::new(
clamp_len(w, min.width, max.width),
clamp_len(h, min.height, max.height),
)
}
fn min_size(el: &Element) -> Size {
Size::new(
el.layout.min.width.unwrap_or(0.0),
el.layout.min.height.unwrap_or(0.0),
)
}
fn max_size(el: &Element) -> Size {
Size::new(
el.layout.max.width.unwrap_or(f64::INFINITY),
el.layout.max.height.unwrap_or(f64::INFINITY),
)
}
fn tracks_end(spans: &[Span]) -> f64 {
spans.last().map(|s| s.offset + s.length).unwrap_or(0.0)
}
struct GridGeometry {
columns: Vec<Span>,
rows: Vec<Span>,
cells: Vec<(usize, usize, usize, usize)>,
sizes: Vec<Size>,
}
#[allow(clippy::too_many_arguments)]
fn grid_geometry(
columns: &[Track],
rows: &[Track],
column_gap: f64,
row_gap: f64,
children: &[Element],
inner: Size,
rows_definite: bool,
font: Option<&Font>,
) -> GridGeometry {
let auto = [Track::Auto];
let columns = if columns.is_empty() {
&auto[..]
} else {
columns
};
let ncols = columns.len();
let spans: Vec<(usize, usize)> = children
.iter()
.map(|c| {
(
(c.layout.column_span.max(1) as usize).min(ncols),
c.layout.row_span.max(1) as usize,
)
})
.collect();
let (placed, nrows) = place_cells(ncols, &spans);
let col_items: Vec<TrackItem> = children
.iter()
.zip(&spans)
.zip(&placed)
.map(|((c, &(cs, _)), &(_, col))| {
let fixed: Option<f64> = columns[col..col + cs]
.iter()
.map(|t| match t {
Track::Fixed(v) => Some(*v),
_ => None,
})
.sum();
let probe = fixed.map(|w| w + column_gap * (cs as f64 - 1.0));
let width = measure(
c,
Size::new(probe.unwrap_or(inner.width), inner.height),
font,
)
.width;
TrackItem {
start: col,
span: cs,
size: width,
}
})
.collect();
let col_avail = inner.width.is_finite().then_some(inner.width);
let col_spans = solve_tracks(columns, col_avail, column_gap, &col_items);
let sizes: Vec<Size> = children
.iter()
.zip(&spans)
.zip(&placed)
.map(|((c, &(cs, _)), &(_, col))| {
let (_, cell_w) = span_extent(&col_spans, col, cs);
measure(c, Size::new(cell_w, inner.height), font)
})
.collect();
let row_tracks: Vec<Track> = (0..nrows.max(rows.len()))
.map(|r| rows.get(r).copied().unwrap_or(Track::Auto))
.collect();
let row_items: Vec<TrackItem> = sizes
.iter()
.zip(&spans)
.zip(&placed)
.map(|((s, &(_, rs)), &(row, _))| TrackItem {
start: row,
span: rs,
size: s.height,
})
.collect();
let row_avail = (rows_definite && inner.height.is_finite()).then_some(inner.height);
let row_spans = solve_tracks(&row_tracks, row_avail, row_gap, &row_items);
GridGeometry {
columns: col_spans,
rows: row_spans,
cells: placed
.iter()
.zip(&spans)
.map(|(&(r, c), &(cs, rs))| (r, c, rs, cs))
.collect(),
sizes,
}
}
fn align_in(align: Align, natural: f64, slot: f64, min: f64, max: f64) -> (f64, f64) {
let length = match align {
Align::Stretch => clamp_len(slot, min, max),
_ => clamp_len(natural.min(slot), min, max),
};
let offset = match align {
Align::Start | Align::Stretch => 0.0,
Align::Center => (slot - length) / 2.0,
Align::End => slot - length,
};
(offset, length)
}
pub fn layout(el: &Element, bounds: Rect, font: Option<&Font>) -> LayoutNode {
layout_with_direction(el, bounds, font, LayoutDirection::Ltr)
}
pub fn layout_with_direction(
el: &Element,
bounds: Rect,
font: Option<&Font>,
direction: LayoutDirection,
) -> LayoutNode {
let dir = el.layout_direction.unwrap_or(direction);
let place = |child: &Element, rect: Rect| {
let rect = if dir.is_rtl() {
mirror_x(rect, bounds)
} else {
rect
};
layout_with_direction(child, rect, font, dir)
};
let content = match &el.kind {
ElementKind::Text {
text,
size,
color,
direction,
underline,
} => NodeContent::Text {
text: text.clone(),
size: *size,
color: *color,
direction: *direction,
underline: *underline,
},
ElementKind::Viewport { id } => NodeContent::Viewport(*id),
ElementKind::Image { source, fit } => NodeContent::Image {
source: source.clone(),
fit: *fit,
padding: if dir.is_rtl() {
el.layout.padding.mirrored()
} else {
el.layout.padding
},
},
_ => NodeContent::None,
};
let mut node = LayoutNode {
bounds,
decoration: el.decoration,
content,
action: el.action,
focus: el.focus,
drag: el.drag,
context: el.context,
drop: el.drop,
drag_source: el.drag_source,
caret: el.caret,
selection: el.selection,
preedit: el.preedit,
text_pos: el.text_pos,
wrap: el.wrap,
native: el.native,
scroll: el.scroll,
clip: el.clip,
cursor: el.cursor,
semantics: el.semantics.clone(),
rtl: dir.is_rtl(),
children: Vec::new(),
};
if let ElementKind::Grid {
columns,
rows,
column_gap,
row_gap,
align,
children,
} = &el.kind
{
let inner = bounds.inset(el.layout.padding);
let g = grid_geometry(
columns,
rows,
*column_gap,
*row_gap,
children,
inner.size,
el.scroll.is_none(),
font,
);
node.children.reserve(children.len());
for (k, child) in children.iter().enumerate() {
let (row, col, rs, cs) = g.cells[k];
let (cx, cw) = span_extent(&g.columns, col, cs);
let (cy, ch) = span_extent(&g.rows, row, rs);
let (min, max, m) = (min_size(child), max_size(child), g.sizes[k]);
let fixed = |a: Align, size: Option<f64>| match (a, size) {
(Align::Stretch, Some(_)) => Align::Start,
_ => a,
};
let (dx, w) = align_in(
fixed(align.0, child.layout.size.width),
m.width,
cw,
min.width,
max.width,
);
let (dy, h) = align_in(
fixed(align.1, child.layout.size.height),
m.height,
ch,
min.height,
max.height,
);
let rect = Rect::from_xywh(inner.min_x() + cx + dx, inner.min_y() + cy + dy, w, h);
node.children.push(place(child, rect));
}
return node;
}
let ElementKind::Stack {
axis,
gap,
main_align,
cross_align,
children,
} = &el.kind
else {
return node;
};
if children.is_empty() {
return node;
}
let inner = bounds.inset(el.layout.padding);
let avail = inner.size;
let axis = *axis;
let main_avail = axis.main(avail);
let measured: Vec<Size> = children.iter().map(|c| measure(c, avail, font)).collect();
let lines = if el.wrap {
let mains: Vec<f64> = measured.iter().map(|m| axis.main(*m)).collect();
break_lines(main_avail, *gap, &mains)
} else {
std::iter::once(0..children.len()).collect()
};
node.children.reserve(children.len());
let mut cross_cursor = 0.0;
for line in lines {
let line_children = &children[line.clone()];
let line_measured = &measured[line.clone()];
let (spans, main_shift) = if el.scroll.is_some() {
let items: Vec<FlexItem> = line_measured
.iter()
.map(|m| FlexItem::fixed(axis.main(*m)))
.collect();
(solve_main_axis(f64::INFINITY, *gap, &items), 0.0)
} else {
let items: Vec<FlexItem> = line_children
.iter()
.zip(line_measured)
.map(|(c, m)| {
FlexItem::fixed(axis.main(*m))
.with_grow(c.layout.grow)
.with_shrink(c.layout.shrink)
.with_bounds(axis.main(min_size(c)), axis.main(max_size(c)))
})
.collect();
let spans = solve_main_axis(main_avail, *gap, &items);
let leftover = (main_avail - tracks_end(&spans)).max(0.0);
let main_shift = match main_align {
Align::Start | Align::Stretch => 0.0,
Align::Center => leftover / 2.0,
Align::End => leftover,
};
(spans, main_shift)
};
let line_cross = if el.wrap {
line_measured
.iter()
.map(|m| axis.cross(*m))
.fold(0.0_f64, f64::max)
} else {
axis.cross(avail)
};
for ((child, m), span) in line_children.iter().zip(line_measured).zip(&spans) {
let (cross_off, cross_len) = align_in(
*cross_align,
axis.cross(*m),
line_cross,
axis.cross(min_size(child)),
axis.cross(max_size(child)),
);
let child_bounds = child_rect(
axis,
inner,
span.offset + main_shift,
span.length,
cross_cursor + cross_off,
cross_len,
);
node.children.push(place(child, child_bounds));
}
cross_cursor += line_cross + *gap;
}
node
}
pub fn paint_focus(
tree: &LayoutNode,
focused: FocusId,
scene: &mut Scene,
font: Option<&Font>,
ring: Color,
caret: Color,
selection: Color,
) {
let Some(node) = find_focus(tree, focused) else {
return;
};
let os_focus = node
.native
.is_some_and(|n| crate::platform::native::draws_focus(n.control));
if !os_focus {
scene.stroke_rect(node.bounds, ring, 2.0);
}
let entry = node
.semantics
.as_ref()
.and_then(|s| s.role)
.is_none_or(|r| {
matches!(
r,
crate::core::a11y::Role::TextField | crate::core::a11y::Role::SpinButton
)
});
if node.action.is_some() || !entry {
return;
}
let Some(leaf) = first_text(node) else { return };
let NodeContent::Text {
text,
size,
direction,
..
} = &leaf.content
else {
return;
};
let bounds = leaf.bounds;
let Some(font) = font else {
let h = *size;
scene.fill_rect(
Rect::from_xywh(bounds.min_x(), bounds.min_y(), 2.0, h),
caret,
);
return;
};
let layout = text_layout(
font,
text,
*size,
*direction,
leaf.wrap,
bounds.width(),
leaf.rtl,
);
let origin = bounds.origin;
if let Some((s, e)) = leaf.selection {
for rect in layout.selection_rects(s, e) {
scene.fill_rect(rect.translate(Vec2::new(origin.x, origin.y)), selection);
}
}
let line_h = font.line_height(*size);
let at = layout.caret_position(leaf.caret.unwrap_or(text.len()));
let cx = (origin.x + at.x).clamp(bounds.min_x(), (bounds.max_x() - 1.0).max(bounds.min_x()));
scene.fill_rect(Rect::from_xywh(cx, origin.y + at.y, 2.0, line_h), caret);
}
fn text_layout<'a>(
font: &'a Font,
text: &'a str,
size: f64,
direction: TextDirection,
wrap: bool,
width: f64,
rtl: bool,
) -> TextLayout<'a> {
let ui = if rtl {
LayoutDirection::Rtl
} else {
LayoutDirection::Ltr
};
font.layout_in(text, size, direction, wrap.then_some(width), ui)
.with_align_width(width)
}
pub fn caret_index_at(node: &LayoutNode, point: Point, font: Option<&Font>) -> Option<usize> {
let leaf = first_text(node)?;
let NodeContent::Text {
text,
size,
direction,
..
} = &leaf.content
else {
return None;
};
let font = font?;
let b = leaf.bounds;
let layout = text_layout(
font,
text,
*size,
*direction,
leaf.wrap,
b.width(),
leaf.rtl,
);
Some(layout.index_at(Point::new(point.x - b.min_x(), point.y - b.min_y())))
}
pub fn paint_hover(tree: &LayoutNode, hovered: ActionId, scene: &mut Scene, highlight: Color) {
if let Some(node) = find_action(tree, hovered) {
if node.native.is_some() {
return;
}
if node.decoration.radius > 0.0 {
scene.fill_round_rect(node.bounds, node.decoration.radius, highlight);
} else {
scene.fill_rect(node.bounds, highlight);
}
}
}
pub fn mark_native_states(
node: &mut LayoutNode,
hovered: Option<ActionId>,
pressed: Option<ActionId>,
focused: Option<FocusId>,
) {
if let Some(native) = &mut node.native {
native.state = NativeState {
hovered: node.action.is_some() && node.action == hovered,
pressed: node.action.is_some() && node.action == pressed,
focused: node.focus.is_some() && node.focus == focused,
};
}
for child in &mut node.children {
mark_native_states(child, hovered, pressed, focused);
}
}
fn paint_native(node: &LayoutNode, scene: &mut Scene) -> bool {
let Some(deco) = node.native else {
return false;
};
let scale = scene.scale();
let size = PhysicalSize::new(
(node.bounds.width() * scale).round() as u32,
(node.bounds.height() * scale).round() as u32,
);
match crate::platform::native::render(deco, size, scale) {
Some(image) => {
scene.draw_image(node.bounds, image);
true
}
None => false,
}
}
pub fn paint(node: &LayoutNode, scene: &mut Scene, font: Option<&Font>) {
if !paint_native(node, scene) {
paint_decoration(&node.decoration, node.bounds, scene);
}
match &node.content {
NodeContent::Text {
text,
size,
color,
direction,
underline,
} => {
if let Some(f) = font
&& !text.is_empty()
{
let w = node.bounds.width();
let layout = text_layout(f, text, *size, *direction, node.wrap, w, node.rtl);
scene.fill_layout(&layout, node.bounds.origin, *color);
if let Some((start, end)) = *underline {
let o = node.bounds.origin;
let thick = (size / 14.0).max(1.0);
let below = f.baseline(*size) + (size / 10.0).max(1.0);
for r in layout.selection_rects(start, end.min(text.len())) {
let at = Rect::from_xywh(
o.x + r.min_x(),
o.y + r.min_y() + below,
r.width(),
thick,
);
scene.fill_rect(at, *color);
}
}
}
}
NodeContent::Viewport(id) => {
scene.fill_viewport(node.bounds, id.0 as u64, VIEWPORT_PLACEHOLDER);
}
NodeContent::Image {
source,
fit,
padding,
} => {
scene.draw_image_source(node.bounds.inset(*padding), source, *fit);
}
NodeContent::None => {}
}
if node.clip && !node.children.is_empty() {
scene.push_clip(node.bounds);
for child in &node.children {
paint(child, scene, font);
}
scene.pop_clip();
} else {
for child in &node.children {
paint(child, scene, font);
}
}
}
pub fn apply_scroll(node: &mut LayoutNode, offsets: &mut std::collections::HashMap<ScrollId, f64>) {
if let Some(id) = node.scroll {
let top = node.bounds.min_y();
let content_bottom = node
.children
.iter()
.map(|c| c.bounds.max_y())
.fold(top, f64::max);
let content_h = content_bottom - top;
let max_off = (content_h - node.bounds.height()).max(0.0);
let off = offsets.entry(id).or_insert(0.0);
*off = off.clamp(0.0, max_off);
let dy = *off;
if dy > 0.0 {
for child in &mut node.children {
translate(child, -dy);
}
}
}
for child in &mut node.children {
apply_scroll(child, offsets);
}
}
fn translate(node: &mut LayoutNode, dy: f64) {
node.bounds = node.bounds.translate(Vec2::new(0.0, dy));
for child in &mut node.children {
translate(child, dy);
}
}
fn mirror_x(rect: Rect, within: Rect) -> Rect {
let span = Span {
offset: rect.min_x() - within.min_x(),
length: rect.width(),
}
.mirrored(within.width());
Rect::from_xywh(
within.min_x() + span.offset,
rect.min_y(),
span.length,
rect.height(),
)
}
fn child_rect(
axis: Axis,
content: Rect,
main_off: f64,
main_len: f64,
cross_off: f64,
cross_len: f64,
) -> Rect {
match axis {
Axis::Horizontal => Rect::from_xywh(
content.min_x() + main_off,
content.min_y() + cross_off,
main_len,
cross_len,
),
Axis::Vertical => Rect::from_xywh(
content.min_x() + cross_off,
content.min_y() + main_off,
cross_len,
main_len,
),
}
}
fn paint_decoration(deco: &BoxStyle, bounds: Rect, scene: &mut Scene) {
if let Some(fill) = deco.fill {
if deco.radius > 0.0 {
scene.fill_round_rect(bounds, deco.radius, fill);
} else {
scene.fill_rect(bounds, fill);
}
}
if let Some((color, width)) = deco.border {
if deco.radius > 0.0 {
scene.stroke_round_rect(bounds, deco.radius, color, width);
} else {
scene.stroke_rect(bounds, color, width);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::element::BoxStyle;
use crate::render::Color;
#[test]
fn fixed_width_constrains_wrapped_text_height() {
let Some(font) = crate::render::Font::system_default() else {
return; };
let text = "several words that need to wrap inside a rather narrow fixed width box";
let para = || Element::text(text, 14.0, Color::rgb(0, 0, 0)).wrap();
let boxed = Element::stack(Axis::Vertical, vec![para()]).width(120.0);
let wide = Size::new(4000.0, 4000.0);
let lines = font.wrap(text, 14.0, 120.0).len();
assert!(lines > 1);
let h = measure(&boxed, wide, Some(&font)).height;
assert_eq!(h, font.line_height(14.0) * lines as f64);
}
#[test]
fn measure_sums_children_with_gap_and_padding() {
let child = || Element::boxed(BoxStyle::default()).width(20.0).height(10.0);
let stack = Element::stack(Axis::Vertical, vec![child(), child(), child()])
.gap(5.0)
.padding(crate::geometry::Insets::uniform(4.0));
let size = measure(&stack, Size::new(1000.0, 1000.0), None);
assert_eq!(size, Size::new(28.0, 48.0));
}
#[test]
fn caret_index_at_resolves_pointer_to_byte_index() {
let Some(font) = Font::system_default() else {
eprintln!("skipping: no system font found");
return;
};
let el = Element::text("hello", 16.0, Color::BLACK);
let node = layout(&el, Rect::from_xywh(10.0, 0.0, 200.0, 20.0), Some(&font));
let y = node.bounds.min_y() + 1.0;
assert_eq!(
caret_index_at(&node, Point::new(0.0, y), Some(&font)),
Some(0)
);
assert_eq!(
caret_index_at(&node, Point::new(9.0, y), Some(&font)),
Some(0)
);
assert_eq!(
caret_index_at(&node, Point::new(10_000.0, y), Some(&font)),
Some(5)
);
let mid = node.bounds.min_x() + font.measure("hel", 16.0).width;
let i = caret_index_at(&node, Point::new(mid, y), Some(&font)).unwrap();
assert!((1..=4).contains(&i), "mid index {i} out of range");
assert_eq!(caret_index_at(&node, Point::new(mid, y), None), None);
}
#[test]
fn viewport_reserves_its_rect_and_paints_a_viewport_command() {
use crate::core::runtime::{ViewportId, collect_viewports, viewport_at};
use crate::render::DrawCmd;
let el = Element::viewport(ViewportId(3)).width(120.0).height(80.0);
assert_eq!(
measure(&el, Size::new(1000.0, 1000.0), None),
Size::new(120.0, 80.0)
);
let tree = layout(&el, Rect::from_xywh(20.0, 10.0, 120.0, 80.0), None);
assert!(matches!(tree.content, NodeContent::Viewport(ViewportId(3))));
let mut found = Vec::new();
collect_viewports(&tree, &mut found);
assert_eq!(
found,
vec![(ViewportId(3), Rect::from_xywh(20.0, 10.0, 120.0, 80.0))]
);
assert_eq!(
viewport_at(&tree, Point::new(30.0, 20.0)).map(|(id, _)| id),
Some(ViewportId(3))
);
assert_eq!(viewport_at(&tree, Point::new(0.0, 0.0)), None);
let mut scene = Scene::new(Size::new(200.0, 120.0));
paint(&tree, &mut scene, None);
assert!(
scene
.commands()
.iter()
.any(|c| matches!(c, DrawCmd::Viewport { id: 3, .. }))
);
}
#[test]
fn grow_child_fills_main_axis() {
let fixed = Element::boxed(BoxStyle::default()).width(40.0).height(10.0);
let flex = Element::boxed(BoxStyle {
fill: Some(Color::WHITE),
..BoxStyle::default()
})
.grow(1.0);
let row =
Element::stack(Axis::Horizontal, vec![fixed, flex]).align(Align::Start, Align::Stretch);
let tree = layout(&row, Rect::from_xywh(0.0, 0.0, 200.0, 20.0), None);
assert_eq!(
tree.children[1].bounds,
Rect::from_xywh(40.0, 0.0, 160.0, 20.0)
);
let mut scene = Scene::new(Size::new(200.0, 20.0));
paint(&tree, &mut scene, None);
assert_eq!(scene.len(), 1); }
fn block(w: f64, h: f64) -> Element {
Element::boxed(BoxStyle::default()).width(w).height(h)
}
fn cell(w: f64, h: f64) -> Element {
Element::boxed(BoxStyle::default())
.min_width(w)
.min_height(h)
}
fn rects(node: &LayoutNode) -> Vec<Rect> {
node.children.iter().map(|c| c.bounds).collect()
}
#[test]
fn measure_clamps_to_min_and_max() {
let big = Size::new(1000.0, 1000.0);
let el = Element::stack(Axis::Vertical, vec![block(20.0, 10.0)])
.min_width(50.0)
.max_height(5.0);
assert_eq!(measure(&el, big, None), Size::new(50.0, 5.0));
let el = block(300.0, 300.0).max_width(120.0).min_height(400.0);
assert_eq!(measure(&el, big, None), Size::new(120.0, 400.0));
}
#[test]
fn max_width_constrains_wrapped_text() {
let Some(font) = Font::system_default() else {
return;
};
let text = "several words that need to wrap inside a rather narrow maximum width";
let el = Element::text(text, 14.0, Color::BLACK)
.wrap()
.max_width(120.0);
let lines = font.wrap(text, 14.0, 120.0).len();
assert!(lines > 1);
let s = measure(&el, Size::new(4000.0, 4000.0), Some(&font));
assert!(s.width <= 120.0);
assert_eq!(s.height, font.line_height(14.0) * lines as f64);
}
#[test]
fn layout_grow_stops_at_max_width() {
let row = Element::stack(
Axis::Horizontal,
vec![
Element::boxed(BoxStyle::default())
.grow(1.0)
.max_width(50.0),
Element::boxed(BoxStyle::default()).grow(1.0),
],
)
.gap(10.0);
let tree = layout(&row, Rect::from_xywh(0.0, 0.0, 210.0, 20.0), None);
assert_eq!(tree.children[0].bounds.width(), 50.0);
assert_eq!(
tree.children[1].bounds,
Rect::from_xywh(60.0, 0.0, 150.0, 0.0)
);
}
#[test]
fn layout_shrink_stops_at_min_width() {
let row = Element::stack(
Axis::Horizontal,
vec![
block(150.0, 10.0).shrink(1.0).min_width(120.0),
block(150.0, 10.0).shrink(1.0),
],
);
let tree = layout(&row, Rect::from_xywh(0.0, 0.0, 200.0, 10.0), None);
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(0.0, 0.0, 120.0, 10.0),
Rect::from_xywh(120.0, 0.0, 80.0, 10.0)
]
);
}
#[test]
fn stretch_honors_cross_max_and_min() {
let col = Element::stack(
Axis::Vertical,
vec![
Element::boxed(BoxStyle::default())
.height(10.0)
.max_width(80.0),
Element::boxed(BoxStyle::default()).height(10.0),
],
)
.align(Align::Start, Align::Stretch);
let tree = layout(&col, Rect::from_xywh(0.0, 0.0, 200.0, 100.0), None);
assert_eq!(tree.children[0].bounds.width(), 80.0);
assert_eq!(tree.children[1].bounds.width(), 200.0);
let col = Element::stack(Axis::Vertical, vec![block(10.0, 10.0).min_width(300.0)])
.align(Align::Start, Align::Center);
let tree = layout(&col, Rect::from_xywh(0.0, 0.0, 200.0, 100.0), None);
assert_eq!(
tree.children[0].bounds,
Rect::from_xywh(-50.0, 0.0, 300.0, 10.0)
);
}
#[test]
fn wrapping_row_measures_lines() {
let row = Element::stack(
Axis::Horizontal,
(0..5).map(|_| block(40.0, 20.0)).collect(),
)
.gap(10.0)
.wrap();
assert_eq!(
measure(&row, Size::new(150.0, 1000.0), None),
Size::new(140.0, 50.0)
);
assert_eq!(
measure(&row, Size::new(f64::INFINITY, 1000.0), None),
Size::new(240.0, 20.0)
);
let padded = row.padding(crate::geometry::Insets::uniform(5.0));
assert_eq!(
measure(&padded, Size::new(160.0, 1000.0), None),
Size::new(150.0, 60.0)
);
}
#[test]
fn wrapping_row_lays_out_lines_with_per_line_alignment() {
let row = Element::stack(
Axis::Horizontal,
vec![block(40.0, 20.0), block(40.0, 30.0), block(40.0, 10.0)],
)
.gap(10.0)
.wrap()
.align(Align::Center, Align::End);
let tree = layout(&row, Rect::from_xywh(0.0, 0.0, 100.0, 200.0), None);
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(5.0, 10.0, 40.0, 20.0),
Rect::from_xywh(55.0, 0.0, 40.0, 30.0),
Rect::from_xywh(30.0, 40.0, 40.0, 10.0),
]
);
}
#[test]
fn wrapping_row_grows_and_stretches_within_each_line() {
let row = Element::stack(
Axis::Horizontal,
vec![
block(40.0, 20.0),
block(40.0, 10.0).grow(1.0),
block(40.0, 10.0).grow(1.0),
],
)
.gap(10.0)
.wrap()
.align(Align::Start, Align::Stretch);
let tree = layout(&row, Rect::from_xywh(0.0, 0.0, 100.0, 200.0), None);
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(0.0, 0.0, 40.0, 20.0),
Rect::from_xywh(50.0, 0.0, 50.0, 20.0),
Rect::from_xywh(0.0, 30.0, 100.0, 10.0),
]
);
}
#[test]
fn grid_places_cells_in_fixed_auto_fr_columns() {
let g = Element::grid(
vec![Track::px(50.0), Track::Auto, Track::fr(1.0)],
vec![
cell(10.0, 10.0),
cell(30.0, 20.0),
cell(5.0, 5.0),
cell(10.0, 40.0),
],
)
.gap(10.0)
.padding(crate::geometry::Insets::uniform(5.0));
assert_eq!(
measure(&g, Size::new(300.0, 1000.0), None),
Size::new(300.0, 5.0 + 20.0 + 10.0 + 40.0 + 5.0)
);
let tree = layout(&g, Rect::from_xywh(0.0, 0.0, 300.0, 80.0), None);
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(5.0, 5.0, 50.0, 20.0),
Rect::from_xywh(65.0, 5.0, 30.0, 20.0),
Rect::from_xywh(105.0, 5.0, 190.0, 20.0),
Rect::from_xywh(5.0, 35.0, 50.0, 40.0),
]
);
}
#[test]
fn grid_aligns_cells_and_honors_bounds() {
let g = Element::grid(
vec![Track::px(100.0), Track::px(100.0)],
vec![block(20.0, 10.0), block(20.0, 30.0).max_width(60.0)],
)
.align(Align::Center, Align::End);
let tree = layout(&g, Rect::from_xywh(0.0, 0.0, 200.0, 30.0), None);
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(40.0, 20.0, 20.0, 10.0),
Rect::from_xywh(140.0, 0.0, 20.0, 30.0),
]
);
let g = Element::grid(
vec![Track::px(100.0)],
vec![Element::boxed(BoxStyle::default()).max_width(60.0)],
)
.rows(vec![Track::px(30.0)]);
let tree = layout(&g, Rect::from_xywh(0.0, 0.0, 100.0, 30.0), None);
assert_eq!(
tree.children[0].bounds,
Rect::from_xywh(0.0, 0.0, 60.0, 30.0)
);
}
#[test]
fn grid_fr_rows_size_to_content_then_share_the_laid_out_height() {
let cells = || vec![cell(10.0, 10.0), cell(10.0, 30.0)];
let g = Element::grid(vec![Track::Auto], cells())
.rows(vec![Track::fr(1.0), Track::fr(1.0)])
.row_gap(10.0);
assert_eq!(
measure(&g, Size::new(500.0, 500.0), None),
Size::new(10.0, 70.0)
);
let tree = layout(&g, Rect::from_xywh(0.0, 0.0, 10.0, 110.0), None);
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(0.0, 0.0, 10.0, 50.0),
Rect::from_xywh(0.0, 60.0, 10.0, 50.0),
]
);
}
#[test]
fn grid_spans_cells() {
let g = Element::grid(
vec![Track::px(40.0), Track::px(60.0)],
vec![
cell(10.0, 10.0).span(2, 1),
cell(10.0, 10.0).span(1, 2),
cell(10.0, 15.0),
cell(10.0, 15.0),
],
)
.gap(5.0);
let tree = layout(&g, Rect::from_xywh(0.0, 0.0, 105.0, 100.0), None);
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(0.0, 0.0, 105.0, 10.0),
Rect::from_xywh(0.0, 15.0, 40.0, 35.0),
Rect::from_xywh(45.0, 15.0, 60.0, 15.0),
Rect::from_xywh(45.0, 35.0, 60.0, 15.0),
]
);
}
#[test]
fn grid_wrapped_text_measures_at_its_column_width() {
let Some(font) = Font::system_default() else {
return;
};
let text = "a paragraph long enough to wrap inside one of two equal grid columns";
let g = Element::grid(
vec![Track::fr(1.0), Track::fr(1.0)],
vec![
Element::text(text, 14.0, Color::BLACK).wrap(),
block(10.0, 10.0),
],
);
let lines = font.wrap(text, 14.0, 150.0).len();
assert!(lines > 1);
let h = measure(&g, Size::new(300.0, 1000.0), Some(&font)).height;
assert_eq!(h, font.line_height(14.0) * lines as f64);
}
#[test]
fn grid_and_wrap_trees_route_input_focus_and_accessibility() {
use crate::core::a11y::accessibility_tree;
use crate::core::runtime::{ActionId, FocusId, collect_focusables, focus_at, hit_test};
let mut tappable = cell(40.0, 20.0);
tappable.action = Some(ActionId(7));
let mut field = Element::text("name", 14.0, Color::BLACK);
field.focus = Some(FocusId(1));
let mut field2 = block(30.0, 20.0);
field2.focus = Some(FocusId(2));
let wrapped = Element::stack(Axis::Horizontal, vec![block(80.0, 20.0), field2])
.wrap()
.gap(4.0);
let g = Element::grid(
vec![Track::px(100.0), Track::px(100.0)],
vec![tappable, field, wrapped],
);
let tree = layout(&g, Rect::from_xywh(0.0, 0.0, 200.0, 100.0), None);
assert_eq!(hit_test(&tree, Point::new(90.0, 10.0)), Some(ActionId(7)));
assert_eq!(hit_test(&tree, Point::new(150.0, 10.0)), None);
assert_eq!(focus_at(&tree, Point::new(10.0, 50.0)), Some(FocusId(2)));
let mut order = Vec::new();
collect_focusables(&tree, &mut order);
assert_eq!(order, vec![FocusId(1), FocusId(2)]);
let a11y = accessibility_tree(&tree, None);
assert!(a11y.count() >= 3);
}
#[test]
fn image_measures_to_natural_size_and_keeps_aspect() {
use crate::geometry::{Insets, PhysicalSize};
use crate::render::{DrawCmd, ImageFit, Pixmap};
use std::sync::Arc;
let pm = Arc::new(Pixmap::new(PhysicalSize::new(40, 20)));
let avail = Size::new(1000.0, 1000.0);
assert_eq!(
measure(&Element::image(&pm), avail, None),
Size::new(40.0, 20.0)
);
assert_eq!(
measure(&Element::image(&pm).width(80.0), avail, None),
Size::new(80.0, 40.0)
);
assert_eq!(
measure(&Element::image(&pm).height(10.0), avail, None),
Size::new(20.0, 10.0)
);
assert_eq!(
measure(&Element::image(&pm).width(30.0).height(30.0), avail, None),
Size::new(30.0, 30.0)
);
let padded = Element::image(&pm).padding(Insets::uniform(5.0));
assert_eq!(measure(&padded, avail, None), Size::new(50.0, 30.0));
let el = Element::image(&pm)
.padding(Insets::uniform(5.0))
.fit(ImageFit::Contain);
let tree = layout(&el, Rect::from_xywh(0.0, 0.0, 50.0, 50.0), None);
let mut scene = Scene::new(Size::new(50.0, 50.0));
paint(&tree, &mut scene, None);
let rects: Vec<Rect> = scene
.commands()
.iter()
.filter_map(|c| match c {
DrawCmd::Image { rect, .. } => Some(*rect),
_ => None,
})
.collect();
assert_eq!(rects, vec![Rect::from_xywh(5.0, 15.0, 40.0, 20.0)]);
}
fn rtl(el: &Element, bounds: Rect) -> LayoutNode {
layout_with_direction(el, bounds, None, LayoutDirection::Rtl)
}
#[test]
fn rtl_row_runs_right_to_left_from_the_start_padding() {
use crate::geometry::Insets;
let row = Element::stack(Axis::Horizontal, vec![block(40.0, 10.0), block(60.0, 10.0)])
.gap(10.0)
.padding(Insets::new(0.0, 15.0, 0.0, 5.0));
let ltr = layout(&row, Rect::from_xywh(0.0, 0.0, 200.0, 10.0), None);
assert_eq!(
rects(<r),
vec![
Rect::from_xywh(5.0, 0.0, 40.0, 10.0),
Rect::from_xywh(55.0, 0.0, 60.0, 10.0)
]
);
assert!(!ltr.rtl && ltr.children.iter().all(|c| !c.rtl));
let tree = rtl(&row, Rect::from_xywh(100.0, 0.0, 200.0, 10.0));
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(255.0, 0.0, 40.0, 10.0),
Rect::from_xywh(185.0, 0.0, 60.0, 10.0)
]
);
assert!(tree.rtl && tree.children.iter().all(|c| c.rtl));
}
#[test]
fn rtl_flips_start_and_end_alignment() {
let row = Element::stack(Axis::Horizontal, vec![block(40.0, 10.0), block(20.0, 10.0)])
.align(Align::End, Align::Start);
let tree = rtl(&row, Rect::from_xywh(0.0, 0.0, 200.0, 10.0));
assert_eq!(rects(&tree)[1], Rect::from_xywh(0.0, 0.0, 20.0, 10.0));
assert_eq!(rects(&tree)[0], Rect::from_xywh(20.0, 0.0, 40.0, 10.0));
let col = Element::stack(Axis::Vertical, vec![block(40.0, 10.0), block(20.0, 10.0)])
.align(Align::Start, Align::Start);
let tree = rtl(&col, Rect::from_xywh(0.0, 0.0, 100.0, 50.0));
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(60.0, 0.0, 40.0, 10.0),
Rect::from_xywh(80.0, 10.0, 20.0, 10.0)
]
);
let centered = col.clone().align(Align::Start, Align::Center);
assert_eq!(
rects(&rtl(¢ered, Rect::from_xywh(0.0, 0.0, 100.0, 50.0)))[0],
Rect::from_xywh(30.0, 0.0, 40.0, 10.0)
);
}
#[test]
fn rtl_grow_and_wrap_mirror_per_line() {
let row = Element::stack(
Axis::Horizontal,
vec![
block(40.0, 10.0),
Element::boxed(BoxStyle::default()).grow(1.0),
],
);
let tree = rtl(&row, Rect::from_xywh(0.0, 0.0, 200.0, 10.0));
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(160.0, 0.0, 40.0, 10.0),
Rect::from_xywh(0.0, 0.0, 160.0, 0.0)
]
);
let wrapped = Element::stack(
Axis::Horizontal,
vec![block(40.0, 10.0), block(40.0, 10.0), block(40.0, 10.0)],
)
.wrap()
.gap(10.0);
let tree = rtl(&wrapped, Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
assert_eq!(
rects(&tree),
vec![
Rect::from_xywh(60.0, 0.0, 40.0, 10.0),
Rect::from_xywh(10.0, 0.0, 40.0, 10.0),
Rect::from_xywh(60.0, 20.0, 40.0, 10.0)
]
);
}
#[test]
fn rtl_grid_counts_columns_from_the_right() {
let g = Element::grid(
vec![Track::px(50.0), Track::fr(1.0), Track::px(30.0)],
vec![
cell(0.0, 10.0),
cell(0.0, 10.0),
cell(0.0, 10.0),
cell(0.0, 10.0).span(2, 1),
],
)
.gap(10.0);
let tree = rtl(&g, Rect::from_xywh(0.0, 0.0, 200.0, 100.0));
let r = rects(&tree);
assert_eq!(r[0], Rect::from_xywh(150.0, 0.0, 50.0, 10.0));
assert_eq!(r[1], Rect::from_xywh(40.0, 0.0, 100.0, 10.0));
assert_eq!(r[2], Rect::from_xywh(0.0, 0.0, 30.0, 10.0));
assert_eq!(r[3], Rect::from_xywh(40.0, 20.0, 160.0, 10.0));
}
#[test]
fn layout_direction_is_inherited_and_overridable() {
let inner = Element::stack(Axis::Horizontal, vec![block(10.0, 10.0), block(20.0, 10.0)])
.layout_direction(LayoutDirection::Ltr);
let outer = Element::stack(Axis::Horizontal, vec![block(50.0, 10.0), inner]);
let tree = rtl(&outer, Rect::from_xywh(0.0, 0.0, 200.0, 10.0));
let inner = &tree.children[1];
assert_eq!(inner.bounds, Rect::from_xywh(120.0, 0.0, 30.0, 10.0));
assert!(!inner.rtl);
assert_eq!(
rects(inner),
vec![
Rect::from_xywh(120.0, 0.0, 10.0, 10.0),
Rect::from_xywh(130.0, 0.0, 20.0, 10.0)
]
);
let sub = Element::stack(Axis::Horizontal, vec![block(10.0, 10.0)])
.width(100.0)
.layout_direction(LayoutDirection::Rtl);
let tree = layout(
&Element::stack(Axis::Horizontal, vec![sub]),
Rect::from_xywh(0.0, 0.0, 300.0, 10.0),
None,
);
assert_eq!(
tree.children[0].children[0].bounds,
Rect::from_xywh(90.0, 0.0, 10.0, 10.0)
);
}
#[test]
fn ltr_layout_is_unchanged_by_the_direction_plumbing() {
use crate::geometry::Insets;
let el = Element::grid(
vec![Track::Auto, Track::fr(1.0)],
vec![
block(30.0, 10.0),
Element::stack(
Axis::Horizontal,
vec![
block(10.0, 10.0),
Element::boxed(BoxStyle::default()).grow(1.0),
],
)
.padding(Insets::new(1.0, 2.0, 3.0, 4.0)),
],
)
.padding(Insets::new(0.0, 0.0, 0.0, 7.0));
let b = Rect::from_xywh(3.0, 4.0, 150.0, 60.0);
fn all(node: &LayoutNode, out: &mut Vec<(Rect, bool)>) {
out.push((node.bounds, node.rtl));
for c in &node.children {
all(c, out);
}
}
let (mut a, mut d) = (Vec::new(), Vec::new());
all(&layout(&el, b, None), &mut a);
all(
&layout_with_direction(&el, b, None, LayoutDirection::Ltr),
&mut d,
);
assert_eq!(a, d);
assert!(a.iter().all(|(_, rtl)| !rtl));
assert_eq!(a[1].0.min_x(), 10.0);
}
#[test]
fn rtl_images_swap_their_side_padding() {
use crate::geometry::{Insets, PhysicalSize};
use crate::render::Pixmap;
let pm = std::sync::Arc::new(Pixmap::new(PhysicalSize::new(40, 20)));
let el = Element::image(&pm).padding(Insets::new(0.0, 0.0, 0.0, 10.0));
let tree = rtl(&el, Rect::from_xywh(0.0, 0.0, 50.0, 20.0));
match &tree.content {
NodeContent::Image { padding, .. } => {
assert_eq!((padding.left, padding.right), (0.0, 10.0));
}
other => panic!("{other:?}"),
}
}
#[test]
fn rtl_auto_text_lines_up_on_the_right() {
let Some(font) = crate::render::Font::system_default() else {
return;
};
let el = Element::text("abc", 16.0, Color::BLACK).width(200.0);
let b = Rect::from_xywh(0.0, 0.0, 200.0, 20.0);
let w = font.measure("abc", 16.0).width;
let caret_x = |tree: &LayoutNode, i: usize| {
text_layout(
&font,
"abc",
16.0,
TextDirection::Auto,
false,
200.0,
tree.rtl,
)
.caret_position(i)
.x
};
let ltr = layout(&el, b, Some(&font));
assert!(caret_x(<r, 0).abs() < 0.01);
let mirrored = layout_with_direction(&el, b, Some(&font), LayoutDirection::Rtl);
assert!(mirrored.rtl);
assert!((caret_x(&mirrored, 0) - (200.0 - w)).abs() < 0.01);
assert!((caret_x(&mirrored, 3) - 200.0).abs() < 0.01);
let hit = caret_index_at(&mirrored, Point::new(199.0, 5.0), Some(&font));
assert_eq!(hit, Some(3));
let forced = el.clone().direction(TextDirection::Ltr);
let forced = layout_with_direction(&forced, b, Some(&font), LayoutDirection::Rtl);
assert_eq!(
caret_index_at(&forced, Point::new(1.0, 5.0), Some(&font)),
Some(0)
);
}
}