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; pub(crate) type ScrollHandler = Rc; pub(crate) type KeyHandler = Rc bool + 'static>; #[derive(Clone, Default)] pub(crate) struct EventBindings { pub(crate) on_press: HashMap, pub(crate) on_scroll: HashMap, pub(crate) on_key: HashMap, } 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, 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>, pub(crate) drag: RefCell>, pub(crate) version: Signal, } 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, pub(crate) text_selection: Rc, pub(crate) hovered_elements: Signal>, pub(crate) pressed_elements: Signal>, pub(crate) interaction_version: Signal, } 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, } impl ShortcutBinding { pub(crate) fn matches( &self, event: &KeyboardEvent, focused_element: Option, 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, ) { 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, ) -> crate::Result<()> { #[cfg(not(target_os = "linux"))] { let _ = (window, interaction_tree, selection); Ok(()) } #[cfg(target_os = "linux")] { 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, direct_target: Option, 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 { 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, focused_element: Option, 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> { 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, ) -> 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 } }