Keyboard input, text input elements
This commit is contained in:
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -1016,6 +1016,7 @@ dependencies = [
|
|||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-protocols",
|
"wayland-protocols",
|
||||||
|
"xkbcommon",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1850,6 +1851,23 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xkbcommon"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"memmap2",
|
||||||
|
"xkeysym",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xkeysym"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xml-rs"
|
name = "xml-rs"
|
||||||
version = "0.8.28"
|
version = "0.8.28"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::Instant;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use ruin_runtime::{TimeoutHandle, clear_timeout, set_timeout};
|
||||||
use ruin_ui::{
|
use ruin_ui::{
|
||||||
Color, CursorIcon, DisplayItem, Edges, Element, ElementId, InteractionTree, LayoutSnapshot,
|
Color, CursorIcon, DisplayItem, Edges, Element, ElementId, InteractionTree, KeyboardEvent,
|
||||||
PlatformEvent, PointerButton, PointerEvent, PointerEventKind, PointerRouter, Quad,
|
KeyboardEventKind, KeyboardKey, LayoutSnapshot, PlatformEvent, PointerButton, PointerEvent,
|
||||||
RoutedPointerEventKind, SceneSnapshot, TextAlign, TextFontFamily, TextSelectionStyle, TextSpan,
|
PointerEventKind, PointerRouter, Quad, RoutedPointerEventKind, SceneSnapshot, TextAlign,
|
||||||
TextSpanSlant, TextSpanWeight, TextStyle, TextSystem, UiSize, WindowSpec, WindowUpdate,
|
TextFontFamily, TextSelectionStyle, TextSpan, TextSpanSlant, TextSpanWeight, TextStyle,
|
||||||
|
TextSystem, TextWrap, UiSize, WindowController, WindowSpec, WindowUpdate,
|
||||||
layout_snapshot_with_text_system,
|
layout_snapshot_with_text_system,
|
||||||
};
|
};
|
||||||
use ruin_ui_platform_wayland::start_wayland_ui;
|
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_TITLE_ID: ElementId = ElementId::new(101);
|
||||||
const HERO_BODY_ID: ElementId = ElementId::new(102);
|
const HERO_BODY_ID: ElementId = ElementId::new(102);
|
||||||
const RUST_LINK_ID: ElementId = ElementId::new(103);
|
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 =
|
const DEMO_SELECTION_STYLE: TextSelectionStyle =
|
||||||
TextSelectionStyle::new(Color::rgba(0x6C, 0x8E, 0xFF, 0xB8))
|
TextSelectionStyle::new(Color::rgba(0x6C, 0x8E, 0xFF, 0xB8))
|
||||||
.with_text_color(Color::rgb(0x0D, 0x14, 0x25));
|
.with_text_color(Color::rgb(0x0D, 0x14, 0x25));
|
||||||
@@ -67,6 +74,153 @@ struct SelectionOutcome {
|
|||||||
copied_text: Option<String>,
|
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() {
|
fn install_tracing() {
|
||||||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||||
EnvFilter::new(
|
EnvFilter::new(
|
||||||
@@ -107,16 +261,43 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let mut in_flight_resize = None;
|
let mut in_flight_resize = None;
|
||||||
let mut latest_submitted_viewport = None;
|
let mut latest_submitted_viewport = None;
|
||||||
let mut current_cursor = CursorIcon::Default;
|
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 = None;
|
||||||
let mut selection_drag = None;
|
let mut selection_drag = None;
|
||||||
|
|
||||||
println!("Opening RUIN paragraph demo window...");
|
println!("Opening RUIN paragraph demo window...");
|
||||||
window.set_cursor_icon(current_cursor)?;
|
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 latest_configuration = None;
|
||||||
let mut resize_presented = false;
|
let mut resize_presented = false;
|
||||||
let mut pointer_events = Vec::new();
|
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 close_requested = false;
|
||||||
let mut closed = 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() => {
|
PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
|
||||||
pointer_events.push(event);
|
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 {
|
PlatformEvent::FramePresented {
|
||||||
window_id,
|
window_id,
|
||||||
scene_version,
|
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!(
|
tracing::trace!(
|
||||||
target: "ruin_ui_text_paragraph_demo::events",
|
target: "ruin_ui_text_paragraph_demo::events",
|
||||||
has_configured = latest_configuration.is_some(),
|
has_configured = latest_configuration.is_some(),
|
||||||
pointer_events = pointer_events.len(),
|
pointer_events = pointer_events.len(),
|
||||||
|
keyboard_events = keyboard_events.len(),
|
||||||
"processing coalesced event batch"
|
"processing coalesced event batch"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -204,7 +408,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let LayoutSnapshot {
|
let LayoutSnapshot {
|
||||||
scene,
|
scene,
|
||||||
interaction_tree: next_interaction_tree,
|
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| {
|
if selection.is_some_and(|selection: TextSelection| {
|
||||||
next_interaction_tree
|
next_interaction_tree
|
||||||
.text_for_element(selection.element_id)
|
.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,
|
build_ms = build_started.elapsed().as_secs_f64() * 1_000.0,
|
||||||
"finished snapshot rebuild for configured size"
|
"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);
|
base_scene = Some(scene);
|
||||||
latest_submitted_viewport = Some(viewport);
|
latest_submitted_viewport = Some(viewport);
|
||||||
in_flight_resize = Some(InFlightResize {
|
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_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 copied_text = None::<String>;
|
||||||
|
let mut request_primary_paste = false;
|
||||||
let resize_active =
|
let resize_active =
|
||||||
has_resize_configuration || pending_resize.is_some() || in_flight_resize.is_some();
|
has_resize_configuration || pending_resize.is_some() || in_flight_resize.is_some();
|
||||||
if !resize_active && let Some(current_interaction_tree) = interaction_tree.as_ref() {
|
if !resize_active && let Some(current_interaction_tree) = interaction_tree.as_ref() {
|
||||||
for event in pointer_events {
|
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(
|
let selection_outcome = handle_selection_event(
|
||||||
current_interaction_tree,
|
current_interaction_tree,
|
||||||
event,
|
event,
|
||||||
&mut selection,
|
&mut selection,
|
||||||
&mut selection_drag,
|
&mut selection_drag,
|
||||||
);
|
);
|
||||||
needs_selection_present |= selection_outcome.changed;
|
needs_overlay_present |= selection_outcome.changed;
|
||||||
if selection_outcome.copied_text.is_some() {
|
if selection_outcome.copied_text.is_some() {
|
||||||
copied_text = selection_outcome.copied_text;
|
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 {
|
if let Some(copied_text) = copied_text {
|
||||||
window.set_primary_selection_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);
|
version = version.wrapping_add(1);
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
target: "ruin_ui_text_paragraph_demo::hover",
|
target: "ruin_ui_text_paragraph_demo::hover",
|
||||||
@@ -297,7 +571,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let LayoutSnapshot {
|
let LayoutSnapshot {
|
||||||
scene,
|
scene,
|
||||||
interaction_tree: next_interaction_tree,
|
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| {
|
if selection.is_some_and(|selection: TextSelection| {
|
||||||
next_interaction_tree
|
next_interaction_tree
|
||||||
.text_for_element(selection.element_id)
|
.text_for_element(selection.element_id)
|
||||||
@@ -306,15 +587,32 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
selection = None;
|
selection = None;
|
||||||
selection_drag = 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);
|
base_scene = Some(scene);
|
||||||
interaction_tree = Some(next_interaction_tree);
|
interaction_tree = Some(next_interaction_tree);
|
||||||
} else if needs_selection_present
|
} else if needs_overlay_present
|
||||||
&& in_flight_resize.is_none()
|
&& in_flight_resize.is_none()
|
||||||
&& let Some(base_scene) = base_scene.as_ref()
|
&& let Some(base_scene) = base_scene.as_ref()
|
||||||
|
&& let Some(current_interaction_tree) = interaction_tree.as_ref()
|
||||||
{
|
{
|
||||||
version = version.wrapping_add(1);
|
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 {
|
if close_requested {
|
||||||
@@ -333,9 +631,11 @@ fn build_snapshot(
|
|||||||
viewport: UiSize,
|
viewport: UiSize,
|
||||||
version: u64,
|
version: u64,
|
||||||
hovered_card: Option<ElementId>,
|
hovered_card: Option<ElementId>,
|
||||||
|
input_fields: &[InputFieldState],
|
||||||
|
focused_element: Option<ElementId>,
|
||||||
text_system: &mut TextSystem,
|
text_system: &mut TextSystem,
|
||||||
) -> LayoutSnapshot {
|
) -> 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)
|
layout_snapshot_with_text_system(version, viewport, &tree, text_system)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +647,9 @@ fn handle_selection_event(
|
|||||||
) -> SelectionOutcome {
|
) -> SelectionOutcome {
|
||||||
let mut outcome = SelectionOutcome::default();
|
let mut outcome = SelectionOutcome::default();
|
||||||
match event.kind {
|
match event.kind {
|
||||||
PointerEventKind::Down { .. } => {
|
PointerEventKind::Down {
|
||||||
|
button: PointerButton::Primary,
|
||||||
|
} => {
|
||||||
let next_selection = interaction_tree
|
let next_selection = interaction_tree
|
||||||
.text_hit_test(event.position)
|
.text_hit_test(event.position)
|
||||||
.and_then(|hit| {
|
.and_then(|hit| {
|
||||||
@@ -382,7 +684,9 @@ fn handle_selection_event(
|
|||||||
*selection = Some(next_selection);
|
*selection = Some(next_selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PointerEventKind::Up { .. } => {
|
PointerEventKind::Up {
|
||||||
|
button: PointerButton::Primary,
|
||||||
|
} => {
|
||||||
let Some(drag) = selection_drag.take() else {
|
let Some(drag) = selection_drag.take() else {
|
||||||
return outcome;
|
return outcome;
|
||||||
};
|
};
|
||||||
@@ -405,27 +709,318 @@ fn handle_selection_event(
|
|||||||
*selection = Some(next_selection);
|
*selection = Some(next_selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PointerEventKind::Down { .. } | PointerEventKind::Up { .. } => {}
|
||||||
PointerEventKind::LeaveWindow => {}
|
PointerEventKind::LeaveWindow => {}
|
||||||
}
|
}
|
||||||
outcome
|
outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scene_with_selection(
|
fn handle_input_focus_event(
|
||||||
base_scene: &SceneSnapshot,
|
interaction_tree: &InteractionTree,
|
||||||
selection: Option<TextSelection>,
|
event: PointerEvent,
|
||||||
version: u64,
|
focused_element: &mut Option<ElementId>,
|
||||||
) -> SceneSnapshot {
|
input_fields: &mut [InputFieldState],
|
||||||
let Some(selection) = selection.filter(|selection| !selection.is_collapsed()) else {
|
selection: &mut Option<TextSelection>,
|
||||||
let mut scene = base_scene.clone();
|
) -> InputOutcome {
|
||||||
scene.version = version;
|
let mut outcome = InputOutcome::default();
|
||||||
return scene;
|
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();
|
let mut scene = base_scene.clone();
|
||||||
scene.version = version;
|
scene.version = version;
|
||||||
let mut items = Vec::with_capacity(scene.items.len() + 8);
|
let mut items = Vec::with_capacity(scene.items.len() + 8);
|
||||||
|
let selection = selection.filter(|selection| !selection.is_collapsed());
|
||||||
for item in scene.items.drain(..) {
|
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)
|
&& prepared_text.element_id == Some(selection.element_id)
|
||||||
{
|
{
|
||||||
for rect in prepared_text.selection_rects(selection.anchor, selection.focus) {
|
for rect in prepared_text.selection_rects(selection.anchor, selection.focus) {
|
||||||
@@ -441,6 +1036,16 @@ fn scene_with_selection(
|
|||||||
}
|
}
|
||||||
items.push(item);
|
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.items = items;
|
||||||
scene
|
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 gutter = (viewport.width * 0.025).clamp(18.0, 30.0);
|
||||||
let sidebar_width = (viewport.width * 0.28).clamp(220.0, 320.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)
|
.id(RUST_LINK_ID)
|
||||||
.cursor(CursorIcon::Pointer),
|
.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::row().flex(1.0).gap(gutter).children([
|
||||||
Element::column()
|
Element::column()
|
||||||
|
|||||||
@@ -79,6 +79,34 @@ fn log_platform_event(event: &PlatformEvent) {
|
|||||||
"pointer event received"
|
"pointer event received"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
PlatformEvent::Keyboard { window_id, event } => {
|
||||||
|
tracing::debug!(
|
||||||
|
event = "keyboard_event",
|
||||||
|
window_id = window_id.raw(),
|
||||||
|
keycode = event.keycode,
|
||||||
|
?event.kind,
|
||||||
|
?event.key,
|
||||||
|
?event.modifiers,
|
||||||
|
text = event.text.as_deref().unwrap_or(""),
|
||||||
|
"keyboard event received"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PlatformEvent::Wake { window_id, token } => {
|
||||||
|
tracing::debug!(
|
||||||
|
event = "wake_event",
|
||||||
|
window_id = window_id.raw(),
|
||||||
|
token,
|
||||||
|
"internal wake event received"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PlatformEvent::PrimarySelectionText { window_id, text } => {
|
||||||
|
tracing::debug!(
|
||||||
|
event = "primary_selection_text",
|
||||||
|
window_id = window_id.raw(),
|
||||||
|
text,
|
||||||
|
"primary selection text received"
|
||||||
|
);
|
||||||
|
}
|
||||||
PlatformEvent::CloseRequested { window_id } => {
|
PlatformEvent::CloseRequested { window_id } => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
event = "close_requested",
|
event = "close_requested",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::scene::Point;
|
|||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum PointerButton {
|
pub enum PointerButton {
|
||||||
Primary,
|
Primary,
|
||||||
|
Middle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -164,6 +165,7 @@ mod tests {
|
|||||||
element_id: None,
|
element_id: None,
|
||||||
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
||||||
pointer_events: false,
|
pointer_events: false,
|
||||||
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: vec![
|
children: vec![
|
||||||
@@ -172,6 +174,7 @@ mod tests {
|
|||||||
element_id: Some(ElementId::new(1)),
|
element_id: Some(ElementId::new(1)),
|
||||||
rect: Rect::new(0.0, 0.0, 120.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 120.0, 120.0),
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
@@ -181,6 +184,7 @@ mod tests {
|
|||||||
element_id: Some(ElementId::new(2)),
|
element_id: Some(ElementId::new(2)),
|
||||||
rect: Rect::new(80.0, 0.0, 120.0, 120.0),
|
rect: Rect::new(80.0, 0.0, 120.0, 120.0),
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
@@ -197,6 +201,7 @@ mod tests {
|
|||||||
element_id: None,
|
element_id: None,
|
||||||
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
||||||
pointer_events: false,
|
pointer_events: false,
|
||||||
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: vec![LayoutNode {
|
children: vec![LayoutNode {
|
||||||
@@ -204,6 +209,7 @@ mod tests {
|
|||||||
element_id: Some(ElementId::new(1)),
|
element_id: Some(ElementId::new(1)),
|
||||||
rect: Rect::new(0.0, 0.0, 160.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 160.0, 120.0),
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: vec![LayoutNode {
|
children: vec![LayoutNode {
|
||||||
@@ -211,6 +217,7 @@ mod tests {
|
|||||||
element_id: Some(ElementId::new(2)),
|
element_id: Some(ElementId::new(2)),
|
||||||
rect: Rect::new(16.0, 16.0, 80.0, 40.0),
|
rect: Rect::new(16.0, 16.0, 80.0, 40.0),
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
|
|||||||
57
lib/ui/src/keyboard.rs
Normal file
57
lib/ui/src/keyboard.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct KeyboardModifiers {
|
||||||
|
pub shift: bool,
|
||||||
|
pub control: bool,
|
||||||
|
pub alt: bool,
|
||||||
|
pub super_key: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum KeyboardKey {
|
||||||
|
Character(String),
|
||||||
|
Enter,
|
||||||
|
Tab,
|
||||||
|
Escape,
|
||||||
|
Backspace,
|
||||||
|
Delete,
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowRight,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown,
|
||||||
|
Home,
|
||||||
|
End,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum KeyboardEventKind {
|
||||||
|
Pressed,
|
||||||
|
Released,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct KeyboardEvent {
|
||||||
|
pub keycode: u32,
|
||||||
|
pub kind: KeyboardEventKind,
|
||||||
|
pub key: KeyboardKey,
|
||||||
|
pub modifiers: KeyboardModifiers,
|
||||||
|
pub text: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyboardEvent {
|
||||||
|
pub fn new(
|
||||||
|
keycode: u32,
|
||||||
|
kind: KeyboardEventKind,
|
||||||
|
key: KeyboardKey,
|
||||||
|
modifiers: KeyboardModifiers,
|
||||||
|
text: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
keycode,
|
||||||
|
kind,
|
||||||
|
key,
|
||||||
|
modifiers,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ pub struct HitTarget {
|
|||||||
pub path: LayoutPath,
|
pub path: LayoutPath,
|
||||||
pub element_id: Option<ElementId>,
|
pub element_id: Option<ElementId>,
|
||||||
pub rect: Rect,
|
pub rect: Rect,
|
||||||
|
pub focusable: bool,
|
||||||
pub cursor: CursorIcon,
|
pub cursor: CursorIcon,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ pub struct LayoutNode {
|
|||||||
pub element_id: Option<ElementId>,
|
pub element_id: Option<ElementId>,
|
||||||
pub rect: Rect,
|
pub rect: Rect,
|
||||||
pub pointer_events: bool,
|
pub pointer_events: bool,
|
||||||
|
pub focusable: bool,
|
||||||
pub cursor: CursorIcon,
|
pub cursor: CursorIcon,
|
||||||
pub prepared_text: Option<PreparedText>,
|
pub prepared_text: Option<PreparedText>,
|
||||||
pub children: Vec<LayoutNode>,
|
pub children: Vec<LayoutNode>,
|
||||||
@@ -184,6 +186,7 @@ fn layout_element(
|
|||||||
element_id: element.id,
|
element_id: element.id,
|
||||||
rect,
|
rect,
|
||||||
pointer_events: element.style.pointer_events,
|
pointer_events: element.style.pointer_events,
|
||||||
|
focusable: element.style.focusable,
|
||||||
cursor,
|
cursor,
|
||||||
prepared_text: None,
|
prepared_text: None,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
@@ -330,6 +333,7 @@ fn hit_path_node(node: &LayoutNode, point: crate::scene::Point) -> Option<Vec<Hi
|
|||||||
path: node.path.clone(),
|
path: node.path.clone(),
|
||||||
element_id: node.element_id,
|
element_id: node.element_id,
|
||||||
rect: node.rect,
|
rect: node.rect,
|
||||||
|
focusable: node.focusable,
|
||||||
cursor: node.cursor,
|
cursor: node.cursor,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -342,6 +346,7 @@ fn hit_path_node(node: &LayoutNode, point: crate::scene::Point) -> Option<Vec<Hi
|
|||||||
path: node.path.clone(),
|
path: node.path.clone(),
|
||||||
element_id: node.element_id,
|
element_id: node.element_id,
|
||||||
rect: node.rect,
|
rect: node.rect,
|
||||||
|
focusable: node.focusable,
|
||||||
cursor: node.cursor,
|
cursor: node.cursor,
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
@@ -373,6 +378,7 @@ fn text_hit_test_node(node: &LayoutNode, point: crate::scene::Point) -> Option<T
|
|||||||
path: node.path.clone(),
|
path: node.path.clone(),
|
||||||
element_id: node.element_id,
|
element_id: node.element_id,
|
||||||
rect: node.rect,
|
rect: node.rect,
|
||||||
|
focusable: node.focusable,
|
||||||
cursor: node.cursor,
|
cursor: node.cursor,
|
||||||
},
|
},
|
||||||
byte_offset: prepared_text.byte_offset_for_position(point),
|
byte_offset: prepared_text.byte_offset_for_position(point),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub(crate) mod trace_targets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod interaction;
|
mod interaction;
|
||||||
|
mod keyboard;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
@@ -23,6 +24,7 @@ pub use interaction::{
|
|||||||
PointerButton, PointerEvent, PointerEventKind, PointerRouter, RoutedPointerEvent,
|
PointerButton, PointerEvent, PointerEventKind, PointerRouter, RoutedPointerEvent,
|
||||||
RoutedPointerEventKind,
|
RoutedPointerEventKind,
|
||||||
};
|
};
|
||||||
|
pub use keyboard::{KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers};
|
||||||
pub use layout::{
|
pub use layout::{
|
||||||
HitTarget, InteractionTree, LayoutNode, LayoutPath, LayoutSnapshot, TextHitTarget,
|
HitTarget, InteractionTree, LayoutNode, LayoutPath, LayoutSnapshot, TextHitTarget,
|
||||||
layout_snapshot, layout_snapshot_with_text_system,
|
layout_snapshot, layout_snapshot_with_text_system,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use ruin_runtime::{WorkerHandle, queue_future, queue_microtask, spawn_worker};
|
|||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::interaction::PointerEvent;
|
use crate::interaction::PointerEvent;
|
||||||
|
use crate::keyboard::KeyboardEvent;
|
||||||
use crate::scene::{SceneSnapshot, UiSize};
|
use crate::scene::{SceneSnapshot, UiSize};
|
||||||
use crate::trace_targets;
|
use crate::trace_targets;
|
||||||
use crate::tree::CursorIcon;
|
use crate::tree::CursorIcon;
|
||||||
@@ -64,6 +65,18 @@ pub enum PlatformEvent {
|
|||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
event: PointerEvent,
|
event: PointerEvent,
|
||||||
},
|
},
|
||||||
|
Keyboard {
|
||||||
|
window_id: WindowId,
|
||||||
|
event: KeyboardEvent,
|
||||||
|
},
|
||||||
|
PrimarySelectionText {
|
||||||
|
window_id: WindowId,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
Wake {
|
||||||
|
window_id: WindowId,
|
||||||
|
token: u64,
|
||||||
|
},
|
||||||
CloseRequested {
|
CloseRequested {
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
},
|
},
|
||||||
@@ -98,6 +111,9 @@ pub enum PlatformRequest {
|
|||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
|
RequestPrimarySelectionText {
|
||||||
|
window_id: WindowId,
|
||||||
|
},
|
||||||
SetCursorIcon {
|
SetCursorIcon {
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
cursor: CursorIcon,
|
cursor: CursorIcon,
|
||||||
@@ -109,6 +125,14 @@ pub enum PlatformRequest {
|
|||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
event: PointerEvent,
|
event: PointerEvent,
|
||||||
},
|
},
|
||||||
|
EmitKeyboardEvent {
|
||||||
|
window_id: WindowId,
|
||||||
|
event: KeyboardEvent,
|
||||||
|
},
|
||||||
|
EmitWake {
|
||||||
|
window_id: WindowId,
|
||||||
|
token: u64,
|
||||||
|
},
|
||||||
Shutdown,
|
Shutdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +247,13 @@ impl PlatformProxy {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn request_primary_selection_text(
|
||||||
|
&self,
|
||||||
|
window_id: WindowId,
|
||||||
|
) -> Result<(), PlatformClosed> {
|
||||||
|
self.send(PlatformRequest::RequestPrimarySelectionText { window_id })
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_cursor_icon(
|
pub fn set_cursor_icon(
|
||||||
&self,
|
&self,
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
@@ -243,6 +274,18 @@ impl PlatformProxy {
|
|||||||
self.send(PlatformRequest::EmitPointerEvent { window_id, event })
|
self.send(PlatformRequest::EmitPointerEvent { window_id, event })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn emit_keyboard_event(
|
||||||
|
&self,
|
||||||
|
window_id: WindowId,
|
||||||
|
event: KeyboardEvent,
|
||||||
|
) -> Result<(), PlatformClosed> {
|
||||||
|
self.send(PlatformRequest::EmitKeyboardEvent { window_id, event })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn emit_wake(&self, window_id: WindowId, token: u64) -> Result<(), PlatformClosed> {
|
||||||
|
self.send(PlatformRequest::EmitWake { window_id, token })
|
||||||
|
}
|
||||||
|
|
||||||
pub fn shutdown(&self) -> Result<(), PlatformClosed> {
|
pub fn shutdown(&self) -> Result<(), PlatformClosed> {
|
||||||
self.send(PlatformRequest::Shutdown)
|
self.send(PlatformRequest::Shutdown)
|
||||||
}
|
}
|
||||||
@@ -286,6 +329,7 @@ pub fn start_headless() -> PlatformRuntime {
|
|||||||
handle_replace_scene(&state, window_id, scene);
|
handle_replace_scene(&state, window_id, scene);
|
||||||
}
|
}
|
||||||
PlatformRequest::SetPrimarySelectionText { .. } => {}
|
PlatformRequest::SetPrimarySelectionText { .. } => {}
|
||||||
|
PlatformRequest::RequestPrimarySelectionText { .. } => {}
|
||||||
PlatformRequest::SetCursorIcon { .. } => {}
|
PlatformRequest::SetCursorIcon { .. } => {}
|
||||||
PlatformRequest::EmitCloseRequested { window_id } => {
|
PlatformRequest::EmitCloseRequested { window_id } => {
|
||||||
let sender = state.borrow().events.clone();
|
let sender = state.borrow().events.clone();
|
||||||
@@ -294,6 +338,14 @@ pub fn start_headless() -> PlatformRuntime {
|
|||||||
PlatformRequest::EmitPointerEvent { window_id, event } => {
|
PlatformRequest::EmitPointerEvent { window_id, event } => {
|
||||||
handle_emit_pointer_event(&state, window_id, event);
|
handle_emit_pointer_event(&state, window_id, event);
|
||||||
}
|
}
|
||||||
|
PlatformRequest::EmitKeyboardEvent { window_id, event } => {
|
||||||
|
let sender = state.borrow().events.clone();
|
||||||
|
let _ = sender.send(PlatformEvent::Keyboard { window_id, event });
|
||||||
|
}
|
||||||
|
PlatformRequest::EmitWake { window_id, token } => {
|
||||||
|
let sender = state.borrow().events.clone();
|
||||||
|
let _ = sender.send(PlatformEvent::Wake { window_id, token });
|
||||||
|
}
|
||||||
PlatformRequest::Shutdown => {
|
PlatformRequest::Shutdown => {
|
||||||
debug!(
|
debug!(
|
||||||
target: trace_targets::PLATFORM,
|
target: trace_targets::PLATFORM,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use ruin_reactivity::{EffectHandle, effect};
|
use ruin_reactivity::{EffectHandle, effect};
|
||||||
|
|
||||||
|
use crate::keyboard::KeyboardEvent;
|
||||||
use crate::platform::{PlatformClosed, PlatformEvent, PlatformProxy, PlatformRuntime};
|
use crate::platform::{PlatformClosed, PlatformEvent, PlatformProxy, PlatformRuntime};
|
||||||
use crate::scene::SceneSnapshot;
|
use crate::scene::SceneSnapshot;
|
||||||
use crate::tree::CursorIcon;
|
use crate::tree::CursorIcon;
|
||||||
@@ -116,6 +117,11 @@ impl WindowController {
|
|||||||
self.proxy.set_primary_selection_text(self.id, text)
|
self.proxy.set_primary_selection_text(self.id, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Requests the current plain-text primary selection contents from the platform.
|
||||||
|
pub fn request_primary_selection_text(&self) -> Result<(), PlatformClosed> {
|
||||||
|
self.proxy.request_primary_selection_text(self.id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates the platform cursor icon for this window.
|
/// Updates the platform cursor icon for this window.
|
||||||
pub fn set_cursor_icon(&self, cursor: CursorIcon) -> Result<(), PlatformClosed> {
|
pub fn set_cursor_icon(&self, cursor: CursorIcon) -> Result<(), PlatformClosed> {
|
||||||
self.proxy.set_cursor_icon(self.id, cursor)
|
self.proxy.set_cursor_icon(self.id, cursor)
|
||||||
@@ -134,6 +140,16 @@ impl WindowController {
|
|||||||
self.proxy.emit_pointer_event(self.id, event)
|
self.proxy.emit_pointer_event(self.id, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delivers a keyboard event for this window through the platform event stream.
|
||||||
|
pub fn emit_keyboard_event(&self, event: KeyboardEvent) -> Result<(), PlatformClosed> {
|
||||||
|
self.proxy.emit_keyboard_event(self.id, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delivers an internal wake event for this window through the platform event stream.
|
||||||
|
pub fn emit_wake(&self, token: u64) -> Result<(), PlatformClosed> {
|
||||||
|
self.proxy.emit_wake(self.id, token)
|
||||||
|
}
|
||||||
|
|
||||||
/// Attaches a reactive effect that rebuilds and replaces the window scene whenever dependent UI
|
/// Attaches a reactive effect that rebuilds and replaces the window scene whenever dependent UI
|
||||||
/// state changes.
|
/// state changes.
|
||||||
pub fn attach_scene_effect(
|
pub fn attach_scene_effect(
|
||||||
@@ -156,7 +172,10 @@ mod tests {
|
|||||||
use crate::platform::PlatformEvent;
|
use crate::platform::PlatformEvent;
|
||||||
use crate::scene::{Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
|
use crate::scene::{Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
|
||||||
use crate::window::{WindowSpec, WindowUpdate};
|
use crate::window::{WindowSpec, WindowUpdate};
|
||||||
use crate::{PointerEvent, PointerEventKind};
|
use crate::{
|
||||||
|
KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers, PointerEvent,
|
||||||
|
PointerEventKind,
|
||||||
|
};
|
||||||
use ruin_runtime::{current_thread_handle, queue_future, run};
|
use ruin_runtime::{current_thread_handle, queue_future, run};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
@@ -310,4 +329,52 @@ mod tests {
|
|||||||
ui.shutdown().expect("shutdown should queue");
|
ui.shutdown().expect("shutdown should queue");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn window_controller_emits_keyboard_events_through_runtime() {
|
||||||
|
run_async_test(async move {
|
||||||
|
let mut ui = UiRuntime::headless();
|
||||||
|
let window = ui
|
||||||
|
.create_window(WindowSpec::new("keyboard-events").visible(true))
|
||||||
|
.expect("window should be created");
|
||||||
|
|
||||||
|
let _ = ui
|
||||||
|
.wait_for_event_matching(|event| {
|
||||||
|
matches!(event, PlatformEvent::Opened { window_id } if *window_id == window.id())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("window should open before keyboard delivery");
|
||||||
|
|
||||||
|
let keyboard_event = KeyboardEvent::new(
|
||||||
|
30,
|
||||||
|
KeyboardEventKind::Pressed,
|
||||||
|
KeyboardKey::Character("a".to_owned()),
|
||||||
|
KeyboardModifiers::default(),
|
||||||
|
Some("a".to_owned()),
|
||||||
|
);
|
||||||
|
window
|
||||||
|
.emit_keyboard_event(keyboard_event.clone())
|
||||||
|
.expect("keyboard event should queue");
|
||||||
|
|
||||||
|
let event = ui
|
||||||
|
.wait_for_event_matching(|event| {
|
||||||
|
matches!(
|
||||||
|
event,
|
||||||
|
PlatformEvent::Keyboard { window_id, .. } if *window_id == window.id()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("keyboard event should be delivered");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
event,
|
||||||
|
PlatformEvent::Keyboard {
|
||||||
|
window_id: window.id(),
|
||||||
|
event: keyboard_event,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.shutdown().expect("shutdown should queue");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,18 @@ impl PreparedText {
|
|||||||
rects
|
rects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn caret_rect(&self, offset: usize, width: f32) -> Option<Rect> {
|
||||||
|
let width = width.max(0.0);
|
||||||
|
let line = self.line_for_offset(offset)?;
|
||||||
|
let x = self.caret_x_for_line_offset(line, offset);
|
||||||
|
Some(Rect::new(
|
||||||
|
x,
|
||||||
|
line.rect.origin.y,
|
||||||
|
width,
|
||||||
|
line.rect.size.height,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_selected_text_color(&mut self, start: usize, end: usize) {
|
pub fn apply_selected_text_color(&mut self, start: usize, end: usize) {
|
||||||
let Some(selected_color) = self.selection_style.text_color else {
|
let Some(selected_color) = self.selection_style.text_color else {
|
||||||
return;
|
return;
|
||||||
@@ -260,6 +272,18 @@ impl PreparedText {
|
|||||||
Some(last)
|
Some(last)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn line_for_offset(&self, offset: usize) -> Option<&PreparedTextLine> {
|
||||||
|
let offset = offset.min(self.text.len());
|
||||||
|
let mut lines = self.lines.iter();
|
||||||
|
let first = lines.next()?;
|
||||||
|
for line in std::iter::once(first).chain(lines) {
|
||||||
|
if offset <= line.text_end {
|
||||||
|
return Some(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(self.lines.last().unwrap_or(first))
|
||||||
|
}
|
||||||
|
|
||||||
fn byte_offset_for_line_position(&self, line: &PreparedTextLine, x: f32) -> usize {
|
fn byte_offset_for_line_position(&self, line: &PreparedTextLine, x: f32) -> usize {
|
||||||
if line.glyph_start == line.glyph_end {
|
if line.glyph_start == line.glyph_end {
|
||||||
return line.text_start;
|
return line.text_start;
|
||||||
@@ -285,6 +309,33 @@ impl PreparedText {
|
|||||||
|
|
||||||
line.text_end
|
line.text_end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn caret_x_for_line_offset(&self, line: &PreparedTextLine, offset: usize) -> f32 {
|
||||||
|
if line.glyph_start == line.glyph_end {
|
||||||
|
return line.rect.origin.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = offset.min(self.text.len());
|
||||||
|
let line_glyphs = &self.glyphs[line.glyph_start..line.glyph_end];
|
||||||
|
let first_glyph = &line_glyphs[0];
|
||||||
|
if offset <= first_glyph.text_start {
|
||||||
|
return first_glyph.position.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
for glyph in line_glyphs {
|
||||||
|
if offset <= glyph.text_start {
|
||||||
|
return glyph.position.x;
|
||||||
|
}
|
||||||
|
if offset <= glyph.text_end {
|
||||||
|
return glyph.position.x + glyph.advance.max(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line_glyphs
|
||||||
|
.last()
|
||||||
|
.map(|glyph| glyph.position.x + glyph.advance.max(0.0))
|
||||||
|
.unwrap_or(line.rect.origin.x)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -409,4 +460,28 @@ mod tests {
|
|||||||
assert_eq!(text.glyphs[1].color, Color::rgb(0x11, 0x12, 0x1A));
|
assert_eq!(text.glyphs[1].color, Color::rgb(0x11, 0x12, 0x1A));
|
||||||
assert_eq!(text.glyphs[2].color, Color::rgb(0x11, 0x12, 0x1A));
|
assert_eq!(text.glyphs[2].color, Color::rgb(0x11, 0x12, 0x1A));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prepared_text_caret_rect_tracks_cluster_boundaries() {
|
||||||
|
let text = PreparedText::monospace(
|
||||||
|
"abcd",
|
||||||
|
Point::new(10.0, 20.0),
|
||||||
|
16.0,
|
||||||
|
8.0,
|
||||||
|
Color::rgb(0xFF, 0xFF, 0xFF),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
text.caret_rect(0, 2.0),
|
||||||
|
Some(Rect::new(10.0, 20.0, 2.0, 16.0))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
text.caret_rect(2, 2.0),
|
||||||
|
Some(Rect::new(26.0, 20.0, 2.0, 16.0))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
text.caret_rect(4, 2.0),
|
||||||
|
Some(Rect::new(42.0, 20.0, 2.0, 16.0))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ pub struct Style {
|
|||||||
pub padding: Edges,
|
pub padding: Edges,
|
||||||
pub background: Option<Color>,
|
pub background: Option<Color>,
|
||||||
pub pointer_events: bool,
|
pub pointer_events: bool,
|
||||||
|
pub focusable: bool,
|
||||||
pub cursor: Option<CursorIcon>,
|
pub cursor: Option<CursorIcon>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ impl Default for Style {
|
|||||||
padding: Edges::ZERO,
|
padding: Edges::ZERO,
|
||||||
background: None,
|
background: None,
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
|
focusable: false,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,6 +200,11 @@ impl Element {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn focusable(mut self, focusable: bool) -> Self {
|
||||||
|
self.style.focusable = focusable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cursor(mut self, cursor: CursorIcon) -> Self {
|
pub fn cursor(mut self, cursor: CursorIcon) -> Self {
|
||||||
self.style.cursor = Some(cursor);
|
self.style.cursor = Some(cursor);
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ tracing = "0.1"
|
|||||||
wayland-backend = { version = "0.3", features = ["client_system"] }
|
wayland-backend = { version = "0.3", features = ["client_system"] }
|
||||||
wayland-client = "0.31"
|
wayland-client = "0.31"
|
||||||
wayland-protocols = { version = "0.32", features = ["client", "staging", "unstable"] }
|
wayland-protocols = { version = "0.32", features = ["client", "staging", "unstable"] }
|
||||||
|
xkbcommon = "0.8"
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ use std::error::Error;
|
|||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
use std::io::Read;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
use std::os::fd::{AsFd, AsRawFd};
|
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
|
||||||
use std::ptr::NonNull;
|
use std::ptr::NonNull;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use raw_window_handle::{
|
use raw_window_handle::{
|
||||||
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
|
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
|
||||||
@@ -18,15 +19,17 @@ use raw_window_handle::{
|
|||||||
use ruin_runtime::channel::mpsc;
|
use ruin_runtime::channel::mpsc;
|
||||||
use ruin_runtime::{WorkerHandle, queue_future, queue_task, spawn_worker};
|
use ruin_runtime::{WorkerHandle, queue_future, queue_task, spawn_worker};
|
||||||
use ruin_ui::{
|
use ruin_ui::{
|
||||||
CursorIcon, PlatformEndpoint, PlatformEvent, PlatformRequest, PlatformRuntime, Point,
|
CursorIcon, KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers, PlatformEndpoint,
|
||||||
PointerButton, PointerEvent, PointerEventKind, SceneSnapshot, UiRuntime, UiSize,
|
PlatformEvent, PlatformRequest, PlatformRuntime, Point, PointerButton, PointerEvent,
|
||||||
WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
|
PointerEventKind, SceneSnapshot, UiRuntime, UiSize, WindowConfigured, WindowId,
|
||||||
|
WindowLifecycle, WindowSpec, WindowUpdate,
|
||||||
};
|
};
|
||||||
use ruin_ui_renderer_wgpu::{RenderError, WgpuSceneRenderer};
|
use ruin_ui_renderer_wgpu::{RenderError, WgpuSceneRenderer};
|
||||||
|
use tracing::Level;
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||||
use wayland_client::protocol::{
|
use wayland_client::protocol::{
|
||||||
wl_callback, wl_compositor, wl_pointer, wl_registry, wl_seat, wl_surface,
|
wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_surface,
|
||||||
};
|
};
|
||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop, event_created_child,
|
Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop, event_created_child,
|
||||||
@@ -39,6 +42,7 @@ use wayland_protocols::wp::primary_selection::zv1::client::{
|
|||||||
zwp_primary_selection_offer_v1, zwp_primary_selection_source_v1,
|
zwp_primary_selection_offer_v1, zwp_primary_selection_source_v1,
|
||||||
};
|
};
|
||||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||||
|
use xkbcommon::xkb;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WaylandSurfaceTarget {
|
pub struct WaylandSurfaceTarget {
|
||||||
@@ -114,6 +118,7 @@ struct WindowWorkerState {
|
|||||||
enum WindowWorkerCommand {
|
enum WindowWorkerCommand {
|
||||||
ReplaceScene(SceneSnapshot),
|
ReplaceScene(SceneSnapshot),
|
||||||
SetPrimarySelectionText(String),
|
SetPrimarySelectionText(String),
|
||||||
|
RequestPrimarySelectionText,
|
||||||
SetCursorIcon(CursorIcon),
|
SetCursorIcon(CursorIcon),
|
||||||
ApplySpec(WindowSpec),
|
ApplySpec(WindowSpec),
|
||||||
Shutdown,
|
Shutdown,
|
||||||
@@ -139,6 +144,11 @@ struct State {
|
|||||||
primary_selection_device: Option<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1>,
|
primary_selection_device: Option<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1>,
|
||||||
qh: QueueHandle<State>,
|
qh: QueueHandle<State>,
|
||||||
pointer: Option<wl_pointer::WlPointer>,
|
pointer: Option<wl_pointer::WlPointer>,
|
||||||
|
keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||||
|
keyboard_focused: bool,
|
||||||
|
xkb_context: xkb::Context,
|
||||||
|
xkb_keymap: Option<xkb::Keymap>,
|
||||||
|
xkb_state: Option<xkb::State>,
|
||||||
current_size: (u32, u32),
|
current_size: (u32, u32),
|
||||||
configured: bool,
|
configured: bool,
|
||||||
pending_size: Option<(u32, u32)>,
|
pending_size: Option<(u32, u32)>,
|
||||||
@@ -146,17 +156,54 @@ struct State {
|
|||||||
frame_callback: Option<wl_callback::WlCallback>,
|
frame_callback: Option<wl_callback::WlCallback>,
|
||||||
pointer_position: Option<Point>,
|
pointer_position: Option<Point>,
|
||||||
pending_pointer_events: Vec<PointerEvent>,
|
pending_pointer_events: Vec<PointerEvent>,
|
||||||
|
pending_keyboard_events: Vec<KeyboardEvent>,
|
||||||
|
keyboard_modifiers: KeyboardModifiers,
|
||||||
|
keyboard_repeat_rate: i32,
|
||||||
|
keyboard_repeat_delay: Duration,
|
||||||
|
keyboard_repeat: Option<KeyboardRepeatState>,
|
||||||
primary_selection_source: Option<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1>,
|
primary_selection_source: Option<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1>,
|
||||||
primary_selection_text: Option<String>,
|
primary_selection_text: Option<String>,
|
||||||
|
primary_selection_offer: Option<zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1>,
|
||||||
|
primary_selection_offer_mime_types: Vec<String>,
|
||||||
last_selection_serial: Option<u32>,
|
last_selection_serial: Option<u32>,
|
||||||
last_pointer_enter_serial: Option<u32>,
|
last_pointer_enter_serial: Option<u32>,
|
||||||
cursor_icon: CursorIcon,
|
cursor_icon: CursorIcon,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct KeyboardRepeatState {
|
||||||
|
keycode: u32,
|
||||||
|
next_at: Instant,
|
||||||
|
interval: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
fn request_redraw(&mut self) {
|
fn request_redraw(&mut self) {
|
||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn queue_ready_keyboard_repeats(&mut self) {
|
||||||
|
let Some(repeat) = self.keyboard_repeat.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(xkb_state) = self.xkb_state.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let now = Instant::now();
|
||||||
|
while now >= repeat.next_at {
|
||||||
|
let keycode = xkb::Keycode::new(repeat.keycode + 8);
|
||||||
|
let text = keyboard_text_for_xkb(xkb_state, keycode);
|
||||||
|
let key = keyboard_key_from_xkb(xkb_state.key_get_one_sym(keycode), text.as_deref());
|
||||||
|
self.pending_keyboard_events.push(KeyboardEvent::new(
|
||||||
|
repeat.keycode,
|
||||||
|
KeyboardEventKind::Pressed,
|
||||||
|
key,
|
||||||
|
self.keyboard_modifiers,
|
||||||
|
text,
|
||||||
|
));
|
||||||
|
repeat.next_at += repeat.interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wayland_cursor_shape(icon: CursorIcon) -> wp_cursor_shape_device_v1::Shape {
|
fn wayland_cursor_shape(icon: CursorIcon) -> wp_cursor_shape_device_v1::Shape {
|
||||||
@@ -178,6 +225,48 @@ fn apply_cursor_icon(state: &mut State) {
|
|||||||
let _ = state._connection.flush();
|
let _ = state._connection.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_modifiers_from_xkb(state: &xkb::State) -> KeyboardModifiers {
|
||||||
|
KeyboardModifiers {
|
||||||
|
shift: state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE),
|
||||||
|
control: state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE),
|
||||||
|
alt: state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE),
|
||||||
|
super_key: state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keyboard_text_for_xkb(state: &xkb::State, keycode: xkb::Keycode) -> Option<String> {
|
||||||
|
let text = state.key_get_utf8(keycode);
|
||||||
|
if text.is_empty() || text.chars().any(char::is_control) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keyboard_key_from_xkb(keysym: xkb::Keysym, text: Option<&str>) -> KeyboardKey {
|
||||||
|
match xkb::keysym_get_name(keysym).as_str() {
|
||||||
|
"BackSpace" => KeyboardKey::Backspace,
|
||||||
|
"Delete" => KeyboardKey::Delete,
|
||||||
|
"Return" => KeyboardKey::Enter,
|
||||||
|
"Tab" => KeyboardKey::Tab,
|
||||||
|
"Escape" => KeyboardKey::Escape,
|
||||||
|
"Left" => KeyboardKey::ArrowLeft,
|
||||||
|
"Right" => KeyboardKey::ArrowRight,
|
||||||
|
"Up" => KeyboardKey::ArrowUp,
|
||||||
|
"Down" => KeyboardKey::ArrowDown,
|
||||||
|
"Home" => KeyboardKey::Home,
|
||||||
|
"End" => KeyboardKey::End,
|
||||||
|
_ => text
|
||||||
|
.filter(|text| !text.is_empty())
|
||||||
|
.map(str::to_owned)
|
||||||
|
.or_else(|| {
|
||||||
|
let utf8 = xkb::keysym_to_utf8(keysym);
|
||||||
|
(!utf8.is_empty() && !utf8.chars().any(char::is_control)).then_some(utf8)
|
||||||
|
})
|
||||||
|
.map(KeyboardKey::Character)
|
||||||
|
.unwrap_or(KeyboardKey::Unknown),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WaylandWindow {
|
impl WaylandWindow {
|
||||||
pub fn open(spec: WindowSpec) -> Result<Self, Box<dyn Error>> {
|
pub fn open(spec: WindowSpec) -> Result<Self, Box<dyn Error>> {
|
||||||
let connection = Connection::connect_to_env()?;
|
let connection = Connection::connect_to_env()?;
|
||||||
@@ -241,6 +330,11 @@ impl WaylandWindow {
|
|||||||
primary_selection_device,
|
primary_selection_device,
|
||||||
qh,
|
qh,
|
||||||
pointer: None,
|
pointer: None,
|
||||||
|
keyboard: None,
|
||||||
|
keyboard_focused: false,
|
||||||
|
xkb_context: xkb::Context::new(xkb::CONTEXT_NO_FLAGS),
|
||||||
|
xkb_keymap: None,
|
||||||
|
xkb_state: None,
|
||||||
current_size: (initial_width, initial_height),
|
current_size: (initial_width, initial_height),
|
||||||
configured: false,
|
configured: false,
|
||||||
pending_size: None,
|
pending_size: None,
|
||||||
@@ -248,8 +342,15 @@ impl WaylandWindow {
|
|||||||
frame_callback: None,
|
frame_callback: None,
|
||||||
pointer_position: None,
|
pointer_position: None,
|
||||||
pending_pointer_events: Vec::new(),
|
pending_pointer_events: Vec::new(),
|
||||||
|
pending_keyboard_events: Vec::new(),
|
||||||
|
keyboard_modifiers: KeyboardModifiers::default(),
|
||||||
|
keyboard_repeat_rate: 25,
|
||||||
|
keyboard_repeat_delay: Duration::from_millis(500),
|
||||||
|
keyboard_repeat: None,
|
||||||
primary_selection_source: None,
|
primary_selection_source: None,
|
||||||
primary_selection_text: None,
|
primary_selection_text: None,
|
||||||
|
primary_selection_offer: None,
|
||||||
|
primary_selection_offer_mime_types: Vec::new(),
|
||||||
last_selection_serial: None,
|
last_selection_serial: None,
|
||||||
last_pointer_enter_serial: None,
|
last_pointer_enter_serial: None,
|
||||||
cursor_icon: CursorIcon::Default,
|
cursor_icon: CursorIcon::Default,
|
||||||
@@ -430,6 +531,43 @@ impl WaylandWindow {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_primary_selection_text(&mut self) -> Result<Option<String>, Box<dyn Error>> {
|
||||||
|
let preferred_mime = self
|
||||||
|
.state
|
||||||
|
.primary_selection_offer_mime_types
|
||||||
|
.iter()
|
||||||
|
.find(|mime| mime.as_str() == "text/plain;charset=utf-8")
|
||||||
|
.or_else(|| {
|
||||||
|
self.state
|
||||||
|
.primary_selection_offer_mime_types
|
||||||
|
.iter()
|
||||||
|
.find(|mime| mime.as_str() == "text/plain")
|
||||||
|
})
|
||||||
|
.cloned();
|
||||||
|
let Some(mime_type) = preferred_mime else {
|
||||||
|
return Ok(self.state.primary_selection_text.clone());
|
||||||
|
};
|
||||||
|
let Some(offer) = self.state.primary_selection_offer.as_ref() else {
|
||||||
|
return Ok(self.state.primary_selection_text.clone());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pipe_fds = [0; 2];
|
||||||
|
let pipe_result = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) };
|
||||||
|
if pipe_result != 0 {
|
||||||
|
return Err(Box::new(std::io::Error::last_os_error()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let read_fd = unsafe { OwnedFd::from_raw_fd(pipe_fds[0]) };
|
||||||
|
let write_fd = unsafe { OwnedFd::from_raw_fd(pipe_fds[1]) };
|
||||||
|
offer.receive(mime_type, write_fd.as_fd());
|
||||||
|
self.state._connection.flush()?;
|
||||||
|
drop(write_fd);
|
||||||
|
let mut file = File::from(read_fd);
|
||||||
|
let mut text = String::new();
|
||||||
|
file.read_to_string(&mut text)?;
|
||||||
|
Ok((!text.is_empty()).then_some(text))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_cursor_icon(&mut self, cursor: CursorIcon) -> Result<(), Box<dyn Error>> {
|
pub fn set_cursor_icon(&mut self, cursor: CursorIcon) -> Result<(), Box<dyn Error>> {
|
||||||
self.state.cursor_icon = cursor;
|
self.state.cursor_icon = cursor;
|
||||||
apply_cursor_icon(&mut self.state);
|
apply_cursor_icon(&mut self.state);
|
||||||
@@ -476,6 +614,11 @@ impl WaylandWindow {
|
|||||||
std::mem::take(&mut self.state.pending_pointer_events)
|
std::mem::take(&mut self.state.pending_pointer_events)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn drain_keyboard_events(&mut self) -> Vec<KeyboardEvent> {
|
||||||
|
self.state.queue_ready_keyboard_repeats();
|
||||||
|
std::mem::take(&mut self.state.pending_keyboard_events)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn prepare_frame(&mut self) -> Option<FrameRequest> {
|
pub fn prepare_frame(&mut self) -> Option<FrameRequest> {
|
||||||
if !self.state.configured {
|
if !self.state.configured {
|
||||||
return None;
|
return None;
|
||||||
@@ -537,6 +680,9 @@ fn run_wayland_platform(mut endpoint: PlatformEndpoint) {
|
|||||||
PlatformRequest::SetPrimarySelectionText { window_id, text } => {
|
PlatformRequest::SetPrimarySelectionText { window_id, text } => {
|
||||||
handle_set_primary_selection_text(&state, window_id, text);
|
handle_set_primary_selection_text(&state, window_id, text);
|
||||||
}
|
}
|
||||||
|
PlatformRequest::RequestPrimarySelectionText { window_id } => {
|
||||||
|
handle_request_primary_selection_text(&state, window_id);
|
||||||
|
}
|
||||||
PlatformRequest::SetCursorIcon { window_id, cursor } => {
|
PlatformRequest::SetCursorIcon { window_id, cursor } => {
|
||||||
handle_set_cursor_icon(&state, window_id, cursor);
|
handle_set_cursor_icon(&state, window_id, cursor);
|
||||||
}
|
}
|
||||||
@@ -546,6 +692,12 @@ fn run_wayland_platform(mut endpoint: PlatformEndpoint) {
|
|||||||
PlatformRequest::EmitPointerEvent { window_id, event } => {
|
PlatformRequest::EmitPointerEvent { window_id, event } => {
|
||||||
emit_wayland_event(&state, PlatformEvent::Pointer { window_id, event });
|
emit_wayland_event(&state, PlatformEvent::Pointer { window_id, event });
|
||||||
}
|
}
|
||||||
|
PlatformRequest::EmitKeyboardEvent { window_id, event } => {
|
||||||
|
emit_wayland_event(&state, PlatformEvent::Keyboard { window_id, event });
|
||||||
|
}
|
||||||
|
PlatformRequest::EmitWake { window_id, token } => {
|
||||||
|
emit_wayland_event(&state, PlatformEvent::Wake { window_id, token });
|
||||||
|
}
|
||||||
PlatformRequest::Shutdown => {
|
PlatformRequest::Shutdown => {
|
||||||
shutdown_wayland_backend(&state);
|
shutdown_wayland_backend(&state);
|
||||||
break;
|
break;
|
||||||
@@ -702,6 +854,21 @@ fn handle_set_primary_selection_text(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_request_primary_selection_text(
|
||||||
|
state: &Rc<RefCell<WaylandBackendState>>,
|
||||||
|
window_id: WindowId,
|
||||||
|
) {
|
||||||
|
if let Some(command_tx) = state
|
||||||
|
.borrow()
|
||||||
|
.windows
|
||||||
|
.get(&window_id)
|
||||||
|
.and_then(|record| record.worker.as_ref())
|
||||||
|
.map(|worker| worker.command_tx.clone())
|
||||||
|
{
|
||||||
|
let _ = command_tx.send(WindowWorkerCommand::RequestPrimarySelectionText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_set_cursor_icon(
|
fn handle_set_cursor_icon(
|
||||||
state: &Rc<RefCell<WaylandBackendState>>,
|
state: &Rc<RefCell<WaylandBackendState>>,
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
@@ -818,6 +985,28 @@ fn spawn_window_worker(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WindowWorkerCommand::RequestPrimarySelectionText => {
|
||||||
|
let mut state_ref = state.borrow_mut();
|
||||||
|
match state_ref.window.read_primary_selection_text() {
|
||||||
|
Ok(Some(text)) => {
|
||||||
|
let _ = state_ref.event_tx.send(
|
||||||
|
PlatformEvent::PrimarySelectionText {
|
||||||
|
window_id: state_ref.window_id,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(error) => {
|
||||||
|
debug!(
|
||||||
|
target: "ruin_ui_platform_wayland::clipboard",
|
||||||
|
window_id = state_ref.window_id.raw(),
|
||||||
|
error = %error,
|
||||||
|
"failed to read primary selection text"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
WindowWorkerCommand::SetCursorIcon(cursor) => {
|
WindowWorkerCommand::SetCursorIcon(cursor) => {
|
||||||
let mut state_ref = state.borrow_mut();
|
let mut state_ref = state.borrow_mut();
|
||||||
if let Err(error) = state_ref.window.set_cursor_icon(cursor) {
|
if let Err(error) = state_ref.window.set_cursor_icon(cursor) {
|
||||||
@@ -877,6 +1066,21 @@ fn pump_window_worker(state: Rc<RefCell<WindowWorkerState>>) {
|
|||||||
event,
|
event,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
for event in state_ref.window.drain_keyboard_events() {
|
||||||
|
tracing::trace!(
|
||||||
|
target: "ruin_ui_platform_wayland::event_bridge",
|
||||||
|
window_id = state_ref.window_id.raw(),
|
||||||
|
keycode = event.keycode,
|
||||||
|
?event.kind,
|
||||||
|
?event.key,
|
||||||
|
text = event.text.as_deref().unwrap_or(""),
|
||||||
|
"forwarding keyboard event to UI runtime"
|
||||||
|
);
|
||||||
|
let _ = state_ref.event_tx.send(PlatformEvent::Keyboard {
|
||||||
|
window_id: state_ref.window_id,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if !state_ref.window.is_running() {
|
if !state_ref.window.is_running() {
|
||||||
emit_window_closed(&mut state_ref, true);
|
emit_window_closed(&mut state_ref, true);
|
||||||
@@ -1086,8 +1290,6 @@ delegate_noop!(
|
|||||||
State: ignore
|
State: ignore
|
||||||
zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1
|
zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1
|
||||||
);
|
);
|
||||||
delegate_noop!(State: ignore zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1);
|
|
||||||
|
|
||||||
impl Dispatch<wl_seat::WlSeat, ()> for State {
|
impl Dispatch<wl_seat::WlSeat, ()> for State {
|
||||||
fn event(
|
fn event(
|
||||||
state: &mut Self,
|
state: &mut Self,
|
||||||
@@ -1116,6 +1318,17 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
|
|||||||
state.pointer_position = None;
|
state.pointer_position = None;
|
||||||
state.last_pointer_enter_serial = None;
|
state.last_pointer_enter_serial = None;
|
||||||
}
|
}
|
||||||
|
if capabilities.contains(wl_seat::Capability::Keyboard) {
|
||||||
|
if state.keyboard.is_none() {
|
||||||
|
state.keyboard = Some(seat.get_keyboard(qh, ()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.keyboard = None;
|
||||||
|
state.xkb_keymap = None;
|
||||||
|
state.xkb_state = None;
|
||||||
|
state.keyboard_modifiers = KeyboardModifiers::default();
|
||||||
|
state.keyboard_repeat = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1178,17 +1391,21 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
|||||||
let Some(position) = state.pointer_position else {
|
let Some(position) = state.pointer_position else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if button != 0x110 {
|
let button = match button {
|
||||||
return;
|
0x110 => PointerButton::Primary,
|
||||||
|
0x112 => PointerButton::Middle,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
if button == PointerButton::Primary {
|
||||||
|
state.last_selection_serial = Some(serial);
|
||||||
}
|
}
|
||||||
state.last_selection_serial = Some(serial);
|
|
||||||
let kind = match button_state {
|
let kind = match button_state {
|
||||||
WEnum::Value(wl_pointer::ButtonState::Pressed) => PointerEventKind::Down {
|
WEnum::Value(wl_pointer::ButtonState::Pressed) => {
|
||||||
button: PointerButton::Primary,
|
PointerEventKind::Down { button }
|
||||||
},
|
}
|
||||||
WEnum::Value(wl_pointer::ButtonState::Released) => PointerEventKind::Up {
|
WEnum::Value(wl_pointer::ButtonState::Released) => {
|
||||||
button: PointerButton::Primary,
|
PointerEventKind::Up { button }
|
||||||
},
|
}
|
||||||
WEnum::Value(_) | WEnum::Unknown(_) => return,
|
WEnum::Value(_) | WEnum::Unknown(_) => return,
|
||||||
};
|
};
|
||||||
state
|
state
|
||||||
@@ -1200,15 +1417,200 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dispatch<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, ()> for State {
|
impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||||
fn event(
|
fn event(
|
||||||
_state: &mut Self,
|
state: &mut Self,
|
||||||
_data_device: &zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1,
|
_keyboard: &wl_keyboard::WlKeyboard,
|
||||||
_event: zwp_primary_selection_device_v1::Event,
|
event: wl_keyboard::Event,
|
||||||
_data: &(),
|
_data: &(),
|
||||||
_conn: &Connection,
|
_conn: &Connection,
|
||||||
_qh: &QueueHandle<Self>,
|
_qh: &QueueHandle<Self>,
|
||||||
) {
|
) {
|
||||||
|
tracing::event!(
|
||||||
|
target: "ruin_ui_platform_wayland::keyboard",
|
||||||
|
Level::INFO,
|
||||||
|
?event,
|
||||||
|
"received keyboard event"
|
||||||
|
);
|
||||||
|
match event {
|
||||||
|
wl_keyboard::Event::Keymap { format, fd, size } => {
|
||||||
|
let WEnum::Value(wl_keyboard::KeymapFormat::XkbV1) = format else {
|
||||||
|
tracing::info!(
|
||||||
|
target: "ruin_ui_platform_wayland::keyboard",
|
||||||
|
?format,
|
||||||
|
"ignored unsupported keymap format"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(size) = usize::try_from(size) else {
|
||||||
|
tracing::info!(
|
||||||
|
target: "ruin_ui_platform_wayland::keyboard",
|
||||||
|
size,
|
||||||
|
"ignored keymap with invalid size"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let keymap = match unsafe {
|
||||||
|
xkb::Keymap::new_from_fd(
|
||||||
|
&state.xkb_context,
|
||||||
|
fd,
|
||||||
|
size,
|
||||||
|
xkb::KEYMAP_FORMAT_TEXT_V1,
|
||||||
|
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
||||||
|
)
|
||||||
|
} {
|
||||||
|
Ok(Some(keymap)) => keymap,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::info!(
|
||||||
|
target: "ruin_ui_platform_wayland::keyboard",
|
||||||
|
"failed to compile XKB keymap"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::info!(
|
||||||
|
target: "ruin_ui_platform_wayland::keyboard",
|
||||||
|
size,
|
||||||
|
error = %error,
|
||||||
|
"failed to map compositor keymap fd"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
state.xkb_state = Some(xkb::State::new(&keymap));
|
||||||
|
state.xkb_keymap = Some(keymap);
|
||||||
|
state.keyboard_modifiers = KeyboardModifiers::default();
|
||||||
|
state.keyboard_repeat = None;
|
||||||
|
tracing::info!(
|
||||||
|
target: "ruin_ui_platform_wayland::keyboard",
|
||||||
|
"installed XKB keymap"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
wl_keyboard::Event::Enter { .. } => {
|
||||||
|
state.keyboard_focused = true;
|
||||||
|
state.keyboard_repeat = None;
|
||||||
|
}
|
||||||
|
wl_keyboard::Event::Leave { .. } => {
|
||||||
|
state.keyboard_focused = false;
|
||||||
|
state.keyboard_modifiers = KeyboardModifiers::default();
|
||||||
|
state.keyboard_repeat = None;
|
||||||
|
}
|
||||||
|
wl_keyboard::Event::RepeatInfo { rate, delay } => {
|
||||||
|
state.keyboard_repeat_rate = rate;
|
||||||
|
state.keyboard_repeat_delay = Duration::from_millis(delay.max(0) as u64);
|
||||||
|
if rate <= 0 {
|
||||||
|
state.keyboard_repeat = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wl_keyboard::Event::Modifiers {
|
||||||
|
mods_depressed,
|
||||||
|
mods_latched,
|
||||||
|
mods_locked,
|
||||||
|
group,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let Some(xkb_state) = state.xkb_state.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
xkb_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
|
||||||
|
state.keyboard_modifiers = keyboard_modifiers_from_xkb(xkb_state);
|
||||||
|
}
|
||||||
|
wl_keyboard::Event::Key {
|
||||||
|
key,
|
||||||
|
state: key_state,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if !state.keyboard_focused {
|
||||||
|
tracing::info!(
|
||||||
|
target: "ruin_ui_platform_wayland::keyboard",
|
||||||
|
key,
|
||||||
|
?key_state,
|
||||||
|
"dropping key because keyboard focus is not active"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(xkb_state) = state.xkb_state.as_mut() else {
|
||||||
|
tracing::info!(
|
||||||
|
target: "ruin_ui_platform_wayland::keyboard",
|
||||||
|
key,
|
||||||
|
?key_state,
|
||||||
|
"dropping key because XKB state is not initialized"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (kind, direction) = match key_state {
|
||||||
|
WEnum::Value(wl_keyboard::KeyState::Pressed) => {
|
||||||
|
(KeyboardEventKind::Pressed, xkb::KeyDirection::Down)
|
||||||
|
}
|
||||||
|
WEnum::Value(wl_keyboard::KeyState::Released) => {
|
||||||
|
(KeyboardEventKind::Released, xkb::KeyDirection::Up)
|
||||||
|
}
|
||||||
|
WEnum::Value(_) | WEnum::Unknown(_) => return,
|
||||||
|
};
|
||||||
|
let keycode = xkb::Keycode::new(key + 8);
|
||||||
|
xkb_state.update_key(keycode, direction);
|
||||||
|
state.keyboard_modifiers = keyboard_modifiers_from_xkb(xkb_state);
|
||||||
|
let text = matches!(kind, KeyboardEventKind::Pressed)
|
||||||
|
.then(|| keyboard_text_for_xkb(xkb_state, keycode))
|
||||||
|
.flatten();
|
||||||
|
let logical_key =
|
||||||
|
keyboard_key_from_xkb(xkb_state.key_get_one_sym(keycode), text.as_deref());
|
||||||
|
state.pending_keyboard_events.push(KeyboardEvent::new(
|
||||||
|
key,
|
||||||
|
kind,
|
||||||
|
logical_key.clone(),
|
||||||
|
state.keyboard_modifiers,
|
||||||
|
text,
|
||||||
|
));
|
||||||
|
tracing::info!(
|
||||||
|
target: "ruin_ui_platform_wayland::keyboard",
|
||||||
|
keycode = key,
|
||||||
|
?kind,
|
||||||
|
?logical_key,
|
||||||
|
modifiers = ?state.keyboard_modifiers,
|
||||||
|
queued = state.pending_keyboard_events.len(),
|
||||||
|
"queued translated keyboard event"
|
||||||
|
);
|
||||||
|
if kind == KeyboardEventKind::Pressed
|
||||||
|
&& state.keyboard_repeat_rate > 0
|
||||||
|
&& state
|
||||||
|
.xkb_keymap
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|keymap| keymap.key_repeats(keycode))
|
||||||
|
{
|
||||||
|
state.keyboard_repeat = Some(KeyboardRepeatState {
|
||||||
|
keycode: key,
|
||||||
|
next_at: Instant::now() + state.keyboard_repeat_delay,
|
||||||
|
interval: Duration::from_secs_f64(
|
||||||
|
1.0 / f64::from(state.keyboard_repeat_rate),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else if state
|
||||||
|
.keyboard_repeat
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|repeat| repeat.keycode == key)
|
||||||
|
{
|
||||||
|
state.keyboard_repeat = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
_data_device: &zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1,
|
||||||
|
event: zwp_primary_selection_device_v1::Event,
|
||||||
|
_data: &(),
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
if let zwp_primary_selection_device_v1::Event::Selection { id } = event {
|
||||||
|
state.primary_selection_offer = id;
|
||||||
|
state.primary_selection_offer_mime_types.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event_created_child!(State, zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, [
|
event_created_child!(State, zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, [
|
||||||
@@ -1217,6 +1619,23 @@ impl Dispatch<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, ()>
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Dispatch<zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
offer: &zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1,
|
||||||
|
event: zwp_primary_selection_offer_v1::Event,
|
||||||
|
_data: &(),
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
if let zwp_primary_selection_offer_v1::Event::Offer { mime_type } = event
|
||||||
|
&& state.primary_selection_offer.as_ref() == Some(offer)
|
||||||
|
{
|
||||||
|
state.primary_selection_offer_mime_types.push(mime_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()> for State {
|
impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()> for State {
|
||||||
fn event(
|
fn event(
|
||||||
state: &mut Self,
|
state: &mut Self,
|
||||||
|
|||||||
Reference in New Issue
Block a user