Преглед изворни кода

implement on mounted for desktop

Evan Almloff пре 2 година
родитељ
комит
7636c046fa

+ 5 - 0
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<Value>,
 
+    /// The receiver for queries about elements
+    pub(super) query: QueryEngine,
+
     pub(super) pending_windows: WebviewQueue,
 
     pub(crate) event_loop: EventLoopWindowTarget<UserWindowEvent>,
@@ -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,

+ 208 - 0
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<WebView>,
+    query: QueryEngine,
+}
+
+impl DesktopElement {
+    pub(crate) fn new(id: ElementId, webview: Rc<WebView>, 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<WebView> {
+        &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<dioxus_html::geometry::euclid::Rect<f64, f64>>,
+            >,
+        >,
+    > {
+        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<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
+        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<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
+        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<RefCell<Slab<()>>>,
+}
+
+#[derive(Clone)]
+pub(crate) struct QueryEngine {
+    sender: Rc<tokio::sync::broadcast::Sender<MountedReturn>>,
+    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<MountedReturn>,
+}
+
+impl Query {
+    async fn resolve(mut self) -> Option<MountedReturnData> {
+        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 {}

+ 56 - 4
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<P: 'static>(root: Component<P>, props: P, cfg: Config)
                 }
 
                 EventData::Ipc(msg) if msg.method() == "user_event" => {
-                    let evt = match serde_json::from_value::<HtmlEvent>(msg.params()) {
+                    let params = msg.params();
+
+                    let evt = match serde_json::from_value::<HtmlEvent>(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::<DesktopContext>()
+                            .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::<MountedReturn>(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::<DesktopContext>()
+                        .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);

+ 24 - 12
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<Rect<f64, f64>> {
-        Err(MountedError::NotSupported)
+    #[allow(clippy::type_complexity)]
+    fn get_client_rect(&self) -> Pin<Box<dyn Future<Output = MountedResult<Rect<f64, f64>>>>> {
+        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<dyn Future<Output = MountedResult<()>>>> {
+        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<dyn Future<Output = MountedResult<()>>>> {
+        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<Rect<f64, f64>> {
-        self.inner.get_client_rect()
+    pub async fn get_client_rect(&self) -> MountedResult<Rect<f64, f64>> {
+        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
     }
 }
 

+ 52 - 0
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<dyn Any>,
             EventData::Transition(data) => Rc::new(data) as Rc<dyn Any>,
             EventData::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Mounted => Rc::new(MountedData::new(())) as Rc<dyn Any>,
         }
     }
 }
@@ -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<MountedReturnData>,
+}
+
+/// 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<f64, f64>),
+    ScrollTo(()),
+}

+ 18 - 8
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<euclid::Rect<f64, f64>> {
+    fn get_client_rect(
+        &self,
+    ) -> Pin<Box<dyn Future<Output = MountedResult<euclid::Rect<f64, f64>>>>> {
         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<Box<dyn Future<Output = MountedResult<()>>>> {
         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::<web_sys::HtmlElement>()
+    fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
+        let result = self
+            .dyn_ref::<web_sys::HtmlElement>()
             .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 })
     }
 }
 

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

+ 85 - 0
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;