Refactor ruin_app, add some README files, minor usability in reactivity
This commit is contained in:
344
lib/ruin_app/src/input.rs
Normal file
344
lib/ruin_app/src/input.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use ruin_ui::{
|
||||
CursorIcon, DisplayItem, ElementId, HitTarget, InteractionTree, KeyboardEvent,
|
||||
PointerButton, PointerEvent, Quad, RoutedPointerEvent,
|
||||
RoutedPointerEventKind, WindowController,
|
||||
};
|
||||
|
||||
use crate::hooks::{Shortcut, ShortcutScope, Signal};
|
||||
use crate::view::View;
|
||||
|
||||
pub(crate) type PressHandler = Rc<dyn Fn(&RoutedPointerEvent) + 'static>;
|
||||
pub(crate) type ScrollHandler = Rc<dyn Fn(&RoutedPointerEvent, &InteractionTree) + 'static>;
|
||||
pub(crate) type KeyHandler = Rc<dyn Fn(&KeyboardEvent, &InteractionTree) -> bool + 'static>;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct EventBindings {
|
||||
pub(crate) on_press: HashMap<ElementId, PressHandler>,
|
||||
pub(crate) on_scroll: HashMap<ElementId, ScrollHandler>,
|
||||
pub(crate) on_key: HashMap<ElementId, KeyHandler>,
|
||||
}
|
||||
|
||||
impl EventBindings {
|
||||
pub(crate) fn extend(&mut self, other: EventBindings) {
|
||||
self.on_press.extend(other.on_press);
|
||||
self.on_scroll.extend(other.on_scroll);
|
||||
self.on_key.extend(other.on_key);
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch(
|
||||
&self,
|
||||
event: &RoutedPointerEvent,
|
||||
interaction_tree: &InteractionTree,
|
||||
hovered_targets: &[HitTarget],
|
||||
) {
|
||||
match event.kind {
|
||||
RoutedPointerEventKind::Up {
|
||||
button: PointerButton::Primary,
|
||||
} => {
|
||||
if let Some(element_id) = event.target.element_id
|
||||
&& let Some(handler) = self.on_press.get(&element_id)
|
||||
{
|
||||
handler(event);
|
||||
}
|
||||
if let Some(handler) = scroll_handler_for_event(
|
||||
&self.on_scroll,
|
||||
event.target.element_id,
|
||||
hovered_targets,
|
||||
) {
|
||||
handler(event, interaction_tree);
|
||||
}
|
||||
}
|
||||
RoutedPointerEventKind::Down {
|
||||
button: PointerButton::Primary,
|
||||
}
|
||||
| RoutedPointerEventKind::Move
|
||||
| RoutedPointerEventKind::Scroll { .. }
|
||||
| RoutedPointerEventKind::Leave => {
|
||||
if let Some(handler) = scroll_handler_for_event(
|
||||
&self.on_scroll,
|
||||
event.target.element_id,
|
||||
hovered_targets,
|
||||
) {
|
||||
handler(event, interaction_tree);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_key(
|
||||
&self,
|
||||
focused_element: Option<ElementId>,
|
||||
event: &KeyboardEvent,
|
||||
interaction_tree: &InteractionTree,
|
||||
) {
|
||||
let Some(handler) =
|
||||
key_handler_for_focus(&self.on_key, focused_element, interaction_tree)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let _ = handler(event, interaction_tree);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) struct ScrollbarDrag {
|
||||
pub(crate) start_pointer_y: f32,
|
||||
pub(crate) start_offset_y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) struct TextSelection {
|
||||
pub(crate) element_id: ElementId,
|
||||
pub(crate) anchor: usize,
|
||||
pub(crate) focus: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) struct TextSelectionDrag {
|
||||
pub(crate) element_id: ElementId,
|
||||
pub(crate) anchor: usize,
|
||||
}
|
||||
|
||||
pub(crate) struct TextSelectionState {
|
||||
pub(crate) selection: RefCell<Option<TextSelection>>,
|
||||
pub(crate) drag: RefCell<Option<TextSelectionDrag>>,
|
||||
pub(crate) version: Signal<u64>,
|
||||
}
|
||||
|
||||
impl TextSelectionState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
selection: RefCell::new(None),
|
||||
drag: RefCell::new(None),
|
||||
version: Signal::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InputState {
|
||||
pub(crate) current_cursor: CursorIcon,
|
||||
pub(crate) focused_element: Option<ElementId>,
|
||||
pub(crate) text_selection: Rc<TextSelectionState>,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
current_cursor: CursorIcon::Default,
|
||||
focused_element: None,
|
||||
text_selection: Rc::new(TextSelectionState::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ShortcutBinding {
|
||||
pub(crate) shortcut: Shortcut,
|
||||
pub(crate) scope: ShortcutScope,
|
||||
pub(crate) action: Rc<dyn Fn(&InteractionTree)>,
|
||||
}
|
||||
|
||||
impl ShortcutBinding {
|
||||
pub(crate) fn matches(
|
||||
&self,
|
||||
event: &KeyboardEvent,
|
||||
focused_element: Option<ElementId>,
|
||||
interaction_tree: &InteractionTree,
|
||||
) -> bool {
|
||||
self.shortcut.matches(event) && self.scope.matches(focused_element, interaction_tree)
|
||||
}
|
||||
|
||||
pub(crate) fn trigger(&self, interaction_tree: &InteractionTree) {
|
||||
(self.action)(interaction_tree);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clamp_scroll_offset(offset_y: f32, max_offset_y: f32) -> f32 {
|
||||
offset_y.clamp(0.0, max_offset_y.max(0.0))
|
||||
}
|
||||
|
||||
pub(crate) fn apply_text_selection_overlay(
|
||||
scene: &mut ruin_ui::SceneSnapshot,
|
||||
selection: Option<TextSelection>,
|
||||
) {
|
||||
let Some(selection) = selection else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut next_items = Vec::with_capacity(scene.items.len());
|
||||
for item in scene.items.drain(..) {
|
||||
match item {
|
||||
DisplayItem::Text(mut text) if text.element_id == Some(selection.element_id) => {
|
||||
for rect in text.selection_rects(selection.anchor, selection.focus) {
|
||||
next_items.push(DisplayItem::Quad(Quad::new(
|
||||
rect,
|
||||
text.selection_style.highlight_color,
|
||||
)));
|
||||
}
|
||||
text.apply_selected_text_color(selection.anchor, selection.focus);
|
||||
next_items.push(DisplayItem::Text(text));
|
||||
}
|
||||
other => next_items.push(other),
|
||||
}
|
||||
}
|
||||
scene.items = next_items;
|
||||
}
|
||||
|
||||
pub(crate) fn sync_primary_selection(
|
||||
window: &WindowController,
|
||||
interaction_tree: &InteractionTree,
|
||||
selection: Option<TextSelection>,
|
||||
) -> crate::Result<()> {
|
||||
let Some(selection) = selection else {
|
||||
window.set_primary_selection_text(String::new())?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(text) = interaction_tree.text_for_element(selection.element_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
let copied = text
|
||||
.selected_text(selection.anchor, selection.focus)
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
window.set_primary_selection_text(copied)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_handler_for_event<'a>(
|
||||
handlers: &'a HashMap<ElementId, ScrollHandler>,
|
||||
direct_target: Option<ElementId>,
|
||||
hovered_targets: &[HitTarget],
|
||||
) -> Option<&'a ScrollHandler> {
|
||||
if let Some(element_id) = direct_target
|
||||
&& let Some(handler) = handlers.get(&element_id)
|
||||
{
|
||||
return Some(handler);
|
||||
}
|
||||
|
||||
hovered_targets
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|target| target.element_id)
|
||||
.find_map(|element_id| handlers.get(&element_id))
|
||||
}
|
||||
|
||||
pub(crate) fn focused_element_for_pointer(
|
||||
interaction_tree: &InteractionTree,
|
||||
event: &PointerEvent,
|
||||
) -> Option<ElementId> {
|
||||
let hit_path = interaction_tree.hit_path(event.position);
|
||||
hit_path
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|target| target.focusable.then_some(target.element_id).flatten())
|
||||
.or_else(|| hit_path.iter().rev().find_map(|target| target.element_id))
|
||||
}
|
||||
|
||||
pub(crate) fn element_contains_element(
|
||||
interaction_tree: &InteractionTree,
|
||||
ancestor: ElementId,
|
||||
descendant: ElementId,
|
||||
) -> bool {
|
||||
fn contains_descendant(node: &ruin_ui::LayoutNode, descendant: ElementId) -> bool {
|
||||
if node.element_id == Some(descendant) {
|
||||
return true;
|
||||
}
|
||||
node.children
|
||||
.iter()
|
||||
.any(|child| contains_descendant(child, descendant))
|
||||
}
|
||||
|
||||
fn ancestor_contains(
|
||||
node: &ruin_ui::LayoutNode,
|
||||
ancestor: ElementId,
|
||||
descendant: ElementId,
|
||||
) -> bool {
|
||||
if node.element_id == Some(ancestor) {
|
||||
return contains_descendant(node, descendant);
|
||||
}
|
||||
node.children
|
||||
.iter()
|
||||
.any(|child| ancestor_contains(child, ancestor, descendant))
|
||||
}
|
||||
|
||||
ancestor_contains(&interaction_tree.root, ancestor, descendant)
|
||||
}
|
||||
|
||||
pub(crate) fn key_handler_for_focus<'a>(
|
||||
handlers: &'a HashMap<ElementId, KeyHandler>,
|
||||
focused_element: Option<ElementId>,
|
||||
interaction_tree: &InteractionTree,
|
||||
) -> Option<&'a KeyHandler> {
|
||||
let focused_element = focused_element?;
|
||||
focused_ancestor_chain(&interaction_tree.root, focused_element)?
|
||||
.into_iter()
|
||||
.find_map(|element_id| handlers.get(&element_id))
|
||||
}
|
||||
|
||||
pub(crate) fn focused_ancestor_chain(
|
||||
node: &ruin_ui::LayoutNode,
|
||||
focused_element: ElementId,
|
||||
) -> Option<Vec<ElementId>> {
|
||||
let mut chain = Vec::new();
|
||||
if build_focused_ancestor_chain(node, focused_element, &mut chain) {
|
||||
Some(chain)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_focused_ancestor_chain(
|
||||
node: &ruin_ui::LayoutNode,
|
||||
focused_element: ElementId,
|
||||
chain: &mut Vec<ElementId>,
|
||||
) -> bool {
|
||||
if node.element_id == Some(focused_element) {
|
||||
if let Some(element_id) = node.element_id {
|
||||
chain.push(element_id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
for child in &node.children {
|
||||
if build_focused_ancestor_chain(child, focused_element, chain) {
|
||||
if let Some(element_id) = node.element_id {
|
||||
chain.push(element_id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
// View helper methods for attaching handlers — used by primitives
|
||||
impl View {
|
||||
pub(crate) fn with_press_handler(mut self, element_id: ElementId, handler: PressHandler) -> Self {
|
||||
self.bindings.on_press.insert(element_id, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_scroll_handler(
|
||||
mut self,
|
||||
element_id: ElementId,
|
||||
handler: ScrollHandler,
|
||||
) -> Self {
|
||||
self.bindings.on_scroll.insert(element_id, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_key_handler(
|
||||
mut self,
|
||||
element_id: ElementId,
|
||||
handler: KeyHandler,
|
||||
) -> Self {
|
||||
self.bindings.on_key.insert(element_id, handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user