Keyboard input, text input elements

This commit is contained in:
2026-03-21 01:17:07 -04:00
parent 423df4ae1f
commit 84077b718f
13 changed files with 1451 additions and 52 deletions

View File

@@ -1,12 +1,14 @@
use std::error::Error;
use std::process::Command;
use std::time::Instant;
use std::time::{Duration, Instant};
use ruin_runtime::{TimeoutHandle, clear_timeout, set_timeout};
use ruin_ui::{
Color, CursorIcon, DisplayItem, Edges, Element, ElementId, InteractionTree, LayoutSnapshot,
PlatformEvent, PointerButton, PointerEvent, PointerEventKind, PointerRouter, Quad,
RoutedPointerEventKind, SceneSnapshot, TextAlign, TextFontFamily, TextSelectionStyle, TextSpan,
TextSpanSlant, TextSpanWeight, TextStyle, TextSystem, UiSize, WindowSpec, WindowUpdate,
Color, CursorIcon, DisplayItem, Edges, Element, ElementId, InteractionTree, KeyboardEvent,
KeyboardEventKind, KeyboardKey, LayoutSnapshot, PlatformEvent, PointerButton, PointerEvent,
PointerEventKind, PointerRouter, 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;
@@ -25,6 +27,11 @@ const STATUS_CARD_ID: ElementId = ElementId::new(6);
const HERO_TITLE_ID: ElementId = ElementId::new(101);
const HERO_BODY_ID: ElementId = ElementId::new(102);
const RUST_LINK_ID: ElementId = ElementId::new(103);
const INPUT_FIELD_ID: ElementId = ElementId::new(104);
const INPUT_TEXT_ID: ElementId = ElementId::new(105);
const SECOND_INPUT_FIELD_ID: ElementId = ElementId::new(106);
const SECOND_INPUT_TEXT_ID: ElementId = ElementId::new(107);
const INPUT_CARET_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const DEMO_SELECTION_STYLE: TextSelectionStyle =
TextSelectionStyle::new(Color::rgba(0x6C, 0x8E, 0xFF, 0xB8))
.with_text_color(Color::rgb(0x0D, 0x14, 0x25));
@@ -67,6 +74,153 @@ struct SelectionOutcome {
copied_text: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct InputFieldState {
field_id: ElementId,
text_id: ElementId,
label: &'static str,
placeholder: &'static str,
text: String,
caret: usize,
}
impl InputFieldState {
fn new(
field_id: ElementId,
text_id: ElementId,
label: &'static str,
placeholder: &'static str,
text: impl Into<String>,
) -> Self {
let text = text.into();
let caret = text.len();
Self {
field_id,
text_id,
label,
placeholder,
text,
caret,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
struct InputOutcome {
focus_changed: bool,
text_changed: bool,
caret_changed: bool,
selection_changed: bool,
request_primary_paste: bool,
}
fn focused_input_index(
focused_element: Option<ElementId>,
input_fields: &[InputFieldState],
) -> Option<usize> {
focused_element.and_then(|focused| {
input_fields
.iter()
.position(|field| field.field_id == focused)
})
}
fn focused_input(
focused_element: Option<ElementId>,
input_fields: &[InputFieldState],
) -> Option<&InputFieldState> {
focused_input_index(focused_element, input_fields).map(|index| &input_fields[index])
}
fn focused_input_mut(
focused_element: Option<ElementId>,
input_fields: &mut [InputFieldState],
) -> Option<&mut InputFieldState> {
focused_input_index(focused_element, input_fields).map(|index| &mut input_fields[index])
}
fn next_input_focus(
focused_element: Option<ElementId>,
input_fields: &[InputFieldState],
) -> ElementId {
match focused_input_index(focused_element, input_fields) {
Some(index) => input_fields[(index + 1) % input_fields.len()].field_id,
None => input_fields[0].field_id,
}
}
fn selection_bounds(selection: TextSelection) -> (usize, usize) {
if selection.anchor <= selection.focus {
(selection.anchor, selection.focus)
} else {
(selection.focus, selection.anchor)
}
}
fn active_input_selection_bounds(
selection: Option<TextSelection>,
input_field: &InputFieldState,
) -> Option<(usize, usize)> {
let selection = selection?;
if selection.element_id != input_field.text_id || selection.is_collapsed() {
return None;
}
Some(selection_bounds(selection))
}
fn clear_input_selection_for(
selection: &mut Option<TextSelection>,
input_field: &InputFieldState,
) -> bool {
if selection.is_some_and(|current| current.element_id == input_field.text_id) {
*selection = None;
return true;
}
false
}
fn replace_input_range(
input_field: &mut InputFieldState,
selection: &mut Option<TextSelection>,
range: (usize, usize),
replacement: &str,
) -> InputOutcome {
let (start, end) = range;
input_field.text.replace_range(start..end, replacement);
input_field.caret = start + replacement.len();
*selection = None;
InputOutcome {
text_changed: true,
caret_changed: true,
selection_changed: true,
..InputOutcome::default()
}
}
fn insert_text_into_focused_input(
focused_element: Option<ElementId>,
input_fields: &mut [InputFieldState],
selection: &mut Option<TextSelection>,
text: &str,
) -> InputOutcome {
let Some(input_field) = focused_input_mut(focused_element, input_fields) else {
return InputOutcome::default();
};
if let Some(range) = active_input_selection_bounds(*selection, input_field) {
return replace_input_range(input_field, selection, range, text);
}
let caret = input_field.caret;
input_field.text.insert_str(caret, text);
input_field.caret += text.len();
let selection_changed = clear_input_selection_for(selection, input_field);
InputOutcome {
text_changed: true,
caret_changed: true,
selection_changed,
..InputOutcome::default()
}
}
fn install_tracing() {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
EnvFilter::new(
@@ -107,16 +261,43 @@ async fn main() -> Result<(), Box<dyn Error>> {
let mut in_flight_resize = None;
let mut latest_submitted_viewport = None;
let mut current_cursor = CursorIcon::Default;
let mut focused_element = None::<ElementId>;
let mut input_fields = [
InputFieldState::new(
INPUT_FIELD_ID,
INPUT_TEXT_ID,
"Primary input",
"Click or press Tab, then type...",
"",
),
InputFieldState::new(
SECOND_INPUT_FIELD_ID,
SECOND_INPUT_TEXT_ID,
"Secondary input",
"Use selection, Backspace, and middle-click paste here too...",
"",
),
];
let mut caret_visible = false;
let mut caret_blink_timer = None::<TimeoutHandle>;
let mut caret_blink_token = 0_u64;
let mut selection = None;
let mut selection_drag = None;
println!("Opening RUIN paragraph demo window...");
window.set_cursor_icon(current_cursor)?;
while let Some(event) = ui.next_event().await {
loop {
let Some(event) = ui.next_event().await else {
break;
};
let mut latest_configuration = None;
let mut resize_presented = false;
let mut pointer_events = Vec::new();
let mut keyboard_events = Vec::new();
let mut pending_blink_token = None::<u64>;
let mut pending_primary_selection_text = None::<String>;
let mut close_requested = false;
let mut closed = false;
@@ -131,6 +312,25 @@ async fn main() -> Result<(), Box<dyn Error>> {
PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
pointer_events.push(event);
}
PlatformEvent::Keyboard { window_id, event } if window_id == window.id() => {
tracing::trace!(
target: "ruin_ui_text_paragraph_demo::input",
keycode = event.keycode,
?event.kind,
?event.key,
text = event.text.as_deref().unwrap_or(""),
"received platform keyboard event"
);
keyboard_events.push(event);
}
PlatformEvent::Wake { window_id, token } if window_id == window.id() => {
pending_blink_token = Some(token);
}
PlatformEvent::PrimarySelectionText { window_id, text }
if window_id == window.id() =>
{
pending_primary_selection_text = Some(text);
}
PlatformEvent::FramePresented {
window_id,
scene_version,
@@ -153,11 +353,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
}
}
if latest_configuration.is_some() || !pointer_events.is_empty() {
if latest_configuration.is_some()
|| !pointer_events.is_empty()
|| !keyboard_events.is_empty()
{
tracing::trace!(
target: "ruin_ui_text_paragraph_demo::events",
has_configured = latest_configuration.is_some(),
pointer_events = pointer_events.len(),
keyboard_events = keyboard_events.len(),
"processing coalesced event batch"
);
}
@@ -204,7 +408,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
let LayoutSnapshot {
scene,
interaction_tree: next_interaction_tree,
} = build_snapshot(viewport, version, hovered_card, &mut text_system);
} = build_snapshot(
viewport,
version,
hovered_card,
&input_fields,
focused_element,
&mut text_system,
);
if selection.is_some_and(|selection: TextSelection| {
next_interaction_tree
.text_for_element(selection.element_id)
@@ -221,7 +432,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
build_ms = build_started.elapsed().as_secs_f64() * 1_000.0,
"finished snapshot rebuild for configured size"
);
window.replace_scene(scene_with_selection(&scene, selection, version))?;
window.replace_scene(scene_with_overlays(
&scene,
&next_interaction_tree,
selection,
focused_element,
&input_fields,
caret_visible,
version,
))?;
base_scene = Some(scene);
latest_submitted_viewport = Some(viewport);
in_flight_resize = Some(InFlightResize {
@@ -232,19 +451,32 @@ async fn main() -> Result<(), Box<dyn Error>> {
}
let mut needs_hover_rebuild = false;
let mut needs_selection_present = false;
let mut needs_input_rebuild = false;
let mut needs_overlay_present = false;
let mut copied_text = None::<String>;
let mut request_primary_paste = false;
let resize_active =
has_resize_configuration || pending_resize.is_some() || in_flight_resize.is_some();
if !resize_active && let Some(current_interaction_tree) = interaction_tree.as_ref() {
for event in pointer_events {
let input_outcome = handle_input_focus_event(
current_interaction_tree,
event,
&mut focused_element,
&mut input_fields,
&mut selection,
);
needs_input_rebuild |= input_outcome.focus_changed;
needs_overlay_present |=
input_outcome.caret_changed || input_outcome.selection_changed;
request_primary_paste |= input_outcome.request_primary_paste;
let selection_outcome = handle_selection_event(
current_interaction_tree,
event,
&mut selection,
&mut selection_drag,
);
needs_selection_present |= selection_outcome.changed;
needs_overlay_present |= selection_outcome.changed;
if selection_outcome.copied_text.is_some() {
copied_text = selection_outcome.copied_text;
}
@@ -283,10 +515,52 @@ async fn main() -> Result<(), Box<dyn Error>> {
}
}
}
for event in keyboard_events {
let input_outcome = handle_keyboard_input_event(
event,
&mut focused_element,
&mut input_fields,
&mut selection,
);
needs_input_rebuild |= input_outcome.focus_changed || input_outcome.text_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(
focused_element,
&mut input_fields,
&mut selection,
&text,
);
needs_input_rebuild |= input_outcome.text_changed;
needs_overlay_present |= input_outcome.caret_changed || input_outcome.selection_changed;
}
if needs_input_rebuild || needs_overlay_present {
reset_caret_blink(
&window,
focused_element,
&input_fields,
&mut caret_visible,
&mut caret_blink_timer,
&mut caret_blink_token,
);
}
if pending_blink_token == Some(caret_blink_token)
&& focused_input(focused_element, &input_fields).is_some()
{
schedule_caret_blink(&window, &mut caret_blink_timer, caret_blink_token);
if in_flight_resize.is_none() {
caret_visible = !caret_visible;
needs_overlay_present = true;
}
}
if let Some(copied_text) = copied_text {
window.set_primary_selection_text(copied_text)?;
}
if needs_hover_rebuild && in_flight_resize.is_none() {
if request_primary_paste {
window.request_primary_selection_text()?;
}
if (needs_hover_rebuild || needs_input_rebuild) && in_flight_resize.is_none() {
version = version.wrapping_add(1);
tracing::trace!(
target: "ruin_ui_text_paragraph_demo::hover",
@@ -297,7 +571,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
let LayoutSnapshot {
scene,
interaction_tree: next_interaction_tree,
} = build_snapshot(viewport, version, hovered_card, &mut text_system);
} = build_snapshot(
viewport,
version,
hovered_card,
&input_fields,
focused_element,
&mut text_system,
);
if selection.is_some_and(|selection: TextSelection| {
next_interaction_tree
.text_for_element(selection.element_id)
@@ -306,15 +587,32 @@ async fn main() -> Result<(), Box<dyn Error>> {
selection = None;
selection_drag = None;
}
window.replace_scene(scene_with_selection(&scene, selection, version))?;
window.replace_scene(scene_with_overlays(
&scene,
&next_interaction_tree,
selection,
focused_element,
&input_fields,
caret_visible,
version,
))?;
base_scene = Some(scene);
interaction_tree = Some(next_interaction_tree);
} else if needs_selection_present
} else if needs_overlay_present
&& in_flight_resize.is_none()
&& let Some(base_scene) = base_scene.as_ref()
&& let Some(current_interaction_tree) = interaction_tree.as_ref()
{
version = version.wrapping_add(1);
window.replace_scene(scene_with_selection(base_scene, selection, version))?;
window.replace_scene(scene_with_overlays(
base_scene,
current_interaction_tree,
selection,
focused_element,
&input_fields,
caret_visible,
version,
))?;
}
if close_requested {
@@ -333,9 +631,11 @@ fn build_snapshot(
viewport: UiSize,
version: u64,
hovered_card: Option<ElementId>,
input_fields: &[InputFieldState],
focused_element: Option<ElementId>,
text_system: &mut TextSystem,
) -> LayoutSnapshot {
let tree = build_document_tree(viewport, hovered_card);
let tree = build_document_tree(viewport, hovered_card, input_fields, focused_element);
layout_snapshot_with_text_system(version, viewport, &tree, text_system)
}
@@ -347,7 +647,9 @@ fn handle_selection_event(
) -> SelectionOutcome {
let mut outcome = SelectionOutcome::default();
match event.kind {
PointerEventKind::Down { .. } => {
PointerEventKind::Down {
button: PointerButton::Primary,
} => {
let next_selection = interaction_tree
.text_hit_test(event.position)
.and_then(|hit| {
@@ -382,7 +684,9 @@ fn handle_selection_event(
*selection = Some(next_selection);
}
}
PointerEventKind::Up { .. } => {
PointerEventKind::Up {
button: PointerButton::Primary,
} => {
let Some(drag) = selection_drag.take() else {
return outcome;
};
@@ -405,27 +709,318 @@ fn handle_selection_event(
*selection = Some(next_selection);
}
}
PointerEventKind::Down { .. } | PointerEventKind::Up { .. } => {}
PointerEventKind::LeaveWindow => {}
}
outcome
}
fn scene_with_selection(
base_scene: &SceneSnapshot,
selection: Option<TextSelection>,
version: u64,
) -> SceneSnapshot {
let Some(selection) = selection.filter(|selection| !selection.is_collapsed()) else {
let mut scene = base_scene.clone();
scene.version = version;
return scene;
fn handle_input_focus_event(
interaction_tree: &InteractionTree,
event: PointerEvent,
focused_element: &mut Option<ElementId>,
input_fields: &mut [InputFieldState],
selection: &mut Option<TextSelection>,
) -> InputOutcome {
let mut outcome = InputOutcome::default();
let is_middle_click = matches!(
event.kind,
PointerEventKind::Down {
button: PointerButton::Middle
}
);
let is_primary_click = matches!(
event.kind,
PointerEventKind::Down {
button: PointerButton::Primary
}
);
if !is_primary_click && !is_middle_click {
return outcome;
}
let next_focus = interaction_tree
.hit_path(event.position)
.iter()
.rev()
.find_map(|target| target.focusable.then_some(target.element_id).flatten());
if next_focus != *focused_element {
*focused_element = next_focus;
outcome.focus_changed = true;
}
if let Some(input_field) = focused_input_mut(next_focus, input_fields) {
let next_caret = interaction_tree
.text_for_element(input_field.text_id)
.map(|prepared_text| prepared_text.byte_offset_for_position(event.position))
.unwrap_or(input_field.text.len());
if next_caret != input_field.caret {
input_field.caret = next_caret;
outcome.caret_changed = true;
}
outcome.selection_changed |= clear_input_selection_for(selection, input_field);
outcome.request_primary_paste = is_middle_click;
}
if outcome.focus_changed || outcome.caret_changed || outcome.request_primary_paste {
tracing::trace!(
target: "ruin_ui_text_paragraph_demo::input",
pointer_x = event.position.x,
pointer_y = event.position.y,
focused = focused_element.map(|id| id.raw()),
caret = focused_input(*focused_element, input_fields)
.map(|input_field| input_field.caret)
.unwrap_or(0),
focus_changed = outcome.focus_changed,
caret_changed = outcome.caret_changed,
request_primary_paste = outcome.request_primary_paste,
"processed pointer focus event"
);
}
outcome
}
fn handle_keyboard_input_event(
event: KeyboardEvent,
focused_element: &mut Option<ElementId>,
input_fields: &mut [InputFieldState],
selection: &mut Option<TextSelection>,
) -> InputOutcome {
let mut outcome = InputOutcome::default();
let focus_before = *focused_element;
let text_len_before = focused_input(*focused_element, input_fields)
.map(|input_field| input_field.text.len())
.unwrap_or(0);
let caret_before = focused_input(*focused_element, input_fields)
.map(|input_field| input_field.caret)
.unwrap_or(0);
if event.kind != KeyboardEventKind::Pressed {
return outcome;
}
if matches!(&event.key, KeyboardKey::Tab) {
*focused_element = Some(next_input_focus(*focused_element, input_fields));
if let Some(input_field) = focused_input_mut(*focused_element, input_fields) {
outcome.selection_changed |= clear_input_selection_for(selection, input_field);
}
outcome.focus_changed = true;
tracing::trace!(
target: "ruin_ui_text_paragraph_demo::input",
keycode = event.keycode,
?event.key,
text = event.text.as_deref().unwrap_or(""),
focus_before = focus_before.map(|id| id.raw()),
focus_after = focused_element.map(|id| id.raw()),
"focused input from keyboard"
);
return outcome;
}
let Some(input_field) = focused_input_mut(*focused_element, input_fields) else {
tracing::trace!(
target: "ruin_ui_text_paragraph_demo::input",
keycode = event.keycode,
?event.key,
text = event.text.as_deref().unwrap_or(""),
focus_before = focus_before.map(|id| id.raw()),
"ignored keyboard event because input is not focused"
);
return outcome;
};
match &event.key {
KeyboardKey::Escape => {
outcome.selection_changed |= clear_input_selection_for(selection, input_field);
*focused_element = None;
outcome.focus_changed = true;
}
KeyboardKey::ArrowLeft => {
if let Some((start, _)) = active_input_selection_bounds(*selection, input_field) {
input_field.caret = start;
*selection = None;
outcome.caret_changed = true;
outcome.selection_changed = true;
} else {
let next_caret = previous_char_boundary(&input_field.text, input_field.caret);
if next_caret != input_field.caret {
input_field.caret = next_caret;
outcome.caret_changed = true;
}
}
}
KeyboardKey::ArrowRight => {
if let Some((_, end)) = active_input_selection_bounds(*selection, input_field) {
input_field.caret = end;
*selection = None;
outcome.caret_changed = true;
outcome.selection_changed = true;
} else {
let next_caret = next_char_boundary(&input_field.text, input_field.caret);
if next_caret != input_field.caret {
input_field.caret = next_caret;
outcome.caret_changed = true;
}
}
}
KeyboardKey::Home => {
if input_field.caret != 0 {
input_field.caret = 0;
outcome.caret_changed = true;
}
outcome.selection_changed |= clear_input_selection_for(selection, input_field);
}
KeyboardKey::End => {
let end = input_field.text.len();
if input_field.caret != end {
input_field.caret = end;
outcome.caret_changed = true;
}
outcome.selection_changed |= clear_input_selection_for(selection, input_field);
}
KeyboardKey::Backspace => {
if let Some(range) = active_input_selection_bounds(*selection, input_field) {
return replace_input_range(input_field, selection, range, "");
}
let previous = previous_char_boundary(&input_field.text, input_field.caret);
if previous != input_field.caret {
input_field
.text
.replace_range(previous..input_field.caret, "");
input_field.caret = previous;
outcome.text_changed = true;
outcome.caret_changed = true;
}
}
KeyboardKey::Delete => {
if let Some(range) = active_input_selection_bounds(*selection, input_field) {
return replace_input_range(input_field, selection, range, "");
}
let next = next_char_boundary(&input_field.text, input_field.caret);
if next != input_field.caret {
input_field.text.replace_range(input_field.caret..next, "");
outcome.text_changed = true;
}
}
KeyboardKey::Enter | KeyboardKey::Tab => {}
_ => {
let inserted_text = event
.text
.clone()
.filter(|text| !text.is_empty())
.or_else(|| match &event.key {
KeyboardKey::Character(text) if !text.is_empty() => Some(text.clone()),
_ => None,
});
if !event.modifiers.control
&& !event.modifiers.alt
&& !event.modifiers.super_key
&& let Some(text) = inserted_text
{
return insert_text_into_focused_input(
*focused_element,
input_fields,
selection,
&text,
);
}
}
}
tracing::trace!(
target: "ruin_ui_text_paragraph_demo::input",
keycode = event.keycode,
?event.key,
text = event.text.as_deref().unwrap_or(""),
focus_before = focus_before.map(|id| id.raw()),
focus_after = focused_element.map(|id| id.raw()),
text_len_before,
text_len_after = focused_input(*focused_element, input_fields)
.map(|input_field| input_field.text.len())
.unwrap_or(0),
caret_before,
caret_after = focused_input(*focused_element, input_fields)
.map(|input_field| input_field.caret)
.unwrap_or(0),
text_changed = outcome.text_changed,
caret_changed = outcome.caret_changed,
focus_changed = outcome.focus_changed,
selection_changed = outcome.selection_changed,
"processed keyboard input event"
);
outcome
}
fn previous_char_boundary(text: &str, offset: usize) -> usize {
let offset = offset.min(text.len());
text[..offset]
.char_indices()
.last()
.map(|(index, _)| index)
.unwrap_or(0)
}
fn next_char_boundary(text: &str, offset: usize) -> usize {
let offset = offset.min(text.len());
if offset >= text.len() {
return text.len();
}
text.char_indices()
.find_map(|(index, _)| (index > offset).then_some(index))
.unwrap_or(text.len())
}
fn schedule_caret_blink(
window: &WindowController,
caret_blink_timer: &mut Option<TimeoutHandle>,
caret_blink_token: u64,
) {
if let Some(handle) = caret_blink_timer.take() {
clear_timeout(&handle);
}
let window = window.clone();
*caret_blink_timer = Some(set_timeout(INPUT_CARET_BLINK_INTERVAL, move || {
let _ = window.emit_wake(caret_blink_token);
}));
}
fn reset_caret_blink(
window: &WindowController,
focused_element: Option<ElementId>,
input_fields: &[InputFieldState],
caret_visible: &mut bool,
caret_blink_timer: &mut Option<TimeoutHandle>,
caret_blink_token: &mut u64,
) {
*caret_blink_token = caret_blink_token.wrapping_add(1);
if let Some(handle) = caret_blink_timer.take() {
clear_timeout(&handle);
}
if focused_input(focused_element, input_fields).is_some() {
*caret_visible = true;
schedule_caret_blink(window, caret_blink_timer, *caret_blink_token);
} else {
*caret_visible = false;
}
}
fn scene_with_overlays(
base_scene: &SceneSnapshot,
interaction_tree: &InteractionTree,
selection: Option<TextSelection>,
focused_element: Option<ElementId>,
input_fields: &[InputFieldState],
caret_visible: bool,
version: u64,
) -> SceneSnapshot {
let mut scene = base_scene.clone();
scene.version = version;
let mut items = Vec::with_capacity(scene.items.len() + 8);
let selection = selection.filter(|selection| !selection.is_collapsed());
for item in scene.items.drain(..) {
if let DisplayItem::Text(prepared_text) = &item
if let Some(selection) = selection
&& let DisplayItem::Text(prepared_text) = &item
&& prepared_text.element_id == Some(selection.element_id)
{
for rect in prepared_text.selection_rects(selection.anchor, selection.focus) {
@@ -441,6 +1036,16 @@ fn scene_with_selection(
}
items.push(item);
}
if caret_visible
&& let Some(input_field) = focused_input(focused_element, input_fields)
&& let Some(prepared_text) = interaction_tree.text_for_element(input_field.text_id)
&& let Some(caret_rect) = prepared_text.caret_rect(input_field.caret, 2.0)
{
items.push(DisplayItem::Quad(Quad::new(
caret_rect,
Color::rgb(0xF5, 0xF7, 0xFB),
)));
}
scene.items = items;
scene
}
@@ -466,7 +1071,54 @@ fn open_rust_website() {
}
}
fn build_document_tree(viewport: UiSize, hovered_card: Option<ElementId>) -> Element {
fn input_field_element(input_field: &InputFieldState, focused: bool) -> Element {
let field_background = if focused {
Color::rgb(0x10, 0x1A, 0x2A)
} else {
Color::rgb(0x12, 0x18, 0x24)
};
let text_element = if !focused && input_field.text.is_empty() {
Element::text(
input_field.placeholder,
TextStyle::new(18.0, Color::rgb(0x7D, 0x89, 0x9E))
.with_line_height(24.0)
.with_wrap(TextWrap::None)
.with_selectable(false),
)
} else {
Element::text(
input_field.text.as_str(),
TextStyle::new(18.0, Color::rgb(0xF5, 0xF7, 0xFB))
.with_line_height(24.0)
.with_wrap(TextWrap::None)
.with_selectable(true),
)
.id(input_field.text_id)
};
Element::column().gap(10.0).children([
Element::paragraph(
input_field.label,
TextStyle::new(16.0, Color::rgb(0xA7, 0xF3, 0xD0))
.with_line_height(22.0)
.with_selectable(false),
),
Element::column()
.id(input_field.field_id)
.focusable(true)
.cursor(CursorIcon::Text)
.padding(Edges::symmetric(14.0, 12.0))
.background(field_background)
.child(text_element.cursor(CursorIcon::Text)),
])
}
fn build_document_tree(
viewport: UiSize,
hovered_card: Option<ElementId>,
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);
@@ -522,6 +1174,14 @@ fn build_document_tree(viewport: UiSize, hovered_card: Option<ElementId>) -> Ele
)
.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()