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