Kaynağa Gözat

added html native events

Evan Almloff 3 yıl önce
ebeveyn
işleme
03ff72dddc
4 değiştirilmiş dosya ile 443 ekleme ve 106 silme
  1. 1 0
      Cargo.toml
  2. 27 24
      examples/keys.rs
  3. 403 79
      src/hooks.rs
  4. 12 3
      src/lib.rs

+ 1 - 0
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"] }

+ 27 - 24
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}"
         }
     })
 }

+ 403 - 79
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<Cell<Option<TermEvent>>>,
-    subscribers: Rc<RefCell<HashMap<ScopeId, bool>>>,
+// a wrapper around the input state for easier access
+// todo: fix loop
+// pub struct InputState(Rc<Rc<RefCell<InnerInputState>>>);
+// impl InputState {
+//     pub fn get(cx: &ScopeState) -> InputState {
+//         let inner = cx
+//             .consume_context::<Rc<RefCell<InnerInputState>>>()
+//             .expect("Rink InputState can only be used in Rink apps!");
+//         (**inner).borrow_mut().subscribe(cx.schedule_update());
+//         InputState(inner)
+//     }
+
+//     pub fn mouse(&self) -> Option<MouseData> {
+//         let data = (**self.0).borrow();
+//         data.mouse.as_ref().map(|m| clone_mouse_data(m))
+//     }
+
+//     pub fn wheel(&self) -> Option<WheelData> {
+//         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<KeyboardData> {
+//         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<dyn Any + Send + Sync> {
+        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<TermEvent>, 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);
 
-        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;
-                }
-            }
-        });
+pub struct InnerInputState {
+    mouse: Option<MouseData>,
+    wheel: Option<WheelData>,
+    last_key_pressed: Option<(KeyboardData, Instant)>,
+    screen: Option<(u16, u16)>,
+    // subscribers: Vec<Rc<dyn Fn() + 'static>>,
+}
 
+impl InnerInputState {
+    fn new() -> Self {
         Self {
-            last_event,
-            subscribers,
+            mouse: None,
+            wheel: None,
+            last_key_pressed: None,
+            screen: None,
+            // subscribers: Vec::new(),
         }
     }
 
-    pub fn subscribe_to_events(&self, scope: ScopeId) {
-        self.subscribers.borrow_mut().insert(scope, false);
+    // 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()));
+            }
+        }
     }
 
-    pub fn get_event(&self, scope: ScopeId) -> Option<TermEvent> {
-        let mut subscribers = self.subscribers.borrow_mut();
-        let received = subscribers.get_mut(&scope)?;
-        if !*received {
-            *received = true;
-            self.last_event.get()
-        } else {
-            None
+    fn update(&mut self, evts: &mut [EventCore]) {
+        for e in evts {
+            self.apply_event(e)
         }
+        // for s in &self.subscribers {
+        //     s();
+        // }
     }
-}
-
-#[derive(Props)]
-pub struct AppHandlerProps<'a> {
-    #[props(default)]
-    onkeydown: EventHandler<'a, KeyEvent>,
 
-    #[props(default)]
-    onmousedown: EventHandler<'a, MouseEvent>,
+    // fn subscribe(&mut self, f: Rc<dyn Fn() + 'static>) {
+    //     self.subscribers.push(f)
+    // }
+}
 
-    #[props(default)]
-    onresize: EventHandler<'a, (u16, u16)>,
+pub struct RinkInputHandler {
+    state: Rc<RefCell<InnerInputState>>,
+    queued_events: Rc<RefCell<Vec<EventCore>>>,
 }
 
-/// 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::<RinkContext>()
-            .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);
-                    // }
+impl RinkInputHandler {
+    /// global context that handles events
+    /// limitations: GUI key modifier is never detected
+    pub fn new(
+        mut receiver: UnboundedReceiver<TermEvent>,
+        cx: &ScopeState,
+    ) -> (Self, Rc<RefCell<InnerInputState>>) {
+        let queued_events = Rc::new(RefCell::new(Vec::new()));
+        let queued_events2 = Rc::<RefCell<std::vec::Vec<_>>>::downgrade(&queued_events);
+
+        cx.push_future(async move {
+            while let Some(evt) = receiver.next().await {
+                if let Some(evt) = get_event(evt) {
+                    if let Some(v) = queued_events2.upgrade() {
+                        (*v).borrow_mut().push(evt);
+                    } else {
+                        break;
+                    }
                 }
-                TermEvent::Mouse(mouse) => {
-                    cx.props.onmousedown.call(mouse.clone());
+            }
+        });
+
+        let state = Rc::new(RefCell::new(InnerInputState::new()));
+
+        (
+            Self {
+                state: state.clone(),
+                queued_events,
+            },
+            state,
+        )
+    }
+
+    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<dyn Any + Send + Sync>)>,
+            resolved: &mut Vec<UserEvent>,
+            node: &VNode,
+        ) {
+            match node {
+                VNode::Fragment(frag) => {
+                    for c in frag.children {
+                        inner(queue, resolved, c);
+                    }
                 }
-                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);
+                    }
                 }
+                _ => (),
+            }
+        }
+
+        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<KeyCode> {
+    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),
     }
+}
 
-    None
+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,
+    }
 }

+ 12 - 3
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<TermEvent>) -> Result<()> {
+pub fn render_vdom(
+    vdom: &mut VirtualDom,
+    ctx: UnboundedSender<TermEvent>,
+    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<TermEvent>) -> Re
             terminal.clear().unwrap();
 
             loop {
+                // resolve events before rendering
+                handler.resolve_events(vdom);
+
                 /*
                 -> collect all the nodes with their layout
                 -> solve their layout