Text, many performance improvements
This commit is contained in:
462
lib/ui/src/layout.rs
Normal file
462
lib/ui/src/layout.rs
Normal file
@@ -0,0 +1,462 @@
|
||||
use crate::scene::{Rect, SceneSnapshot, UiSize};
|
||||
use crate::text::TextSystem;
|
||||
use crate::tree::{Edges, Element, FlexDirection};
|
||||
|
||||
pub fn layout_scene(version: u64, logical_size: UiSize, root: &Element) -> SceneSnapshot {
|
||||
let mut text_system = TextSystem::new();
|
||||
layout_scene_with_text_system(version, logical_size, root, &mut text_system)
|
||||
}
|
||||
|
||||
pub fn layout_scene_with_text_system(
|
||||
version: u64,
|
||||
logical_size: UiSize,
|
||||
root: &Element,
|
||||
text_system: &mut TextSystem,
|
||||
) -> SceneSnapshot {
|
||||
let mut scene = SceneSnapshot::new(version, logical_size);
|
||||
layout_element(
|
||||
root,
|
||||
Rect::new(
|
||||
0.0,
|
||||
0.0,
|
||||
logical_size.width.max(0.0),
|
||||
logical_size.height.max(0.0),
|
||||
),
|
||||
&mut scene,
|
||||
text_system,
|
||||
);
|
||||
scene
|
||||
}
|
||||
|
||||
fn layout_element(
|
||||
element: &Element,
|
||||
rect: Rect,
|
||||
scene: &mut SceneSnapshot,
|
||||
text_system: &mut TextSystem,
|
||||
) {
|
||||
if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(color) = element.style.background {
|
||||
scene.push_quad(rect, color);
|
||||
}
|
||||
|
||||
if let Some(text) = element.text_node() {
|
||||
let content = inset_rect(rect, element.style.padding);
|
||||
if content.size.width > 0.0 && content.size.height > 0.0 {
|
||||
scene.push_text(text_system.prepare(
|
||||
text.text.clone(),
|
||||
content.origin,
|
||||
text.style.with_bounds(content.size),
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if element.children.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = inset_rect(rect, element.style.padding);
|
||||
if content.size.width <= 0.0 || content.size.height <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let gap_count = element.children.len().saturating_sub(1) as f32;
|
||||
let total_gap = element.style.gap * gap_count;
|
||||
let available_main = main_axis_size(content.size, element.style.direction).max(0.0) - total_gap;
|
||||
let available_main = available_main.max(0.0);
|
||||
let available_cross = cross_axis_size(content.size, element.style.direction).max(0.0);
|
||||
|
||||
let mut measured_children = Vec::with_capacity(element.children.len());
|
||||
let mut fixed_total = 0.0;
|
||||
let mut flex_total = 0.0;
|
||||
for child in &element.children {
|
||||
let cross = child_cross_size(child, element.style.direction)
|
||||
.unwrap_or(available_cross)
|
||||
.clamp(0.0, available_cross);
|
||||
let explicit_main =
|
||||
child_main_size(child, element.style.direction).map(|main| main.max(0.0));
|
||||
let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0;
|
||||
let measured_main = explicit_main.unwrap_or_else(|| {
|
||||
if is_flex {
|
||||
0.0
|
||||
} else {
|
||||
intrinsic_main_size(
|
||||
child,
|
||||
element.style.direction,
|
||||
cross,
|
||||
available_main,
|
||||
text_system,
|
||||
)
|
||||
}
|
||||
});
|
||||
if is_flex {
|
||||
flex_total += child_flex_weight(child);
|
||||
} else {
|
||||
fixed_total += measured_main;
|
||||
}
|
||||
measured_children.push(MeasuredChild {
|
||||
cross,
|
||||
main: measured_main,
|
||||
is_flex,
|
||||
});
|
||||
}
|
||||
|
||||
let remaining_main = (available_main - fixed_total).max(0.0);
|
||||
let mut cursor = main_axis_origin(content, element.style.direction);
|
||||
|
||||
for (child, measured) in element.children.iter().zip(measured_children) {
|
||||
let child_main = if measured.is_flex {
|
||||
if flex_total <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
remaining_main * (child_flex_weight(child) / flex_total)
|
||||
}
|
||||
} else {
|
||||
measured.main
|
||||
};
|
||||
let child_rect = child_rect(
|
||||
content,
|
||||
element.style.direction,
|
||||
cursor,
|
||||
child_main.max(0.0),
|
||||
measured.cross,
|
||||
);
|
||||
layout_element(child, child_rect, scene, text_system);
|
||||
cursor += child_main.max(0.0) + element.style.gap;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct MeasuredChild {
|
||||
cross: f32,
|
||||
main: f32,
|
||||
is_flex: bool,
|
||||
}
|
||||
|
||||
fn intrinsic_main_size(
|
||||
child: &Element,
|
||||
direction: FlexDirection,
|
||||
cross_size: f32,
|
||||
available_main: f32,
|
||||
text_system: &mut TextSystem,
|
||||
) -> f32 {
|
||||
if let Some(text) = child.text_node() {
|
||||
let constraints = match direction {
|
||||
FlexDirection::Row => (Some(available_main.max(0.0)), Some(cross_size.max(0.0))),
|
||||
FlexDirection::Column => (Some(cross_size.max(0.0)), None),
|
||||
};
|
||||
let content = text_system.measure(&text.text, text.style, constraints.0, constraints.1);
|
||||
let padding = main_axis_padding(child.style.padding, direction);
|
||||
return main_axis_size(content, direction) + padding;
|
||||
}
|
||||
|
||||
let available_size = match direction {
|
||||
FlexDirection::Row => UiSize::new(available_main.max(0.0), cross_size.max(0.0)),
|
||||
FlexDirection::Column => UiSize::new(cross_size.max(0.0), available_main.max(0.0)),
|
||||
};
|
||||
main_axis_size(
|
||||
intrinsic_size(child, available_size, text_system),
|
||||
direction,
|
||||
)
|
||||
}
|
||||
|
||||
fn intrinsic_size(
|
||||
element: &Element,
|
||||
available_size: UiSize,
|
||||
text_system: &mut TextSystem,
|
||||
) -> UiSize {
|
||||
if let Some(text) = element.text_node() {
|
||||
let measured = text_system.measure(
|
||||
&text.text,
|
||||
text.style,
|
||||
Some(available_size.width.max(0.0)),
|
||||
Some(available_size.height.max(0.0)),
|
||||
);
|
||||
return UiSize::new(
|
||||
element.style.width.unwrap_or(
|
||||
measured.width + element.style.padding.left + element.style.padding.right,
|
||||
),
|
||||
element.style.height.unwrap_or(
|
||||
measured.height + element.style.padding.top + element.style.padding.bottom,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let explicit_width = element.style.width;
|
||||
let explicit_height = element.style.height;
|
||||
let content_width = explicit_width.unwrap_or(available_size.width).max(0.0)
|
||||
- element.style.padding.left
|
||||
- element.style.padding.right;
|
||||
let content_height = explicit_height.unwrap_or(available_size.height).max(0.0)
|
||||
- element.style.padding.top
|
||||
- element.style.padding.bottom;
|
||||
let content_size = UiSize::new(content_width.max(0.0), content_height.max(0.0));
|
||||
|
||||
if element.children.is_empty() {
|
||||
return UiSize::new(
|
||||
explicit_width.unwrap_or(element.style.padding.left + element.style.padding.right),
|
||||
explicit_height.unwrap_or(element.style.padding.top + element.style.padding.bottom),
|
||||
);
|
||||
}
|
||||
|
||||
let gap_total = element.style.gap * element.children.len().saturating_sub(1) as f32;
|
||||
let (intrinsic_content_width, intrinsic_content_height) = match element.style.direction {
|
||||
FlexDirection::Column => {
|
||||
let mut width: f32 = 0.0;
|
||||
let mut height = gap_total;
|
||||
for child in &element.children {
|
||||
if child.style.flex_grow > 0.0 && child.style.height.is_none() {
|
||||
continue;
|
||||
}
|
||||
let child_size = intrinsic_size(
|
||||
child,
|
||||
UiSize::new(
|
||||
child.style.width.unwrap_or(content_size.width),
|
||||
child.style.height.unwrap_or(content_size.height),
|
||||
),
|
||||
text_system,
|
||||
);
|
||||
width = width.max(child.style.width.unwrap_or(child_size.width));
|
||||
height += child.style.height.unwrap_or(child_size.height);
|
||||
}
|
||||
(width, height)
|
||||
}
|
||||
FlexDirection::Row => {
|
||||
let mut width = gap_total;
|
||||
let mut height: f32 = 0.0;
|
||||
for child in &element.children {
|
||||
if child.style.flex_grow > 0.0 && child.style.width.is_none() {
|
||||
continue;
|
||||
}
|
||||
let child_size = intrinsic_size(
|
||||
child,
|
||||
UiSize::new(
|
||||
child.style.width.unwrap_or(content_size.width),
|
||||
child.style.height.unwrap_or(content_size.height),
|
||||
),
|
||||
text_system,
|
||||
);
|
||||
width += child.style.width.unwrap_or(child_size.width);
|
||||
height = height.max(child.style.height.unwrap_or(child_size.height));
|
||||
}
|
||||
(width, height)
|
||||
}
|
||||
};
|
||||
|
||||
UiSize::new(
|
||||
explicit_width.unwrap_or(
|
||||
intrinsic_content_width + element.style.padding.left + element.style.padding.right,
|
||||
),
|
||||
explicit_height.unwrap_or(
|
||||
intrinsic_content_height + element.style.padding.top + element.style.padding.bottom,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn inset_rect(rect: Rect, edges: Edges) -> Rect {
|
||||
let width = (rect.size.width - edges.left - edges.right).max(0.0);
|
||||
let height = (rect.size.height - edges.top - edges.bottom).max(0.0);
|
||||
Rect::new(
|
||||
rect.origin.x + edges.left,
|
||||
rect.origin.y + edges.top,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
}
|
||||
|
||||
fn child_main_size(child: &Element, direction: FlexDirection) -> Option<f32> {
|
||||
match direction {
|
||||
FlexDirection::Row => child.style.width,
|
||||
FlexDirection::Column => child.style.height,
|
||||
}
|
||||
}
|
||||
|
||||
fn child_cross_size(child: &Element, direction: FlexDirection) -> Option<f32> {
|
||||
match direction {
|
||||
FlexDirection::Row => child.style.height,
|
||||
FlexDirection::Column => child.style.width,
|
||||
}
|
||||
}
|
||||
|
||||
fn child_flex_weight(child: &Element) -> f32 {
|
||||
if child.style.flex_grow > 0.0 {
|
||||
child.style.flex_grow
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
fn main_axis_padding(edges: Edges, direction: FlexDirection) -> f32 {
|
||||
match direction {
|
||||
FlexDirection::Row => edges.left + edges.right,
|
||||
FlexDirection::Column => edges.top + edges.bottom,
|
||||
}
|
||||
}
|
||||
|
||||
fn main_axis_size(size: UiSize, direction: FlexDirection) -> f32 {
|
||||
match direction {
|
||||
FlexDirection::Row => size.width,
|
||||
FlexDirection::Column => size.height,
|
||||
}
|
||||
}
|
||||
|
||||
fn cross_axis_size(size: UiSize, direction: FlexDirection) -> f32 {
|
||||
match direction {
|
||||
FlexDirection::Row => size.height,
|
||||
FlexDirection::Column => size.width,
|
||||
}
|
||||
}
|
||||
|
||||
fn main_axis_origin(rect: Rect, direction: FlexDirection) -> f32 {
|
||||
match direction {
|
||||
FlexDirection::Row => rect.origin.x,
|
||||
FlexDirection::Column => rect.origin.y,
|
||||
}
|
||||
}
|
||||
|
||||
fn child_rect(
|
||||
content: Rect,
|
||||
direction: FlexDirection,
|
||||
main_origin: f32,
|
||||
main_size: f32,
|
||||
cross_size: f32,
|
||||
) -> Rect {
|
||||
match direction {
|
||||
FlexDirection::Row => Rect::new(main_origin, content.origin.y, main_size, cross_size),
|
||||
FlexDirection::Column => Rect::new(content.origin.x, main_origin, cross_size, main_size),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::layout_scene;
|
||||
use crate::scene::{Color, DisplayItem, Quad, Rect, UiSize};
|
||||
use crate::text::{TextStyle, TextWrap};
|
||||
use crate::tree::{Edges, Element};
|
||||
|
||||
#[test]
|
||||
fn row_layout_apportions_fixed_and_flex_children() {
|
||||
let root = Element::row()
|
||||
.padding(Edges::all(10.0))
|
||||
.gap(10.0)
|
||||
.children([
|
||||
Element::new()
|
||||
.width(50.0)
|
||||
.background(Color::rgb(0xAA, 0x11, 0x11)),
|
||||
Element::new()
|
||||
.flex(1.0)
|
||||
.background(Color::rgb(0x11, 0xAA, 0x11)),
|
||||
Element::new()
|
||||
.flex(2.0)
|
||||
.background(Color::rgb(0x11, 0x11, 0xAA)),
|
||||
]);
|
||||
|
||||
let scene = layout_scene(1, UiSize::new(300.0, 100.0), &root);
|
||||
let quads: Vec<Quad> = scene
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
DisplayItem::Quad(quad) => Some(*quad),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
quads,
|
||||
vec![
|
||||
Quad::new(
|
||||
Rect::new(10.0, 10.0, 50.0, 80.0),
|
||||
Color::rgb(0xAA, 0x11, 0x11)
|
||||
),
|
||||
Quad::new(
|
||||
Rect::new(70.0, 10.0, 70.0, 80.0),
|
||||
Color::rgb(0x11, 0xAA, 0x11)
|
||||
),
|
||||
Quad::new(
|
||||
Rect::new(150.0, 10.0, 140.0, 80.0),
|
||||
Color::rgb(0x11, 0x11, 0xAA)
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_layout_reflows_text_and_moves_following_children() {
|
||||
let root = Element::column()
|
||||
.padding(Edges::all(10.0))
|
||||
.gap(10.0)
|
||||
.children([
|
||||
Element::text(
|
||||
"RUIN text nodes should reflow inside narrow layout columns instead of acting like overlays.",
|
||||
TextStyle::new(16.0, Color::rgb(0xFF, 0xFF, 0xFF))
|
||||
.with_line_height(20.0)
|
||||
.with_wrap(TextWrap::Word),
|
||||
)
|
||||
.padding(Edges::all(8.0))
|
||||
.background(Color::rgb(0x22, 0x2C, 0x46)),
|
||||
Element::new()
|
||||
.height(40.0)
|
||||
.background(Color::rgb(0x44, 0x55, 0x66)),
|
||||
]);
|
||||
|
||||
let scene = layout_scene(1, UiSize::new(160.0, 220.0), &root);
|
||||
let text = scene
|
||||
.items
|
||||
.iter()
|
||||
.find_map(|item| match item {
|
||||
DisplayItem::Text(text) => Some(text),
|
||||
_ => None,
|
||||
})
|
||||
.expect("layout should emit a text display item");
|
||||
let quads: Vec<Quad> = scene
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
DisplayItem::Quad(quad) => Some(*quad),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let text_bounds = text.bounds.expect("text layout should provide bounds");
|
||||
assert_eq!(text.origin.x, 18.0);
|
||||
assert_eq!(text.origin.y, 18.0);
|
||||
assert_eq!(text_bounds.width, 124.0);
|
||||
assert!(text_bounds.height > 20.0);
|
||||
assert_eq!(quads.len(), 2);
|
||||
assert!(quads[1].rect.origin.y > text.origin.y + text_bounds.height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_container_with_text_children_gets_intrinsic_height() {
|
||||
let root = Element::column().child(
|
||||
Element::column()
|
||||
.padding(Edges::all(12.0))
|
||||
.background(Color::rgb(0x22, 0x33, 0x44))
|
||||
.child(Element::paragraph(
|
||||
"Paragraph containers should not collapse to zero height anymore.",
|
||||
TextStyle::new(18.0, Color::rgb(0xFF, 0xFF, 0xFF)).with_line_height(24.0),
|
||||
)),
|
||||
);
|
||||
|
||||
let scene = layout_scene(1, UiSize::new(420.0, 240.0), &root);
|
||||
let quads: Vec<Quad> = scene
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
DisplayItem::Quad(quad) => Some(*quad),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(quads.iter().any(|quad| quad.rect.size.height > 24.0));
|
||||
assert!(
|
||||
scene
|
||||
.items
|
||||
.iter()
|
||||
.any(|item| matches!(item, DisplayItem::Text(_)))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user