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;