From 920fcf728c5b6a22357ef5149ab6be8c09360ab7 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 16:34:57 -0500 Subject: [PATCH 01/10] create onmounted event --- packages/html/Cargo.toml | 5 ++ packages/html/src/events.rs | 3 + packages/html/src/events/mounted.rs | 85 ++++++++++++++++++++++++ packages/html/src/web_sys_bind/events.rs | 49 +++++++++++++- 4 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 packages/html/src/events/mounted.rs diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index e60fcd8e5..513cc253a 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -39,6 +39,11 @@ features = [ "FocusEvent", "CompositionEvent", "ClipboardEvent", + "Element", + "DomRect", + "ScrollIntoViewOptions", + "ScrollLogicalPosition", + "ScrollBehavior", ] [dev-dependencies] diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 853400c28..85a408d0b 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -33,6 +33,7 @@ mod form; mod image; mod keyboard; mod media; +mod mounted; mod mouse; mod pointer; mod scroll; @@ -51,6 +52,7 @@ pub use form::*; pub use image::*; pub use keyboard::*; pub use media::*; +pub use mounted::*; pub use mouse::*; pub use pointer::*; pub use scroll::*; @@ -144,6 +146,7 @@ pub fn event_bubbles(evt: &str) -> bool { "animationiteration" => true, "transitionend" => true, "toggle" => true, + "mounted" => false, _ => true, } } diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs new file mode 100644 index 000000000..a5413bb5b --- /dev/null +++ b/packages/html/src/events/mounted.rs @@ -0,0 +1,85 @@ +//! Handles quering data from the renderer + +use euclid::Rect; + +use std::{any::Any, rc::Rc}; + +/// An Element that has been rendered and allows reading and modifying information about it. +/// +/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. +pub trait RenderedElementBacking { + /// Get the renderer specific element for the given id + fn get_raw_element(&self) -> Option<&dyn Any> { + None + } + + /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) + fn get_client_rect(&self) -> Option> { + None + } + + /// Scroll to make the element visible + fn scroll_to(&self, _behavior: ScrollBehavior) -> Option<()> { + None + } + + /// Set the focus on the element + fn set_focus(&self, _focus: bool) -> Option<()> { + None + } +} + +/// The way that scrolling should be performed +pub enum ScrollBehavior { + /// Scroll to the element immediately + Instant, + /// Scroll to the element smoothly + Smooth, +} + +/// An Element that has been rendered and allows reading and modifying information about it. +/// +/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. +pub struct MountedData { + inner: Rc, +} + +impl MountedData { + /// Create a new MountedData + pub fn new(registry: impl RenderedElementBacking + 'static) -> Self { + Self { + inner: Rc::new(registry), + } + } + + /// Get the renderer specific element for the given id + pub fn get_raw_element(&self) -> Option<&dyn Any> { + self.inner.get_raw_element() + } + + /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) + pub fn get_client_rect(&self) -> Option> { + self.inner.get_client_rect() + } + + /// Scroll to make the element visible + pub fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + self.inner.scroll_to(behavior) + } + + /// Set the focus on the element + pub fn set_focus(&self, focus: bool) -> Option<()> { + self.inner.set_focus(focus) + } +} + +use dioxus_core::Event; + +pub type MountedEvent = Event; + +impl_event! [ + MountedData; + + /// mounted + onmounted +]; diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 196099444..01b25dfc2 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -4,14 +4,14 @@ use crate::events::{ }; use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; -use crate::DragData; +use crate::{DragData, MountedData, RenderedElementBacking, ScrollBehavior}; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; use std::str::FromStr; use wasm_bindgen::JsCast; use web_sys::{ - AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, - TransitionEvent, WheelEvent, + AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, + ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent, }; macro_rules! uncheck_convert { @@ -193,3 +193,46 @@ impl From<&TransitionEvent> for TransitionData { } } } + +impl From<&web_sys::Element> for MountedData { + fn from(e: &web_sys::Element) -> Self { + MountedData::new(e.clone()) + } +} + +impl RenderedElementBacking for web_sys::Element { + fn get_client_rect(&self) -> Option> { + let rect = self.get_bounding_client_rect(); + Some(euclid::Rect::new( + euclid::Point2D::new(rect.left(), rect.top()), + euclid::Size2D::new(rect.width(), rect.height()), + )) + } + + fn get_raw_element(&self) -> Option<&dyn std::any::Any> { + Some(self) + } + + fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + match behavior { + ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options( + ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant), + ), + ScrollBehavior::Smooth => self.scroll_into_view_with_scroll_into_view_options( + ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Smooth), + ), + } + + Some(()) + } + + fn set_focus(&self, focus: bool) -> Option<()> { + self.dyn_ref::().and_then(|e| { + if focus { + e.focus().ok() + } else { + e.blur().ok() + } + }) + } +} From 1aad285853a4bbe340d9c4775fe8d1b7248a8ea2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 17:02:12 -0500 Subject: [PATCH 02/10] provide nicer error types --- packages/html/src/events/mounted.rs | 57 ++++++++++++++++++------ packages/html/src/web_sys_bind/events.rs | 44 +++++++++++------- 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index a5413bb5b..dafbf46c8 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -2,30 +2,34 @@ use euclid::Rect; -use std::{any::Any, rc::Rc}; +use std::{ + any::Any, + fmt::{Display, Formatter}, + rc::Rc, +}; /// An Element that has been rendered and allows reading and modifying information about it. /// /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. pub trait RenderedElementBacking { /// Get the renderer specific element for the given id - fn get_raw_element(&self) -> Option<&dyn Any> { - None + fn get_raw_element(&self) -> MountedResult<&dyn Any> { + Err(MountedError::NotSupported) } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - fn get_client_rect(&self) -> Option> { - None + fn get_client_rect(&self) -> MountedResult> { + Err(MountedError::NotSupported) } /// Scroll to make the element visible - fn scroll_to(&self, _behavior: ScrollBehavior) -> Option<()> { - None + fn scroll_to(&self, _behavior: ScrollBehavior) -> MountedResult<()> { + Err(MountedError::NotSupported) } /// Set the focus on the element - fn set_focus(&self, _focus: bool) -> Option<()> { - None + fn set_focus(&self, _focus: bool) -> MountedResult<()> { + Err(MountedError::NotSupported) } } @@ -53,22 +57,22 @@ impl MountedData { } /// Get the renderer specific element for the given id - pub fn get_raw_element(&self) -> Option<&dyn Any> { + pub fn get_raw_element(&self) -> MountedResult<&dyn Any> { self.inner.get_raw_element() } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - pub fn get_client_rect(&self) -> Option> { + pub fn get_client_rect(&self) -> MountedResult> { self.inner.get_client_rect() } /// Scroll to make the element visible - pub fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + pub fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { self.inner.scroll_to(behavior) } /// Set the focus on the element - pub fn set_focus(&self, focus: bool) -> Option<()> { + pub fn set_focus(&self, focus: bool) -> MountedResult<()> { self.inner.set_focus(focus) } } @@ -83,3 +87,30 @@ impl_event! [ /// mounted onmounted ]; + +/// The MountedResult type for the MountedData +pub type MountedResult = Result; + +#[derive(Debug)] +/// The error type for the MountedData +pub enum MountedError { + /// The renderer does not support the requested operation + NotSupported, + /// The element was not found + OperationFailed(Box), +} + +impl Display for MountedError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + MountedError::NotSupported => { + write!(f, "The renderer does not support the requested operation") + } + MountedError::OperationFailed(e) => { + write!(f, "The operation failed: {}", e) + } + } + } +} + +impl std::error::Error for MountedError {} diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 01b25dfc2..1983a31da 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -4,11 +4,13 @@ use crate::events::{ }; use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; -use crate::{DragData, MountedData, RenderedElementBacking, ScrollBehavior}; +use crate::{ + DragData, MountedData, MountedError, MountedResult, RenderedElementBacking, ScrollBehavior, +}; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; use std::str::FromStr; -use wasm_bindgen::JsCast; +use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent, @@ -201,19 +203,19 @@ impl From<&web_sys::Element> for MountedData { } impl RenderedElementBacking for web_sys::Element { - fn get_client_rect(&self) -> Option> { + fn get_client_rect(&self) -> MountedResult> { let rect = self.get_bounding_client_rect(); - Some(euclid::Rect::new( + Ok(euclid::Rect::new( euclid::Point2D::new(rect.left(), rect.top()), euclid::Size2D::new(rect.width(), rect.height()), )) } - fn get_raw_element(&self) -> Option<&dyn std::any::Any> { - Some(self) + fn get_raw_element(&self) -> MountedResult<&dyn std::any::Any> { + Ok(self) } - fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { match behavior { ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options( ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant), @@ -223,16 +225,26 @@ impl RenderedElementBacking for web_sys::Element { ), } - Some(()) + Ok(()) } - fn set_focus(&self, focus: bool) -> Option<()> { - self.dyn_ref::().and_then(|e| { - if focus { - e.focus().ok() - } else { - e.blur().ok() - } - }) + fn set_focus(&self, focus: bool) -> MountedResult<()> { + self.dyn_ref::() + .ok_or_else(|| MountedError::OperationFailed(Box::new(FocusError(self.into())))) + .and_then(|e| { + (if focus { e.focus() } else { e.blur() }) + .map_err(|err| MountedError::OperationFailed(Box::new(FocusError(err)))) + }) } } + +#[derive(Debug)] +struct FocusError(JsValue); + +impl std::fmt::Display for FocusError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "failed to focus element {:?}", self.0) + } +} + +impl std::error::Error for FocusError {} From cb5cb56ad352f7d4edc1fb53b96e24a9fcd5d795 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 18:28:34 -0500 Subject: [PATCH 03/10] implement onmounted for the web renderer --- .../interpreter/src/sledgehammer_bindings.rs | 7 +++ packages/web/src/dom.rs | 52 +++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 33f6b4420..650e721ef 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -116,6 +116,10 @@ mod js { export function set_node(id, node) { nodes[id] = node; } + export function get_node(id) { + console.log(nodes, id); + return nodes[id]; + } export function initilize(root, handler) { listeners.handler = handler; nodes = [root]; @@ -166,6 +170,9 @@ mod js { #[wasm_bindgen] pub fn set_node(id: u32, node: Node); + #[wasm_bindgen] + pub fn get_node(id: u32) -> Node; + #[wasm_bindgen] pub fn initilize(root: Node, handler: &Function); } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 4d17749d8..8aae595eb 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -10,8 +10,8 @@ use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; -use dioxus_html::{event_bubbles, CompositionData, FormData}; -use dioxus_interpreter_js::{save_template, Channel}; +use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData}; +use dioxus_interpreter_js::{get_node, save_template, Channel}; use futures_channel::mpsc; use rustc_hash::FxHashMap; use std::{any::Any, rc::Rc}; @@ -27,6 +27,7 @@ pub struct WebsysDom { templates: FxHashMap, max_template_id: u32, pub(crate) interpreter: Channel, + event_channel: mpsc::UnboundedSender, } pub struct UiEvent { @@ -34,7 +35,6 @@ pub struct UiEvent { pub bubbles: bool, pub element: ElementId, pub data: Rc, - pub event: Event, } impl WebsysDom { @@ -48,8 +48,9 @@ impl WebsysDom { }; let interpreter = Channel::default(); - let handler: Closure = - Closure::wrap(Box::new(move |event: &web_sys::Event| { + let handler: Closure = Closure::wrap(Box::new({ + let event_channel = event_channel.clone(); + move |event: &web_sys::Event| { let name = event.type_(); let element = walk_event_for_id(event); let bubbles = dioxus_html::event_bubbles(name.as_str()); @@ -69,10 +70,10 @@ impl WebsysDom { bubbles, element, data, - event: event.clone(), }); } - })); + } + })); dioxus_interpreter_js::initilize( root.clone().unchecked_into(), @@ -85,6 +86,7 @@ impl WebsysDom { interpreter, templates: FxHashMap::default(), max_template_id: 0, + event_channel, } } @@ -156,6 +158,8 @@ impl WebsysDom { pub fn apply_edits(&mut self, mut edits: Vec) { use Mutation::*; let i = &mut self.interpreter; + // we need to apply the mount events last, so we collect them here + let mut to_mount = Vec::new(); for edit in &edits { match edit { AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32), @@ -206,17 +210,43 @@ impl WebsysDom { }, SetText { value, id } => i.set_text(id.0 as u32, value), NewEventListener { name, id, .. } => { - i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8); - } - RemoveEventListener { name, id } => { - i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + match *name { + // mounted events are fired immediately after the element is mounted. + "mounted" => { + to_mount.push(*id); + } + _ => { + i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8); + } + } } + RemoveEventListener { name, id } => match *name { + "mounted" => {} + _ => { + i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8); + } + }, Remove { id } => i.remove(id.0 as u32), PushRoot { id } => i.push_root(id.0 as u32), } } edits.clear(); i.flush(); + + for id in to_mount { + let node = get_node(id.0 as u32); + if let Some(element) = node.dyn_ref::() { + log::info!("mounted event fired: {}", id.0); + let data: MountedData = element.into(); + let data = Rc::new(data); + let _ = self.event_channel.unbounded_send(UiEvent { + name: "mounted".to_string(), + bubbles: false, + element: id, + data, + }); + } + } } } From 7636c046faa7b94f6ddca727a2ecdfd5511888c3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 20 Mar 2023 16:10:34 -0500 Subject: [PATCH 04/10] implement on mounted for desktop --- packages/desktop/src/desktop_context.rs | 5 + packages/desktop/src/element.rs | 208 +++++++++++++++++++++++ packages/desktop/src/lib.rs | 60 ++++++- packages/html/src/events/mounted.rs | 36 ++-- packages/html/src/transit.rs | 52 ++++++ packages/html/src/web_sys_bind/events.rs | 26 ++- packages/interpreter/Cargo.toml | 2 + packages/interpreter/src/interpreter.js | 85 +++++++++ 8 files changed, 450 insertions(+), 24 deletions(-) create mode 100644 packages/desktop/src/element.rs diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 2ed4bbee9..ccb8c7588 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use std::rc::Weak; use crate::create_new_window; +use crate::element::QueryEngine; use crate::eval::EvalResult; use crate::events::IpcMessage; use crate::shortcut::IntoKeyCode; @@ -62,6 +63,9 @@ pub struct DesktopContext { /// The receiver for eval results since eval is async pub(super) eval: tokio::sync::broadcast::Sender, + /// The receiver for queries about elements + pub(super) query: QueryEngine, + pub(super) pending_windows: WebviewQueue, pub(crate) event_loop: EventLoopWindowTarget, @@ -97,6 +101,7 @@ impl DesktopContext { proxy, event_loop, eval: tokio::sync::broadcast::channel(8).0, + query: Default::default(), pending_windows: webviews, event_handlers, shortcut_manager, diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs new file mode 100644 index 000000000..60a995f1b --- /dev/null +++ b/packages/desktop/src/element.rs @@ -0,0 +1,208 @@ +use std::{cell::RefCell, rc::Rc}; + +use dioxus_core::ElementId; +use dioxus_html::{ + MountedResult, MountedReturn, MountedReturnData, NodeUpdate, NodeUpdateData, + RenderedElementBacking, +}; +use slab::Slab; +use wry::webview::WebView; + +/// A mounted element passed to onmounted events +pub struct DesktopElement { + id: ElementId, + webview: Rc, + query: QueryEngine, +} + +impl DesktopElement { + pub(crate) fn new(id: ElementId, webview: Rc, query: QueryEngine) -> Self { + Self { id, webview, query } + } + + /// Get the id of the element + pub fn id(&self) -> ElementId { + self.id + } + + /// Get the webview the element is mounted in + pub fn webview(&self) -> &Rc { + &self.webview + } +} + +impl RenderedElementBacking for DesktopElement { + fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { + Ok(self) + } + + fn get_client_rect( + &self, + ) -> std::pin::Pin< + Box< + dyn futures_util::Future< + Output = dioxus_html::MountedResult>, + >, + >, + > { + let fut = self + .query + .new_query(self.id, NodeUpdateData::GetClientRect {}, &self.webview) + .resolve(); + Box::pin(async move { + match fut.await { + Some(MountedReturnData::GetClientRect(rect)) => Ok(rect), + Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::MismatchedReturn), + )), + None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( + DesktopQueryError::FailedToQuery, + ))), + } + }) + } + + fn scroll_to( + &self, + behavior: dioxus_html::ScrollBehavior, + ) -> std::pin::Pin>>> { + let fut = self + .query + .new_query( + self.id, + NodeUpdateData::ScrollTo { behavior }, + &self.webview, + ) + .resolve(); + Box::pin(async move { + match fut.await { + Some(MountedReturnData::ScrollTo(())) => Ok(()), + Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::MismatchedReturn), + )), + None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( + DesktopQueryError::FailedToQuery, + ))), + } + }) + } + + fn set_focus( + &self, + focus: bool, + ) -> std::pin::Pin>>> { + let fut = self + .query + .new_query(self.id, NodeUpdateData::SetFocus { focus }, &self.webview) + .resolve(); + Box::pin(async move { + match fut.await { + Some(MountedReturnData::SetFocus(())) => Ok(()), + Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::MismatchedReturn), + )), + None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( + DesktopQueryError::FailedToQuery, + ))), + } + }) + } +} + +#[derive(Default, Clone)] +struct SharedSlab { + slab: Rc>>, +} + +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + fn new_query(&self, id: ElementId, update: NodeUpdateData, webview: &WebView) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + let update = NodeUpdate { + id: id.0 as u32, + request_id, + data: update, + }; + + // start the query + webview + .evaluate_script(&format!( + "window.interpreter.handleNodeUpdate({})", + serde_json::to_string(&update).unwrap() + )) + .unwrap(); + + Query { + slab: self.active_requests.clone(), + id: request_id, + reciever: self.sender.subscribe(), + } + } + + pub fn send(&self, data: MountedReturn) { + self.sender.send(data).unwrap(); + } +} + +struct Query { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, +} + +impl Query { + async fn resolve(mut self) -> Option { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break result.data; + } + } + Err(_) => { + break None; + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Debug)] +enum DesktopQueryError { + FailedToQuery, + MismatchedReturn, +} + +impl std::fmt::Display for DesktopQueryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), + DesktopQueryError::MismatchedReturn => { + write!(f, "The return type did not match the query") + } + } + } +} + +impl std::error::Error for DesktopQueryError {} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 75c67dd22..079331fdb 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -5,6 +5,7 @@ mod cfg; mod desktop_context; +mod element; mod escape; mod eval; mod events; @@ -19,7 +20,8 @@ pub use desktop_context::{ }; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; -use dioxus_html::HtmlEvent; +use dioxus_html::{HtmlEvent, MountedData, MountedReturn}; +use element::DesktopElement; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; use shortcut::ShortcutRegistry; @@ -220,19 +222,69 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) } EventData::Ipc(msg) if msg.method() == "user_event" => { - let evt = match serde_json::from_value::(msg.params()) { + let params = msg.params(); + + let evt = match serde_json::from_value::(params) { Ok(value) => value, Err(_) => return, }; + let HtmlEvent { + element, + name, + bubbles, + data, + } = evt; + let view = webviews.get_mut(&event.1).unwrap(); - view.dom - .handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles); + // check for a mounted event placeholder and replace it with a desktop specific element + let as_any = if let dioxus_html::EventData::Mounted = &data { + let query = view + .dom + .base_scope() + .consume_context::() + .unwrap() + .query; + + let element = DesktopElement::new(element, view.webview.clone(), query); + + Rc::new(MountedData::new(element)) + } else { + data.into_any() + }; + + view.dom.handle_event(&name, as_any, element, bubbles); send_edits(view.dom.render_immediate(), &view.webview); } + EventData::Ipc(msg) if msg.method() == "node_update" => { + let params = msg.params(); + println!("node_update: {:?}", params); + + // check for a mounted event + let evt = match serde_json::from_value::(params) { + Ok(value) => value, + Err(err) => { + println!("node_update: {:?}", err); + return; + } + }; + + let view = webviews.get(&event.1).unwrap(); + let query = view + .dom + .base_scope() + .consume_context::() + .unwrap() + .query; + + println!("node_update: {:?}", evt); + + query.send(evt); + } + EventData::Ipc(msg) if msg.method() == "initialize" => { let view = webviews.get_mut(&event.1).unwrap(); send_edits(view.dom.rebuild(), &view.webview); diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index dafbf46c8..548359b5c 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -5,12 +5,15 @@ use euclid::Rect; use std::{ any::Any, fmt::{Display, Formatter}, + future::Future, + pin::Pin, rc::Rc, }; /// An Element that has been rendered and allows reading and modifying information about it. /// /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. +// we can not use async_trait here because it does not create a trait that is object safe pub trait RenderedElementBacking { /// Get the renderer specific element for the given id fn get_raw_element(&self) -> MountedResult<&dyn Any> { @@ -18,26 +21,35 @@ pub trait RenderedElementBacking { } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - fn get_client_rect(&self) -> MountedResult> { - Err(MountedError::NotSupported) + #[allow(clippy::type_complexity)] + fn get_client_rect(&self) -> Pin>>>> { + Box::pin(async { Err(MountedError::NotSupported) }) } /// Scroll to make the element visible - fn scroll_to(&self, _behavior: ScrollBehavior) -> MountedResult<()> { - Err(MountedError::NotSupported) + fn scroll_to( + &self, + _behavior: ScrollBehavior, + ) -> Pin>>> { + Box::pin(async { Err(MountedError::NotSupported) }) } /// Set the focus on the element - fn set_focus(&self, _focus: bool) -> MountedResult<()> { - Err(MountedError::NotSupported) + fn set_focus(&self, _focus: bool) -> Pin>>> { + Box::pin(async { Err(MountedError::NotSupported) }) } } +impl RenderedElementBacking for () {} + /// The way that scrolling should be performed +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum ScrollBehavior { /// Scroll to the element immediately + #[cfg_attr(feature = "serialize", serde(rename = "instant"))] Instant, /// Scroll to the element smoothly + #[cfg_attr(feature = "serialize", serde(rename = "smooth"))] Smooth, } @@ -62,18 +74,18 @@ impl MountedData { } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - pub fn get_client_rect(&self) -> MountedResult> { - self.inner.get_client_rect() + pub async fn get_client_rect(&self) -> MountedResult> { + self.inner.get_client_rect().await } /// Scroll to make the element visible - pub fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { - self.inner.scroll_to(behavior) + pub async fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { + self.inner.scroll_to(behavior).await } /// Set the focus on the element - pub fn set_focus(&self, focus: bool) -> MountedResult<()> { - self.inner.set_focus(focus) + pub async fn set_focus(&self, focus: bool) -> MountedResult<()> { + self.inner.set_focus(focus).await } } diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index 651b7e4f6..12c35bd28 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -2,6 +2,7 @@ use std::{any::Any, rc::Rc}; use crate::events::*; use dioxus_core::ElementId; +use euclid::Rect; use serde::{Deserialize, Serialize}; #[derive(Serialize, Debug, Clone, PartialEq)] @@ -113,6 +114,9 @@ fn fun_name( // Toggle "toggle" => Toggle(de(data)?), + // Mounted + "mounted" => Mounted, + // ImageData => "load" | "error"; // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange"; other => { @@ -151,6 +155,7 @@ pub enum EventData { Animation(AnimationData), Transition(TransitionData), Toggle(ToggleData), + Mounted, } impl EventData { @@ -172,6 +177,7 @@ impl EventData { EventData::Animation(data) => Rc::new(data) as Rc, EventData::Transition(data) => Rc::new(data) as Rc, EventData::Toggle(data) => Rc::new(data) as Rc, + EventData::Mounted => Rc::new(MountedData::new(())) as Rc, } } } @@ -215,3 +221,49 @@ fn test_back_and_forth() { assert_eq!(data, p); } + +/// Message to update a node to support MountedData +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct NodeUpdate { + /// The id of the node to update + pub id: u32, + /// The id of the request + pub request_id: usize, + /// The data to update the node with + pub data: NodeUpdateData, +} + +/// Message to update a node to support MountedData +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] +pub enum NodeUpdateData { + SetFocus { focus: bool }, + GetClientRect {}, + ScrollTo { behavior: ScrollBehavior }, +} + +/// The result of a element query +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct MountedReturn { + /// A unique id for the query + pub id: usize, + /// The result of the query + pub data: Option, +} + +/// The data of a element query +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] +pub enum MountedReturnData { + SetFocus(()), + GetClientRect(Rect), + ScrollTo(()), +} diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 1983a31da..bcbe80dda 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -9,6 +9,8 @@ use crate::{ }; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; +use std::future::Future; +use std::pin::Pin; use std::str::FromStr; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ @@ -203,19 +205,25 @@ impl From<&web_sys::Element> for MountedData { } impl RenderedElementBacking for web_sys::Element { - fn get_client_rect(&self) -> MountedResult> { + fn get_client_rect( + &self, + ) -> Pin>>>> { let rect = self.get_bounding_client_rect(); - Ok(euclid::Rect::new( + let result = Ok(euclid::Rect::new( euclid::Point2D::new(rect.left(), rect.top()), euclid::Size2D::new(rect.width(), rect.height()), - )) + )); + Box::pin(async { result }) } fn get_raw_element(&self) -> MountedResult<&dyn std::any::Any> { Ok(self) } - fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { + fn scroll_to( + &self, + behavior: ScrollBehavior, + ) -> Pin>>> { match behavior { ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options( ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant), @@ -225,16 +233,18 @@ impl RenderedElementBacking for web_sys::Element { ), } - Ok(()) + Box::pin(async { Ok(()) }) } - fn set_focus(&self, focus: bool) -> MountedResult<()> { - self.dyn_ref::() + fn set_focus(&self, focus: bool) -> Pin>>> { + let result = self + .dyn_ref::() .ok_or_else(|| MountedError::OperationFailed(Box::new(FocusError(self.into())))) .and_then(|e| { (if focus { e.focus() } else { e.blur() }) .map_err(|err| MountedError::OperationFailed(Box::new(FocusError(err)))) - }) + }); + Box::pin(async { result }) } } diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 7c7d79607..bb8f7ec60 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -19,8 +19,10 @@ js-sys = { version = "0.3.56", optional = true } web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } sledgehammer_bindgen = { version = "0.1.3", optional = true } sledgehammer_utils = { version = "0.1.0", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } [features] default = [] +serialize = ["serde"] web = ["wasm-bindgen", "js-sys", "web-sys"] sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"] diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index b921c7c22..ce2a010c0 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -204,6 +204,76 @@ class Interpreter { node.removeAttribute(name); } } + + GetClientRect(id) { + const node= this.nodes[id]; + if (!node) { + return; + } + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [ + rect.x, + rect.y, + ], + size: [ + rect.width, + rect.height, + ] + }; + } + + ScrollTo(id, behavior) { + const node = this.nodes[id]; + if (!node) { + return; + } + node.scrollIntoView({ + behavior: behavior + }); + return { + type: "ScrollTo", + }; + } + + /// Set the focus on the element + SetFocus(id, focus) { + const node = this.nodes[id]; + if (!node) { + return; + } + if (focus) { + node.focus(); + } else { + node.blur(); + } + return { + type: "SetFocus", + }; + } + + handleNodeUpdate(edit) { + let data; + switch (edit.data.type) { + case "SetFocus": + data = this.SetFocus(edit.id, edit.data.focus); + break; + case "ScrollTo": + data = this.ScrollTo(edit.id, edit.data.behavior); + break; + case "GetClientRect": + data = this.GetClientRect(edit.id); + break; + } + window.ipc.postMessage( + serializeIpcMessage("node_update", { + id: edit.request_id, + data: data + }) + ); + } + handleEdits(edits) { for (let template of edits.templates) { this.SaveTemplate(template); @@ -345,6 +415,19 @@ class Interpreter { let bubbles = event_bubbles(edit.name); + // if this is a mounted listener, we send the event immediately + if (edit.name === "mounted") { + window.ipc.postMessage( + serializeIpcMessage("user_event", { + name: edit.name, + element: edit.id, + data: null, + bubbles, + }) + ); + } + + // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event) => { @@ -921,6 +1004,8 @@ function event_bubbles(event) { return true; case "toggle": return true; + case "mounted": + return false; } return true; From fa9f0d0f6ca6037c687c99afcf6dfd8163d89d4b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 23 Mar 2023 15:19:00 -0500 Subject: [PATCH 05/10] Generalize Query system for use in use_eval and node querys --- packages/desktop/src/desktop_context.rs | 33 +---- packages/desktop/src/element.rs | 165 ++++++------------------ packages/desktop/src/eval.rs | 22 ++-- packages/desktop/src/lib.rs | 52 +++----- packages/desktop/src/query.rs | 105 +++++++++++++++ packages/html/src/events/mounted.rs | 11 +- packages/html/src/transit.rs | 47 ------- packages/interpreter/src/interpreter.js | 33 +---- 8 files changed, 186 insertions(+), 282 deletions(-) create mode 100644 packages/desktop/src/query.rs diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index ccb8c7588..6af6401f3 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,9 +3,9 @@ use std::rc::Rc; use std::rc::Weak; use crate::create_new_window; -use crate::element::QueryEngine; use crate::eval::EvalResult; use crate::events::IpcMessage; +use crate::query::QueryEngine; use crate::shortcut::IntoKeyCode; use crate::shortcut::IntoModifersState; use crate::shortcut::ShortcutId; @@ -17,7 +17,6 @@ use dioxus_core::ScopeState; use dioxus_core::VirtualDom; #[cfg(all(feature = "hot-reload", debug_assertions))] use dioxus_hot_reload::HotReloadMsg; -use serde_json::Value; use slab::Slab; use wry::application::event::Event; use wry::application::event_loop::EventLoopProxy; @@ -60,10 +59,7 @@ pub struct DesktopContext { /// The proxy to the event loop pub proxy: ProxyType, - /// The receiver for eval results since eval is async - pub(super) eval: tokio::sync::broadcast::Sender, - - /// The receiver for queries about elements + /// The receiver for queries about the current window pub(super) query: QueryEngine, pub(super) pending_windows: WebviewQueue, @@ -100,7 +96,6 @@ impl DesktopContext { webview, proxy, event_loop, - eval: tokio::sync::broadcast::channel(8).0, query: Default::default(), pending_windows: webviews, event_handlers, @@ -215,28 +210,10 @@ impl DesktopContext { /// Evaluate a javascript expression pub fn eval(&self, code: &str) -> EvalResult { - // Embed the return of the eval in a function so we can send it back to the main thread - let script = format!( - r#" - window.ipc.postMessage( - JSON.stringify({{ - "method":"eval_result", - "params": ( - function(){{ - {code} - }} - )() - }}) - ); - "# - ); + // the query id lets us keep track of the eval result and send it back to the main thread + let query = self.query.new_query(code, &self.webview); - if let Err(e) = self.webview.evaluate_script(&script) { - // send an error to the eval receiver - log::warn!("Eval script error: {e}"); - } - - EvalResult::new(self.eval.clone()) + EvalResult::new(query) } /// Create a wry event handler that listens for wry events. diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs index 60a995f1b..e94a481c7 100644 --- a/packages/desktop/src/element.rs +++ b/packages/desktop/src/element.rs @@ -1,13 +1,11 @@ -use std::{cell::RefCell, rc::Rc}; +use std::rc::Rc; use dioxus_core::ElementId; -use dioxus_html::{ - MountedResult, MountedReturn, MountedReturnData, NodeUpdate, NodeUpdateData, - RenderedElementBacking, -}; -use slab::Slab; +use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking}; use wry::webview::WebView; +use crate::query::QueryEngine; + /// A mounted element passed to onmounted events pub struct DesktopElement { id: ElementId, @@ -19,16 +17,6 @@ impl DesktopElement { pub(crate) fn new(id: ElementId, webview: Rc, query: QueryEngine) -> Self { Self { id, webview, query } } - - /// Get the id of the element - pub fn id(&self) -> ElementId { - self.id - } - - /// Get the webview the element is mounted in - pub fn webview(&self) -> &Rc { - &self.webview - } } impl RenderedElementBacking for DesktopElement { @@ -45,19 +33,21 @@ impl RenderedElementBacking for DesktopElement { >, >, > { + let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + let fut = self .query - .new_query(self.id, NodeUpdateData::GetClientRect {}, &self.webview) + .new_query::>>(&script, &self.webview) .resolve(); Box::pin(async move { match fut.await { - Some(MountedReturnData::GetClientRect(rect)) => Ok(rect), - Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( - Box::new(DesktopQueryError::MismatchedReturn), + Ok(Some(rect)) => Ok(rect), + Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), )), - None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( - DesktopQueryError::FailedToQuery, - ))), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } } }) } @@ -66,23 +56,25 @@ impl RenderedElementBacking for DesktopElement { &self, behavior: dioxus_html::ScrollBehavior, ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.ScrollTo({}, {});", + self.id.0, + serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") + ); + let fut = self .query - .new_query( - self.id, - NodeUpdateData::ScrollTo { behavior }, - &self.webview, - ) + .new_query::(&script, &self.webview) .resolve(); Box::pin(async move { match fut.await { - Some(MountedReturnData::ScrollTo(())) => Ok(()), - Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( - Box::new(DesktopQueryError::MismatchedReturn), + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), )), - None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( - DesktopQueryError::FailedToQuery, - ))), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } } }) } @@ -91,116 +83,41 @@ impl RenderedElementBacking for DesktopElement { &self, focus: bool, ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.SetFocus({}, {});", + self.id.0, focus + ); + + println!("script: {}", script); let fut = self .query - .new_query(self.id, NodeUpdateData::SetFocus { focus }, &self.webview) + .new_query::(&script, &self.webview) .resolve(); + println!("fut"); + Box::pin(async move { match fut.await { - Some(MountedReturnData::SetFocus(())) => Ok(()), - Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( - Box::new(DesktopQueryError::MismatchedReturn), + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), )), - None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( - DesktopQueryError::FailedToQuery, - ))), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } } }) } } -#[derive(Default, Clone)] -struct SharedSlab { - slab: Rc>>, -} - -#[derive(Clone)] -pub(crate) struct QueryEngine { - sender: Rc>, - active_requests: SharedSlab, -} - -impl Default for QueryEngine { - fn default() -> Self { - let (sender, _) = tokio::sync::broadcast::channel(8); - Self { - sender: Rc::new(sender), - active_requests: SharedSlab::default(), - } - } -} - -impl QueryEngine { - fn new_query(&self, id: ElementId, update: NodeUpdateData, webview: &WebView) -> Query { - let request_id = self.active_requests.slab.borrow_mut().insert(()); - - let update = NodeUpdate { - id: id.0 as u32, - request_id, - data: update, - }; - - // start the query - webview - .evaluate_script(&format!( - "window.interpreter.handleNodeUpdate({})", - serde_json::to_string(&update).unwrap() - )) - .unwrap(); - - Query { - slab: self.active_requests.clone(), - id: request_id, - reciever: self.sender.subscribe(), - } - } - - pub fn send(&self, data: MountedReturn) { - self.sender.send(data).unwrap(); - } -} - -struct Query { - slab: SharedSlab, - id: usize, - reciever: tokio::sync::broadcast::Receiver, -} - -impl Query { - async fn resolve(mut self) -> Option { - let result = loop { - match self.reciever.recv().await { - Ok(result) => { - if result.id == self.id { - break result.data; - } - } - Err(_) => { - break None; - } - } - }; - - // Remove the query from the slab - self.slab.slab.borrow_mut().remove(self.id); - - result - } -} - #[derive(Debug)] enum DesktopQueryError { FailedToQuery, - MismatchedReturn, } impl std::fmt::Display for DesktopQueryError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), - DesktopQueryError::MismatchedReturn => { - write!(f, "The return type did not match the query") - } } } } diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index 7a92bedf5..1e11cad18 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -1,36 +1,32 @@ use std::rc::Rc; +use crate::query::Query; +use crate::query::QueryError; use crate::use_window; use dioxus_core::ScopeState; -use serde::de::Error; use std::future::Future; use std::future::IntoFuture; use std::pin::Pin; /// A future that resolves to the result of a JavaScript evaluation. pub struct EvalResult { - pub(crate) broadcast: tokio::sync::broadcast::Sender, + pub(crate) query: Query, } impl EvalResult { - pub(crate) fn new(sender: tokio::sync::broadcast::Sender) -> Self { - Self { broadcast: sender } + pub(crate) fn new(query: Query) -> Self { + Self { query } } } impl IntoFuture for EvalResult { - type Output = Result; + type Output = Result; - type IntoFuture = Pin>>>; + type IntoFuture = Pin>>>; fn into_future(self) -> Self::IntoFuture { - Box::pin(async move { - let mut reciever = self.broadcast.subscribe(); - match reciever.recv().await { - Ok(result) => Ok(result), - Err(_) => Err(serde_json::Error::custom("No result returned")), - } - }) as Pin>>> + Box::pin(self.query.resolve()) + as Pin>>> } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 079331fdb..8e92d90aa 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -10,17 +10,19 @@ mod escape; mod eval; mod events; mod protocol; +mod query; mod shortcut; mod waker; mod webview; +use crate::query::QueryResult; pub use cfg::Config; pub use desktop_context::{ use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId, }; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; -use dioxus_html::{HtmlEvent, MountedData, MountedReturn}; +use dioxus_html::{HtmlEvent, MountedData}; use element::DesktopElement; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; @@ -259,30 +261,21 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) send_edits(view.dom.render_immediate(), &view.webview); } - EventData::Ipc(msg) if msg.method() == "node_update" => { + // When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query + EventData::Ipc(msg) if msg.method() == "query" => { let params = msg.params(); - println!("node_update: {:?}", params); - // check for a mounted event - let evt = match serde_json::from_value::(params) { - Ok(value) => value, - Err(err) => { - println!("node_update: {:?}", err); - return; - } - }; + if let Ok(result) = dbg!(serde_json::from_value::(params)) { + let view = webviews.get(&event.1).unwrap(); + let query = view + .dom + .base_scope() + .consume_context::() + .unwrap() + .query; - let view = webviews.get(&event.1).unwrap(); - let query = view - .dom - .base_scope() - .consume_context::() - .unwrap() - .query; - - println!("node_update: {:?}", evt); - - query.send(evt); + query.send(result); + } } EventData::Ipc(msg) if msg.method() == "initialize" => { @@ -290,21 +283,6 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) send_edits(view.dom.rebuild(), &view.webview); } - // When the webview chirps back with the result of the eval, we send it to the active receiver - // - // This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once, - // you might the wrong result. This should be fixed - EventData::Ipc(msg) if msg.method() == "eval_result" => { - webviews[&event.1] - .dom - .base_scope() - .consume_context::() - .unwrap() - .eval - .send(msg.params()) - .unwrap(); - } - EventData::Ipc(msg) if msg.method() == "browser_open" => { if let Some(temp) = msg.params().as_object() { if temp.contains_key("href") { diff --git a/packages/desktop/src/query.rs b/packages/desktop/src/query.rs new file mode 100644 index 000000000..8b0cf3fc3 --- /dev/null +++ b/packages/desktop/src/query.rs @@ -0,0 +1,105 @@ +use std::{cell::RefCell, rc::Rc}; + +use serde::{de::DeserializeOwned, Deserialize}; +use serde_json::Value; +use slab::Slab; +use thiserror::Error; +use tokio::sync::broadcast::error::RecvError; +use wry::webview::WebView; + +#[derive(Default, Clone)] +struct SharedSlab { + slab: Rc>>, +} + +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + pub fn new_query(&self, script: &str, webview: &WebView) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + // start the query + // We embed the return of the eval in a function so we can send it back to the main thread + if let Err(err) = webview.evaluate_script(&format!( + r#"window.ipc.postMessage( + JSON.stringify({{ + "method":"query", + "params": {{ + "id": {request_id}, + "data": (function(){{{script}}})() + }} + }}) + );"# + )) { + log::warn!("Query error: {err}"); + } + + Query { + slab: self.active_requests.clone(), + id: request_id, + reciever: self.sender.subscribe(), + phantom: std::marker::PhantomData, + } + } + + pub fn send(&self, data: QueryResult) { + let _ = self.sender.send(data); + } +} + +pub(crate) struct Query { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, + phantom: std::marker::PhantomData, +} + +impl Query { + pub async fn resolve(mut self) -> Result { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break V::deserialize(result.data).map_err(QueryError::DeserializeError); + } + } + Err(err) => { + break Err(QueryError::RecvError(err)); + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Error, Debug)] +pub enum QueryError { + #[error("Error receiving query result: {0}")] + RecvError(RecvError), + #[error("Error deserializing query result: {0}")] + DeserializeError(serde_json::Error), +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct QueryResult { + id: usize, + data: Value, +} diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index 548359b5c..293b51b9e 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -79,13 +79,16 @@ impl MountedData { } /// Scroll to make the element visible - pub async fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { - self.inner.scroll_to(behavior).await + pub fn scroll_to( + &self, + behavior: ScrollBehavior, + ) -> Pin>>> { + self.inner.scroll_to(behavior) } /// Set the focus on the element - pub async fn set_focus(&self, focus: bool) -> MountedResult<()> { - self.inner.set_focus(focus).await + pub fn set_focus(&self, focus: bool) -> Pin>>> { + self.inner.set_focus(focus) } } diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index 12c35bd28..ef0e7c70d 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -2,7 +2,6 @@ use std::{any::Any, rc::Rc}; use crate::events::*; use dioxus_core::ElementId; -use euclid::Rect; use serde::{Deserialize, Serialize}; #[derive(Serialize, Debug, Clone, PartialEq)] @@ -221,49 +220,3 @@ fn test_back_and_forth() { assert_eq!(data, p); } - -/// Message to update a node to support MountedData -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct NodeUpdate { - /// The id of the node to update - pub id: u32, - /// The id of the request - pub request_id: usize, - /// The data to update the node with - pub data: NodeUpdateData, -} - -/// Message to update a node to support MountedData -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type") -)] -pub enum NodeUpdateData { - SetFocus { focus: bool }, - GetClientRect {}, - ScrollTo { behavior: ScrollBehavior }, -} - -/// The result of a element query -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] -pub struct MountedReturn { - /// A unique id for the query - pub id: usize, - /// The result of the query - pub data: Option, -} - -/// The data of a element query -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type") -)] -pub enum MountedReturnData { - SetFocus(()), - GetClientRect(Rect), - ScrollTo(()), -} diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index ce2a010c0..2c919dcde 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -227,51 +227,26 @@ class Interpreter { ScrollTo(id, behavior) { const node = this.nodes[id]; if (!node) { - return; + return false; } node.scrollIntoView({ behavior: behavior }); - return { - type: "ScrollTo", - }; + return true; } /// Set the focus on the element SetFocus(id, focus) { const node = this.nodes[id]; if (!node) { - return; + return false; } if (focus) { node.focus(); } else { node.blur(); } - return { - type: "SetFocus", - }; - } - - handleNodeUpdate(edit) { - let data; - switch (edit.data.type) { - case "SetFocus": - data = this.SetFocus(edit.id, edit.data.focus); - break; - case "ScrollTo": - data = this.ScrollTo(edit.id, edit.data.behavior); - break; - case "GetClientRect": - data = this.GetClientRect(edit.id); - break; - } - window.ipc.postMessage( - serializeIpcMessage("node_update", { - id: edit.request_id, - data: data - }) - ); + return true; } handleEdits(edits) { From 8243dfe00dceff7c0ee49d5312d1cbf5a15a40b6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 23 Mar 2023 16:52:29 -0500 Subject: [PATCH 06/10] implement a query engine for liveview --- packages/desktop/src/query.rs | 5 ++ packages/liveview/Cargo.toml | 2 + packages/liveview/src/element.rs | 125 +++++++++++++++++++++++++++++++ packages/liveview/src/lib.rs | 2 + packages/liveview/src/main.js | 16 +++- packages/liveview/src/pool.rs | 76 ++++++++++++++++--- packages/liveview/src/query.rs | 113 ++++++++++++++++++++++++++++ 7 files changed, 323 insertions(+), 16 deletions(-) create mode 100644 packages/liveview/src/element.rs create mode 100644 packages/liveview/src/query.rs diff --git a/packages/desktop/src/query.rs b/packages/desktop/src/query.rs index 8b0cf3fc3..bd8b2ee2a 100644 --- a/packages/desktop/src/query.rs +++ b/packages/desktop/src/query.rs @@ -7,11 +7,13 @@ use thiserror::Error; use tokio::sync::broadcast::error::RecvError; use wry::webview::WebView; +/// Tracks what query ids are currently active #[derive(Default, Clone)] struct SharedSlab { slab: Rc>>, } +/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. #[derive(Clone)] pub(crate) struct QueryEngine { sender: Rc>, @@ -29,6 +31,7 @@ impl Default for QueryEngine { } impl QueryEngine { + /// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id. pub fn new_query(&self, script: &str, webview: &WebView) -> Query { let request_id = self.active_requests.slab.borrow_mut().insert(()); @@ -56,6 +59,7 @@ impl QueryEngine { } } + /// Send a query result pub fn send(&self, data: QueryResult) { let _ = self.sender.send(data); } @@ -69,6 +73,7 @@ pub(crate) struct Query { } impl Query { + /// Resolve the query pub async fn resolve(mut self) -> Result { let result = loop { match self.reciever.recv().await { diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 5269927b0..8b80fedb8 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -14,6 +14,8 @@ license = "MIT/Apache-2.0" [dependencies] thiserror = "1.0.38" +log = "0.4.14" +slab = "0.4" futures-util = { version = "0.3.25", default-features = false, features = [ "sink", ] } diff --git a/packages/liveview/src/element.rs b/packages/liveview/src/element.rs new file mode 100644 index 000000000..21be23bee --- /dev/null +++ b/packages/liveview/src/element.rs @@ -0,0 +1,125 @@ +use dioxus_core::ElementId; +use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::query::QueryEngine; + +/// A mounted element passed to onmounted events +pub struct LiveviewElement { + id: ElementId, + query_tx: UnboundedSender, + query: QueryEngine, +} + +impl LiveviewElement { + pub(crate) fn new(id: ElementId, tx: UnboundedSender, query: QueryEngine) -> Self { + Self { + id, + query_tx: tx, + query, + } + } +} + +impl RenderedElementBacking for LiveviewElement { + fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { + Ok(self) + } + + fn get_client_rect( + &self, + ) -> std::pin::Pin< + Box< + dyn futures_util::Future< + Output = dioxus_html::MountedResult>, + >, + >, + > { + let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + + let fut = self + .query + .new_query::>>(&script, &self.query_tx) + .resolve(); + Box::pin(async move { + match fut.await { + Ok(Some(rect)) => Ok(rect), + Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } + + fn scroll_to( + &self, + behavior: dioxus_html::ScrollBehavior, + ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.ScrollTo({}, {});", + self.id.0, + serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") + ); + + let fut = self + .query + .new_query::(&script, &self.query_tx) + .resolve(); + Box::pin(async move { + match fut.await { + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } + + fn set_focus( + &self, + focus: bool, + ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.SetFocus({}, {});", + self.id.0, focus + ); + + let fut = self + .query + .new_query::(&script, &self.query_tx) + .resolve(); + + Box::pin(async move { + match fut.await { + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } +} + +#[derive(Debug)] +enum DesktopQueryError { + FailedToQuery, +} + +impl std::fmt::Display for DesktopQueryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), + } + } +} + +impl std::error::Error for DesktopQueryError {} diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index ffddf0272..0c6fc9b2a 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -18,7 +18,9 @@ pub mod adapters { pub use adapters::*; +mod element; pub mod pool; +mod query; use futures_util::{SinkExt, StreamExt}; pub use pool::*; diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 7dd9758d6..594c764c2 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -26,11 +26,19 @@ class IPC { // todo: retry the connection }; - ws.onmessage = (event) => { + ws.onmessage = (message) => { // Ignore pongs - if (event.data != "__pong__") { - let edits = JSON.parse(event.data); - window.interpreter.handleEdits(edits); + if (message.data != "__pong__") { + const event = JSON.parse(message.data); + switch (event.type) { + case "edits": + let edits = event.data; + window.interpreter.handleEdits(edits); + break; + case "query": + Function("Eval", `"use strict";${event.data};`)(); + break; + } } }; diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index e02c8195b..089d0bad4 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -1,8 +1,13 @@ -use crate::LiveViewError; -use dioxus_core::prelude::*; -use dioxus_html::HtmlEvent; +use crate::{ + element::LiveviewElement, + query::{QueryEngine, QueryResult}, + LiveViewError, +}; +use dioxus_core::{prelude::*, Mutations}; +use dioxus_html::{EventData, HtmlEvent, MountedData}; use futures_util::{pin_mut, SinkExt, StreamExt}; -use std::time::Duration; +use serde::Serialize; +use std::{rc::Rc, time::Duration}; use tokio_util::task::LocalPoolHandle; #[derive(Clone)] @@ -115,7 +120,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li }; // todo: use an efficient binary packed format for this - let edits = serde_json::to_string(&vdom.rebuild()).unwrap(); + let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap(); // pin the futures so we can use select! pin_mut!(ws); @@ -123,11 +128,19 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li // send the initial render to the client ws.send(edits).await?; + // Create the a proxy for query engine + let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel(); + let query_engine = QueryEngine::default(); + // desktop uses this wrapper struct thing around the actual event itself // this is sorta driven by tao/wry - #[derive(serde::Deserialize)] - struct IpcMessage { - params: HtmlEvent, + #[derive(serde::Deserialize, Debug)] + #[serde(tag = "method", content = "params")] + enum IpcMessage { + #[serde(rename = "user_event")] + Event(HtmlEvent), + #[serde(rename = "query")] + Query(QueryResult), } loop { @@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li ws.send("__pong__".to_string()).await?; } Some(Ok(evt)) => { - if let Ok(IpcMessage { params }) = serde_json::from_str::(evt) { - vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles); + if let Ok(message) = serde_json::from_str::(evt) { + match message { + IpcMessage::Event(evt) => { + // Intercept the mounted event and insert a custom element type + if let EventData::Mounted = &evt.data { + let element = LiveviewElement::new(evt.element, query_tx.clone(), query_engine.clone()); + vdom.handle_event( + &evt.name, + Rc::new(MountedData::new(element)), + evt.element, + evt.bubbles, + ); + } + else{ + vdom.handle_event( + &evt.name, + evt.data.into_any(), + evt.element, + evt.bubbles, + ); + } + } + IpcMessage::Query(result) => { + query_engine.send(result); + }, + } } } // log this I guess? when would we get an error here? - Some(Err(_e)) => {}, + Some(Err(_e)) => {} None => return Ok(()), } } + // handle any new queries + Some(query) = query_rx.recv() => { + ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?; + } + Some(msg) = hot_reload_wait => { #[cfg(all(feature = "hot-reload", debug_assertions))] match msg{ @@ -176,6 +218,16 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) .await; - ws.send(serde_json::to_string(&edits).unwrap()).await?; + ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap()) + .await?; } } + +#[derive(Serialize)] +#[serde(tag = "type", content = "data")] +enum ClientUpdate<'a> { + #[serde(rename = "edits")] + Edits(Mutations<'a>), + #[serde(rename = "query")] + Query(String), +} diff --git a/packages/liveview/src/query.rs b/packages/liveview/src/query.rs new file mode 100644 index 000000000..04d531a63 --- /dev/null +++ b/packages/liveview/src/query.rs @@ -0,0 +1,113 @@ +use std::{cell::RefCell, rc::Rc}; + +use serde::{de::DeserializeOwned, Deserialize}; +use serde_json::Value; +use slab::Slab; +use thiserror::Error; +use tokio::sync::{broadcast::error::RecvError, mpsc::UnboundedSender}; + +/// Tracks what query ids are currently active +#[derive(Default, Clone)] +struct SharedSlab { + slab: Rc>>, +} + +/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + /// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id. + pub fn new_query( + &self, + script: &str, + tx: &UnboundedSender, + ) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + // start the query + // We embed the return of the eval in a function so we can send it back to the main thread + if let Err(err) = tx.send(format!( + r#"window.ipc.postMessage( + JSON.stringify({{ + "method":"query", + "params": {{ + "id": {request_id}, + "data": (function(){{{script}}})() + }} + }}) + );"# + )) { + log::warn!("Query error: {err}"); + } + + Query { + slab: self.active_requests.clone(), + id: request_id, + reciever: self.sender.subscribe(), + phantom: std::marker::PhantomData, + } + } + + /// Send a query result + pub fn send(&self, data: QueryResult) { + let _ = self.sender.send(data); + } +} + +pub(crate) struct Query { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, + phantom: std::marker::PhantomData, +} + +impl Query { + /// Resolve the query + pub async fn resolve(mut self) -> Result { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break V::deserialize(result.data).map_err(QueryError::DeserializeError); + } + } + Err(err) => { + break Err(QueryError::RecvError(err)); + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Error, Debug)] +pub enum QueryError { + #[error("Error receiving query result: {0}")] + RecvError(RecvError), + #[error("Error deserializing query result: {0}")] + DeserializeError(serde_json::Error), +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct QueryResult { + id: usize, + data: Value, +} From e0d46d982092074304693917e73cbb3ea0202131 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 23 Mar 2023 17:33:27 -0500 Subject: [PATCH 07/10] implement mounted event for TUI --- packages/tui/src/element.rs | 92 +++++++++++++++++++++++++++++++++++++ packages/tui/src/lib.rs | 10 ++++ 2 files changed, 102 insertions(+) create mode 100644 packages/tui/src/element.rs diff --git a/packages/tui/src/element.rs b/packages/tui/src/element.rs new file mode 100644 index 000000000..5ea1e56af --- /dev/null +++ b/packages/tui/src/element.rs @@ -0,0 +1,92 @@ +use std::{ + fmt::{Display, Formatter}, + rc::Rc, +}; + +use dioxus_core::{ElementId, Mutations, VirtualDom}; +use dioxus_html::{ + geometry::euclid::{Point2D, Rect, Size2D}, + MountedData, MountedError, RenderedElementBacking, +}; + +use crate::query::{ElementRef, Query}; + +pub(crate) fn find_mount_events(mutations: &Mutations) -> Vec { + let mut mount_events = Vec::new(); + for mutation in &mutations.edits { + if let dioxus_core::Mutation::NewEventListener { + name: "mounted", + id, + } = mutation + { + mount_events.push(*id); + } + } + mount_events +} + +pub(crate) fn send_mounted_events(vdom: &mut VirtualDom, mount_events: Vec) { + let query: Query = vdom + .base_scope() + .consume_context() + .expect("Query should be in context"); + for id in mount_events { + let element = TuiElement { + query: query.clone(), + id, + }; + vdom.handle_event("mounted", Rc::new(MountedData::new(element)), id, false); + } +} + +struct TuiElement { + query: Query, + id: ElementId, +} + +impl TuiElement { + pub(crate) fn element(&self) -> ElementRef { + self.query.get(self.id) + } +} + +impl RenderedElementBacking for TuiElement { + fn get_client_rect( + &self, + ) -> std::pin::Pin< + Box< + dyn futures::Future< + Output = dioxus_html::MountedResult>, + >, + >, + > { + let layout = self.element().layout(); + Box::pin(async move { + match layout { + Some(layout) => { + let x = layout.location.x as f64; + let y = layout.location.y as f64; + let width = layout.size.width as f64; + let height = layout.size.height as f64; + Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height))) + } + None => Err(MountedError::OperationFailed(Box::new(TuiElementNotFound))), + } + }) + } + + fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { + Ok(self) + } +} + +#[derive(Debug)] +struct TuiElementNotFound; + +impl Display for TuiElementNotFound { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "TUI element not found") + } +} + +impl std::error::Error for TuiElementNotFound {} diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 75c865de5..911c7c4db 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -7,6 +7,7 @@ use crossterm::{ }; use dioxus_core::*; use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, NodeMask, SendAnyMap}; +use element::{find_mount_events, send_mounted_events}; use focus::FocusState; use futures::{ channel::mpsc::{UnboundedReceiver, UnboundedSender}, @@ -26,6 +27,7 @@ use tokio::select; use tui::{backend::CrosstermBackend, layout::Rect, Terminal}; mod config; +mod element; mod focus; mod hooks; mod layout; @@ -114,7 +116,11 @@ pub fn launch_cfg_with_props(app: Component, props: Props { let mut rdom = rdom.borrow_mut(); let mutations = dom.rebuild(); + // Search for mount events that happened during the rebuild + let mounted = find_mount_events(&mutations); let (to_update, _) = rdom.apply_mutations(mutations); + // Send the mount events + send_mounted_events(&mut dom, mounted); let mut any_map = SendAnyMap::new(); any_map.insert(taffy.clone()); let _to_rerender = rdom.update_state(to_update, any_map); @@ -307,8 +313,12 @@ fn render_vdom( let mut rdom = rdom.borrow_mut(); let mutations = vdom.render_immediate(); handler.prune(&mutations, &rdom); + // Search for mount events that happened during the rebuild + let mounted = find_mount_events(&mutations); // updates the dom's nodes let (to_update, dirty) = rdom.apply_mutations(mutations); + // Send the mount events + send_mounted_events(vdom, mounted); // update the style and layout let mut any_map = SendAnyMap::new(); any_map.insert(taffy.clone()); From a551c0fcb8e4f5481c49d6d2b4a0ab2ab18081be Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Mar 2023 11:32:42 -0500 Subject: [PATCH 08/10] add examples for onmounted --- examples/controll_focus.rs | 44 +++++++++++++++++++++++++ examples/read_size.rs | 57 +++++++++++++++++++++++++++++++++ examples/scroll_to_top.rs | 33 +++++++++++++++++++ packages/desktop/src/element.rs | 2 -- packages/desktop/src/lib.rs | 2 +- 5 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 examples/controll_focus.rs create mode 100644 examples/read_size.rs create mode 100644 examples/scroll_to_top.rs diff --git a/examples/controll_focus.rs b/examples/controll_focus.rs new file mode 100644 index 000000000..0f14a940b --- /dev/null +++ b/examples/controll_focus.rs @@ -0,0 +1,44 @@ +use std::rc::Rc; + +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let elements: &UseRef>> = use_ref(cx, || Vec::new()); + let running = use_state(cx, || true); + + use_future!(cx, |(elements, running)| async move { + let mut focused = 0; + if *running.current() { + loop { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + if let Some(element) = elements.read().get(focused) { + element.set_focus(true); + } else { + focused = 0; + } + focused += 1; + } + } + }); + + cx.render(rsx!( + div { + h1 { "Input Roulette" } + for i in 0..100 { + input { + value: "{i}", + onmounted: move |cx| { + elements.write().push(cx.inner().clone()); + }, + oninput: move |_| { + running.set(false); + } + } + } + } + )) +} diff --git a/examples/read_size.rs b/examples/read_size.rs new file mode 100644 index 000000000..be996e2dd --- /dev/null +++ b/examples/read_size.rs @@ -0,0 +1,57 @@ +use std::rc::Rc; + +use dioxus::{html::geometry::euclid::Rect, prelude::*}; + +fn main() { + dioxus_desktop::launch_cfg( + app, + dioxus_desktop::Config::default().with_custom_head( + r#" + +"# + .to_owned(), + ), + ); +} + +fn app(cx: Scope) -> Element { + let div_element: &UseRef>> = use_ref(cx, || None); + + let dimentions = use_ref(cx, || Rect::zero()); + + cx.render(rsx!( + div { + width: "50%", + height: "50%", + background_color: "red", + onmounted: move |cx| { + div_element.set(Some(cx.inner().clone())); + }, + "This element is {dimentions.read():?}" + } + + button { + onclick: move |_| { + to_owned![div_element, dimentions]; + async move { + if let Some(div) = div_element.read().as_ref() { + if let Ok(rect)=div.get_client_rect().await{ + dimentions.set(rect); + } + } + } + }, + "Read dimentions" + } + )) +} diff --git a/examples/scroll_to_top.rs b/examples/scroll_to_top.rs new file mode 100644 index 000000000..4eb38a451 --- /dev/null +++ b/examples/scroll_to_top.rs @@ -0,0 +1,33 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let header_element = use_ref(cx, || None); + + cx.render(rsx!( + div { + h1 { + onmounted: move |cx| { + header_element.set(Some(cx.inner().clone())); + }, + "Scroll to top example" + } + + for i in 0..100 { + div { "Item {i}" } + } + + button { + onclick: move |_| { + if let Some(header) = header_element.read().as_ref() { + header.scroll_to(ScrollBehavior::Smooth); + } + }, + "Scroll to top" + } + } + )) +} diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs index e94a481c7..94db28f0b 100644 --- a/packages/desktop/src/element.rs +++ b/packages/desktop/src/element.rs @@ -88,12 +88,10 @@ impl RenderedElementBacking for DesktopElement { self.id.0, focus ); - println!("script: {}", script); let fut = self .query .new_query::(&script, &self.webview) .resolve(); - println!("fut"); Box::pin(async move { match fut.await { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 8e92d90aa..701c0e0bb 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -265,7 +265,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) EventData::Ipc(msg) if msg.method() == "query" => { let params = msg.params(); - if let Ok(result) = dbg!(serde_json::from_value::(params)) { + if let Ok(result) = serde_json::from_value::(params) { let view = webviews.get(&event.1).unwrap(); let query = view .dom From c4027c2618228b14dc00a1bbf8c9f690c11c24a3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 26 Mar 2023 07:23:32 -0500 Subject: [PATCH 09/10] fix clippy --- examples/controll_focus.rs | 2 +- examples/read_size.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/controll_focus.rs b/examples/controll_focus.rs index 0f14a940b..4f4c2ddcb 100644 --- a/examples/controll_focus.rs +++ b/examples/controll_focus.rs @@ -7,7 +7,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let elements: &UseRef>> = use_ref(cx, || Vec::new()); + let elements: &UseRef>> = use_ref(cx, Vec::new); let running = use_state(cx, || true); use_future!(cx, |(elements, running)| async move { diff --git a/examples/read_size.rs b/examples/read_size.rs index be996e2dd..806f71f80 100644 --- a/examples/read_size.rs +++ b/examples/read_size.rs @@ -1,3 +1,4 @@ +#![allow(clippy::await_holding_refcell_ref)] use std::rc::Rc; use dioxus::{html::geometry::euclid::Rect, prelude::*}; @@ -27,7 +28,7 @@ fn main() { fn app(cx: Scope) -> Element { let div_element: &UseRef>> = use_ref(cx, || None); - let dimentions = use_ref(cx, || Rect::zero()); + let dimentions = use_ref(cx, Rect::zero); cx.render(rsx!( div { @@ -44,8 +45,10 @@ fn app(cx: Scope) -> Element { onclick: move |_| { to_owned![div_element, dimentions]; async move { - if let Some(div) = div_element.read().as_ref() { - if let Ok(rect)=div.get_client_rect().await{ + let read = div_element.read(); + let client_rect = read.as_ref().map(|el| el.get_client_rect()); + if let Some(client_rect) = client_rect { + if let Ok(rect) = client_rect.await { dimentions.set(rect); } } From 4d90bdee473bae9601ebe56a524de99e7e2a7be4 Mon Sep 17 00:00:00 2001 From: ealmloff Date: Sat, 29 Apr 2023 20:42:47 -0500 Subject: [PATCH 10/10] Remove console.log --- packages/interpreter/src/sledgehammer_bindings.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index a15d7bc81..2cb148e89 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -118,7 +118,6 @@ mod js { nodes[id] = node; } export function get_node(id) { - console.log(nodes, id); return nodes[id]; } export function initilize(root, handler) {