From 7a1fdeb673eee58e63902b36cb1515a72e4d2c00 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Feb 2022 05:57:57 -0600 Subject: [PATCH] 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();