Pointer
This commit is contained in:
@@ -1,21 +1,37 @@
|
||||
use std::error::Error;
|
||||
|
||||
use ruin_ui::{
|
||||
Color, Edges, Element, SceneSnapshot, TextAlign, TextFontFamily, TextSpan, TextSpanSlant,
|
||||
TextSpanWeight, TextStyle, TextSystem, UiSize, WindowSpec, layout_scene_with_text_system,
|
||||
Color, Edges, Element, ElementId, LayoutSnapshot, PointerRouter, TextAlign, TextFontFamily,
|
||||
TextSpan, TextSpanSlant, TextSpanWeight, TextStyle, TextSystem, UiSize, WindowSpec,
|
||||
layout_snapshot_with_text_system,
|
||||
};
|
||||
use ruin_ui_platform_wayland::WaylandWindow;
|
||||
use ruin_ui_renderer_wgpu::{RenderError, WgpuSceneRenderer};
|
||||
|
||||
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 WHY_CARD_ID: ElementId = ElementId::new(1);
|
||||
const CALM_CARD_ID: ElementId = ElementId::new(2);
|
||||
const NEXT_CARD_ID: ElementId = ElementId::new(3);
|
||||
const QUOTE_CARD_ID: ElementId = ElementId::new(4);
|
||||
const NOTE_CARD_ID: ElementId = ElementId::new(5);
|
||||
const STATUS_CARD_ID: ElementId = ElementId::new(6);
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct SidebarCardOptions {
|
||||
align: Option<TextAlign>,
|
||||
font_family: Option<TextFontFamily>,
|
||||
max_lines: Option<usize>,
|
||||
}
|
||||
|
||||
#[ruin_runtime::main]
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut viewport = UiSize::new(1040.0, 760.0);
|
||||
let mut version = 1_u64;
|
||||
let mut text_system = TextSystem::new();
|
||||
let mut scene = build_scene(viewport, version, &mut text_system);
|
||||
let mut pointer_router = PointerRouter::new();
|
||||
let mut hovered_card = None;
|
||||
let mut snapshot = build_snapshot(viewport, version, hovered_card, &mut text_system);
|
||||
|
||||
let mut window = WaylandWindow::open(
|
||||
WindowSpec::new("RUIN paragraph demo")
|
||||
@@ -33,16 +49,34 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
while window.is_running() {
|
||||
window.dispatch()?;
|
||||
let mut needs_scene_rebuild = false;
|
||||
for event in window.drain_pointer_events() {
|
||||
let _ = pointer_router.route(&snapshot.interaction_tree, event);
|
||||
let next_hover = pointer_router
|
||||
.hovered_target()
|
||||
.and_then(|target| target.element_id)
|
||||
.filter(|id| is_hoverable_card(*id));
|
||||
if next_hover != hovered_card {
|
||||
hovered_card = next_hover;
|
||||
version = version.wrapping_add(1);
|
||||
needs_scene_rebuild = true;
|
||||
}
|
||||
}
|
||||
if needs_scene_rebuild {
|
||||
snapshot = build_snapshot(viewport, version, hovered_card, &mut text_system);
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
if let Some(frame) = window.prepare_frame() {
|
||||
if frame.resized {
|
||||
renderer.resize(frame.width, frame.height);
|
||||
viewport = UiSize::new(frame.width as f32, frame.height as f32);
|
||||
version = version.wrapping_add(1);
|
||||
scene = build_scene(viewport, version, &mut text_system);
|
||||
snapshot = build_snapshot(viewport, version, hovered_card, &mut text_system);
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
match renderer.render(&scene) {
|
||||
match renderer.render(&snapshot.scene) {
|
||||
Ok(()) => {}
|
||||
Err(RenderError::Lost | RenderError::Outdated) => {
|
||||
renderer.resize(frame.width, frame.height);
|
||||
@@ -58,12 +92,17 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_scene(viewport: UiSize, version: u64, text_system: &mut TextSystem) -> SceneSnapshot {
|
||||
let tree = build_document_tree(viewport);
|
||||
layout_scene_with_text_system(version, viewport, &tree, text_system)
|
||||
fn build_snapshot(
|
||||
viewport: UiSize,
|
||||
version: u64,
|
||||
hovered_card: Option<ElementId>,
|
||||
text_system: &mut TextSystem,
|
||||
) -> LayoutSnapshot {
|
||||
let tree = build_document_tree(viewport, hovered_card);
|
||||
layout_snapshot_with_text_system(version, viewport, &tree, text_system)
|
||||
}
|
||||
|
||||
fn build_document_tree(viewport: UiSize) -> Element {
|
||||
fn build_document_tree(viewport: UiSize, hovered_card: Option<ElementId>) -> Element {
|
||||
let gutter = (viewport.width * 0.025).clamp(18.0, 30.0);
|
||||
let sidebar_width = (viewport.width * 0.28).clamp(220.0, 320.0);
|
||||
|
||||
@@ -113,17 +152,33 @@ fn build_document_tree(viewport: UiSize) -> Element {
|
||||
.flex(1.0)
|
||||
.gap(gutter)
|
||||
.children([
|
||||
text_card("Why paragraphs matter", BODY_ONE, gutter),
|
||||
text_card("Calmer inspection surface", BODY_TWO, gutter),
|
||||
text_card(
|
||||
WHY_CARD_ID,
|
||||
hovered_card,
|
||||
"Why paragraphs matter",
|
||||
BODY_ONE,
|
||||
gutter,
|
||||
),
|
||||
text_card(
|
||||
CALM_CARD_ID,
|
||||
hovered_card,
|
||||
"Calmer inspection surface",
|
||||
BODY_TWO,
|
||||
gutter,
|
||||
),
|
||||
Element::column()
|
||||
.id(NEXT_CARD_ID)
|
||||
.padding(Edges::all(gutter))
|
||||
.gap(gutter * 0.45)
|
||||
.background(Color::rgb(0x1A, 0x22, 0x31))
|
||||
.background(card_background(NEXT_CARD_ID, hovered_card))
|
||||
.children([
|
||||
Element::paragraph(
|
||||
"Next direction",
|
||||
TextStyle::new(20.0, Color::rgb(0xF5, 0xF7, 0xFB))
|
||||
.with_line_height(26.0),
|
||||
TextStyle::new(
|
||||
20.0,
|
||||
card_title_color(NEXT_CARD_ID, hovered_card),
|
||||
)
|
||||
.with_line_height(26.0),
|
||||
),
|
||||
Element::rich_paragraph(
|
||||
[
|
||||
@@ -151,36 +206,50 @@ fn build_document_tree(viewport: UiSize) -> Element {
|
||||
.gap(gutter)
|
||||
.children([
|
||||
sidebar_card(
|
||||
QUOTE_CARD_ID,
|
||||
hovered_card,
|
||||
"Centered pull quote",
|
||||
"“A retained layout tree is only really convincing once text can participate in it naturally.”",
|
||||
gutter,
|
||||
Some(TextAlign::Center),
|
||||
Some(TextFontFamily::Serif),
|
||||
None,
|
||||
SidebarCardOptions {
|
||||
align: Some(TextAlign::Center),
|
||||
font_family: Some(TextFontFamily::Serif),
|
||||
max_lines: None,
|
||||
},
|
||||
),
|
||||
rich_sidebar_card(gutter),
|
||||
rich_sidebar_card(hovered_card, gutter),
|
||||
sidebar_card(
|
||||
STATUS_CARD_ID,
|
||||
hovered_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,
|
||||
SidebarCardOptions {
|
||||
align: Some(TextAlign::End),
|
||||
..SidebarCardOptions::default()
|
||||
},
|
||||
),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
fn text_card(title: &str, body: &str, gutter: f32) -> Element {
|
||||
fn text_card(
|
||||
id: ElementId,
|
||||
hovered_card: Option<ElementId>,
|
||||
title: &str,
|
||||
body: &str,
|
||||
gutter: f32,
|
||||
) -> Element {
|
||||
Element::column()
|
||||
.id(id)
|
||||
.padding(Edges::all(gutter))
|
||||
.gap(gutter * 0.45)
|
||||
.background(Color::rgb(0x18, 0x20, 0x2F))
|
||||
.background(card_background(id, hovered_card))
|
||||
.children([
|
||||
Element::paragraph(
|
||||
title,
|
||||
TextStyle::new(24.0, Color::rgb(0xF4, 0xF7, 0xFF)).with_line_height(30.0),
|
||||
TextStyle::new(24.0, card_title_color(id, hovered_card)).with_line_height(30.0),
|
||||
),
|
||||
Element::paragraph(
|
||||
body,
|
||||
@@ -190,46 +259,49 @@ fn text_card(title: &str, body: &str, gutter: f32) -> Element {
|
||||
}
|
||||
|
||||
fn sidebar_card(
|
||||
id: ElementId,
|
||||
hovered_card: Option<ElementId>,
|
||||
title: &str,
|
||||
body: &str,
|
||||
gutter: f32,
|
||||
align: Option<TextAlign>,
|
||||
font_family: Option<TextFontFamily>,
|
||||
max_lines: Option<usize>,
|
||||
options: SidebarCardOptions,
|
||||
) -> Element {
|
||||
let mut body_style = TextStyle::new(16.0, Color::rgb(0xD4, 0xDB, 0xEA)).with_line_height(25.0);
|
||||
if let Some(align) = align {
|
||||
if let Some(align) = options.align {
|
||||
body_style = body_style.with_align(align);
|
||||
}
|
||||
if let Some(font_family) = font_family {
|
||||
if let Some(font_family) = options.font_family {
|
||||
body_style = body_style.with_font_family(font_family);
|
||||
}
|
||||
if let Some(max_lines) = max_lines {
|
||||
if let Some(max_lines) = options.max_lines {
|
||||
body_style = body_style.with_max_lines(max_lines);
|
||||
}
|
||||
|
||||
Element::column()
|
||||
.id(id)
|
||||
.padding(Edges::all(gutter * 0.9))
|
||||
.gap(gutter * 0.35)
|
||||
.background(Color::rgb(0x1C, 0x24, 0x34))
|
||||
.background(card_background(id, hovered_card))
|
||||
.children([
|
||||
Element::paragraph(
|
||||
title,
|
||||
TextStyle::new(18.0, Color::rgb(0xF4, 0xF7, 0xFF)).with_line_height(24.0),
|
||||
TextStyle::new(18.0, card_title_color(id, hovered_card)).with_line_height(24.0),
|
||||
),
|
||||
Element::paragraph(body, body_style),
|
||||
])
|
||||
}
|
||||
|
||||
fn rich_sidebar_card(gutter: f32) -> Element {
|
||||
fn rich_sidebar_card(hovered_card: Option<ElementId>, gutter: f32) -> Element {
|
||||
Element::column()
|
||||
.id(NOTE_CARD_ID)
|
||||
.padding(Edges::all(gutter * 0.9))
|
||||
.gap(gutter * 0.35)
|
||||
.background(Color::rgb(0x1C, 0x24, 0x34))
|
||||
.background(card_background(NOTE_CARD_ID, hovered_card))
|
||||
.children([
|
||||
Element::paragraph(
|
||||
"Clamped note",
|
||||
TextStyle::new(18.0, Color::rgb(0xF4, 0xF7, 0xFF)).with_line_height(24.0),
|
||||
TextStyle::new(18.0, card_title_color(NOTE_CARD_ID, hovered_card))
|
||||
.with_line_height(24.0),
|
||||
),
|
||||
Element::rich_paragraph(
|
||||
[
|
||||
@@ -255,3 +327,30 @@ fn rich_sidebar_card(gutter: f32) -> Element {
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn is_hoverable_card(id: ElementId) -> bool {
|
||||
matches!(
|
||||
id,
|
||||
WHY_CARD_ID | CALM_CARD_ID | NEXT_CARD_ID | QUOTE_CARD_ID | NOTE_CARD_ID | STATUS_CARD_ID
|
||||
)
|
||||
}
|
||||
|
||||
fn card_background(id: ElementId, hovered_card: Option<ElementId>) -> Color {
|
||||
if hovered_card == Some(id) {
|
||||
return Color::rgb(0x24, 0x31, 0x46);
|
||||
}
|
||||
|
||||
match id {
|
||||
WHY_CARD_ID | CALM_CARD_ID => Color::rgb(0x18, 0x20, 0x2F),
|
||||
NEXT_CARD_ID => Color::rgb(0x1A, 0x22, 0x31),
|
||||
QUOTE_CARD_ID | NOTE_CARD_ID | STATUS_CARD_ID => Color::rgb(0x1C, 0x24, 0x34),
|
||||
_ => Color::rgb(0x18, 0x20, 0x2F),
|
||||
}
|
||||
}
|
||||
|
||||
fn card_title_color(id: ElementId, hovered_card: Option<ElementId>) -> Color {
|
||||
if hovered_card == Some(id) {
|
||||
return Color::rgb(0xFF, 0xE4, 0x9A);
|
||||
}
|
||||
Color::rgb(0xF4, 0xF7, 0xFF)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user