Ver código fonte

implement a query engine for liveview

Evan Almloff 2 anos atrás
pai
commit
8243dfe00d

+ 5 - 0
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<RefCell<Slab<()>>>,
 }
 
+/// 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<tokio::sync::broadcast::Sender<QueryResult>>,
@@ -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<V: DeserializeOwned>(&self, script: &str, webview: &WebView) -> Query<V> {
         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<V: DeserializeOwned> {
 }
 
 impl<V: DeserializeOwned> Query<V> {
+    /// Resolve the query
     pub async fn resolve(mut self) -> Result<V, QueryError> {
         let result = loop {
             match self.reciever.recv().await {

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

+ 125 - 0
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<String>,
+    query: QueryEngine,
+}
+
+impl LiveviewElement {
+    pub(crate) fn new(id: ElementId, tx: UnboundedSender<String>, 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<dioxus_html::geometry::euclid::Rect<f64, f64>>,
+            >,
+        >,
+    > {
+        let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
+
+        let fut = self
+            .query
+            .new_query::<Option<Rect<f64, f64>>>(&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<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
+        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::<bool>(&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<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
+        let script = format!(
+            "return window.interpreter.SetFocus({}, {});",
+            self.id.0, focus
+        );
+
+        let fut = self
+            .query
+            .new_query::<bool>(&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 {}

+ 2 - 0
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::*;
 

+ 12 - 4
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;
+        }
       }
     };
 

+ 64 - 12
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::<IpcMessage>(evt) {
-                            vdom.handle_event(&params.name, params.data.into_any(), params.element, params.bubbles);
+                        if let Ok(message) = serde_json::from_str::<IpcMessage>(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),
+}

+ 113 - 0
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<RefCell<Slab<()>>>,
+}
+
+/// 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<tokio::sync::broadcast::Sender<QueryResult>>,
+    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<V: DeserializeOwned>(
+        &self,
+        script: &str,
+        tx: &UnboundedSender<String>,
+    ) -> Query<V> {
+        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<V: DeserializeOwned> {
+    slab: SharedSlab,
+    id: usize,
+    reciever: tokio::sync::broadcast::Receiver<QueryResult>,
+    phantom: std::marker::PhantomData<V>,
+}
+
+impl<V: DeserializeOwned> Query<V> {
+    /// Resolve the query
+    pub async fn resolve(mut self) -> Result<V, QueryError> {
+        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,
+}