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, +}