From 03ff72dddc83d6f16126e4549a19c8d1d2a28c3a Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 5 Feb 2022 16:28:19 -0600 Subject: [PATCH 1/5] added html native events --- Cargo.toml | 1 + examples/keys.rs | 51 ++--- src/hooks.rs | 496 +++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 15 +- 4 files changed, 450 insertions(+), 113 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fde5cd137..8b8b96616 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ crossterm = "0.22.1" anyhow = "1.0.42" thiserror = "1.0.24" dioxus = "0.1.8" +dioxus-html = "0.1.6" hecs = "0.7.3" ctrlc = "3.2.1" bumpalo = { version = "3.8.0", features = ["boxed"] } diff --git a/examples/keys.rs b/examples/keys.rs index 77bf7683d..8df6cbfb5 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -1,15 +1,17 @@ -use crossterm::event::{KeyCode, KeyEvent, MouseEvent}; use dioxus::prelude::*; +use dioxus_html::on::{KeyboardEvent, MouseEvent}; +use dioxus_html::KeyCode; fn main() { rink::launch(app); } fn app(cx: Scope) -> Element { - let (key, set_key) = use_state(&cx, || KeyCode::Null); + let (key, set_key) = use_state(&cx, || KeyCode::Space); let (mouse, set_mouse) = use_state(&cx, || (0, 0)); - let (size, set_size) = use_state(&cx, || (0, 0)); let (count, set_count) = use_state(&cx, || 0); + let (buttons, set_buttons) = use_state(&cx, || 0); + let (mouse_clicked, set_mouse_clicked) = use_state(&cx, || false); cx.render(rsx! { div { @@ -19,30 +21,31 @@ fn app(cx: Scope) -> Element { justify_content: "center", align_items: "center", flex_direction: "column", - - rink::InputHandler { - onkeydown: move |evt: KeyEvent| { - use crossterm::event::KeyCode::*; - match evt.code { - Left => set_count(count + 1), - Right => set_count(count - 1), - Up => set_count(count + 10), - Down => set_count(count - 10), - _ => {}, - } - set_key(evt.code); - }, - onmousedown: move |evt: MouseEvent| { - set_mouse((evt.row, evt.column)); - }, - onresize: move |dims| { - set_size(dims); - }, + onkeydown: move |evt: KeyboardEvent| { + match evt.data.key_code { + KeyCode::LeftArrow => set_count(count + 1), + KeyCode::RightArrow => set_count(count - 1), + KeyCode::UpArrow => set_count(count + 10), + KeyCode::DownArrow => set_count(count - 10), + _ => {}, + } + set_key(evt.key_code); }, + onmousedown: move |evt: MouseEvent| { + set_mouse((evt.data.screen_x, evt.data.screen_y)); + set_buttons(evt.data.buttons); + set_mouse_clicked(true); + }, + onmouseup: move |evt: MouseEvent| { + set_buttons(evt.data.buttons); + set_mouse_clicked(false); + }, + "count: {count:?}", "key: {key:?}", - "mouse: {mouse:?}", - "resize: {size:?}", + "mouse buttons: {buttons:b}", + "mouse pos: {mouse:?}", + "mouse button pressed: {mouse_clicked}" } }) } diff --git a/src/hooks.rs b/src/hooks.rs index b34068c19..8871c121e 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -1,109 +1,433 @@ -use crossterm::event::{Event as TermEvent, KeyEvent, MouseEvent}; +use crossterm::event::{ + Event as TermEvent, KeyCode as TermKeyCode, KeyModifiers, MouseButton, MouseEventKind, +}; use dioxus::core::*; -use dioxus::prelude::Props; + +use dioxus_html::{on::*, KeyCode}; use futures::{channel::mpsc::UnboundedReceiver, StreamExt}; use std::{ - cell::{Cell, RefCell}, - collections::HashMap, + any::Any, + borrow::BorrowMut, + cell::RefCell, rc::Rc, + sync::Arc, + time::{Duration, Instant}, }; -pub struct RinkContext { - last_event: Rc>>, - subscribers: Rc>>, +// 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| clone_mouse_data(m)) +// } + +// pub fn wheel(&self) -> Option { +// let data = (**self.0).borrow(); +// data.wheel.as_ref().map(|w| clone_wheel_data(w)) +// } + +// 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| clone_keyboard_data(&k.0)) +// } +// } + +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), + } + } } -impl RinkContext { - pub fn new(mut receiver: UnboundedReceiver, cx: &ScopeState) -> Self { - let updater = cx.schedule_update_any(); - let last_event = Rc::new(Cell::new(None)); - let last_event2 = last_event.clone(); - let subscribers = Rc::new(RefCell::new(HashMap::new())); - let subscribers2 = subscribers.clone(); +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 { + EventData::Mouse(ref mut m) => match &mut self.mouse { + Some(state) => { + *state = clone_mouse_data(m); + // crossterm always outputs the left mouse button on mouse up + // let mut buttons = state.buttons; + // *state = clone_mouse_data(m); + // match evt.0 { + // "mouseup" => { + // buttons &= !m.buttons; + // } + // "mousedown" => { + // buttons |= m.buttons; + // } + // _ => (), + // } + // state.buttons = buttons; + // m.buttons = buttons; + } + None => { + self.mouse = Some(clone_mouse_data(m)); + } + }, + EventData::Wheel(ref w) => self.wheel = Some(clone_wheel_data(w)), + EventData::Screen(ref s) => self.screen = Some(s.clone()), + 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 mut new = clone_keyboard_data(k); + new.repeat = repeat; + self.last_key_pressed = Some((new, Instant::now())); + } + } + } + + fn update(&mut self, evts: &mut [EventCore]) { + for e in evts { + self.apply_event(e) + } + // for s in &self.subscribers { + // s(); + // } + } + + // 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 + pub fn new( + mut receiver: UnboundedReceiver, + cx: &ScopeState, + ) -> (Self, Rc>) { + let queued_events = Rc::new(RefCell::new(Vec::new())); + let queued_events2 = Rc::>>::downgrade(&queued_events); cx.push_future(async move { while let Some(evt) = receiver.next().await { - last_event2.replace(Some(evt)); - for (subscriber, received) in subscribers2.borrow_mut().iter_mut() { - updater(*subscriber); - *received = false; + if let Some(evt) = get_event(evt) { + if let Some(v) = queued_events2.upgrade() { + (*v).borrow_mut().push(evt); + } else { + break; + } } } }); - Self { - last_event, - subscribers, - } + let state = Rc::new(RefCell::new(InnerInputState::new())); + + ( + Self { + state: state.clone(), + queued_events, + }, + state, + ) } - pub fn subscribe_to_events(&self, scope: ScopeId) { - self.subscribers.borrow_mut().insert(scope, false); - } - - pub fn get_event(&self, scope: ScopeId) -> Option { - let mut subscribers = self.subscribers.borrow_mut(); - let received = subscribers.get_mut(&scope)?; - if !*received { - *received = true; - self.last_event.get() - } else { - None - } - } -} - -#[derive(Props)] -pub struct AppHandlerProps<'a> { - #[props(default)] - onkeydown: EventHandler<'a, KeyEvent>, - - #[props(default)] - onmousedown: EventHandler<'a, MouseEvent>, - - #[props(default)] - onresize: EventHandler<'a, (u16, u16)>, -} - -/// This component lets you handle input events -/// -/// Once attached to the DOM, it will listen for input events from the terminal -/// -/// -pub fn InputHandler<'a>(cx: Scope<'a, AppHandlerProps<'a>>) -> Element { - let rcx = cx.use_hook(|_| { - let rcx = cx - .consume_context::() - .unwrap_or_else(|| panic!("Rink InputHandlers can only be used in Rink apps!")); - - // our component will only re-render if new events are received ... or if the parent is updated - // todo: if update was not caused by a new event, we should not re-render - // perhaps add some tracking to context? - rcx.subscribe_to_events(cx.scope_id()); - - rcx - }); - - { - if let Some(evet) = rcx.get_event(cx.scope_id()) { - match evet { - TermEvent::Key(key) => { - cx.props.onkeydown.call(key.clone()); - // let mut handler = cx.props.keydown.borrow_mut(); - // handler(*key); - // if let Some(handler) = cx.props.onkeydown { - // handler(*key); - // } + pub fn resolve_events(&self, dom: &mut VirtualDom) { + // todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus + fn inner( + queue: &Vec<(&'static str, Arc)>, + resolved: &mut Vec, + node: &VNode, + ) { + match node { + VNode::Fragment(frag) => { + for c in frag.children { + inner(queue, resolved, c); + } } - TermEvent::Mouse(mouse) => { - cx.props.onmousedown.call(mouse.clone()); - } - TermEvent::Resize(x, y) => { - cx.props.onresize.call((x, y)); + VNode::Element(el) => { + for l in el.listeners { + for (name, data) in queue.iter() { + if *name == l.event { + if let Some(id) = el.id.get() { + resolved.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: *name, + element: Some(id), + data: data.clone(), + }); + } + } + } + } + for c in el.children { + inner(queue, resolved, c); + } } + _ => (), } } - } - None + let mut resolved_events = Vec::new(); + + (*self.state) + .borrow_mut() + .update(&mut (*self.queued_events).borrow_mut()); + + let events: Vec<_> = self + .queued_events + .replace(Vec::new()) + .into_iter() + .map(|e| (e.0, e.1.into_any())) + .collect(); + + inner(&events, &mut resolved_events, dom.base_scope().root_node()); + + for e in resolved_events { + dom.handle_message(SchedulerMsg::Event(e)); + } + } +} + +fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { + let (name, data): (&str, EventData) = match evt { + TermEvent::Key(k) => { + let key = translate_key_code(k.code)?; + ( + "keydown", + // from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent + EventData::Keyboard(KeyboardData { + char_code: key.raw_code(), + key: format!("{key:?}"), + key_code: key, + alt_key: k.modifiers.contains(KeyModifiers::ALT), + ctrl_key: k.modifiers.contains(KeyModifiers::CONTROL), + meta_key: false, + shift_key: k.modifiers.contains(KeyModifiers::SHIFT), + locale: Default::default(), + location: 0x00, + repeat: Default::default(), + which: Default::default(), + }), + ) + } + 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 = |b| { + let buttons = match b { + None => 0, + Some(MouseButton::Left) => 1, + Some(MouseButton::Right) => 2, + Some(MouseButton::Middle) => 4, + }; + let button_state = match b { + None => 0, + Some(MouseButton::Left) => 0, + Some(MouseButton::Middle) => 1, + Some(MouseButton::Right) => 2, + }; + // from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + EventData::Mouse(MouseData { + alt_key: alt, + button: button_state, + buttons, + client_x: x, + client_y: y, + ctrl_key: ctrl, + meta_key: meta, + page_x: x, + page_y: y, + screen_x: x, + screen_y: y, + shift_key: shift, + }) + }; + + let get_wheel_data = |up| { + // from https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent + EventData::Wheel(WheelData { + delta_mode: 0x01, + delta_x: 0.0, + delta_y: if up { -1.0 } else { 1.0 }, + delta_z: 0.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 => ("scroll", get_wheel_data(false)), + MouseEventKind::ScrollUp => ("scroll", get_wheel_data(true)), + } + } + TermEvent::Resize(x, y) => ("resize", EventData::Screen((x, y))), + }; + + Some((name, data)) +} + +fn translate_key_code(c: TermKeyCode) -> Option { + match c { + TermKeyCode::Backspace => Some(KeyCode::Backspace), + TermKeyCode::Enter => Some(KeyCode::Enter), + TermKeyCode::Left => Some(KeyCode::LeftArrow), + TermKeyCode::Right => Some(KeyCode::RightArrow), + TermKeyCode::Up => Some(KeyCode::UpArrow), + TermKeyCode::Down => Some(KeyCode::DownArrow), + TermKeyCode::Home => Some(KeyCode::Home), + TermKeyCode::End => Some(KeyCode::End), + TermKeyCode::PageUp => Some(KeyCode::PageUp), + TermKeyCode::PageDown => Some(KeyCode::PageDown), + TermKeyCode::Tab => Some(KeyCode::Tab), + TermKeyCode::BackTab => None, + TermKeyCode::Delete => Some(KeyCode::Delete), + TermKeyCode::Insert => Some(KeyCode::Insert), + TermKeyCode::F(fn_num) => match fn_num { + 1 => Some(KeyCode::F1), + 2 => Some(KeyCode::F2), + 3 => Some(KeyCode::F3), + 4 => Some(KeyCode::F4), + 5 => Some(KeyCode::F5), + 6 => Some(KeyCode::F6), + 7 => Some(KeyCode::F7), + 8 => Some(KeyCode::F8), + 9 => Some(KeyCode::F9), + 10 => Some(KeyCode::F10), + 11 => Some(KeyCode::F11), + 12 => Some(KeyCode::F12), + _ => None, + }, + TermKeyCode::Char(c) => match c.to_uppercase().next().unwrap() { + 'A' => Some(KeyCode::A), + 'B' => Some(KeyCode::B), + 'C' => Some(KeyCode::C), + 'D' => Some(KeyCode::D), + 'E' => Some(KeyCode::E), + 'F' => Some(KeyCode::F), + 'G' => Some(KeyCode::G), + 'H' => Some(KeyCode::H), + 'I' => Some(KeyCode::I), + 'J' => Some(KeyCode::J), + 'K' => Some(KeyCode::K), + 'L' => Some(KeyCode::L), + 'M' => Some(KeyCode::M), + 'N' => Some(KeyCode::N), + 'O' => Some(KeyCode::O), + 'P' => Some(KeyCode::P), + 'Q' => Some(KeyCode::Q), + 'R' => Some(KeyCode::R), + 'S' => Some(KeyCode::S), + 'T' => Some(KeyCode::T), + 'U' => Some(KeyCode::U), + 'V' => Some(KeyCode::V), + 'W' => Some(KeyCode::W), + 'X' => Some(KeyCode::X), + 'Y' => Some(KeyCode::Y), + 'Z' => Some(KeyCode::Z), + _ => None, + }, + TermKeyCode::Null => None, + TermKeyCode::Esc => Some(KeyCode::Escape), + } +} + +fn clone_mouse_data(m: &MouseData) -> MouseData { + MouseData { + client_x: m.client_x, + client_y: m.client_y, + page_x: m.page_x, + page_y: m.page_y, + screen_x: m.screen_x, + screen_y: m.screen_y, + alt_key: m.alt_key, + ctrl_key: m.ctrl_key, + meta_key: m.meta_key, + shift_key: m.shift_key, + button: m.button, + buttons: m.buttons, + } +} + +fn clone_keyboard_data(k: &KeyboardData) -> KeyboardData { + KeyboardData { + char_code: k.char_code, + key: k.key.clone(), + key_code: k.key_code, + alt_key: k.alt_key, + ctrl_key: k.ctrl_key, + meta_key: k.meta_key, + shift_key: k.shift_key, + locale: k.locale.clone(), + location: k.location, + repeat: k.repeat, + which: k.which, + } +} + +fn clone_wheel_data(w: &WheelData) -> WheelData { + WheelData { + delta_mode: w.delta_mode, + delta_x: w.delta_x, + delta_y: w.delta_y, + delta_z: w.delta_x, + } } diff --git a/src/lib.rs b/src/lib.rs index 7a8e581f4..df24c0b37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,11 +31,13 @@ pub fn launch(app: Component<()>) { let cx = dom.base_scope(); - cx.provide_root_context(RinkContext::new(rx, cx)); + let (handler, state) = RinkInputHandler::new(rx, cx); + + cx.provide_root_context(state); dom.rebuild(); - render_vdom(&mut dom, tx).unwrap(); + render_vdom(&mut dom, tx, handler).unwrap(); } pub struct TuiNode<'a> { @@ -44,7 +46,11 @@ pub struct TuiNode<'a> { pub node: &'a VNode<'a>, } -pub fn render_vdom(vdom: &mut VirtualDom, ctx: UnboundedSender) -> Result<()> { +pub fn render_vdom( + vdom: &mut VirtualDom, + ctx: UnboundedSender, + handler: RinkInputHandler, +) -> Result<()> { // Setup input handling let (tx, mut rx) = unbounded(); std::thread::spawn(move || { @@ -84,6 +90,9 @@ pub fn render_vdom(vdom: &mut VirtualDom, ctx: UnboundedSender) -> Re terminal.clear().unwrap(); loop { + // resolve events before rendering + handler.resolve_events(vdom); + /* -> collect all the nodes with their layout -> solve their layout From 6f50da162ba0e8e8c2c72727f642a42914cec871 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 5 Feb 2022 16:30:04 -0600 Subject: [PATCH 2/5] updated comment --- src/hooks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks.rs b/src/hooks.rs index 8871c121e..e67b1c935 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -152,7 +152,7 @@ pub struct RinkInputHandler { impl RinkInputHandler { /// global context that handles events - /// limitations: GUI key modifier is never detected + /// limitations: GUI key modifier is never detected, key up events are not detected, and mouse up events are not specific to a key pub fn new( mut receiver: UnboundedReceiver, cx: &ScopeState, From ff92df73bdae6ca0fe9e7c74587d53f85d2e5a7d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 5 Feb 2022 16:45:40 -0600 Subject: [PATCH 3/5] changed onscroll to onwheel --- examples/keys.rs | 7 +++++++ src/hooks.rs | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/keys.rs b/examples/keys.rs index 8df6cbfb5..44c2dd745 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -1,3 +1,4 @@ +use dioxus::events::WheelEvent; use dioxus::prelude::*; use dioxus_html::on::{KeyboardEvent, MouseEvent}; use dioxus_html::KeyCode; @@ -31,6 +32,12 @@ fn app(cx: Scope) -> Element { } set_key(evt.key_code); }, + onwheel: move |evt: WheelEvent| { + set_count(count + evt.data.delta_y as i64); + }, + ondrag: move |evt: MouseEvent| { + set_mouse((evt.data.screen_x, evt.data.screen_y)); + }, onmousedown: move |evt: MouseEvent| { set_mouse((evt.data.screen_x, evt.data.screen_y)); set_buttons(evt.data.buttons); diff --git a/src/hooks.rs b/src/hooks.rs index e67b1c935..143957e05 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -315,8 +315,8 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { 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 => ("scroll", get_wheel_data(false)), - MouseEventKind::ScrollUp => ("scroll", get_wheel_data(true)), + MouseEventKind::ScrollDown => ("wheel", get_wheel_data(false)), + MouseEventKind::ScrollUp => ("wheel", get_wheel_data(true)), } } TermEvent::Resize(x, y) => ("resize", EventData::Screen((x, y))), From a03ab3edfb00f104c4b9f1031c5f5f1897905a4e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 6 Feb 2022 07:08:15 -0600 Subject: [PATCH 4/5] fixed mouse --- examples/keys.rs | 6 ++--- src/hooks.rs | 63 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/examples/keys.rs b/examples/keys.rs index 44c2dd745..509199b5b 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -8,7 +8,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let (key, set_key) = use_state(&cx, || KeyCode::Space); + let (key, set_key) = use_state(&cx, || "".to_string()); let (mouse, set_mouse) = use_state(&cx, || (0, 0)); let (count, set_count) = use_state(&cx, || 0); let (buttons, set_buttons) = use_state(&cx, || 0); @@ -30,7 +30,7 @@ fn app(cx: Scope) -> Element { KeyCode::DownArrow => set_count(count - 10), _ => {}, } - set_key(evt.key_code); + set_key(format!("{:?} repeating: {:?}", evt.key, evt.repeat)); }, onwheel: move |evt: WheelEvent| { set_count(count + evt.data.delta_y as i64); @@ -49,7 +49,7 @@ fn app(cx: Scope) -> Element { }, "count: {count:?}", - "key: {key:?}", + "key: {key}", "mouse buttons: {buttons:b}", "mouse pos: {mouse:?}", "mouse button pressed: {mouse_clicked}" diff --git a/src/hooks.rs b/src/hooks.rs index 143957e05..21858a662 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -72,7 +72,7 @@ impl EventData { const MAX_REPEAT_TIME: Duration = Duration::from_millis(100); pub struct InnerInputState { - mouse: Option, + mouse: Option<(MouseData, Vec)>, wheel: Option, last_key_pressed: Option<(KeyboardData, Instant)>, screen: Option<(u16, u16)>, @@ -93,26 +93,53 @@ impl InnerInputState { // 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) => match &mut self.mouse { Some(state) => { - *state = clone_mouse_data(m); - // crossterm always outputs the left mouse button on mouse up - // let mut buttons = state.buttons; - // *state = clone_mouse_data(m); - // match evt.0 { - // "mouseup" => { - // buttons &= !m.buttons; - // } - // "mousedown" => { - // buttons |= m.buttons; - // } - // _ => (), - // } - // state.buttons = buttons; - // m.buttons = buttons; + let mut buttons = state.0.buttons; + state.0 = clone_mouse_data(m); + match evt.0 { + // this code only runs when there are no buttons down + "mouseup" => { + buttons = 0; + state.1 = Vec::new(); + } + "mousedown" => { + if state.1.contains(&m.buttons) { + // if we already pressed a button and there is another button released the button crossterm sends is the button remaining + if state.1.len() > 1 { + state.1 = vec![m.buttons]; + } + // otherwise some other button was pressed. In testing it was consistantly this mapping + else { + match m.buttons { + 0x01 => state.1.push(0x02), + 0x02 => state.1.push(0x01), + 0x04 => state.1.push(0x01), + _ => (), + } + } + } else { + state.1.push(m.buttons); + } + + buttons = state.1.iter().copied().reduce(|a, b| a | b).unwrap(); + } + _ => (), + } + state.0.buttons = buttons; + m.buttons = buttons; + // println!("{buttons}") } None => { - self.mouse = Some(clone_mouse_data(m)); + self.mouse = Some(( + clone_mouse_data(m), + if m.buttons == 0 { + Vec::new() + } else { + vec![m.buttons] + }, + )); } }, EventData::Wheel(ref w) => self.wheel = Some(clone_wheel_data(w)), @@ -152,7 +179,7 @@ pub struct RinkInputHandler { impl RinkInputHandler { /// global context that handles events - /// limitations: GUI key modifier is never detected, key up events are not detected, and mouse up events are not specific to a key + /// 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( mut receiver: UnboundedReceiver, cx: &ScopeState, From 7a1fdeb673eee58e63902b36cb1515a72e4d2c00 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Feb 2022 05:57:57 -0600 Subject: [PATCH 5/5] added more mouse events --- examples/hover.rs | 102 +++++++++++++++++++++ src/hooks.rs | 222 ++++++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 14 ++- 3 files changed, 319 insertions(+), 19 deletions(-) create mode 100644 examples/hover.rs diff --git a/examples/hover.rs b/examples/hover.rs new file mode 100644 index 000000000..10916ba2e --- /dev/null +++ b/examples/hover.rs @@ -0,0 +1,102 @@ +use std::{convert::TryInto, sync::Arc}; + +use dioxus::{events::MouseData, prelude::*}; + +fn main() { + rink::launch(app); +} + +fn app(cx: Scope) -> Element { + fn to_str(c: &[i32; 3]) -> String { + "#".to_string() + &c.iter().map(|c| format!("{c:02X?}")).collect::() + } + + fn get_brightness(m: Arc) -> i32 { + let mb = m.buttons; + let b: i32 = m.buttons.count_ones().try_into().unwrap(); + 127 * b + } + + let (q1_color, set_q1_color) = use_state(&cx, || [200; 3]); + let (q2_color, set_q2_color) = use_state(&cx, || [200; 3]); + let (q3_color, set_q3_color) = use_state(&cx, || [200; 3]); + let (q4_color, set_q4_color) = use_state(&cx, || [200; 3]); + + let q1_color_str = to_str(q1_color); + let q2_color_str = to_str(q2_color); + let q3_color_str = to_str(q3_color); + let q4_color_str = to_str(q4_color); + + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + + div { + width: "100%", + height: "50%", + flex_direction: "row", + div { + border_width: "1px", + width: "50%", + height: "100%", + justify_content: "center", + align_items: "center", + background_color: "{q1_color_str}", + onmouseenter: move |m| set_q1_color([get_brightness(m.data), 0, 0]), + onmousedown: move |m| set_q1_color([get_brightness(m.data), 0, 0]), + onmouseup: move |m| set_q1_color([get_brightness(m.data), 0, 0]), + onwheel: move |w| set_q1_color([q1_color[0] + (10.0*w.delta_y) as i32, 0, 0]), + onmouseleave: move |_| set_q1_color([200; 3]), + "click me" + } + div { + width: "50%", + height: "100%", + justify_content: "center", + align_items: "center", + background_color: "{q2_color_str}", + onmouseenter: move |m| set_q2_color([get_brightness(m.data); 3]), + onmousedown: move |m| set_q2_color([get_brightness(m.data); 3]), + onmouseup: move |m| set_q2_color([get_brightness(m.data); 3]), + onwheel: move |w| set_q2_color([q2_color[0] + (10.0*w.delta_y) as i32;3]), + onmouseleave: move |_| set_q2_color([200; 3]), + "click me" + } + } + + div { + width: "100%", + height: "50%", + flex_direction: "row", + div { + width: "50%", + height: "100%", + justify_content: "center", + align_items: "center", + background_color: "{q3_color_str}", + onmouseenter: move |m| set_q3_color([0, get_brightness(m.data), 0]), + onmousedown: move |m| set_q3_color([0, get_brightness(m.data), 0]), + onmouseup: move |m| set_q3_color([0, get_brightness(m.data), 0]), + onwheel: move |w| set_q3_color([0, q3_color[1] + (10.0*w.delta_y) as i32, 0]), + onmouseleave: move |_| set_q3_color([200; 3]), + "click me" + } + div { + width: "50%", + height: "100%", + justify_content: "center", + align_items: "center", + background_color: "{q4_color_str}", + onmouseenter: move |m| set_q4_color([0, 0, get_brightness(m.data)]), + onmousedown: move |m| set_q4_color([0, 0, get_brightness(m.data)]), + onmouseup: move |m| set_q4_color([0, 0, get_brightness(m.data)]), + onwheel: move |w| set_q4_color([0, 0, q4_color[2] + (10.0*w.delta_y) as i32]), + onmouseleave: move |_| set_q4_color([200; 3]), + "click me" + } + } + } + }) +} diff --git a/src/hooks.rs b/src/hooks.rs index 21858a662..dd0469ab1 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -7,12 +7,15 @@ use dioxus_html::{on::*, KeyCode}; use futures::{channel::mpsc::UnboundedReceiver, StreamExt}; use std::{ any::Any, - borrow::BorrowMut, cell::RefCell, + collections::{HashMap, HashSet}, rc::Rc, sync::Arc, time::{Duration, Instant}, }; +use stretch2::{prelude::Layout, Stretch}; + +use crate::TuiNode; // a wrapper around the input state for easier access // todo: fix loop @@ -108,6 +111,7 @@ impl InnerInputState { if state.1.contains(&m.buttons) { // if we already pressed a button and there is another button released the button crossterm sends is the button remaining if state.1.len() > 1 { + evt.0 = "mouseup"; state.1 = vec![m.buttons]; } // otherwise some other button was pressed. In testing it was consistantly this mapping @@ -129,7 +133,6 @@ impl InnerInputState { } state.0.buttons = buttons; m.buttons = buttons; - // println!("{buttons}") } None => { self.mouse = Some(( @@ -151,17 +154,194 @@ impl InnerInputState { .filter(|k2| k2.0.key == k.key && k2.1.elapsed() < MAX_REPEAT_TIME) .is_some(); k.repeat = repeat; - let mut new = clone_keyboard_data(k); - new.repeat = repeat; + let new = clone_keyboard_data(k); self.last_key_pressed = Some((new, Instant::now())); } } } - fn update(&mut self, evts: &mut [EventCore]) { - for e in evts { - self.apply_event(e) + fn update<'a>( + &mut self, + dom: &'a VirtualDom, + evts: &mut Vec, + resolved_events: &mut Vec, + layout: &Stretch, + layouts: &mut HashMap>, + node: &'a VNode<'a>, + ) { + struct Data<'b> { + new_pos: (i32, i32), + old_pos: Option<(i32, i32)>, + clicked: bool, + released: bool, + wheel_delta: f64, + mouse_data: &'b MouseData, + wheel_data: &'b Option, + }; + + fn layout_contains_point(layout: &Layout, point: (i32, i32)) -> bool { + layout.location.x as i32 <= point.0 + && layout.location.x as i32 + layout.size.width as i32 >= point.0 + && layout.location.y as i32 <= point.1 + && layout.location.y as i32 + layout.size.height as i32 >= point.1 } + + fn get_mouse_events<'c, 'd>( + dom: &'c VirtualDom, + resolved_events: &mut Vec, + layout: &Stretch, + layouts: &HashMap>, + node: &'c VNode<'c>, + data: &'d Data<'d>, + ) -> HashSet<&'static str> { + match node { + VNode::Fragment(f) => { + let mut union = HashSet::new(); + for child in f.children { + union = union + .union(&get_mouse_events( + dom, + resolved_events, + layout, + layouts, + child, + data, + )) + .copied() + .collect(); + } + return union; + } + + VNode::Component(vcomp) => { + let idx = vcomp.scope.get().unwrap(); + let new_node = dom.get_scope(idx).unwrap().root_node(); + return get_mouse_events(dom, resolved_events, layout, layouts, new_node, data); + } + + VNode::Placeholder(_) => return HashSet::new(), + + VNode::Element(_) | VNode::Text(_) => {} + } + + let id = node.try_mounted_id().unwrap(); + let node = layouts.get(&id).unwrap(); + + let node_layout = layout.layout(node.layout).unwrap(); + + let previously_contained = data + .old_pos + .filter(|pos| layout_contains_point(node_layout, *pos)) + .is_some(); + let currently_contains = layout_contains_point(node_layout, data.new_pos); + + match node.node { + VNode::Element(el) => { + let mut events = HashSet::new(); + if previously_contained || currently_contains { + for c in el.children { + events = events + .union(&get_mouse_events( + dom, + resolved_events, + layout, + layouts, + c, + data, + )) + .copied() + .collect(); + } + } + let mut try_create_event = |name| { + // only trigger event if the event was not triggered already by a child + if events.insert(name) { + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name, + element: Some(el.id.get().unwrap()), + data: Arc::new(clone_mouse_data(data.mouse_data)), + }) + } + }; + if currently_contains { + if !previously_contained { + try_create_event("mouseenter"); + try_create_event("mouseover"); + } + if data.clicked { + try_create_event("mousedown"); + } + if data.released { + try_create_event("mouseup"); + match data.mouse_data.button { + 0 => try_create_event("click"), + 2 => try_create_event("contextmenu"), + _ => (), + } + } + if let Some(w) = data.wheel_data { + if data.wheel_delta != 0.0 { + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: "wheel", + element: Some(el.id.get().unwrap()), + data: Arc::new(clone_wheel_data(w)), + }) + } + } + } else { + if previously_contained { + try_create_event("mouseleave"); + try_create_event("mouseout"); + } + } + events + } + VNode::Text(_) => HashSet::new(), + _ => todo!(), + } + } + + let previous_mouse = self + .mouse + .as_ref() + .map(|m| (clone_mouse_data(&m.0), m.1.clone())); + // println!("{previous_mouse:?}"); + + self.wheel = None; + + for e in evts.iter_mut() { + self.apply_event(e); + } + + // resolve hover events + if let Some(mouse) = &self.mouse { + let new_pos = (mouse.0.screen_x, mouse.0.screen_y); + let old_pos = previous_mouse + .as_ref() + .map(|m| (m.0.screen_x, m.0.screen_y)); + let clicked = + (!mouse.0.buttons & previous_mouse.as_ref().map(|m| m.0.buttons).unwrap_or(0)) > 0; + let released = + (mouse.0.buttons & !previous_mouse.map(|m| m.0.buttons).unwrap_or(0)) > 0; + let wheel_delta = self.wheel.as_ref().map_or(0.0, |w| w.delta_y); + let mouse_data = &mouse.0; + let wheel_data = &self.wheel; + let data = Data { + new_pos, + old_pos, + clicked, + released, + wheel_delta, + mouse_data, + wheel_data, + }; + get_mouse_events(dom, resolved_events, layout, layouts, node, &data); + } + // for s in &self.subscribers { // s(); // } @@ -210,7 +390,13 @@ impl RinkInputHandler { ) } - pub fn resolve_events(&self, dom: &mut VirtualDom) { + pub fn get_events<'a>( + &self, + dom: &'a VirtualDom, + layout: &Stretch, + layouts: &mut HashMap>, + node: &'a VNode<'a>, + ) -> Vec { // todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus fn inner( queue: &Vec<(&'static str, Arc)>, @@ -249,25 +435,31 @@ impl RinkInputHandler { let mut resolved_events = Vec::new(); - (*self.state) - .borrow_mut() - .update(&mut (*self.queued_events).borrow_mut()); + (*self.state).borrow_mut().update( + dom, + &mut (*self.queued_events).borrow_mut(), + &mut resolved_events, + layout, + layouts, + node, + ); let events: Vec<_> = self .queued_events .replace(Vec::new()) .into_iter() + // these events were added in the update stage + .filter(|e| !["mousedown", "mouseup", "mousemove", "drag", "wheel"].contains(&e.0)) .map(|e| (e.0, e.1.into_any())) .collect(); - inner(&events, &mut resolved_events, dom.base_scope().root_node()); + inner(&events, &mut resolved_events, node); - for e in resolved_events { - dom.handle_message(SchedulerMsg::Event(e)); - } + 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) => { diff --git a/src/lib.rs b/src/lib.rs index df24c0b37..145e59d1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,12 +90,10 @@ pub fn render_vdom( terminal.clear().unwrap(); loop { - // resolve events before rendering - handler.resolve_events(vdom); - /* -> collect all the nodes with their layout -> solve their layout + -> resolve events -> render the nodes in the right place with tui/crosstream -> while rendering, apply styling @@ -111,10 +109,11 @@ pub fn render_vdom( let root_node = vdom.base_scope().root_node(); layout::collect_layout(&mut layout, &mut nodes, vdom, root_node); /* - Compute the layout given th terminal size + Compute the layout given the terminal size */ let node_id = root_node.try_mounted_id().unwrap(); let root_layout = nodes[&node_id].layout; + let mut events = Vec::new(); terminal.draw(|frame| { // size is guaranteed to not change when rendering @@ -130,10 +129,17 @@ pub fn render_vdom( }, ) .unwrap(); + + // resolve events before rendering + events = handler.get_events(vdom, &layout, &mut nodes, root_node); render::render_vnode(frame, &layout, &mut nodes, vdom, root_node); assert!(nodes.is_empty()); })?; + for e in events { + vdom.handle_message(SchedulerMsg::Event(e)); + } + use futures::future::{select, Either}; { let wait = vdom.wait_for_work();