Images!
This commit is contained in:
@@ -4,12 +4,12 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use ruin_runtime::{TimeoutHandle, clear_timeout, set_timeout};
|
||||
use ruin_ui::{
|
||||
Color, CursorIcon, DisplayItem, Edges, Element, ElementId, InteractionTree, KeyboardEvent,
|
||||
KeyboardEventKind, KeyboardKey, LayoutSnapshot, PlatformEvent, PointerButton, PointerEvent,
|
||||
PointerEventKind, PointerRouter, PreparedText, Quad, RoutedPointerEventKind, SceneSnapshot,
|
||||
TextAlign, TextFontFamily, TextSelectionStyle, TextSpan, TextSpanSlant, TextSpanWeight,
|
||||
TextStyle, TextSystem, TextWrap, UiSize, WindowController, WindowSpec, WindowUpdate,
|
||||
layout_snapshot_with_text_system,
|
||||
Color, CursorIcon, DisplayItem, Edges, Element, ElementId, ImageFit, ImageResource,
|
||||
InteractionTree, KeyboardEvent, KeyboardEventKind, KeyboardKey, LayoutSnapshot, PlatformEvent,
|
||||
PointerButton, PointerEvent, PointerEventKind, PointerRouter, PreparedText, Quad,
|
||||
RoutedPointerEventKind, SceneSnapshot, TextAlign, TextFontFamily, TextSelectionStyle, TextSpan,
|
||||
TextSpanSlant, TextSpanWeight, TextStyle, TextSystem, TextWrap, UiSize, WindowController,
|
||||
WindowSpec, WindowUpdate, layout_snapshot_with_text_system,
|
||||
};
|
||||
use ruin_ui_platform_wayland::start_wayland_ui;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
@@ -34,6 +34,7 @@ const SECOND_INPUT_TEXT_ID: ElementId = ElementId::new(107);
|
||||
const INPUT_CARET_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
const MULTI_CLICK_INTERVAL: Duration = Duration::from_millis(350);
|
||||
const MULTI_CLICK_DISTANCE_SQUARED: f32 = 36.0;
|
||||
const HERO_IMAGE_ASSET: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ruin.png");
|
||||
const DEMO_SELECTION_STYLE: TextSelectionStyle =
|
||||
TextSelectionStyle::new(Color::rgba(0x6C, 0x8E, 0xFF, 0xB8))
|
||||
.with_text_color(Color::rgb(0x0D, 0x14, 0x25));
|
||||
@@ -192,7 +193,10 @@ fn active_input_selection_bounds(
|
||||
Some(selection_bounds(selection))
|
||||
}
|
||||
|
||||
fn has_active_input_selection(selection: Option<TextSelection>, input_field: &InputFieldState) -> bool {
|
||||
fn has_active_input_selection(
|
||||
selection: Option<TextSelection>,
|
||||
input_field: &InputFieldState,
|
||||
) -> bool {
|
||||
active_input_selection_bounds(selection, input_field).is_some()
|
||||
}
|
||||
|
||||
@@ -348,6 +352,7 @@ fn install_tracing() {
|
||||
#[ruin_runtime::async_main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
install_tracing();
|
||||
let hero_image = ImageResource::load_path(HERO_IMAGE_ASSET).await?;
|
||||
let mut ui = start_wayland_ui();
|
||||
let window = ui.create_window(
|
||||
WindowSpec::new("RUIN paragraph demo")
|
||||
@@ -522,6 +527,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
viewport,
|
||||
version,
|
||||
hovered_card,
|
||||
&hero_image,
|
||||
&input_fields,
|
||||
focused_element,
|
||||
&mut text_system,
|
||||
@@ -656,8 +662,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
&text,
|
||||
);
|
||||
needs_input_rebuild |= input_outcome.text_changed;
|
||||
needs_overlay_present |=
|
||||
input_outcome.caret_changed || input_outcome.selection_changed;
|
||||
needs_overlay_present |= input_outcome.caret_changed || input_outcome.selection_changed;
|
||||
}
|
||||
if let Some(text) = pending_primary_selection_text {
|
||||
let input_outcome = insert_text_into_focused_input(
|
||||
@@ -715,6 +720,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
viewport,
|
||||
version,
|
||||
hovered_card,
|
||||
&hero_image,
|
||||
&input_fields,
|
||||
focused_element,
|
||||
&mut text_system,
|
||||
@@ -771,11 +777,18 @@ fn build_snapshot(
|
||||
viewport: UiSize,
|
||||
version: u64,
|
||||
hovered_card: Option<ElementId>,
|
||||
hero_image: &ImageResource,
|
||||
input_fields: &[InputFieldState],
|
||||
focused_element: Option<ElementId>,
|
||||
text_system: &mut TextSystem,
|
||||
) -> LayoutSnapshot {
|
||||
let tree = build_document_tree(viewport, hovered_card, input_fields, focused_element);
|
||||
let tree = build_document_tree(
|
||||
viewport,
|
||||
hovered_card,
|
||||
hero_image,
|
||||
input_fields,
|
||||
focused_element,
|
||||
);
|
||||
layout_snapshot_with_text_system(version, viewport, &tree, text_system)
|
||||
}
|
||||
|
||||
@@ -1051,17 +1064,14 @@ fn handle_keyboard_input_event(
|
||||
let prepared_text = interaction_tree.text_for_element(text_id);
|
||||
if event.modifiers.shift
|
||||
&& let Some(prepared_text) = prepared_text
|
||||
&& let Some(next_caret) =
|
||||
navigation_target_offset(
|
||||
prepared_text,
|
||||
selection
|
||||
.filter(|current| current.element_id == text_id)
|
||||
.map(|current| {
|
||||
selection_navigation_anchor_and_focus(current, &event.key).1
|
||||
})
|
||||
.unwrap_or(input_fields[input_index].caret),
|
||||
&event.key,
|
||||
)
|
||||
&& let Some(next_caret) = navigation_target_offset(
|
||||
prepared_text,
|
||||
selection
|
||||
.filter(|current| current.element_id == text_id)
|
||||
.map(|current| selection_navigation_anchor_and_focus(current, &event.key).1)
|
||||
.unwrap_or(input_fields[input_index].caret),
|
||||
&event.key,
|
||||
)
|
||||
{
|
||||
let anchor = selection
|
||||
.filter(|current| current.element_id == text_id)
|
||||
@@ -1312,10 +1322,6 @@ fn demo_body_style(font_size: f32, color: Color) -> TextStyle {
|
||||
TextStyle::new(font_size, color).with_selection_style(DEMO_SELECTION_STYLE)
|
||||
}
|
||||
|
||||
fn demo_unselectable_title(text: impl Into<String>, id: ElementId, style: TextStyle) -> Element {
|
||||
Element::paragraph(text, style.with_selectable(false)).id(id)
|
||||
}
|
||||
|
||||
fn open_rust_website() {
|
||||
if let Err(error) = Command::new("xdg-open")
|
||||
.arg("https://www.rust-lang.org")
|
||||
@@ -1374,72 +1380,76 @@ fn input_field_element(input_field: &InputFieldState, focused: bool) -> Element
|
||||
fn build_document_tree(
|
||||
viewport: UiSize,
|
||||
hovered_card: Option<ElementId>,
|
||||
hero_image: &ImageResource,
|
||||
input_fields: &[InputFieldState],
|
||||
focused_element: 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);
|
||||
let hero_image_width = (viewport.width * 0.32).clamp(200.0, 320.0);
|
||||
let hero_image_height = (hero_image_width * 0.34).clamp(72.0, 128.0);
|
||||
|
||||
Element::column()
|
||||
.background(Color::rgb(0x0F, 0x13, 0x1E))
|
||||
.padding(Edges::all(gutter))
|
||||
.gap(gutter)
|
||||
.children([
|
||||
Element::column()
|
||||
Element::row()
|
||||
.padding(Edges::all(gutter))
|
||||
.gap(gutter * 0.45)
|
||||
.gap(gutter)
|
||||
.background(Color::rgb(0x16, 0x1D, 0x2B))
|
||||
.children([
|
||||
demo_unselectable_title(
|
||||
"RUIN paragraph demo",
|
||||
HERO_TITLE_ID,
|
||||
TextStyle::new(34.0, Color::rgb(0xF5, 0xF7, 0xFB))
|
||||
.with_line_height(40.0)
|
||||
.with_align(TextAlign::Center),
|
||||
),
|
||||
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")
|
||||
Element::image(hero_image.clone())
|
||||
.id(HERO_TITLE_ID)
|
||||
.width(hero_image_width)
|
||||
.height(hero_image_height)
|
||||
.image_fit(ImageFit::Contain)
|
||||
.pointer_events(false),
|
||||
Element::column().flex(1.0).gap(gutter * 0.45).children([
|
||||
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.",
|
||||
),
|
||||
],
|
||||
demo_body_style(18.0, Color::rgb(0xC9, 0xD2, 0xE3))
|
||||
.with_line_height(28.0)
|
||||
.with_align(TextAlign::Center),
|
||||
)
|
||||
.id(HERO_BODY_ID),
|
||||
Element::rich_paragraph(
|
||||
[TextSpan::new("Visit the Rust website")
|
||||
.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.",
|
||||
),
|
||||
],
|
||||
demo_body_style(18.0, Color::rgb(0xC9, 0xD2, 0xE3))
|
||||
.with_line_height(28.0)
|
||||
.with_align(TextAlign::Center),
|
||||
)
|
||||
.id(HERO_BODY_ID),
|
||||
Element::rich_paragraph(
|
||||
[TextSpan::new("Visit the Rust website")
|
||||
.weight(TextSpanWeight::Bold)
|
||||
.color(Color::rgb(0x60, 0xA5, 0xFA))],
|
||||
TextStyle::new(18.0, Color::rgb(0x60, 0xA5, 0xFA))
|
||||
.with_line_height(24.0)
|
||||
.with_selectable(false),
|
||||
)
|
||||
.id(RUST_LINK_ID)
|
||||
.cursor(CursorIcon::Pointer),
|
||||
input_field_element(
|
||||
&input_fields[0],
|
||||
focused_element == Some(input_fields[0].field_id),
|
||||
),
|
||||
input_field_element(
|
||||
&input_fields[1],
|
||||
focused_element == Some(input_fields[1].field_id),
|
||||
),
|
||||
.color(Color::rgb(0x60, 0xA5, 0xFA))],
|
||||
TextStyle::new(18.0, Color::rgb(0x60, 0xA5, 0xFA))
|
||||
.with_line_height(24.0)
|
||||
.with_selectable(false),
|
||||
)
|
||||
.id(RUST_LINK_ID)
|
||||
.cursor(CursorIcon::Pointer),
|
||||
input_field_element(
|
||||
&input_fields[0],
|
||||
focused_element == Some(input_fields[0].field_id),
|
||||
),
|
||||
input_field_element(
|
||||
&input_fields[1],
|
||||
focused_element == Some(input_fields[1].field_id),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
Element::row().flex(1.0).gap(gutter).children([
|
||||
Element::column()
|
||||
|
||||
Reference in New Issue
Block a user