351 lines
10 KiB
Rust
351 lines
10 KiB
Rust
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>,
|
|
pub(crate) hovered_elements: Signal<Vec<ElementId>>,
|
|
pub(crate) pressed_elements: Signal<Vec<ElementId>>,
|
|
pub(crate) interaction_version: Signal<u64>,
|
|
}
|
|
|
|
impl InputState {
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
current_cursor: CursorIcon::Default,
|
|
focused_element: None,
|
|
text_selection: Rc::new(TextSelectionState::new()),
|
|
hovered_elements: Signal::new(Vec::new()),
|
|
pressed_elements: Signal::new(Vec::new()),
|
|
interaction_version: Signal::new(0),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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
|
|
}
|
|
}
|