Keyboard input, text input elements
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user