use crossterm::event::{ Event as TermEvent, KeyCode as TermKeyCode, KeyModifiers, MouseButton, MouseEventKind, }; use dioxus_core::*; use fxhash::{FxHashMap, FxHashSet}; use dioxus_html::geometry::euclid::{Point2D, Rect, Size2D}; use dioxus_html::geometry::{ ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint, WheelDelta, }; use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; use dioxus_html::{on::*, KeyCode}; use std::str::FromStr; use std::{ any::Any, cell::RefCell, rc::Rc, sync::Arc, time::{Duration, Instant}, }; use stretch2::geometry::{Point, Size}; use stretch2::{prelude::Layout, Stretch}; use crate::{Dom, Node}; // a wrapper around the input state for easier access // todo: fix loop // pub struct InputState(Rc>>); // impl InputState { // pub fn get(cx: &ScopeState) -> InputState { // let inner = cx // .consume_context::>>() // .expect("Rink InputState can only be used in Rink apps!"); // (**inner).borrow_mut().subscribe(cx.schedule_update()); // InputState(inner) // } // pub fn mouse(&self) -> Option { // let data = (**self.0).borrow(); // data.mouse.as_ref().map(|m| m.clone()) // } // pub fn wheel(&self) -> Option { // let data = (**self.0).borrow(); // data.wheel.as_ref().map(|w| w.clone()) // } // pub fn screen(&self) -> Option<(u16, u16)> { // let data = (**self.0).borrow(); // data.screen.as_ref().map(|m| m.clone()) // } // pub fn last_key_pressed(&self) -> Option { // let data = (**self.0).borrow(); // data.last_key_pressed // .as_ref() // .map(|k| &k.0.clone()) // } // } type EventCore = (&'static str, EventData); #[derive(Debug)] enum EventData { Mouse(MouseData), Wheel(WheelData), Screen((u16, u16)), Keyboard(KeyboardData), } impl EventData { fn into_any(self) -> Arc { match self { Self::Mouse(m) => Arc::new(m), Self::Wheel(w) => Arc::new(w), Self::Screen(s) => Arc::new(s), Self::Keyboard(k) => Arc::new(k), } } } const MAX_REPEAT_TIME: Duration = Duration::from_millis(100); pub struct InnerInputState { mouse: Option, wheel: Option, last_key_pressed: Option<(KeyboardData, Instant)>, screen: Option<(u16, u16)>, // subscribers: Vec>, } impl InnerInputState { fn new() -> Self { Self { mouse: None, wheel: None, last_key_pressed: None, screen: None, // subscribers: Vec::new(), } } // stores current input state and transforms events based on that state fn apply_event(&mut self, evt: &mut EventCore) { match evt.1 { // limitations: only two buttons may be held at once EventData::Mouse(ref mut m) => { let mut held_buttons = match &self.mouse { Some(previous_data) => previous_data.held_buttons(), None => MouseButtonSet::empty(), }; match evt.0 { "mousedown" => { held_buttons.insert( m.trigger_button() .expect("No trigger button for mousedown event"), ); } "mouseup" => { held_buttons.remove( m.trigger_button() .expect("No trigger button for mouseup event"), ); } _ => {} } let new_mouse_data = MouseData::new( m.coordinates(), m.trigger_button(), held_buttons, m.modifiers(), ); self.mouse = Some(new_mouse_data.clone()); *m = new_mouse_data; } EventData::Wheel(ref w) => self.wheel = Some(w.clone()), EventData::Screen(ref s) => self.screen = Some(*s), EventData::Keyboard(ref mut k) => { let repeat = self .last_key_pressed .as_ref() .filter(|k2| k2.0.key == k.key && k2.1.elapsed() < MAX_REPEAT_TIME) .is_some(); k.repeat = repeat; let new = k.clone(); self.last_key_pressed = Some((new, Instant::now())); } } } fn update( &mut self, evts: &mut [EventCore], resolved_events: &mut Vec, layout: &Stretch, dom: &mut Dom, ) { let previous_mouse = self.mouse.clone(); self.wheel = None; for e in evts.iter_mut() { self.apply_event(e); } self.resolve_mouse_events(previous_mouse, resolved_events, layout, dom); // for s in &self.subscribers { // s(); // } } fn resolve_mouse_events( &self, previous_mouse: Option, resolved_events: &mut Vec, layout: &Stretch, dom: &mut Dom, ) { fn layout_contains_point(layout: &Layout, point: ScreenPoint) -> bool { let Point { x, y } = layout.location; let Size { width, height } = layout.size; let layout_rect = Rect::new(Point2D::new(x, y), Size2D::new(width, height)); layout_rect.contains(point.cast()) } fn try_create_event( name: &'static str, data: Arc, will_bubble: &mut FxHashSet, resolved_events: &mut Vec, node: &Node, dom: &Dom, ) { // only trigger event if the event was not triggered already by a child if will_bubble.insert(node.id) { let mut parent = node.parent; while let Some(parent_id) = parent { will_bubble.insert(parent_id); parent = dom[parent_id.0].parent; } resolved_events.push(UserEvent { scope_id: None, priority: EventPriority::Medium, name, element: Some(node.id), data, }) } } fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData { let Point { x, y } = layout.location; let node_origin = ClientPoint::new(x.into(), y.into()); let new_client_coordinates = (mouse_data.client_coordinates() - node_origin) .to_point() .cast_unit(); let coordinates = Coordinates::new( mouse_data.screen_coordinates(), mouse_data.client_coordinates(), new_client_coordinates, mouse_data.page_coordinates(), ); MouseData::new( coordinates, mouse_data.trigger_button(), mouse_data.held_buttons(), mouse_data.modifiers(), ) } if let Some(mouse_data) = &self.mouse { let new_pos = mouse_data.screen_coordinates(); let old_pos = previous_mouse.as_ref().map(|m| m.screen_coordinates()); // a mouse button is pressed if a button was not down and is now down let previous_buttons = previous_mouse .map_or(MouseButtonSet::empty(), |previous_data| { previous_data.held_buttons() }); let was_pressed = !(mouse_data.held_buttons() - previous_buttons).is_empty(); // a mouse button is released if a button was down and is now not down let was_released = !(previous_buttons - mouse_data.held_buttons()).is_empty(); let was_scrolled = self .wheel .as_ref() .map_or(false, |data| !data.delta().is_zero()); let wheel_data = &self.wheel; { // mousemove if old_pos != Some(new_pos) { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mousemove") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let previously_contained = old_pos .filter(|pos| layout_contains_point(node_layout, *pos)) .is_some(); let currently_contains = layout_contains_point(node_layout, new_pos); if currently_contains && previously_contained { try_create_event( "mousemove", Arc::new(prepare_mouse_data(mouse_data, node_layout)), &mut will_bubble, resolved_events, node, dom, ); } } } } { // mouseenter let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseenter") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let previously_contained = old_pos .filter(|pos| layout_contains_point(node_layout, *pos)) .is_some(); let currently_contains = layout_contains_point(node_layout, new_pos); if currently_contains && !previously_contained { try_create_event( "mouseenter", Arc::new(mouse_data.clone()), &mut will_bubble, resolved_events, node, dom, ); } } } { // mouseover let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseover") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let previously_contained = old_pos .filter(|pos| layout_contains_point(node_layout, *pos)) .is_some(); let currently_contains = layout_contains_point(node_layout, new_pos); if currently_contains && !previously_contained { try_create_event( "mouseover", Arc::new(prepare_mouse_data(mouse_data, node_layout)), &mut will_bubble, resolved_events, node, dom, ); } } } // mousedown if was_pressed { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mousedown") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let currently_contains = layout_contains_point(node_layout, new_pos); if currently_contains { try_create_event( "mousedown", Arc::new(prepare_mouse_data(mouse_data, node_layout)), &mut will_bubble, resolved_events, node, dom, ); } } } { // mouseup if was_released { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseup") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let currently_contains = layout_contains_point(node_layout, new_pos); if currently_contains { try_create_event( "mouseup", Arc::new(prepare_mouse_data(mouse_data, node_layout)), &mut will_bubble, resolved_events, node, dom, ); } } } } { // click if mouse_data.trigger_button() == Some(DioxusMouseButton::Primary) && was_released { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("click") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let currently_contains = layout_contains_point(node_layout, new_pos); if currently_contains { try_create_event( "click", Arc::new(prepare_mouse_data(mouse_data, node_layout)), &mut will_bubble, resolved_events, node, dom, ); } } } } { // contextmenu if mouse_data.trigger_button() == Some(DioxusMouseButton::Secondary) && was_released { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("contextmenu") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let currently_contains = layout_contains_point(node_layout, new_pos); if currently_contains { try_create_event( "contextmenu", Arc::new(prepare_mouse_data(mouse_data, node_layout)), &mut will_bubble, resolved_events, node, dom, ); } } } } { // wheel if let Some(w) = wheel_data { if was_scrolled { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("wheel") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let currently_contains = layout_contains_point(node_layout, new_pos); if currently_contains { try_create_event( "wheel", Arc::new(w.clone()), &mut will_bubble, resolved_events, node, dom, ); } } } } } { // mouseleave let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseleave") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let previously_contained = old_pos .filter(|pos| layout_contains_point(node_layout, *pos)) .is_some(); let currently_contains = layout_contains_point(node_layout, new_pos); if !currently_contains && previously_contained { try_create_event( "mouseleave", Arc::new(prepare_mouse_data(mouse_data, node_layout)), &mut will_bubble, resolved_events, node, dom, ); } } } { // mouseout let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseout") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let previously_contained = old_pos .filter(|pos| layout_contains_point(node_layout, *pos)) .is_some(); let currently_contains = layout_contains_point(node_layout, new_pos); if !currently_contains && previously_contained { try_create_event( "mouseout", Arc::new(prepare_mouse_data(mouse_data, node_layout)), &mut will_bubble, resolved_events, node, dom, ); } } } } } // fn subscribe(&mut self, f: Rc) { // self.subscribers.push(f) // } } pub struct RinkInputHandler { state: Rc>, queued_events: Rc>>, } impl RinkInputHandler { /// global context that handles events /// limitations: GUI key modifier is never detected, key up events are not detected, and only two mouse buttons may be pressed at once pub fn new() -> ( Self, Rc>, impl FnMut(crossterm::event::Event), ) { let queued_events = Rc::new(RefCell::new(Vec::new())); let queued_events2 = Rc::downgrade(&queued_events); let regester_event = move |evt: crossterm::event::Event| { if let Some(evt) = get_event(evt) { if let Some(v) = queued_events2.upgrade() { (*v).borrow_mut().push(evt); } } }; let state = Rc::new(RefCell::new(InnerInputState::new())); ( Self { state: state.clone(), queued_events, }, state, regester_event, ) } pub(crate) fn get_events(&self, layout: &Stretch, dom: &mut Dom) -> Vec { let mut resolved_events = Vec::new(); (*self.state).borrow_mut().update( &mut (*self.queued_events).borrow_mut(), &mut resolved_events, layout, dom, ); let events = self .queued_events .replace(Vec::new()) .into_iter() // these events were added in the update stage .filter(|e| { ![ "mouseenter", "mouseover", "mouseleave", "mouseout", "mousedown", "mouseup", "mousemove", "drag", "wheel", "click", "contextmenu", ] .contains(&e.0) }) .map(|evt| (evt.0, evt.1.into_any())); // todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus let mut hm: FxHashMap<&'static str, Vec>> = FxHashMap::default(); for (event, data) in events { if let Some(v) = hm.get_mut(event) { v.push(data); } else { hm.insert(event, vec![data]); } } for (event, datas) in hm { for node in dom.get_listening_sorted(event) { for data in &datas { resolved_events.push(UserEvent { scope_id: None, priority: EventPriority::Medium, name: event, element: Some(node.id), data: data.clone(), }); } } } resolved_events } } // translate crossterm events into dioxus events fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { let (name, data): (&str, EventData) = match evt { TermEvent::Key(k) => ("keydown", translate_key_event(k)?), TermEvent::Mouse(m) => { let (x, y) = (m.column.into(), m.row.into()); let alt = m.modifiers.contains(KeyModifiers::ALT); let shift = m.modifiers.contains(KeyModifiers::SHIFT); let ctrl = m.modifiers.contains(KeyModifiers::CONTROL); let meta = false; let get_mouse_data = |crossterm_button: Option| { let button = crossterm_button.map(|b| match b { MouseButton::Left => DioxusMouseButton::Primary, MouseButton::Right => DioxusMouseButton::Secondary, MouseButton::Middle => DioxusMouseButton::Auxiliary, }); // from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent // The `page` and `screen` coordinates are inconsistent with the MDN definition, as they are relative to the viewport (client), not the target element/page/screen, respectively. // todo? // But then, MDN defines them in terms of pixels, yet crossterm provides only row/column, and it might not be possible to get pixels. So we can't get 100% consistency anyway. let coordinates = Coordinates::new( ScreenPoint::new(x, y), ClientPoint::new(x, y), // offset x/y are set when the origin of the event is assigned to an element ElementPoint::new(0., 0.), PagePoint::new(x, y), ); let mut modifiers = Modifiers::empty(); if shift { modifiers.insert(Modifiers::SHIFT); } if ctrl { modifiers.insert(Modifiers::CONTROL); } if meta { modifiers.insert(Modifiers::META); } if alt { modifiers.insert(Modifiers::ALT); } // held mouse buttons get set later by maintaining state, as crossterm does not provide them EventData::Mouse(MouseData::new( coordinates, button, DioxusMouseButtons::empty(), modifiers, )) }; let get_wheel_data = |up| { let y = if up { -1.0 } else { 1.0 }; EventData::Wheel(WheelData::new(WheelDelta::lines(0., y, 0.))) }; match m.kind { MouseEventKind::Down(b) => ("mousedown", get_mouse_data(Some(b))), MouseEventKind::Up(b) => ("mouseup", get_mouse_data(Some(b))), MouseEventKind::Drag(b) => ("drag", get_mouse_data(Some(b))), MouseEventKind::Moved => ("mousemove", get_mouse_data(None)), MouseEventKind::ScrollDown => ("wheel", get_wheel_data(false)), MouseEventKind::ScrollUp => ("wheel", get_wheel_data(true)), } } TermEvent::Resize(x, y) => ("resize", EventData::Screen((x, y))), }; Some((name, data)) } fn translate_key_event(event: crossterm::event::KeyEvent) -> Option { let key = key_from_crossterm_key_code(event.code); // crossterm does not provide code. we make a guess as to which key might have been pressed // this is probably garbage if the user has a custom keyboard layout let code = guess_code_from_crossterm_key_code(event.code)?; let modifiers = modifiers_from_crossterm_modifiers(event.modifiers); Some(EventData::Keyboard(KeyboardData::new( key, code, Location::Standard, false, modifiers, ))) } /// The crossterm key_code nicely represents the meaning of the key and we can mostly convert it without any issues /// /// Exceptions: /// BackTab and Null are converted to Key::Unidentified fn key_from_crossterm_key_code(key_code: TermKeyCode) -> Key { match key_code { TermKeyCode::Backspace => Key::Backspace, TermKeyCode::Enter => Key::Enter, TermKeyCode::Left => Key::ArrowLeft, TermKeyCode::Right => Key::ArrowRight, TermKeyCode::Up => Key::ArrowUp, TermKeyCode::Down => Key::ArrowDown, TermKeyCode::Home => Key::Home, TermKeyCode::End => Key::End, TermKeyCode::PageUp => Key::PageUp, TermKeyCode::PageDown => Key::PageDown, TermKeyCode::Tab => Key::Tab, // ? no corresponding Key TermKeyCode::BackTab => Key::Unidentified, TermKeyCode::Delete => Key::Delete, TermKeyCode::Insert => Key::Insert, TermKeyCode::F(1) => Key::F1, TermKeyCode::F(2) => Key::F2, TermKeyCode::F(3) => Key::F3, TermKeyCode::F(4) => Key::F4, TermKeyCode::F(5) => Key::F5, TermKeyCode::F(6) => Key::F6, TermKeyCode::F(7) => Key::F7, TermKeyCode::F(8) => Key::F8, TermKeyCode::F(9) => Key::F9, TermKeyCode::F(10) => Key::F10, TermKeyCode::F(11) => Key::F11, TermKeyCode::F(12) => Key::F12, TermKeyCode::F(13) => Key::F13, TermKeyCode::F(14) => Key::F14, TermKeyCode::F(15) => Key::F15, TermKeyCode::F(16) => Key::F16, TermKeyCode::F(17) => Key::F17, TermKeyCode::F(18) => Key::F18, TermKeyCode::F(19) => Key::F19, TermKeyCode::F(20) => Key::F20, TermKeyCode::F(21) => Key::F21, TermKeyCode::F(22) => Key::F22, TermKeyCode::F(23) => Key::F23, TermKeyCode::F(24) => Key::F24, TermKeyCode::F(other) => { panic!("Unexpected function key: {other:?}") } TermKeyCode::Char(c) => Key::Character(c.to_string()), TermKeyCode::Null => Key::Unidentified, TermKeyCode::Esc => Key::Escape, } } // Crossterm does not provide a way to get the `code` (physical key on keyboard) // So we make a guess based on their `key_code`, but this is probably going to break on anything other than a very standard european keyboard // It may look fine, but it's a horrible hack. fn guess_code_from_crossterm_key_code(key_code: TermKeyCode) -> Option { let code = match key_code { TermKeyCode::Backspace => Code::Backspace, TermKeyCode::Enter => Code::Enter, TermKeyCode::Left => Code::ArrowLeft, TermKeyCode::Right => Code::ArrowRight, TermKeyCode::Up => Code::ArrowUp, TermKeyCode::Down => Code::ArrowDown, TermKeyCode::Home => Code::Home, TermKeyCode::End => Code::End, TermKeyCode::PageUp => Code::PageUp, TermKeyCode::PageDown => Code::PageDown, TermKeyCode::Tab => Code::Tab, // ? no corresponding Code TermKeyCode::BackTab => Code::Tab, TermKeyCode::Delete => Code::Delete, TermKeyCode::Insert => Code::Insert, TermKeyCode::F(1) => Code::F1, TermKeyCode::F(2) => Code::F2, TermKeyCode::F(3) => Code::F3, TermKeyCode::F(4) => Code::F4, TermKeyCode::F(5) => Code::F5, TermKeyCode::F(6) => Code::F6, TermKeyCode::F(7) => Code::F7, TermKeyCode::F(8) => Code::F8, TermKeyCode::F(9) => Code::F9, TermKeyCode::F(10) => Code::F10, TermKeyCode::F(11) => Code::F11, TermKeyCode::F(12) => Code::F12, TermKeyCode::F(13) => Code::F13, TermKeyCode::F(14) => Code::F14, TermKeyCode::F(15) => Code::F15, TermKeyCode::F(16) => Code::F16, TermKeyCode::F(17) => Code::F17, TermKeyCode::F(18) => Code::F18, TermKeyCode::F(19) => Code::F19, TermKeyCode::F(20) => Code::F20, TermKeyCode::F(21) => Code::F21, TermKeyCode::F(22) => Code::F22, TermKeyCode::F(23) => Code::F23, TermKeyCode::F(24) => Code::F24, TermKeyCode::F(other) => { panic!("Unexpected function key: {other:?}") } // this is a horrible way for crossterm to represent keys but we have to deal with it TermKeyCode::Char(c) => match c { 'A'..='Z' | 'a'..='z' => match c.to_ascii_uppercase() { 'A' => Code::KeyA, 'B' => Code::KeyB, 'C' => Code::KeyC, 'D' => Code::KeyD, 'E' => Code::KeyE, 'F' => Code::KeyF, 'G' => Code::KeyG, 'H' => Code::KeyH, 'I' => Code::KeyI, 'J' => Code::KeyJ, 'K' => Code::KeyK, 'L' => Code::KeyL, 'M' => Code::KeyM, 'N' => Code::KeyN, 'O' => Code::KeyO, 'P' => Code::KeyP, 'Q' => Code::KeyQ, 'R' => Code::KeyR, 'S' => Code::KeyS, 'T' => Code::KeyT, 'U' => Code::KeyU, 'V' => Code::KeyV, 'W' => Code::KeyW, 'X' => Code::KeyX, 'Y' => Code::KeyY, 'Z' => Code::KeyZ, _ => unreachable!(), }, ' ' => Code::Space, '[' | '{' => Code::BracketLeft, ']' | '}' => Code::BracketRight, ';' => Code::Semicolon, ':' => Code::Semicolon, ',' => Code::Comma, '<' => Code::Comma, '.' => Code::Period, '>' => Code::Period, '1' => Code::Digit1, '2' => Code::Digit2, '3' => Code::Digit3, '4' => Code::Digit4, '5' => Code::Digit5, '6' => Code::Digit6, '7' => Code::Digit7, '8' => Code::Digit8, '9' => Code::Digit9, '0' => Code::Digit0, '!' => Code::Digit1, '@' => Code::Digit2, '#' => Code::Digit3, '$' => Code::Digit4, '%' => Code::Digit5, '^' => Code::Digit6, '&' => Code::Digit7, '*' => Code::Digit8, '(' => Code::Digit9, ')' => Code::Digit0, // numpad charicter are ambiguous to tui // '*' => Code::Multiply, // '/' => Code::Divide, // '-' => Code::Subtract, // '+' => Code::Add, '+' => Code::Equal, '-' | '_' => Code::Minus, '\'' => Code::Quote, '"' => Code::Quote, '\\' => Code::Backslash, '|' => Code::Backslash, '/' => Code::Slash, '?' => Code::Slash, '=' => Code::Equal, '`' => Code::Backquote, '~' => Code::Backquote, _ => return None, }, TermKeyCode::Null => return None, TermKeyCode::Esc => Code::Escape, }; Some(code) } fn modifiers_from_crossterm_modifiers(src: KeyModifiers) -> Modifiers { let mut modifiers = Modifiers::empty(); if src.contains(KeyModifiers::SHIFT) { modifiers.insert(Modifiers::SHIFT); } if src.contains(KeyModifiers::ALT) { modifiers.insert(Modifiers::ALT); } if src.contains(KeyModifiers::CONTROL) { modifiers.insert(Modifiers::CONTROL); } modifiers }