Generalize Query system for use in use_eval and node querys

This commit is contained in:
Evan Almloff committed 2023-03-23 15:19:00 -05:00
1 parent 7636c046fa
commit fa9f0d0f6c
8 files changed
+186 -282

No files matched your search

+5 -28
View File
@@ -3,9 +3,9 @@ use std::rc::Rc;
use std::rc::Weak; use std::rc::Weak;
use crate::create_new_window; use crate::create_new_window;
use crate::element::QueryEngine;
use crate::eval::EvalResult; use crate::eval::EvalResult;
use crate::events::IpcMessage; use crate::events::IpcMessage;
use crate::query::QueryEngine;
use crate::shortcut::IntoKeyCode; use crate::shortcut::IntoKeyCode;
use crate::shortcut::IntoModifersState; use crate::shortcut::IntoModifersState;
use crate::shortcut::ShortcutId; use crate::shortcut::ShortcutId;
@@ -17,7 +17,6 @@ use dioxus_core::ScopeState;
use dioxus_core::VirtualDom; use dioxus_core::VirtualDom;
#[cfg(all(feature = "hot-reload", debug_assertions))] #[cfg(all(feature = "hot-reload", debug_assertions))]
use dioxus_hot_reload::HotReloadMsg; use dioxus_hot_reload::HotReloadMsg;
use serde_json::Value;
use slab::Slab; use slab::Slab;
use wry::application::event::Event; use wry::application::event::Event;
use wry::application::event_loop::EventLoopProxy; use wry::application::event_loop::EventLoopProxy;
@@ -60,10 +59,7 @@ pub struct DesktopContext {
/// The proxy to the event loop /// The proxy to the event loop
pub proxy: ProxyType, pub proxy: ProxyType,
/// The receiver for eval results since eval is async /// The receiver for queries about the current window
pub(super) eval: tokio::sync::broadcast::Sender<Value>,
/// The receiver for queries about elements
pub(super) query: QueryEngine, pub(super) query: QueryEngine,
pub(super) pending_windows: WebviewQueue, pub(super) pending_windows: WebviewQueue,
@@ -100,7 +96,6 @@ impl DesktopContext {
webview, webview,
proxy, proxy,
event_loop, event_loop,
eval: tokio::sync::broadcast::channel(8).0,
query: Default::default(), query: Default::default(),
pending_windows: webviews, pending_windows: webviews,
event_handlers, event_handlers,
@@ -215,28 +210,10 @@ impl DesktopContext {
/// Evaluate a javascript expression /// Evaluate a javascript expression
pub fn eval(&self, code: &str) -> EvalResult { 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 // the query id lets us keep track of the eval result and send it back to the main thread
let script = format!( let query = self.query.new_query(code, &self.webview);
r#"
window.ipc.postMessage(
JSON.stringify({{
"method":"eval_result",
"params": (
function(){{
{code}
}}
)()
}})
);
"#
);
if let Err(e) = self.webview.evaluate_script(&script) { EvalResult::new(query)
// send an error to the eval receiver
log::warn!("Eval script error: {e}");
}
EvalResult::new(self.eval.clone())
} }
/// Create a wry event handler that listens for wry events. /// Create a wry event handler that listens for wry events.
+41 -124
View File
@@ -1,13 +1,11 @@
use std::{cell::RefCell, rc::Rc}; use std::rc::Rc;
use dioxus_core::ElementId; use dioxus_core::ElementId;
use dioxus_html::{ use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
MountedResult, MountedReturn, MountedReturnData, NodeUpdate, NodeUpdateData,
RenderedElementBacking,
};
use slab::Slab;
use wry::webview::WebView; use wry::webview::WebView;
use crate::query::QueryEngine;
/// A mounted element passed to onmounted events /// A mounted element passed to onmounted events
pub struct DesktopElement { pub struct DesktopElement {
id: ElementId, id: ElementId,
@@ -19,16 +17,6 @@ impl DesktopElement {
pub(crate) fn new(id: ElementId, webview: Rc<WebView>, query: QueryEngine) -> Self { pub(crate) fn new(id: ElementId, webview: Rc<WebView>, query: QueryEngine) -> Self {
Self { id, webview, query } 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 { 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 let fut = self
.query .query
.new_query(self.id, NodeUpdateData::GetClientRect {}, &self.webview) .new_query::<Option<Rect<f64, f64>>>(&script, &self.webview)
.resolve(); .resolve();
Box::pin(async move { Box::pin(async move {
match fut.await { match fut.await {
Some(MountedReturnData::GetClientRect(rect)) => Ok(rect), Ok(Some(rect)) => Ok(rect),
Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::MismatchedReturn), Box::new(DesktopQueryError::FailedToQuery),
)), )),
None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( Err(err) => {
DesktopQueryError::FailedToQuery, MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
))), }
} }
}) })
} }
@@ -66,23 +56,25 @@ impl RenderedElementBacking for DesktopElement {
&self, &self,
behavior: dioxus_html::ScrollBehavior, behavior: dioxus_html::ScrollBehavior,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> { ) -> 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 let fut = self
.query .query
.new_query( .new_query::<bool>(&script, &self.webview)
self.id,
NodeUpdateData::ScrollTo { behavior },
&self.webview,
)
.resolve(); .resolve();
Box::pin(async move { Box::pin(async move {
match fut.await { match fut.await {
Some(MountedReturnData::ScrollTo(())) => Ok(()), Ok(true) => Ok(()),
Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::MismatchedReturn), Box::new(DesktopQueryError::FailedToQuery),
)), )),
None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( Err(err) => {
DesktopQueryError::FailedToQuery, MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
))), }
} }
}) })
} }
@@ -91,116 +83,41 @@ impl RenderedElementBacking for DesktopElement {
&self, &self,
focus: bool, focus: bool,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> { ) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.SetFocus({}, {});",
self.id.0, focus
);
println!("script: {}", script);
let fut = self let fut = self
.query .query
.new_query(self.id, NodeUpdateData::SetFocus { focus }, &self.webview) .new_query::<bool>(&script, &self.webview)
.resolve(); .resolve();
println!("fut");
Box::pin(async move { Box::pin(async move {
match fut.await { match fut.await {
Some(MountedReturnData::SetFocus(())) => Ok(()), Ok(true) => Ok(()),
Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::MismatchedReturn), Box::new(DesktopQueryError::FailedToQuery),
)), )),
None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( Err(err) => {
DesktopQueryError::FailedToQuery, MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
))), }
} }
}) })
} }
} }
#[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)] #[derive(Debug)]
enum DesktopQueryError { enum DesktopQueryError {
FailedToQuery, FailedToQuery,
MismatchedReturn,
} }
impl std::fmt::Display for DesktopQueryError { impl std::fmt::Display for DesktopQueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"),
DesktopQueryError::MismatchedReturn => {
write!(f, "The return type did not match the query")
}
} }
} }
} }
+9 -13
View File
@@ -1,36 +1,32 @@
use std::rc::Rc; use std::rc::Rc;
use crate::query::Query;
use crate::query::QueryError;
use crate::use_window; use crate::use_window;
use dioxus_core::ScopeState; use dioxus_core::ScopeState;
use serde::de::Error;
use std::future::Future; use std::future::Future;
use std::future::IntoFuture; use std::future::IntoFuture;
use std::pin::Pin; use std::pin::Pin;
/// A future that resolves to the result of a JavaScript evaluation. /// A future that resolves to the result of a JavaScript evaluation.
pub struct EvalResult { pub struct EvalResult {
pub(crate) broadcast: tokio::sync::broadcast::Sender<serde_json::Value>, pub(crate) query: Query<serde_json::Value>,
} }
impl EvalResult { impl EvalResult {
pub(crate) fn new(sender: tokio::sync::broadcast::Sender<serde_json::Value>) -> Self { pub(crate) fn new(query: Query<serde_json::Value>) -> Self {
Self { broadcast: sender } Self { query }
} }
} }
impl IntoFuture for EvalResult { impl IntoFuture for EvalResult {
type Output = Result<serde_json::Value, serde_json::Error>; type Output = Result<serde_json::Value, QueryError>;
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>; type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>;
fn into_future(self) -> Self::IntoFuture { fn into_future(self) -> Self::IntoFuture {
Box::pin(async move { Box::pin(self.query.resolve())
let mut reciever = self.broadcast.subscribe(); as Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>
match reciever.recv().await {
Ok(result) => Ok(result),
Err(_) => Err(serde_json::Error::custom("No result returned")),
}
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
} }
} }
+15 -37
View File
@@ -10,17 +10,19 @@ mod escape;
mod eval; mod eval;
mod events; mod events;
mod protocol; mod protocol;
mod query;
mod shortcut; mod shortcut;
mod waker; mod waker;
mod webview; mod webview;
use crate::query::QueryResult;
pub use cfg::Config; pub use cfg::Config;
pub use desktop_context::{ pub use desktop_context::{
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId, use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
}; };
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
use dioxus_core::*; use dioxus_core::*;
use dioxus_html::{HtmlEvent, MountedData, MountedReturn}; use dioxus_html::{HtmlEvent, MountedData};
use element::DesktopElement; use element::DesktopElement;
pub use eval::{use_eval, EvalResult}; pub use eval::{use_eval, EvalResult};
use futures_util::{pin_mut, FutureExt}; use futures_util::{pin_mut, FutureExt};
@@ -259,30 +261,21 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
send_edits(view.dom.render_immediate(), &view.webview); 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(); let params = msg.params();
println!("node_update: {:?}", params);
// check for a mounted event if let Ok(result) = dbg!(serde_json::from_value::<QueryResult>(params)) {
let evt = match serde_json::from_value::<MountedReturn>(params) { let view = webviews.get(&event.1).unwrap();
Ok(value) => value, let query = view
Err(err) => { .dom
println!("node_update: {:?}", err); .base_scope()
return; .consume_context::<DesktopContext>()
} .unwrap()
}; .query;
let view = webviews.get(&event.1).unwrap(); query.send(result);
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" => { EventData::Ipc(msg) if msg.method() == "initialize" => {
@@ -290,21 +283,6 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
send_edits(view.dom.rebuild(), &view.webview); 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::<DesktopContext>()
.unwrap()
.eval
.send(msg.params())
.unwrap();
}
EventData::Ipc(msg) if msg.method() == "browser_open" => { EventData::Ipc(msg) if msg.method() == "browser_open" => {
if let Some(temp) = msg.params().as_object() { if let Some(temp) = msg.params().as_object() {
if temp.contains_key("href") { if temp.contains_key("href") {
+105
View File
@@ -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<RefCell<Slab<()>>>,
}
#[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 {
pub fn new_query<V: DeserializeOwned>(&self, script: &str, webview: &WebView) -> 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) = 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<V: DeserializeOwned> {
slab: SharedSlab,
id: usize,
reciever: tokio::sync::broadcast::Receiver<QueryResult>,
phantom: std::marker::PhantomData<V>,
}
impl<V: DeserializeOwned> Query<V> {
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,
}
+7 -4
View File
@@ -79,13 +79,16 @@ impl MountedData {
} }
/// Scroll to make the element visible /// Scroll to make the element visible
pub async fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { pub fn scroll_to(
self.inner.scroll_to(behavior).await &self,
behavior: ScrollBehavior,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
self.inner.scroll_to(behavior)
} }
/// Set the focus on the element /// Set the focus on the element
pub async fn set_focus(&self, focus: bool) -> MountedResult<()> { pub fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
self.inner.set_focus(focus).await self.inner.set_focus(focus)
} }
} }
-47
View File
@@ -2,7 +2,6 @@ use std::{any::Any, rc::Rc};
use crate::events::*; use crate::events::*;
use dioxus_core::ElementId; use dioxus_core::ElementId;
use euclid::Rect;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug, Clone, PartialEq)] #[derive(Serialize, Debug, Clone, PartialEq)]
@@ -221,49 +220,3 @@ fn test_back_and_forth() {
assert_eq!(data, p); 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(()),
}
+4 -29
View File
@@ -227,51 +227,26 @@ class Interpreter {
ScrollTo(id, behavior) { ScrollTo(id, behavior) {
const node = this.nodes[id]; const node = this.nodes[id];
if (!node) { if (!node) {
return; return false;
} }
node.scrollIntoView({ node.scrollIntoView({
behavior: behavior behavior: behavior
}); });
return { return true;
type: "ScrollTo",
};
} }
/// Set the focus on the element /// Set the focus on the element
SetFocus(id, focus) { SetFocus(id, focus) {
const node = this.nodes[id]; const node = this.nodes[id];
if (!node) { if (!node) {
return; return false;
} }
if (focus) { if (focus) {
node.focus(); node.focus();
} else { } else {
node.blur(); node.blur();
} }
return { return true;
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) { handleEdits(edits) {