Text, many performance improvements

This commit is contained in:
2026-03-20 19:23:55 -04:00
parent 39ede248cf
commit 00fe1daa0c
18 changed files with 3432 additions and 26 deletions

462
lib/ui/src/layout.rs Normal file
View 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(_)))
);
}
}

View File

@@ -10,17 +10,23 @@ pub(crate) mod trace_targets {
pub const SCENE: &str = "ruin_ui::scene";
}
mod layout;
mod platform;
mod runtime;
mod scene;
mod text;
mod tree;
mod window;
pub use layout::{layout_scene, layout_scene_with_text_system};
pub use platform::{PlatformClosed, PlatformEvent, PlatformProxy, PlatformRuntime, start_headless};
pub use runtime::{EventStreamClosed, UiRuntime, WindowController};
pub use scene::{
Color, DisplayItem, GlyphInstance, Point, PreparedText, Quad, Rect, SceneSnapshot, Translation,
UiSize,
};
pub use text::{TextAlign, TextStyle, TextSystem, TextWrap};
pub use tree::{Edges, Element, FlexDirection, Style};
pub use window::{
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
};

View File

@@ -1,5 +1,6 @@
//! Renderer-oriented scene snapshot types.
use cosmic_text::CacheKey;
use tracing::debug;
use crate::trace_targets;
@@ -92,12 +93,16 @@ pub struct GlyphInstance {
pub glyph: String,
pub position: Point,
pub advance: f32,
pub cache_key: Option<CacheKey>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PreparedText {
pub text: String,
pub origin: Point,
pub bounds: Option<UiSize>,
pub font_size: f32,
pub line_height: f32,
pub color: Color,
pub glyphs: Vec<GlyphInstance>,
}
@@ -118,13 +123,17 @@ impl PreparedText {
glyph: ch.to_string(),
position: Point::new(x, origin.y),
advance,
cache_key: None,
});
x += advance;
}
Self {
text,
origin,
bounds: None,
font_size,
line_height: font_size,
color,
glyphs,
}

251
lib/ui/src/text.rs Normal file
View File

