Merge pull request #894 from Demonthos/query-system
Create onmounted Event
This commit is contained in:
commit
379ea09b12
|
@ -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::eval::EvalResult;
|
||||
use crate::events::IpcMessage;
|
||||
use crate::query::QueryEngine;
|
||||
use crate::shortcut::IntoKeyCode;
|
||||
use crate::shortcut::IntoModifersState;
|
||||
use crate::shortcut::ShortcutId;
|
||||
|
@ -16,7 +17,6 @@ use dioxus_core::ScopeState;
|
|||
use dioxus_core::VirtualDom;
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use serde_json::Value;
|
||||
use slab::Slab;
|
||||
use wry::application::event::Event;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
|
@ -59,8 +59,8 @@ pub struct DesktopContext {
|
|||
/// The proxy to the event loop
|
||||
pub proxy: ProxyType,
|
||||
|
||||
/// The receiver for eval results since eval is async
|
||||
pub(super) eval: tokio::sync::broadcast::Sender<Value>,
|
||||
/// The receiver for queries about the current window
|
||||
pub(super) query: QueryEngine,
|
||||
|
||||
pub(super) pending_windows: WebviewQueue,
|
||||
|
||||
|
@ -96,7 +96,7 @@ impl DesktopContext {
|
|||
webview,
|
||||
proxy,
|
||||
event_loop,
|
||||
eval: tokio::sync::broadcast::channel(8).0,
|
||||
query: Default::default(),
|
||||
pending_windows: webviews,
|
||||
event_handlers,
|
||||
shortcut_manager,
|
||||
|
@ -210,28 +210,10 @@ impl DesktopContext {
|
|||
|
||||
/// Evaluate a javascript expression
|
||||
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
|
||||
let script = format!(
|
||||
r#"
|
||||
window.ipc.postMessage(
|
||||
JSON.stringify({{
|
||||
"method":"eval_result",
|
||||
"params": (
|
||||
function(){{
|
||||
{code}
|
||||
}}
|
||||
)()
|
||||
}})
|
||||
);
|
||||
"#
|
||||
);
|
||||
// the query id lets us keep track of the eval result and send it back to the main thread
|
||||
let query = self.query.new_query(code, &self.webview);
|
||||
|
||||
if let Err(e) = self.webview.evaluate_script(&script) {
|
||||
// send an error to the eval receiver
|
||||
log::warn!("Eval script error: {e}");
|
||||
}
|
||||
|
||||
EvalResult::new(self.eval.clone())
|
||||
EvalResult::new(query)
|
||||
}
|
||||
|
||||
/// 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 crate::query::Query;
|
||||
use crate::query::QueryError;
|
||||
use crate::use_window;
|
||||
use dioxus_core::ScopeState;
|
||||
use serde::de::Error;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A future that resolves to the result of a JavaScript evaluation.
|
||||
pub struct EvalResult {
|
||||
pub(crate) broadcast: tokio::sync::broadcast::Sender<serde_json::Value>,
|
||||
pub(crate) query: Query<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl EvalResult {
|
||||
pub(crate) fn new(sender: tokio::sync::broadcast::Sender<serde_json::Value>) -> Self {
|
||||
Self { broadcast: sender }
|
||||
pub(crate) fn new(query: Query<serde_json::Value>) -> Self {
|
||||
Self { query }
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Box::pin(async move {
|
||||
let mut reciever = self.broadcast.subscribe();
|
||||
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>>>>
|
||||
Box::pin(self.query.resolve())
|
||||
as Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,22 +5,27 @@
|
|||
|
||||
mod cfg;
|
||||
mod desktop_context;
|
||||
mod element;
|
||||
mod escape;
|
||||
mod eval;
|
||||
mod events;
|
||||
mod file_upload;
|
||||
mod protocol;
|
||||
mod query;
|
||||
mod shortcut;
|
||||
mod waker;
|
||||
mod webview;
|
||||
|
||||
use crate::query::QueryResult;
|
||||
pub use cfg::Config;
|
||||
pub use desktop_context::{
|
||||
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
|
||||
};
|
||||
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
||||
use dioxus_core::*;
|
||||
use dioxus_html::MountedData;
|
||||
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
||||
use element::DesktopElement;
|
||||
pub use eval::{use_eval, EvalResult};
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
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" => {
|
||||
let evt = match serde_json::from_value::<HtmlEvent>(msg.params()) {
|
||||
let params = msg.params();
|
||||
|
||||
let evt = match serde_json::from_value::<HtmlEvent>(params) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let HtmlEvent {
|
||||
element,
|
||||
name,
|
||||
bubbles,
|
||||
data,
|
||||
} = evt;
|
||||
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
|
||||
view.dom
|
||||
.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles);
|
||||
// check for a mounted event placeholder and replace it with a desktop specific element
|
||||
let as_any = if let dioxus_html::EventData::Mounted = &data {
|
||||
let query = view
|
||||
.dom
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.query;
|
||||
|
||||
let element = DesktopElement::new(element, view.webview.clone(), query);
|
||||
|
||||
Rc::new(MountedData::new(element))
|
||||
} else {
|
||||
data.into_any()
|
||||
};
|
||||
|
||||
view.dom.handle_event(&name, as_any, element, bubbles);
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.webview);
|
||||
}
|
||||
|
||||
// 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" => {
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
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" => {
|
||||
if let Some(temp) = msg.params().as_object() {
|
||||
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 widgets;
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
ops::Deref,
|
||||
rc::Rc,
|
||||
sync::{Arc, RwLock},
|
||||
|
@ -12,6 +14,7 @@ use dioxus_html::EventData;
|
|||
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
|
||||
use dioxus_native_core::prelude::*;
|
||||
|
||||
use element::{create_mounted_events, find_mount_events};
|
||||
pub use rink::{query::Query, Config, RenderingMode, Size, TuiContext};
|
||||
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(),
|
||||
});
|
||||
let muts = vdom.rebuild();
|
||||
let mut rdom = rdom.write().unwrap();
|
||||
dioxus_state
|
||||
.write()
|
||||
.unwrap()
|
||||
.apply_mutations(&mut rdom, muts);
|
||||
|
||||
let mut queued_events = Vec::new();
|
||||
|
||||
{
|
||||
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 {
|
||||
vdom,
|
||||
dioxus_state,
|
||||
queued_events,
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
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 {
|
||||
vdom: VirtualDom,
|
||||
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))]
|
||||
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 mut rdom = rdom.write().unwrap();
|
||||
self.dioxus_state
|
||||
.write()
|
||||
.unwrap()
|
||||
.apply_mutations(&mut rdom, muts);
|
||||
|
||||
{
|
||||
// Find any mount events
|
||||
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 = ()> + '_>> {
|
||||
// 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))]
|
||||
return Box::pin(async {
|
||||
let hot_reload_wait = self.hot_reload_rx.recv();
|
||||
|
|
|
@ -41,6 +41,11 @@ features = [
|
|||
"FocusEvent",
|
||||
"CompositionEvent",
|
||||
"ClipboardEvent",
|
||||
"Element",
|
||||
"DomRect",
|
||||
"ScrollIntoViewOptions",
|
||||
"ScrollLogicalPosition",
|
||||
"ScrollBehavior",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -33,6 +33,7 @@ mod form;
|
|||
mod image;
|
||||
mod keyboard;
|
||||
mod media;
|
||||
mod mounted;
|
||||
mod mouse;
|
||||
mod pointer;
|
||||
mod scroll;
|
||||
|
@ -51,6 +52,7 @@ pub use form::*;
|
|||
pub use image::*;
|
||||
pub use keyboard::*;
|
||||
pub use media::*;
|
||||
pub use mounted::*;
|
||||
pub use mouse::*;
|
||||
pub use pointer::*;
|
||||
pub use scroll::*;
|
||||
|
@ -144,6 +146,7 @@ pub fn event_bubbles(evt: &str) -> bool {
|
|||
"animationiteration" => true,
|
||||
"transitionend" => true,
|
||||
"toggle" => true,
|
||||
"mounted" => false,
|
||||
_ => 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(de(data)?),
|
||||
|
||||
// Mounted
|
||||
"mounted" => Mounted,
|
||||
|
||||
// ImageData => "load" | "error";
|
||||
// OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
|
||||
other => {
|
||||
|
@ -151,6 +154,7 @@ pub enum EventData {
|
|||
Animation(AnimationData),
|
||||
Transition(TransitionData),
|
||||
Toggle(ToggleData),
|
||||
Mounted,
|
||||
}
|
||||
|
||||
impl EventData {
|
||||
|
@ -172,6 +176,7 @@ impl EventData {
|
|||
EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
|
||||
EventData::Transition(data) => Rc::new(data) as Rc<dyn Any>,
|
||||
EventData::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
|
||||
EventData::Mounted => Rc::new(MountedData::new(())) as Rc<dyn Any>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,18 @@ use crate::events::{
|
|||
};
|
||||
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
|
||||
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 std::convert::TryInto;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{
|
||||
AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent,
|
||||
TransitionEvent, WheelEvent,
|
||||
AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent,
|
||||
ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent,
|
||||
};
|
||||
|
||||
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"] }
|
||||
sledgehammer_bindgen = { version = "0.2.1", optional = true }
|
||||
sledgehammer_utils = { version = "0.1.1", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
serialize = ["serde"]
|
||||
web = ["wasm-bindgen", "js-sys", "web-sys"]
|
||||
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
|
||||
|
|
|
@ -204,6 +204,45 @@ class Interpreter {
|
|||
node.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
|
||||
GetClientRect(id) {
|
||||
const node = this.nodes[id];
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const rect = node.getBoundingClientRect();
|
||||
return {
|
||||
type: "GetClientRect",
|
||||
origin: [rect.x, rect.y],
|
||||
size: [rect.width, rect.height],
|
||||
};
|
||||
}
|
||||
|
||||
ScrollTo(id, behavior) {
|
||||
const node = this.nodes[id];
|
||||
if (!node) {
|
||||
return 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) {
|
||||
for (let template of edits.templates) {
|
||||
this.SaveTemplate(template);
|
||||
|
@ -346,9 +385,21 @@ class Interpreter {
|
|||
case "NewEventListener":
|
||||
let bubbles = event_bubbles(edit.name);
|
||||
|
||||
this.NewEventListener(edit.name, edit.id, bubbles, (event) => {
|
||||
handler(event, edit.name, bubbles);
|
||||
});
|
||||
// if this is a mounted listener, we send the event immediately
|
||||
if (edit.name === "mounted") {
|
||||
window.ipc.postMessage(
|
||||
serializeIpcMessage("user_event", {
|
||||
name: edit.name,
|
||||
element: edit.id,
|
||||
data: null,
|
||||
bubbles,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.NewEventListener(edit.name, edit.id, bubbles, (event) => {
|
||||
handler(event, edit.name, bubbles);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -933,6 +984,8 @@ function event_bubbles(event) {
|
|||
return true;
|
||||
case "toggle":
|
||||
return true;
|
||||
case "mounted":
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -117,6 +117,9 @@ mod js {
|
|||
export function set_node(id, node) {
|
||||
nodes[id] = node;
|
||||
}
|
||||
export function get_node(id) {
|
||||
return nodes[id];
|
||||
}
|
||||
export function initilize(root, handler) {
|
||||
listeners.handler = handler;
|
||||
nodes = [root];
|
||||
|
@ -167,6 +170,9 @@ mod js {
|
|||
#[wasm_bindgen]
|
||||
pub fn set_node(id: u32, node: Node);
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_node(id: u32) -> Node;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn initilize(root: Node, handler: &Function);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
] }
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
mod element;
|
||||
pub mod pool;
|
||||
mod query;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
pub use pool::*;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
use crate::LiveViewError;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_html::HtmlEvent;
|
||||
use crate::{
|
||||
element::LiveviewElement,
|
||||
query::{QueryEngine, QueryResult},
|
||||
LiveViewError,
|
||||
};
|
||||
use dioxus_core::{prelude::*, Mutations};
|
||||
use dioxus_html::{EventData, HtmlEvent, MountedData};
|
||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
||||
use std::time::Duration;
|
||||
use serde::Serialize;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -115,7 +120,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
|||
};
|
||||
|
||||
// todo: use an efficient binary packed format for this
|
||||
let edits = serde_json::to_string(&vdom.rebuild()).unwrap();
|
||||
let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap();
|
||||
|
||||
// pin the futures so we can use select!
|
||||
pin_mut!(ws);
|
||||
|
@ -123,11 +128,19 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
|||
// send the initial render to the client
|
||||
ws.send(edits).await?;
|
||||
|
||||
// Create the a proxy for query engine
|
||||
let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let query_engine = QueryEngine::default();
|
||||
|
||||
// desktop uses this wrapper struct thing around the actual event itself
|
||||
// this is sorta driven by tao/wry
|
||||
#[derive(serde::Deserialize)]
|
||||
struct IpcMessage {
|
||||
params: HtmlEvent,
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
#[serde(tag = "method", content = "params")]
|
||||
enum IpcMessage {
|
||||
#[serde(rename = "user_event")]
|
||||
Event(HtmlEvent),
|
||||
#[serde(rename = "query")]
|
||||
Query(QueryResult),
|
||||
}
|
||||
|
||||
loop {
|
||||
|
@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
|||
ws.send("__pong__".to_string()).await?;
|
||||
}
|
||||
Some(Ok(evt)) => {
|
||||
if let Ok(IpcMessage { params }) = serde_json::from_str::<IpcMessage>(evt) {
|
||||
vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles);
|
||||
if let Ok(message) = serde_json::from_str::<IpcMessage>(evt) {
|
||||
match message {
|
||||
IpcMessage::Event(evt) => {
|
||||
// Intercept the mounted event and insert a custom element type
|
||||
if let EventData::Mounted = &evt.data {
|
||||
let element = LiveviewElement::new(evt.element, query_tx.clone(), query_engine.clone());
|
||||
vdom.handle_event(
|
||||
&evt.name,
|
||||
Rc::new(MountedData::new(element)),
|
||||
evt.element,
|
||||
evt.bubbles,
|
||||
);
|
||||
}
|
||||
else{
|
||||
vdom.handle_event(
|
||||
&evt.name,
|
||||
evt.data.into_any(),
|
||||
evt.element,
|
||||
evt.bubbles,
|
||||
);
|
||||
}
|
||||
}
|
||||
IpcMessage::Query(result) => {
|
||||
query_engine.send(result);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
// log this I guess? when would we get an error here?
|
||||
Some(Err(_e)) => {},
|
||||
Some(Err(_e)) => {}
|
||||
None => return Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
// handle any new queries
|
||||
Some(query) = query_rx.recv() => {
|
||||
ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?;
|
||||
}
|
||||
|
||||
Some(msg) = hot_reload_wait => {
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
match msg{
|
||||
|
@ -176,6 +218,16 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
|||
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
|
||||
.await;
|
||||
|
||||
ws.send(serde_json::to_string(&edits).unwrap()).await?;
|
||||
ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
enum ClientUpdate<'a> {
|
||||
#[serde(rename = "edits")]
|
||||
Edits(Mutations<'a>),
|
||||
#[serde(rename = "query")]
|
||||
Query(String),
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
let layout = self
|
||||
.stretch
|
||||
.layout(
|
||||
self.inner
|
||||
.get(self.id)
|
||||
.unwrap()
|
||||
.get::<TaffyLayout>()
|
||||
.unwrap()
|
||||
.node
|
||||
.ok()?,
|
||||
)
|
||||
.layout(self.inner.get(self.id)?.get::<TaffyLayout>()?.node.ok()?)
|
||||
.ok();
|
||||
layout.map(|layout| Layout {
|
||||
order: layout.order,
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
use dioxus_core::{
|
||||
BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
|
||||
};
|
||||
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData};
|
||||
use dioxus_interpreter_js::{save_template, Channel};
|
||||
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData, MountedData};
|
||||
use dioxus_interpreter_js::{get_node, save_template, Channel};
|
||||
use futures_channel::mpsc;
|
||||
use js_sys::Array;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
@ -28,6 +28,7 @@ pub struct WebsysDom {
|
|||
templates: FxHashMap<String, u32>,
|
||||
max_template_id: u32,
|
||||
pub(crate) interpreter: Channel,
|
||||
event_channel: mpsc::UnboundedSender<UiEvent>,
|
||||
}
|
||||
|
||||
pub struct UiEvent {
|
||||
|
@ -35,7 +36,6 @@ pub struct UiEvent {
|
|||
pub bubbles: bool,
|
||||
pub element: ElementId,
|
||||
pub data: Rc<dyn Any>,
|
||||
pub event: Event,
|
||||
}
|
||||
|
||||
impl WebsysDom {
|
||||
|
@ -49,8 +49,9 @@ impl WebsysDom {
|
|||
};
|
||||
let interpreter = Channel::default();
|
||||
|
||||
let handler: Closure<dyn FnMut(&Event)> =
|
||||
Closure::wrap(Box::new(move |event: &web_sys::Event| {
|
||||
let handler: Closure<dyn FnMut(&Event)> = Closure::wrap(Box::new({
|
||||
let event_channel = event_channel.clone();
|
||||
move |event: &web_sys::Event| {
|
||||
let name = event.type_();
|
||||
let element = walk_event_for_id(event);
|
||||
let bubbles = dioxus_html::event_bubbles(name.as_str());
|
||||
|
@ -74,10 +75,10 @@ impl WebsysDom {
|
|||
bubbles,
|
||||
element,
|
||||
data,
|
||||
event: event.clone(),
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
dioxus_interpreter_js::initilize(
|
||||
root.clone().unchecked_into(),
|
||||
|
@ -90,6 +91,7 @@ impl WebsysDom {
|
|||
interpreter,
|
||||
templates: FxHashMap::default(),
|
||||
max_template_id: 0,
|
||||
event_channel,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,6 +163,8 @@ impl WebsysDom {
|
|||
pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
|
||||
use Mutation::*;
|
||||
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 {
|
||||
match edit {
|
||||
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),
|
||||
NewEventListener { name, id, .. } => {
|
||||
console::log_1(&format!("new event listener: {}", name).into());
|
||||
i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
|
||||
}
|
||||
RemoveEventListener { name, id } => {
|
||||
i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
match *name {
|
||||
// mounted events are fired immediately after the element is mounted.
|
||||
"mounted" => {
|
||||
to_mount.push(*id);
|
||||
}
|
||||
_ => {
|
||||
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),
|
||||
PushRoot { id } => i.push_root(id.0 as u32),
|
||||
}
|
||||
}
|
||||
edits.clear();
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue