From 61b4bb2f19bf7d14b003186ce9256da162e09658 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 22 Jun 2022 13:57:05 -0500 Subject: [PATCH 1/3] handle bubbling events --- packages/core/src/events.rs | 3 + packages/core/src/scopes.rs | 6 +- packages/desktop/src/events.rs | 4 +- packages/html/src/events.rs | 88 ++++++++ packages/interpreter/src/bindings.rs | 10 +- packages/interpreter/src/interpreter.js | 257 ++++++++++++++++++++++-- packages/tui/src/hooks.rs | 4 +- packages/web/src/dom.rs | 16 +- packages/web/src/rehydrate.rs | 2 + 9 files changed, 363 insertions(+), 27 deletions(-) diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 0b24f5563..92c86dec8 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -63,6 +63,9 @@ pub struct UserEvent { /// The event type IE "onclick" or "onmouseover" pub name: &'static str, + /// If the event is bubbles up through the vdom + pub bubbles: bool, + /// The event data to be passed onto the event handler pub data: Arc, } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 8db5efe56..ba6afcfe2 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -335,7 +335,7 @@ impl ScopeArena { log::trace!("calling listener {:?}", listener.event); if state.canceled.get() { // stop bubbling if canceled - break; + return; } let mut cb = listener.callback.borrow_mut(); @@ -349,6 +349,10 @@ impl ScopeArena { data: event.data.clone(), }); } + + if !event.bubbles { + return; + } } } diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index fd0675f10..803767a09 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -4,6 +4,7 @@ use std::any::Any; use std::sync::Arc; use dioxus_core::{ElementId, EventPriority, UserEvent}; +use dioxus_html::event_bubbles; use dioxus_html::on::*; #[derive(serde::Serialize, serde::Deserialize)] @@ -47,8 +48,8 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { } = serde_json::from_value(val).unwrap(); let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); - let name = event_name_from_type(&event); + let event = make_synthetic_event(&event, contents); UserEvent { @@ -56,6 +57,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { priority: EventPriority::Low, scope_id: None, element: mounted_dom_id, + bubbles: event_bubbles(name), data: event, } } diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index beac81167..67522e87a 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -1324,3 +1324,91 @@ pub(crate) fn _event_meta(event: &UserEvent) -> (bool, EventPriority) { _ => (true, Low), } } + +pub fn event_bubbles(evt: &str) -> bool { + match evt { + "copy" => true, + "cut" => true, + "paste" => true, + "compositionend" => true, + "compositionstart" => true, + "compositionupdate" => true, + "keydown" => true, + "keypress" => true, + "keyup" => true, + "focus" => false, + "focusout" => true, + "focusin" => true, + "blur" => false, + "change" => true, + "input" => true, + "invalid" => true, + "reset" => true, + "submit" => true, + "click" => true, + "contextmenu" => true, + "doubleclick" => true, + "dblclick" => true, + "drag" => true, + "dragend" => true, + "dragenter" => false, + "dragexit" => false, + "dragleave" => true, + "dragover" => true, + "dragstart" => true, + "drop" => true, + "mousedown" => true, + "mouseenter" => false, + "mouseleave" => false, + "mousemove" => true, + "mouseout" => true, + "scroll" => false, + "mouseover" => true, + "mouseup" => true, + "pointerdown" => true, + "pointermove" => true, + "pointerup" => true, + "pointercancel" => true, + "gotpointercapture" => true, + "lostpointercapture" => true, + "pointerenter" => false, + "pointerleave" => false, + "pointerover" => true, + "pointerout" => true, + "select" => true, + "touchcancel" => true, + "touchend" => true, + "touchmove" => true, + "touchstart" => true, + "wheel" => true, + "abort" => false, + "canplay" => true, + "canplaythrough" => true, + "durationchange" => true, + "emptied" => true, + "encrypted" => true, + "ended" => true, + "error" => false, + "loadeddata" => true, + "loadedmetadata" => true, + "loadstart" => false, + "pause" => true, + "play" => true, + "playing" => true, + "progress" => false, + "ratechange" => true, + "seeked" => true, + "seeking" => true, + "stalled" => true, + "suspend" => true, + "timeupdate" => true, + "volumechange" => true, + "waiting" => true, + "animationstart" => true, + "animationend" => true, + "animationiteration" => true, + "transitionend" => true, + "toggle" => true, + _ => panic!("unsupported event type {:?}", evt), + } +} diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index a08ef9c13..80df47c2c 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -48,10 +48,16 @@ extern "C" { pub fn CreatePlaceholder(this: &Interpreter, root: u64); #[wasm_bindgen(method)] - pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function); + pub fn NewEventListener( + this: &Interpreter, + name: &str, + root: u64, + handler: &Function, + bubbles: bool, + ); #[wasm_bindgen(method)] - pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); + pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str, bubbles: bool); #[wasm_bindgen(method)] pub fn SetText(this: &Interpreter, root: u64, text: JsValue); diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 0c129efda..759e83c9f 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -5,11 +5,64 @@ export function main() { window.ipc.postMessage(serializeIpcMessage("initialize")); } } + +class ListenerMap { + constructor(root) { + // bubbling events can listen at the root element + this.global = {}; + // non bubbling events listen at the element the listener was created at + this.local = {}; + this.root = root; + } + + createBubbling(event_name, handler) { + if (this.global[event_name] === undefined) { + this.global[event_name] = {}; + this.global[event_name].active = 1; + this.global[event_name].callback = handler; + this.root.addEventListener(event_name, handler); + } else { + this.global[event_name].active++; + } + } + + createNonBubbling(event_name, element, handler) { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + this.local[id][event_name] = handler; + element.addEventListener(event_name, handler); + } + + removeBubbling(event_name) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + + removeNonBubbling(element, event_name) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (this.local[id].length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, handler); + } + + removeAllNonBubbling(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } +} + export class Interpreter { constructor(root) { this.root = root; this.stack = [root]; - this.listeners = {}; + this.listeners = new ListenerMap(root); this.handlers = {}; this.lastNodeWasText = false; this.nodes = [root]; @@ -40,6 +93,7 @@ export class Interpreter { ReplaceWith(root_id, m) { let root = this.nodes[root_id]; let els = this.stack.splice(this.stack.length - m); + this.listeners.removeAllNonBubbling(root); root.replaceWith(...els); } InsertAfter(root, n) { @@ -54,6 +108,7 @@ export class Interpreter { } Remove(root) { let node = this.nodes[root]; + this.listeners.removeAllNonBubbling(node); if (node !== undefined) { node.remove(); } @@ -79,25 +134,24 @@ export class Interpreter { this.stack.push(el); this.nodes[root] = el; } - NewEventListener(event_name, root, handler) { + NewEventListener(event_name, root, handler, bubbles) { const element = this.nodes[root]; element.setAttribute("data-dioxus-id", `${root}`); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 1; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } else { - this.listeners[event_name]++; + if (bubbles) { + this.listeners.createBubbling(event_name, handler); + } + else { + this.listeners.createNonBubbling(event_name, element, handler); } } - RemoveEventListener(root, event_name) { + RemoveEventListener(root, event_name, bubbles) { const element = this.nodes[root]; element.removeAttribute(`data-dioxus-id`); - this.listeners[event_name]--; - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; + if (bubbles) { + this.listeners.removeBubbling(event_name) + } + else { + this.listeners.removeNonBubbling(element, event_name); } } SetText(root, text) { @@ -198,12 +252,9 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": - console.log(this.listeners); - // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event) => { - console.log(event); let target = event.target; if (target != null) { @@ -292,7 +343,8 @@ export class Interpreter { ); } }; - this.NewEventListener(edit.event_name, edit.root, handler); + this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name)); + break; case "SetText": this.SetText(edit.root, edit.text); @@ -607,3 +659,172 @@ const bool_attrs = { selected: true, truespeed: true, }; + +function event_bubbles(event) { + switch (event) { + case "copy": + return true; + case "cut": + return true; + case "paste": + return true; + case "compositionend": + return true; + case "compositionstart": + return true; + case "compositionupdate": + return true; + case "keydown": + return true; + case "keypress": + return true; + case "keyup": + return true; + case "focus": + return false; + case "focusout": + return true; + case "focusin": + return true; + case "blur": + return false; + case "change": + return true; + case "input": + return true; + case "invalid": + return true; + case "reset": + return true; + case "submit": + return true; + case "click": + return true; + case "contextmenu": + return true; + case "doubleclick": + return true; + case "dblclick": + return true; + case "drag": + return true; + case "dragend": + return true; + case "dragenter": + return false; + case "dragexit": + return false; + case "dragleave": + return true; + case "dragover": + return true; + case "dragstart": + return true; + case "drop": + return true; + case "mousedown": + return true; + case "mouseenter": + return false; + case "mouseleave": + return false; + case "mousemove": + return true; + case "mouseout": + return true; + case "scroll": + return false; + case "mouseover": + return true; + case "mouseup": + return true; + case "pointerdown": + return true; + case "pointermove": + return true; + case "pointerup": + return true; + case "pointercancel": + return true; + case "gotpointercapture": + return true; + case "lostpointercapture": + return true; + case "pointerenter": + return false; + case "pointerleave": + return false; + case "pointerover": + return true; + case "pointerout": + return true; + case "select": + return true; + case "touchcancel": + return true; + case "touchend": + return true; + case "touchmove": + return true; + case "touchstart": + return true; + case "wheel": + return true; + case "abort": + return false; + case "canplay": + return true; + case "canplaythrough": + return true; + case "durationchange": + return true; + case "emptied": + return true; + case "encrypted": + return true; + case "ended": + return true; + case "error": + return false; + case "loadeddata": + return true; + case "loadedmetadata": + return true; + case "loadstart": + return false; + case "pause": + return true; + case "play": + return true; + case "playing": + return true; + case "progress": + return false; + case "ratechange": + return true; + case "seeked": + return true; + case "seeking": + return true; + case "stalled": + return true; + case "suspend": + return true; + case "timeupdate": + return true; + case "volumechange": + return true; + case "waiting": + return true; + case "animationstart": + return true; + case "animationend": + return true; + case "animationiteration": + return true; + case "transitionend": + return true; + case "toggle": + return true; + } +} \ No newline at end of file diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 3a1646fdf..7d58f9628 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -9,7 +9,7 @@ use dioxus_html::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, S use dioxus_html::input_data::keyboard_types::Modifiers; use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; -use dioxus_html::{on::*, KeyCode}; +use dioxus_html::{event_bubbles, on::*, KeyCode}; use std::{ any::Any, cell::RefCell, @@ -207,6 +207,7 @@ impl InnerInputState { name, element: Some(node.id), data, + bubbles: event_bubbles(name), }) } } @@ -574,6 +575,7 @@ impl RinkInputHandler { name: event, element: Some(node.id), data: data.clone(), + bubbles: event_bubbles(event), }); } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index ebcdefbcc..0e434c1a0 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -8,6 +8,7 @@ //! - Partial delegation?> use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; +use dioxus_html::event_bubbles; use dioxus_interpreter_js::Interpreter; use js_sys::Function; use std::{any::Any, rc::Rc, sync::Arc}; @@ -45,6 +46,7 @@ impl WebsysDom { element: Some(ElementId(id)), scope_id: None, priority: dioxus_core::EventPriority::Medium, + bubbles: event.bubbles(), }); } Some(Err(e)) => { @@ -64,6 +66,7 @@ impl WebsysDom { element: None, scope_id: None, priority: dioxus_core::EventPriority::Low, + bubbles: event.bubbles(), }); } } @@ -121,12 +124,17 @@ impl WebsysDom { event_name, root, .. } => { let handler: &Function = self.handler.as_ref().unchecked_ref(); - self.interpreter.NewEventListener(event_name, root, handler); + self.interpreter.NewEventListener( + event_name, + root, + handler, + event_bubbles(event_name), + ); } - DomEdit::RemoveEventListener { root, event } => { - self.interpreter.RemoveEventListener(root, event) - } + DomEdit::RemoveEventListener { root, event } => self + .interpreter + .RemoveEventListener(root, event, event_bubbles(event)), DomEdit::RemoveAttribute { root, name, ns } => { self.interpreter.RemoveAttribute(root, name, ns) diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 24e46d11e..a6b7df999 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -1,5 +1,6 @@ use crate::dom::WebsysDom; use dioxus_core::{VNode, VirtualDom}; +use dioxus_html::event_bubbles; use wasm_bindgen::JsCast; use web_sys::{Comment, Element, Node, Text}; @@ -111,6 +112,7 @@ impl WebsysDom { listener.event, listener.mounted_node.get().unwrap().as_u64(), self.handler.as_ref().unchecked_ref(), + event_bubbles(listener.event), ); } From 2d45e1dc8d0f44caf8886b0c5b09bff3473cf8fe Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 22 Jun 2022 14:24:03 -0500 Subject: [PATCH 2/3] add bubbling data to events after merge --- packages/liveview/src/events.rs | 2 ++ packages/tui/src/hooks.rs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs index 10d1ffd38..2c10925b5 100644 --- a/packages/liveview/src/events.rs +++ b/packages/liveview/src/events.rs @@ -6,6 +6,7 @@ use std::any::Any; use std::sync::Arc; use dioxus_core::{ElementId, EventPriority, UserEvent}; +use dioxus_html::event_bubbles; use dioxus_html::on::*; #[derive(serde::Serialize, serde::Deserialize)] @@ -46,6 +47,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { scope_id: None, element: mounted_dom_id, data: event, + bubbles: event_bubbles(name), } } diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 45a428607..6d633eab8 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -187,6 +187,7 @@ impl InnerInputState { name: "focus", element: Some(id), data: Arc::new(FocusData {}), + bubbles: event_bubbles("focus"), }); resolved_events.push(UserEvent { scope_id: None, @@ -194,6 +195,7 @@ impl InnerInputState { name: "focusin", element: Some(id), data: Arc::new(FocusData {}), + bubbles: event_bubbles("focusin"), }); } if let Some(id) = old_focus { @@ -203,6 +205,7 @@ impl InnerInputState { name: "focusout", element: Some(id), data: Arc::new(FocusData {}), + bubbles: event_bubbles("focusout"), }); } } From c52a9334cb5339525c3a6a75a620ae428d5a21b3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Jun 2022 14:05:19 -0500 Subject: [PATCH 3/3] fix tui tests --- packages/tui/tests/events.rs | 50 ++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/tui/tests/events.rs b/packages/tui/tests/events.rs index ffe9ea9ec..ab6b70b2e 100644 --- a/packages/tui/tests/events.rs +++ b/packages/tui/tests/events.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent}; use dioxus_core::VNode; use dioxus_core::*; @@ -7,6 +5,31 @@ use dioxus_core_macro::*; use dioxus_hooks::*; use dioxus_html as dioxus_elements; use dioxus_tui::TuiContext; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +/// The tui renderer will look for any event that has occured or any future that has resolved in a loop. +/// It will resolve at most one event per loop. +/// This future will resolve after a certain number of polls. If the number of polls is greater than the number of events triggered, and the event has not been recieved there is an issue with the event system. +struct PollN(usize); +impl PollN { + fn new(n: usize) -> Self { + PollN(n) + } +} +impl Future for PollN { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { + if self.0 == 0 { + Poll::Ready(()) + } else { + self.0 -= 1; + Poll::Pending + } + } +} #[test] fn key_down() { @@ -17,12 +40,17 @@ fn key_down() { let tui_ctx: TuiContext = cx.consume_context().unwrap(); let render_count_handle = render_count.clone(); cx.spawn(async move { - tokio::time::sleep(Duration::from_millis(100)).await; + PollN::new(3).await; render_count_handle.modify(|x| *x + 1); }); if *render_count.get() > 2 { panic!("Event was not received"); } + // focus the element + tui_ctx.inject_event(Event::Key(KeyEvent { + code: KeyCode::Tab, + modifiers: KeyModifiers::NONE, + })); tui_ctx.inject_event(Event::Key(KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::NONE, @@ -49,7 +77,7 @@ fn mouse_down() { let tui_ctx: TuiContext = cx.consume_context().unwrap(); let render_count_handle = render_count.clone(); cx.spawn(async move { - tokio::time::sleep(Duration::from_millis(100)).await; + PollN::new(2).await; render_count_handle.modify(|x| *x + 1); }); if *render_count.get() > 2 { @@ -83,7 +111,7 @@ fn mouse_up() { let tui_ctx: TuiContext = cx.consume_context().unwrap(); let render_count_handle = render_count.clone(); cx.spawn(async move { - tokio::time::sleep(Duration::from_millis(100)).await; + PollN::new(3).await; render_count_handle.modify(|x| *x + 1); }); if *render_count.get() > 2 { @@ -122,7 +150,7 @@ fn mouse_enter() { let tui_ctx: TuiContext = cx.consume_context().unwrap(); let render_count_handle = render_count.clone(); cx.spawn(async move { - tokio::time::sleep(Duration::from_millis(100)).await; + PollN::new(3).await; render_count_handle.modify(|x| *x + 1); }); if *render_count.get() > 2 { @@ -161,7 +189,7 @@ fn mouse_exit() { let tui_ctx: TuiContext = cx.consume_context().unwrap(); let render_count_handle = render_count.clone(); cx.spawn(async move { - tokio::time::sleep(Duration::from_millis(100)).await; + PollN::new(3).await; render_count_handle.modify(|x| *x + 1); }); if *render_count.get() > 2 { @@ -200,7 +228,7 @@ fn mouse_move() { let tui_ctx: TuiContext = cx.consume_context().unwrap(); let render_count_handle = render_count.clone(); cx.spawn(async move { - tokio::time::sleep(Duration::from_millis(100)).await; + PollN::new(3).await; render_count_handle.modify(|x| *x + 1); }); if *render_count.get() > 2 { @@ -239,7 +267,7 @@ fn wheel() { let tui_ctx: TuiContext = cx.consume_context().unwrap(); let render_count_handle = render_count.clone(); cx.spawn(async move { - tokio::time::sleep(Duration::from_millis(100)).await; + PollN::new(3).await; render_count_handle.modify(|x| *x + 1); }); if *render_count.get() > 2 { @@ -279,7 +307,7 @@ fn click() { let tui_ctx: TuiContext = cx.consume_context().unwrap(); let render_count_handle = render_count.clone(); cx.spawn(async move { - tokio::time::sleep(Duration::from_millis(100)).await; + PollN::new(3).await; render_count_handle.modify(|x| *x + 1); }); if *render_count.get() > 2 { @@ -318,7 +346,7 @@ fn context_menu() { let tui_ctx: TuiContext = cx.consume_context().unwrap(); let render_count_handle = render_count.clone(); cx.spawn(async move { - tokio::time::sleep(Duration::from_millis(100)).await; + PollN::new(3).await; render_count_handle.modify(|x| *x + 1); }); if *render_count.get() > 2 {