@@ -0,0 +1,251 @@
use cosmic_text::{Attrs, Buffer, FontSystem, Metrics, Shaping, Wrap};
use crate::{Color, GlyphInstance, Point, PreparedText, UiSize};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TextAlign {
Start,
Center,
End,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TextWrap {
None,
Word,
}
impl TextWrap {
const fn to_cosmic(self) -> Wrap {
match self {
Self::None => Wrap::None,
Self::Word => Wrap::WordOrGlyph,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextStyle {
pub font_size: f32,
pub line_height: f32,
pub color: Color,
pub bounds: Option<UiSize>,
pub wrap: TextWrap,
pub align: TextAlign,
pub max_lines: Option<usize>,
}
impl TextStyle {
pub const fn new(font_size: f32, color: Color) -> Self {
Self {
font_size,
line_height: font_size * 1.2,
color,
bounds: None,
wrap: TextWrap::None,
align: TextAlign::Start,
max_lines: None,
}
}
pub const fn with_line_height(mut self, line_height: f32) -> Self {
self.line_height = line_height;
self
}
pub const fn with_bounds(mut self, bounds: UiSize) -> Self {
self.bounds = Some(bounds);
self
}
pub const fn with_wrap(mut self, wrap: TextWrap) -> Self {
self.wrap = wrap;
self
}
pub const fn with_align(mut self, align: TextAlign) -> Self {
self.align = align;
self
}
pub const fn with_max_lines(mut self, max_lines: usize) -> Self {
self.max_lines = Some(max_lines);
self
}
}
pub struct TextSystem {
font_system: FontSystem,
}
#[derive(Clone, Debug, PartialEq)]
struct TextLayout {
glyphs: Vec<GlyphInstance>,
size: UiSize,
}
impl Default for TextSystem {
fn default() -> Self {
Self::new()
}
}
impl TextSystem {
pub fn new() -> Self {
Self {
font_system: FontSystem::new(),
}
}
pub fn prepare(
&mut self,
text: impl Into<String>,
origin: Point,
style: TextStyle,
) -> PreparedText {
let text = text.into();
let layout = self.layout(
&text,
style,
style.bounds.map(|bounds| bounds.width),
style.bounds.map(|bounds| bounds.height),
);
let glyphs = layout
.glyphs
.into_iter()
.map(|glyph| GlyphInstance {
glyph: glyph.glyph,
position: Point::new(origin.x + glyph.position.x, origin.y + glyph.position.y),
advance: glyph.advance,
cache_key: glyph.cache_key,
})
.collect();
PreparedText {
text,
origin,
bounds: style.bounds,
font_size: style.font_size,
line_height: style.line_height,
color: style.color,
glyphs,
}
}
pub fn measure(
&mut self,
text: &str,
style: TextStyle,
width: Option<f32>,
height: Option<f32>,
) -> UiSize {
self.layout(text, style, width, height).size
}
fn layout(
&mut self,
text: &str,
style: TextStyle,
width: Option<f32>,
height: Option<f32>,
) -> TextLayout {
let mut buffer = Buffer::new_empty(Metrics::new(style.font_size, style.line_height));
{
let mut borrowed = buffer.borrow_with(&mut self.font_system);
borrowed.set_wrap(style.wrap.to_cosmic());
borrowed.set_size(width, height);
borrowed.set_text(text, &Attrs::new(), Shaping::Advanced, None);
}
let mut measured_width: f32 = 0.0;
let mut measured_height: f32 = 0.0;
let mut glyphs = Vec::new();
for (line_index, run) in buffer.layout_runs().enumerate() {
if matches!(style.max_lines, Some(max_lines) if line_index >= max_lines) {
break;
}
measured_width = measured_width.max(run.line_w);
measured_height = measured_height.max(run.line_top + run.line_height);
let x_offset = aligned_line_offset(style.align, width, run.line_w);
glyphs.extend(run.glyphs.iter().map(move |glyph| {
let physical = glyph.physical((x_offset, run.line_y), 1.0);
GlyphInstance {
glyph: run.text[glyph.start..glyph.end].to_string(),
position: Point::new(physical.x as f32, physical.y as f32),
advance: glyph.w,
cache_key: Some(physical.cache_key),
}
}));
}
let measured_width = width.map_or(measured_width, |limit| measured_width.min(limit));
let measured_height = height.map_or(measured_height, |limit| measured_height.min(limit));
TextLayout {
glyphs,
size: UiSize::new(measured_width.max(0.0), measured_height.max(0.0)),
}
}
}
fn aligned_line_offset(align: TextAlign, width: Option<f32>, line_width: f32) -> f32 {
let Some(width) = width else {
return 0.0;
};
let remaining = (width - line_width).max(0.0);
match align {
TextAlign::Start => 0.0,
TextAlign::Center => remaining * 0.5,
TextAlign::End => remaining,
}
}
#[cfg(test)]
mod tests {
use super::{TextAlign, TextStyle, TextSystem, TextWrap};
use crate::{Color, Point, UiSize};
#[test]
fn max_lines_limits_measured_height() {
let mut text_system = TextSystem::new();
let style = TextStyle::new(16.0, Color::rgb(0xFF, 0xFF, 0xFF))
.with_line_height(20.0)
.with_wrap(TextWrap::Word);
let unclamped = text_system.measure(
"alpha beta gamma delta epsilon zeta eta theta iota kappa",
style,
Some(120.0),
None,
);
let clamped = text_system.measure(
"alpha beta gamma delta epsilon zeta eta theta iota kappa",
style.with_max_lines(2),
Some(120.0),
None,
);
assert!(unclamped.height > clamped.height);
assert!(clamped.height <= 40.0);
}
#[test]
fn centered_text_shifts_glyph_positions_within_bounds() {
let mut text_system = TextSystem::new();
let origin = Point::new(0.0, 0.0);
let start = text_system.prepare(
"title",
origin,
TextStyle::new(20.0, Color::rgb(0xFF, 0xFF, 0xFF))
.with_bounds(UiSize::new(200.0, 40.0)),
);
let centered = text_system.prepare(
"title",
origin,
TextStyle::new(20.0, Color::rgb(0xFF, 0xFF, 0xFF))
.with_bounds(UiSize::new(200.0, 40.0))
.with_align(TextAlign::Center),
);
assert!(!start.glyphs.is_empty());
assert!(!centered.glyphs.is_empty());
assert!(centered.glyphs[0].position.x > start.glyphs[0].position.x);
}
}

187
lib/ui/src/tree.rs Normal file
View File

@@ -0,0 +1,187 @@
use crate::scene::Color;
use crate::text::{TextStyle, TextWrap};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FlexDirection {
Row,
Column,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Edges {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
impl Edges {
pub const ZERO: Self = Self {
top: 0.0,
right: 0.0,
bottom: 0.0,
left: 0.0,
};
pub const fn all(value: f32) -> Self {
Self {
top: value,
right: value,
bottom: value,
left: value,
}
}
pub const fn symmetric(horizontal: f32, vertical: f32) -> Self {
Self {
top: vertical,
right: horizontal,
bottom: vertical,
left: horizontal,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Style {
pub direction: FlexDirection,
pub width: Option<f32>,
pub height: Option<f32>,
pub flex_grow: f32,
pub gap: f32,
pub padding: Edges,
pub background: Option<Color>,
}
impl Default for Style {
fn default() -> Self {
Self {
direction: FlexDirection::Column,
width: None,
height: None,
flex_grow: 0.0,
gap: 0.0,
padding: Edges::ZERO,
background: None,
}
}
}
#[derive(Clone, Debug, PartialEq)]
enum ElementContent {
Container,
Text(TextNode),
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct TextNode {
pub text: String,
pub style: TextStyle,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Element {
pub style: Style,
pub children: Vec<Element>,
content: ElementContent,
}
impl Element {
pub fn new() -> Self {
Self {
style: Style::default(),
children: Vec::new(),
content: ElementContent::Container,
}
}
pub fn text(text: impl Into<String>, style: TextStyle) -> Self {
Self {
style: Style::default(),
children: Vec::new(),
content: ElementContent::Text(TextNode {
text: text.into(),
style,
}),
}
}
pub fn paragraph(text: impl Into<String>, style: TextStyle) -> Self {
Self::text(text, style.with_wrap(TextWrap::Word))
}
pub fn row() -> Self {
Self::new().direction(FlexDirection::Row)
}
pub fn column() -> Self {
Self::new().direction(FlexDirection::Column)
}
pub fn direction(mut self, direction: FlexDirection) -> Self {
self.style.direction = direction;
self
}
pub fn width(mut self, width: f32) -> Self {
self.style.width = Some(width);
self
}
pub fn height(mut self, height: f32) -> Self {
self.style.height = Some(height);
self
}
pub fn flex(mut self, flex_grow: f32) -> Self {
self.style.flex_grow = flex_grow.max(0.0);
self
}
pub fn gap(mut self, gap: f32) -> Self {
self.style.gap = gap.max(0.0);
self
}
pub fn padding(mut self, padding: Edges) -> Self {
self.style.padding = padding;
self
}
pub fn background(mut self, color: Color) -> Self {
self.style.background = Some(color);
self
}
pub fn child(mut self, child: Element) -> Self {
self.assert_container();
self.children.push(child);
self
}
pub fn children(mut self, children: impl IntoIterator<Item = Element>) -> Self {
self.assert_container();
self.children.extend(children);
self
}
pub(crate) fn text_node(&self) -> Option<&TextNode> {
match &self.content {
ElementContent::Text(text) => Some(text),
ElementContent::Container => None,
}
}
fn assert_container(&self) {
assert!(
matches!(self.content, ElementContent::Container),
"text elements cannot contain children"
);
}
}
impl Default for Element {
fn default() -> Self {
Self::new()
}
}