Merge pull request #894 from Demonthos/query-system
Create onmounted Event
This commit is contained in:
26 files changed
+1221
-109
No files matched your search
@@ -0,0 +1,44 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let elements: &UseRef<Vec<Rc<MountedData>>> = use_ref(cx, Vec::new);
|
||||||
|
let running = use_state(cx, || true);
|
||||||
|
|
||||||
|
use_future!(cx, |(elements, running)| async move {
|
||||||
|
let mut focused = 0;
|
||||||
|
if *running.current() {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||||
|
if let Some(element) = elements.read().get(focused) {
|
||||||
|
element.set_focus(true);
|
||||||
|
} else {
|
||||||
|
focused = 0;
|
||||||
|
}
|
||||||
|
focused += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx!(
|
||||||
|
div {
|
||||||
|
h1 { "Input Roulette" }
|
||||||
|
for i in 0..100 {
|
||||||
|
input {
|
||||||
|
value: "{i}",
|
||||||
|
onmounted: move |cx| {
|
||||||
|
elements.write().push(cx.inner().clone());
|
||||||
|
},
|
||||||
|
oninput: move |_| {
|
||||||
|
running.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#![allow(clippy::await_holding_refcell_ref)]
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dioxus::{html::geometry::euclid::Rect, prelude::*};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch_cfg(
|
||||||
|
app,
|
||||||
|
dioxus_desktop::Config::default().with_custom_head(
|
||||||
|
r#"
|
||||||
|
<style type="text/css">
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#main {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
"#
|
||||||
|
.to_owned(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let div_element: &UseRef<Option<Rc<MountedData>>> = use_ref(cx, || None);
|
||||||
|
|
||||||
|
let dimentions = use_ref(cx, Rect::zero);
|
||||||
|
|
||||||
|
cx.render(rsx!(
|
||||||
|
div {
|
||||||
|
width: "50%",
|
||||||
|
height: "50%",
|
||||||
|
background_color: "red",
|
||||||
|
onmounted: move |cx| {
|
||||||
|
div_element.set(Some(cx.inner().clone()));
|
||||||
|
},
|
||||||
|
"This element is {dimentions.read():?}"
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
to_owned![div_element, dimentions];
|
||||||
|
async move {
|
||||||
|
let read = div_element.read();
|
||||||
|
let client_rect = read.as_ref().map(|el| el.get_client_rect());
|
||||||
|
if let Some(client_rect) = client_rect {
|
||||||
|
if let Ok(rect) = client_rect.await {
|
||||||
|
dimentions.set(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Read dimentions"
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let header_element = use_ref(cx, || None);
|
||||||
|
|
||||||
|
cx.render(rsx!(
|
||||||
|
div {
|
||||||
|
h1 {
|
||||||
|
onmounted: move |cx| {
|
||||||
|
header_element.set(Some(cx.inner().clone()));
|
||||||
|
},
|
||||||
|
"Scroll to top example"
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..100 {
|
||||||
|
div { "Item {i}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
if let Some(header) = header_element.read().as_ref() {
|
||||||
|
header.scroll_to(ScrollBehavior::Smooth);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Scroll to top"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ use std::rc::Weak;
|
|||||||
use crate::create_new_window;
|
use crate::create_new_window;
|
||||||
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;
|
||||||
@@ -16,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;
|
||||||
@@ -59,8 +59,8 @@ 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>,
|
pub(super) query: QueryEngine,
|
||||||
|
|
||||||
pub(super) pending_windows: WebviewQueue,
|
pub(super) pending_windows: WebviewQueue,
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ impl DesktopContext {
|
|||||||
webview,
|
webview,
|
||||||
proxy,
|
proxy,
|
||||||
event_loop,
|
event_loop,
|
||||||
eval: tokio::sync::broadcast::channel(8).0,
|
query: Default::default(),
|
||||||
pending_windows: webviews,
|
pending_windows: webviews,
|
||||||
event_handlers,
|
event_handlers,
|
||||||
shortcut_manager,
|
shortcut_manager,
|
||||||
@@ -210,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.
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dioxus_core::ElementId;
|
||||||
|
use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
|
||||||
|
use wry::webview::WebView;
|
||||||
|
|
||||||
|
use crate::query::QueryEngine;
|
||||||
|
|
||||||
|
/// 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 script = format!("return window.interpreter.GetClientRect({});", self.id.0);
|
||||||
|
|
||||||
|
let fut = self
|
||||||
|
.query
|
||||||
|
.new_query::<Option<Rect<f64, f64>>>(&script, &self.webview)
|
||||||
|
.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.webview)
|
||||||
|
.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.webview)
|
||||||
|
.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 {}
|
||||||
@@ -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>>>>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-18
@@ -5,22 +5,27 @@
|
|||||||
|
|
||||||
mod cfg;
|
mod cfg;
|
||||||
mod desktop_context;
|
mod desktop_context;
|
||||||
|
mod element;
|
||||||
mod escape;
|
mod escape;
|
||||||
mod eval;
|
mod eval;
|
||||||
mod events;
|
mod events;
|
||||||
mod file_upload;
|
mod file_upload;
|
||||||
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::MountedData;
|
||||||
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
||||||
|
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};
|
||||||
use shortcut::ShortcutRegistry;
|
use shortcut::ShortcutRegistry;
|
||||||
@@ -221,39 +226,65 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||||||
}
|
}
|
||||||
|
|
||||||
EventData::Ipc(msg) if msg.method() == "user_event" => {
|
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,
|
Ok(value) => value,
|
||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let HtmlEvent {
|
||||||
|
element,
|
||||||
|
name,
|
||||||
|
bubbles,
|
||||||
|
data,
|
||||||
|
} = evt;
|
||||||
|
|
||||||
let view = webviews.get_mut(&event.1).unwrap();
|
let view = webviews.get_mut(&event.1).unwrap();
|
||||||
|
|
||||||
view.dom
|
// check for a mounted event placeholder and replace it with a desktop specific element
|
||||||
.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles);
|
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);
|
send_edits(view.dom.render_immediate(), &view.webview);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
if let Ok(result) = serde_json::from_value::<QueryResult>(params) {
|
||||||
|
let view = webviews.get(&event.1).unwrap();
|
||||||
|
let query = view
|
||||||
|
.dom
|
||||||
|
.base_scope()
|
||||||
|
.consume_context::<DesktopContext>()
|
||||||
|
.unwrap()
|
||||||
|
.query;
|
||||||
|
|
||||||
|
query.send(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
||||||
let view = webviews.get_mut(&event.1).unwrap();
|
let view = webviews.get_mut(&event.1).unwrap();
|
||||||
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") {
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/// 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, 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use std::{
|
||||||
|
any::Any,
|
||||||
|
fmt::{Display, Formatter},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use dioxus_core::{ElementId, Mutations, VirtualDom};
|
||||||
|
use dioxus_html::{
|
||||||
|
geometry::euclid::{Point2D, Rect, Size2D},
|
||||||
|
MountedData, MountedError, RenderedElementBacking,
|
||||||
|
};
|
||||||
|
|
||||||
|
use dioxus_native_core::NodeId;
|
||||||
|
use rink::query::{ElementRef, Query};
|
||||||
|
|
||||||
|
pub(crate) fn find_mount_events(mutations: &Mutations) -> Vec<ElementId> {
|
||||||
|
let mut mount_events = Vec::new();
|
||||||
|
for mutation in &mutations.edits {
|
||||||
|
if let dioxus_core::Mutation::NewEventListener {
|
||||||
|
name: "mounted",
|
||||||
|
id,
|
||||||
|
} = mutation
|
||||||
|
{
|
||||||
|
mount_events.push(*id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mount_events
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to queue the mounted events to give rink time to rendere and resolve the layout of elements after they are created
|
||||||
|
pub(crate) fn create_mounted_events(
|
||||||
|
vdom: &VirtualDom,
|
||||||
|
events: &mut Vec<(ElementId, &'static str, Rc<dyn Any>, bool)>,
|
||||||
|
mount_events: impl Iterator<Item = (ElementId, NodeId)>,
|
||||||
|
) {
|
||||||
|
let query: Query = vdom
|
||||||
|
.base_scope()
|
||||||
|
.consume_context()
|
||||||
|
.expect("Query should be in context");
|
||||||
|
for (id, node_id) in mount_events {
|
||||||
|
let element = TuiElement {
|
||||||
|
query: query.clone(),
|
||||||
|
id: node_id,
|
||||||
|
};
|
||||||
|
events.push((id, "mounted", Rc::new(MountedData::new(element)), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TuiElement {
|
||||||
|
query: Query,
|
||||||
|
id: NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TuiElement {
|
||||||
|
pub(crate) fn element(&self) -> ElementRef {
|
||||||
|
self.query.get(self.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderedElementBacking for TuiElement {
|
||||||
|
fn get_client_rect(
|
||||||
|
&self,
|
||||||
|
) -> std::pin::Pin<
|
||||||
|
Box<
|
||||||
|
dyn futures::Future<
|
||||||
|
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
let layout = self.element().layout();
|
||||||
|
Box::pin(async move {
|
||||||
|
match layout {
|
||||||
|
Some(layout) => {
|
||||||
|
let x = layout.location.x as f64;
|
||||||
|
let y = layout.location.y as f64;
|
||||||
|
let width = layout.size.width as f64;
|
||||||
|
let height = layout.size.height as f64;
|
||||||
|
Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height)))
|
||||||
|
}
|
||||||
|
None => Err(MountedError::OperationFailed(Box::new(TuiElementNotFound))),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TuiElementNotFound;
|
||||||
|
|
||||||
|
impl Display for TuiElementNotFound {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "TUI element not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for TuiElementNotFound {}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
mod element;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
@@ -12,6 +14,7 @@ use dioxus_html::EventData;
|
|||||||
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
|
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
|
||||||
use dioxus_native_core::prelude::*;
|
use dioxus_native_core::prelude::*;
|
||||||
|
|
||||||
|
use element::{create_mounted_events, find_mount_events};
|
||||||
pub use rink::{query::Query, Config, RenderingMode, Size, TuiContext};
|
pub use rink::{query::Query, Config, RenderingMode, Size, TuiContext};
|
||||||
use rink::{render, Driver};
|
use rink::{render, Driver};
|
||||||
|
|
||||||
@@ -37,14 +40,32 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
|
|||||||
mapping: dioxus_state.clone(),
|
mapping: dioxus_state.clone(),
|
||||||
});
|
});
|
||||||
let muts = vdom.rebuild();
|
let muts = vdom.rebuild();
|
||||||
let mut rdom = rdom.write().unwrap();
|
|
||||||
dioxus_state
|
let mut queued_events = Vec::new();
|
||||||
.write()
|
|
||||||
.unwrap()
|
{
|
||||||
.apply_mutations(&mut rdom, muts);
|
let mut rdom = rdom.write().unwrap();
|
||||||
|
let mut dioxus_state = dioxus_state.write().unwrap();
|
||||||
|
|
||||||
|
// Find any mount events
|
||||||
|
let mounted = dbg!(find_mount_events(&muts));
|
||||||
|
|
||||||
|
dioxus_state.apply_mutations(&mut rdom, muts);
|
||||||
|
|
||||||
|
// Send the mount events
|
||||||
|
create_mounted_events(
|
||||||
|
&vdom,
|
||||||
|
&mut queued_events,
|
||||||
|
mounted
|
||||||
|
.iter()
|
||||||
|
.map(|id| (*dbg!(id), dioxus_state.element_to_node_id(*id))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
DioxusRenderer {
|
DioxusRenderer {
|
||||||
vdom,
|
vdom,
|
||||||
dioxus_state,
|
dioxus_state,
|
||||||
|
queued_events,
|
||||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
hot_reload_rx: {
|
hot_reload_rx: {
|
||||||
let (hot_reload_tx, hot_reload_rx) =
|
let (hot_reload_tx, hot_reload_rx) =
|
||||||
@@ -62,6 +83,8 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
|
|||||||
struct DioxusRenderer {
|
struct DioxusRenderer {
|
||||||
vdom: VirtualDom,
|
vdom: VirtualDom,
|
||||||
dioxus_state: Rc<RwLock<DioxusState>>,
|
dioxus_state: Rc<RwLock<DioxusState>>,
|
||||||
|
// Events that are queued up to be sent to the vdom next time the vdom is polled
|
||||||
|
queued_events: Vec<(ElementId, &'static str, Rc<dyn Any>, bool)>,
|
||||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
|
hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
|
||||||
}
|
}
|
||||||
@@ -71,10 +94,23 @@ impl Driver for DioxusRenderer {
|
|||||||
let muts = self.vdom.render_immediate();
|
let muts = self.vdom.render_immediate();
|
||||||
{
|
{
|
||||||
let mut rdom = rdom.write().unwrap();
|
let mut rdom = rdom.write().unwrap();
|
||||||
self.dioxus_state
|
|
||||||
.write()
|
{
|
||||||
.unwrap()
|
// Find any mount events
|
||||||
.apply_mutations(&mut rdom, muts);
|
let mounted = find_mount_events(&muts);
|
||||||
|
|
||||||
|
let mut dioxus_state = self.dioxus_state.write().unwrap();
|
||||||
|
dioxus_state.apply_mutations(&mut rdom, muts);
|
||||||
|
|
||||||
|
// Send the mount events
|
||||||
|
create_mounted_events(
|
||||||
|
&self.vdom,
|
||||||
|
&mut self.queued_events,
|
||||||
|
mounted
|
||||||
|
.iter()
|
||||||
|
.map(|id| (*id, dioxus_state.element_to_node_id(*id))),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +130,11 @@ impl Driver for DioxusRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
|
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
|
||||||
|
// Add any queued events
|
||||||
|
for (id, event, value, bubbles) in self.queued_events.drain(..) {
|
||||||
|
self.vdom.handle_event(event, value, id, bubbles);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
return Box::pin(async {
|
return Box::pin(async {
|
||||||
let hot_reload_wait = self.hot_reload_rx.recv();
|
let hot_reload_wait = self.hot_reload_rx.recv();
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ features = [
|
|||||||
"FocusEvent",
|
"FocusEvent",
|
||||||
"CompositionEvent",
|
"CompositionEvent",
|
||||||
"ClipboardEvent",
|
"ClipboardEvent",
|
||||||
|
"Element",
|
||||||
|
"DomRect",
|
||||||
|
"ScrollIntoViewOptions",
|
||||||
|
"ScrollLogicalPosition",
|
||||||
|
"ScrollBehavior",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ mod form;
|
|||||||
mod image;
|
mod image;
|
||||||
mod keyboard;
|
mod keyboard;
|
||||||
mod media;
|
mod media;
|
||||||
|
mod mounted;
|
||||||
mod mouse;
|
mod mouse;
|
||||||
mod pointer;
|
mod pointer;
|
||||||
mod scroll;
|
mod scroll;
|
||||||
@@ -51,6 +52,7 @@ pub use form::*;
|
|||||||
pub use image::*;
|
pub use image::*;
|
||||||
pub use keyboard::*;
|
pub use keyboard::*;
|
||||||
pub use media::*;
|
pub use media::*;
|
||||||
|
pub use mounted::*;
|
||||||
pub use mouse::*;
|
pub use mouse::*;
|
||||||
pub use pointer::*;
|
pub use pointer::*;
|
||||||
pub use scroll::*;
|
pub use scroll::*;
|
||||||
@@ -144,6 +146,7 @@ pub fn event_bubbles(evt: &str) -> bool {
|
|||||||
"animationiteration" => true,
|
"animationiteration" => true,
|
||||||
"transitionend" => true,
|
"transitionend" => true,
|
||||||
"toggle" => true,
|
"toggle" => true,
|
||||||
|
"mounted" => false,
|
||||||
_ => true,
|
_ => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
//! Handles quering data from the renderer
|
||||||
|
|
||||||
|
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> {
|
||||||
|
Err(MountedError::NotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position)
|
||||||
|
#[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,
|
||||||
|
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
|
||||||
|
Box::pin(async { Err(MountedError::NotSupported) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the focus on the element
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub struct MountedData {
|
||||||
|
inner: Rc<dyn RenderedElementBacking>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MountedData {
|
||||||
|
/// Create a new MountedData
|
||||||
|
pub fn new(registry: impl RenderedElementBacking + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Rc::new(registry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the renderer specific element for the given id
|
||||||
|
pub fn get_raw_element(&self) -> MountedResult<&dyn Any> {
|
||||||
|
self.inner.get_raw_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position)
|
||||||
|
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,
|
||||||
|
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
|
||||||
|
self.inner.scroll_to(behavior)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the focus on the element
|
||||||
|
pub fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
|
||||||
|
self.inner.set_focus(focus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use dioxus_core::Event;
|
||||||
|
|
||||||
|
pub type MountedEvent = Event<MountedData>;
|
||||||
|
|
||||||
|
impl_event! [
|
||||||
|
MountedData;
|
||||||
|
|
||||||
|
/// mounted
|
||||||
|
onmounted
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The MountedResult type for the MountedData
|
||||||
|
pub type MountedResult<T> = Result<T, MountedError>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// The error type for the MountedData
|
||||||
|
pub enum MountedError {
|
||||||
|
/// The renderer does not support the requested operation
|
||||||
|
NotSupported,
|
||||||
|
/// The element was not found
|
||||||
|
OperationFailed(Box<dyn std::error::Error>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MountedError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MountedError::NotSupported => {
|
||||||
|
write!(f, "The renderer does not support the requested operation")
|
||||||
|
}
|
||||||
|
MountedError::OperationFailed(e) => {
|
||||||
|
write!(f, "The operation failed: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for MountedError {}
|
||||||
@@ -113,6 +113,9 @@ fn fun_name(
|
|||||||
// Toggle
|
// Toggle
|
||||||
"toggle" => Toggle(de(data)?),
|
"toggle" => Toggle(de(data)?),
|
||||||
|
|
||||||
|
// Mounted
|
||||||
|
"mounted" => Mounted,
|
||||||
|
|
||||||
// ImageData => "load" | "error";
|
// 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";
|
// 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 => {
|
other => {
|
||||||
@@ -151,6 +154,7 @@ pub enum EventData {
|
|||||||
Animation(AnimationData),
|
Animation(AnimationData),
|
||||||
Transition(TransitionData),
|
Transition(TransitionData),
|
||||||
Toggle(ToggleData),
|
Toggle(ToggleData),
|
||||||
|
Mounted,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventData {
|
impl EventData {
|
||||||
@@ -172,6 +176,7 @@ impl EventData {
|
|||||||
EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
|
EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
|
||||||
EventData::Transition(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::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
|
||||||
|
EventData::Mounted => Rc::new(MountedData::new(())) as Rc<dyn Any>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,18 @@ use crate::events::{
|
|||||||
};
|
};
|
||||||
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
|
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
|
||||||
use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
|
use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
|
||||||
use crate::DragData;
|
use crate::{
|
||||||
|
DragData, MountedData, MountedError, MountedResult, RenderedElementBacking, ScrollBehavior,
|
||||||
|
};
|
||||||
use keyboard_types::{Code, Key, Modifiers};
|
use keyboard_types::{Code, Key, Modifiers};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::{JsCast, JsValue};
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent,
|
AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent,
|
||||||
TransitionEvent, WheelEvent,
|
ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
macro_rules! uncheck_convert {
|
macro_rules! uncheck_convert {
|
||||||
@@ -193,3 +197,64 @@ impl From<&TransitionEvent> for TransitionData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&web_sys::Element> for MountedData {
|
||||||
|
fn from(e: &web_sys::Element) -> Self {
|
||||||
|
MountedData::new(e.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderedElementBacking for web_sys::Element {
|
||||||
|
fn get_client_rect(
|
||||||
|
&self,
|
||||||
|
) -> Pin<Box<dyn Future<Output = MountedResult<euclid::Rect<f64, f64>>>>> {
|
||||||
|
let rect = self.get_bounding_client_rect();
|
||||||
|
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,
|
||||||
|
) -> 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),
|
||||||
|
),
|
||||||
|
ScrollBehavior::Smooth => self.scroll_into_view_with_scroll_into_view_options(
|
||||||
|
ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Smooth),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
Box::pin(async { Ok(()) })
|
||||||
|
}
|
||||||
|
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FocusError(JsValue);
|
||||||
|
|
||||||
|
impl std::fmt::Display for FocusError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "failed to focus element {:?}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for FocusError {}
|
||||||
@@ -18,8 +18,10 @@ js-sys = { version = "0.3.56", optional = true }
|
|||||||
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
|
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
|
||||||
sledgehammer_bindgen = { version = "0.2.1", optional = true }
|
sledgehammer_bindgen = { version = "0.2.1", optional = true }
|
||||||
sledgehammer_utils = { version = "0.1.1", optional = true }
|
sledgehammer_utils = { version = "0.1.1", optional = true }
|
||||||
|
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
serialize = ["serde"]
|
||||||
web = ["wasm-bindgen", "js-sys", "web-sys"]
|
web = ["wasm-bindgen", "js-sys", "web-sys"]
|
||||||
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
|
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
|
||||||
@@ -204,6 +204,45 @@ class Interpreter {
|
|||||||
node.removeAttribute(name);
|
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 false;
|
||||||
|
}
|
||||||
|
node.scrollIntoView({
|
||||||
|
behavior: behavior,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the focus on the element
|
||||||
|
SetFocus(id, focus) {
|
||||||
|
const node = this.nodes[id];
|
||||||
|
if (!node) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (focus) {
|
||||||
|
node.focus();
|
||||||
|
} else {
|
||||||
|
node.blur();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
handleEdits(edits) {
|
handleEdits(edits) {
|
||||||
for (let template of edits.templates) {
|
for (let template of edits.templates) {
|
||||||
this.SaveTemplate(template);
|
this.SaveTemplate(template);
|
||||||
@@ -346,9 +385,21 @@ class Interpreter {
|
|||||||
case "NewEventListener":
|
case "NewEventListener":
|
||||||
let bubbles = event_bubbles(edit.name);
|
let bubbles = event_bubbles(edit.name);
|
||||||
|
|
||||||
this.NewEventListener(edit.name, edit.id, bubbles, (event) => {
|
// if this is a mounted listener, we send the event immediately
|
||||||
handler(event, edit.name, bubbles);
|
if (edit.name === "mounted") {
|
||||||
});
|
window.ipc.postMessage(
|
||||||
|
serializeIpcMessage("user_event", {
|
||||||
|
name: edit.name,
|
||||||
|
element: edit.id,
|
||||||
|
data: null,
|
||||||
|
bubbles,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.NewEventListener(edit.name, edit.id, bubbles, (event) => {
|
||||||
|
handler(event, edit.name, bubbles);
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -933,6 +984,8 @@ function event_bubbles(event) {
|
|||||||
return true;
|
return true;
|
||||||
case "toggle":
|
case "toggle":
|
||||||
return true;
|
return true;
|
||||||
|
case "mounted":
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ mod js {
|
|||||||
export function set_node(id, node) {
|
export function set_node(id, node) {
|
||||||
nodes[id] = node;
|
nodes[id] = node;
|
||||||
}
|
}
|
||||||
|
export function get_node(id) {
|
||||||
|
return nodes[id];
|
||||||
|
}
|
||||||
export function initilize(root, handler) {
|
export function initilize(root, handler) {
|
||||||
listeners.handler = handler;
|
listeners.handler = handler;
|
||||||
nodes = [root];
|
nodes = [root];
|
||||||
@@ -167,6 +170,9 @@ mod js {
|
|||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn set_node(id: u32, node: Node);
|
pub fn set_node(id: u32, node: Node);
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn get_node(id: u32) -> Node;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn initilize(root: Node, handler: &Function);
|
pub fn initilize(root: Node, handler: &Function);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ license = "MIT/Apache-2.0"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1.0.38"
|
thiserror = "1.0.38"
|
||||||
|
log = "0.4.14"
|
||||||
|
slab = "0.4"
|
||||||
futures-util = { version = "0.3.25", default-features = false, features = [
|
futures-util = { version = "0.3.25", default-features = false, features = [
|
||||||
"sink",
|
"sink",
|
||||||
] }
|
] }
|
||||||
|
|||||||
@@ -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 {}
|
||||||
@@ -18,7 +18,9 @@ pub mod adapters {
|
|||||||
|
|
||||||
pub use adapters::*;
|
pub use adapters::*;
|
||||||
|
|
||||||
|
mod element;
|
||||||
pub mod pool;
|
pub mod pool;
|
||||||
|
mod query;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
pub use pool::*;
|
pub use pool::*;
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,19 @@ class IPC {
|
|||||||
// todo: retry the connection
|
// todo: retry the connection
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (message) => {
|
||||||
// Ignore pongs
|
// Ignore pongs
|
||||||
if (event.data != "__pong__") {
|
if (message.data != "__pong__") {
|
||||||
let edits = JSON.parse(event.data);
|
const event = JSON.parse(message.data);
|
||||||
window.interpreter.handleEdits(edits);
|
switch (event.type) {
|
||||||
|
case "edits":
|
||||||
|
let edits = event.data;
|
||||||
|
window.interpreter.handleEdits(edits);
|
||||||
|
break;
|
||||||
|
case "query":
|
||||||
|
Function("Eval", `"use strict";${event.data};`)();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
use crate::LiveViewError;
|
use crate::{
|
||||||
use dioxus_core::prelude::*;
|
element::LiveviewElement,
|
||||||
use dioxus_html::HtmlEvent;
|
query::{QueryEngine, QueryResult},
|
||||||
|
LiveViewError,
|
||||||
|
};
|
||||||
|
use dioxus_core::{prelude::*, Mutations};
|
||||||
|
use dioxus_html::{EventData, HtmlEvent, MountedData};
|
||||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
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;
|
use tokio_util::task::LocalPoolHandle;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[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
|
// 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 the futures so we can use select!
|
||||||
pin_mut!(ws);
|
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
|
// send the initial render to the client
|
||||||
ws.send(edits).await?;
|
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
|
// desktop uses this wrapper struct thing around the actual event itself
|
||||||
// this is sorta driven by tao/wry
|
// this is sorta driven by tao/wry
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct IpcMessage {
|
#[serde(tag = "method", content = "params")]
|
||||||
params: HtmlEvent,
|
enum IpcMessage {
|
||||||
|
#[serde(rename = "user_event")]
|
||||||
|
Event(HtmlEvent),
|
||||||
|
#[serde(rename = "query")]
|
||||||
|
Query(QueryResult),
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
|||||||
ws.send("__pong__".to_string()).await?;
|
ws.send("__pong__".to_string()).await?;
|
||||||
}
|
}
|
||||||
Some(Ok(evt)) => {
|
Some(Ok(evt)) => {
|
||||||
if let Ok(IpcMessage { params }) = serde_json::from_str::<IpcMessage>(evt) {
|
if let Ok(message) = serde_json::from_str::<IpcMessage>(evt) {
|
||||||
vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles);
|
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?
|
// log this I guess? when would we get an error here?
|
||||||
Some(Err(_e)) => {},
|
Some(Err(_e)) => {}
|
||||||
None => return Ok(()),
|
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 => {
|
Some(msg) = hot_reload_wait => {
|
||||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
match msg{
|
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)))
|
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
|
||||||
.await;
|
.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),
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -91,15 +91,7 @@ impl<'a> ElementRef<'a> {
|
|||||||
pub fn layout(&self) -> Option<Layout> {
|
pub fn layout(&self) -> Option<Layout> {
|
||||||
let layout = self
|
let layout = self
|
||||||
.stretch
|
.stretch
|
||||||
.layout(
|
.layout(self.inner.get(self.id)?.get::<TaffyLayout>()?.node.ok()?)
|
||||||
self.inner
|
|
||||||
.get(self.id)
|
|
||||||
.unwrap()
|
|
||||||
.get::<TaffyLayout>()
|
|
||||||
.unwrap()
|
|
||||||
.node
|
|
||||||
.ok()?,
|
|
||||||
)
|
|
||||||
.ok();
|
.ok();
|
||||||
layout.map(|layout| Layout {
|
layout.map(|layout| Layout {
|
||||||
order: layout.order,
|
order: layout.order,
|
||||||
|
|||||||
+41
-12
@@ -10,8 +10,8 @@
|
|||||||
use dioxus_core::{
|
use dioxus_core::{
|
||||||
BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
|
BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
|
||||||
};
|
};
|
||||||
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData};
|
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData, MountedData};
|
||||||
use dioxus_interpreter_js::{save_template, Channel};
|
use dioxus_interpreter_js::{get_node, save_template, Channel};
|
||||||
use futures_channel::mpsc;
|
use futures_channel::mpsc;
|
||||||
use js_sys::Array;
|
use js_sys::Array;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
@@ -28,6 +28,7 @@ pub struct WebsysDom {
|
|||||||
templates: FxHashMap<String, u32>,
|
templates: FxHashMap<String, u32>,
|
||||||
max_template_id: u32,
|
max_template_id: u32,
|
||||||
pub(crate) interpreter: Channel,
|
pub(crate) interpreter: Channel,
|
||||||
|
event_channel: mpsc::UnboundedSender<UiEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiEvent {
|
pub struct UiEvent {
|
||||||
@@ -35,7 +36,6 @@ pub struct UiEvent {
|
|||||||
pub bubbles: bool,
|
pub bubbles: bool,
|
||||||
pub element: ElementId,
|
pub element: ElementId,
|
||||||
pub data: Rc<dyn Any>,
|
pub data: Rc<dyn Any>,
|
||||||
pub event: Event,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebsysDom {
|
impl WebsysDom {
|
||||||
@@ -49,8 +49,9 @@ impl WebsysDom {
|
|||||||
};
|
};
|
||||||
let interpreter = Channel::default();
|
let interpreter = Channel::default();
|
||||||
|
|
||||||
let handler: Closure<dyn FnMut(&Event)> =
|
let handler: Closure<dyn FnMut(&Event)> = Closure::wrap(Box::new({
|
||||||
Closure::wrap(Box::new(move |event: &web_sys::Event| {
|
let event_channel = event_channel.clone();
|
||||||
|
move |event: &web_sys::Event| {
|
||||||
let name = event.type_();
|
let name = event.type_();
|
||||||
let element = walk_event_for_id(event);
|
let element = walk_event_for_id(event);
|
||||||
let bubbles = dioxus_html::event_bubbles(name.as_str());
|
let bubbles = dioxus_html::event_bubbles(name.as_str());
|
||||||
@@ -74,10 +75,10 @@ impl WebsysDom {
|
|||||||
bubbles,
|
bubbles,
|
||||||
element,
|
element,
|
||||||
data,
|
data,
|
||||||
event: event.clone(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}));
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
dioxus_interpreter_js::initilize(
|
dioxus_interpreter_js::initilize(
|
||||||
root.clone().unchecked_into(),
|
root.clone().unchecked_into(),
|
||||||
@@ -90,6 +91,7 @@ impl WebsysDom {
|
|||||||
interpreter,
|
interpreter,
|
||||||
templates: FxHashMap::default(),
|
templates: FxHashMap::default(),
|
||||||
max_template_id: 0,
|
max_template_id: 0,
|
||||||
|
event_channel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +163,8 @@ impl WebsysDom {
|
|||||||
pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
|
pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
|
||||||
use Mutation::*;
|
use Mutation::*;
|
||||||
let i = &mut self.interpreter;
|
let i = &mut self.interpreter;
|
||||||
|
// we need to apply the mount events last, so we collect them here
|
||||||
|
let mut to_mount = Vec::new();
|
||||||
for edit in &edits {
|
for edit in &edits {
|
||||||
match edit {
|
match edit {
|
||||||
AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
|
AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
|
||||||
@@ -211,18 +215,43 @@ impl WebsysDom {
|
|||||||
},
|
},
|
||||||
SetText { value, id } => i.set_text(id.0 as u32, value),
|
SetText { value, id } => i.set_text(id.0 as u32, value),
|
||||||
NewEventListener { name, id, .. } => {
|
NewEventListener { name, id, .. } => {
|
||||||
console::log_1(&format!("new event listener: {}", name).into());
|
match *name {
|
||||||
i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
|
// mounted events are fired immediately after the element is mounted.
|
||||||
}
|
"mounted" => {
|
||||||
RemoveEventListener { name, id } => {
|
to_mount.push(*id);
|
||||||
i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
}
|
||||||
|
_ => {
|
||||||
|
i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
RemoveEventListener { name, id } => match *name {
|
||||||
|
"mounted" => {}
|
||||||
|
_ => {
|
||||||
|
i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
|
||||||
|
}
|
||||||
|
},
|
||||||
Remove { id } => i.remove(id.0 as u32),
|
Remove { id } => i.remove(id.0 as u32),
|
||||||
PushRoot { id } => i.push_root(id.0 as u32),
|
PushRoot { id } => i.push_root(id.0 as u32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
edits.clear();
|
edits.clear();
|
||||||
i.flush();
|
i.flush();
|
||||||
|
|
||||||
|
for id in to_mount {
|
||||||
|
let node = get_node(id.0 as u32);
|
||||||
|
if let Some(element) = node.dyn_ref::<Element>() {
|
||||||
|
log::info!("mounted event fired: {}", id.0);
|
||||||
|
let data: MountedData = element.into();
|
||||||
|
let data = Rc::new(data);
|
||||||
|
let _ = self.event_channel.unbounded_send(UiEvent {
|
||||||
|
name: "mounted".to_string(),
|
||||||
|
bubbles: false,
|
||||||
|
element: id,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user