Text paragraphs, styling, fontconfig

This commit is contained in:
2026-03-20 20:15:27 -04:00
parent 00fe1daa0c
commit f71e03317d
9 changed files with 687 additions and 43 deletions

View File

@@ -1,17 +1,14 @@
use std::error::Error;
use ruin_ui::{
Color, Edges, Element, SceneSnapshot, TextAlign, TextStyle, TextSystem, UiSize, WindowSpec,
layout_scene_with_text_system,
Color, Edges, Element, SceneSnapshot, TextAlign, TextFontFamily, TextSpan, TextSpanSlant,
TextSpanWeight, TextStyle, TextSystem, UiSize, WindowSpec, layout_scene_with_text_system,
};
use ruin_ui_platform_wayland::WaylandWindow;
use ruin_ui_renderer_wgpu::{RenderError, WgpuSceneRenderer};
const INTRO: &str = "RUIN is exploring a retained layout tree backed by explicit scene building, a dedicated platform thread, and a renderer that can stay simple while the higher-level UI model evolves. This example is intentionally calm: no animated panels, no pulsing widths, just a document-like surface that makes paragraph behavior easier to inspect.";
const BODY_ONE: &str = "Paragraph widgets are the next useful layer above raw text leaves. They should be able to wrap naturally inside containers, respect alignment, clamp to a maximum number of lines when appropriate, and participate in layout without forcing every example to become a custom text experiment. That gets us closer to real application surfaces instead of just proving that glyphs can reach the screen.";
const BODY_TWO: &str = "This demo keeps the overall layout mostly static while still responding to window resizing. The centered title, the body copy, and the clamped sidebar notes all use the same retained layout pipeline, but they exercise different paragraph semantics. It should be a much less exhausting place to look at text than the reactive dashboard stress test.";
const SIDEBAR_NOTE: &str = "Clamped note: the renderer now uses a shared glyph atlas for prepared text, so the remaining debug-build cost mostly comes from paragraph layout and scene building rather than per-frame CPU text compositing.";
const FOOTER: &str = "Next up after this slice: richer paragraph/block rules, then inline style runs, then interactive text editing and selection.";
#[ruin_runtime::main]
fn main() -> Result<(), Box<dyn Error>> {
@@ -86,8 +83,26 @@ fn build_document_tree(viewport: UiSize) -> Element {
.with_line_height(40.0)
.with_align(TextAlign::Center),
),
Element::paragraph(
INTRO,
Element::rich_paragraph(
[
TextSpan::new("RUIN is exploring a "),
TextSpan::new("retained")
.weight(TextSpanWeight::Semibold)
.color(Color::rgb(0xF5, 0xD0, 0x74)),
TextSpan::new(" layout tree backed by explicit scene building, a dedicated "),
TextSpan::new("platform")
.weight(TextSpanWeight::Semibold)
.color(Color::rgb(0x7D, 0xD3, 0xFC)),
TextSpan::new(" thread, and a "),
TextSpan::new("renderer")
.weight(TextSpanWeight::Bold)
.slant(TextSpanSlant::Oblique)
.family(TextFontFamily::Monospace)
.color(Color::rgb(0xA7, 0xF3, 0xD0)),
TextSpan::new(
" that can stay simple while the higher-level UI model evolves. This example is intentionally calm: no animated panels, no pulsing widths, just a document-like surface that makes paragraph behavior easier to inspect.",
),
],
TextStyle::new(18.0, Color::rgb(0xC9, 0xD2, 0xE3))
.with_line_height(28.0)
.with_align(TextAlign::Center),
@@ -110,8 +125,22 @@ fn build_document_tree(viewport: UiSize) -> Element {
TextStyle::new(20.0, Color::rgb(0xF5, 0xF7, 0xFB))
.with_line_height(26.0),
),
Element::paragraph(
FOOTER,
Element::rich_paragraph(
[
TextSpan::new("Next up after this slice: "),
TextSpan::new("paragraph/block")
.weight(TextSpanWeight::Semibold)
.color(Color::rgb(0xF5, 0xD0, 0x74)),
TextSpan::new(" rules, then "),
TextSpan::new("inline")
.weight(TextSpanWeight::Semibold)
.color(Color::rgb(0xA7, 0xF3, 0xD0)),
TextSpan::new(" style runs, then "),
TextSpan::new("interactive")
.weight(TextSpanWeight::Bold)
.color(Color::rgb(0x7D, 0xD3, 0xFC)),
TextSpan::new(" text editing and selection."),
],
TextStyle::new(17.0, Color::rgb(0xD8, 0xDF, 0xED))
.with_line_height(26.0),
),
@@ -126,21 +155,17 @@ fn build_document_tree(viewport: UiSize) -> Element {
"“A retained layout tree is only really convincing once text can participate in it naturally.”",
gutter,
Some(TextAlign::Center),
Some(TextFontFamily::Serif),
None,
),
sidebar_card(
"Clamped note",
SIDEBAR_NOTE,
gutter,
None,
Some(4),
),
rich_sidebar_card(gutter),
sidebar_card(
"Status",
"Static layout, responsive resize, paragraph wrapping, centered headings, and line clamping all share the same UI pipeline now.",
gutter,
Some(TextAlign::End),
None,
None,
),
]),
]),
@@ -169,12 +194,16 @@ fn sidebar_card(
body: &str,
gutter: f32,
align: Option<TextAlign>,
font_family: Option<TextFontFamily>,
max_lines: Option<usize>,
) -> Element {
let mut body_style = TextStyle::new(16.0, Color::rgb(0xD4, 0xDB, 0xEA)).with_line_height(25.0);
if let Some(align) = align {
body_style = body_style.with_align(align);
}
if let Some(font_family) = font_family {
body_style = body_style.with_font_family(font_family);
}
if let Some(max_lines) = max_lines {
body_style = body_style.with_max_lines(max_lines);
}
@@ -191,3 +220,38 @@ fn sidebar_card(
Element::paragraph(body, body_style),
])
}
fn rich_sidebar_card(gutter: f32) -> Element {
Element::column()
.padding(Edges::all(gutter * 0.9))
.gap(gutter * 0.35)
.background(Color::rgb(0x1C, 0x24, 0x34))
.children([
Element::paragraph(
"Clamped note",
TextStyle::new(18.0, Color::rgb(0xF4, 0xF7, 0xFF)).with_line_height(24.0),
),
Element::rich_paragraph(
[
TextSpan::new("Clamped note: the renderer now uses a "),
TextSpan::new("shared")
.weight(TextSpanWeight::Bold)
.color(Color::rgb(0xA7, 0xF3, 0xD0)),
TextSpan::new(" glyph atlas for prepared text, so the remaining "),
TextSpan::new("debug-build")
.weight(TextSpanWeight::Semibold)
.color(Color::rgb(0xF5, 0xD0, 0x74)),
TextSpan::new(" cost mostly comes from "),
TextSpan::new("paragraph")
.weight(TextSpanWeight::Semibold)
.color(Color::rgb(0x7D, 0xD3, 0xFC)),
TextSpan::new(
" layout and scene building rather than per-frame CPU text compositing.",
),
],
TextStyle::new(16.0, Color::rgb(0xD4, 0xDB, 0xEA))
.with_line_height(25.0)
.with_max_lines(4),
),
])
}