diff --git a/examples/control_focus.rs b/examples/control_focus.rs new file mode 100644 index 000000000..4f4c2ddcb --- /dev/null +++ b/examples/control_focus.rs @@ -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>> = 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); + } + } + } + } + )) +} diff --git a/examples/read_size.rs b/examples/read_size.rs new file mode 100644 index 000000000..806f71f80 --- /dev/null +++ b/examples/read_size.rs @@ -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#" + +"# + .to_owned(), + ), + ); +} + +fn app(cx: Scope) -> Element { + let div_element: &UseRef>> = 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" + } + )) +} diff --git a/examples/scroll_to_top.rs b/examples/scroll_to_top.rs new file mode 100644 index 000000000..4eb38a451 --- /dev/null +++ b/examples/scroll_to_top.rs @@ -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" + } + } + )) +} diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 2ed4bbee9..6af6401f3 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -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, + /// 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. diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs new file mode 100644 index 000000000..94db28f0b --- /dev/null +++ b/packages/desktop/src/element.rs @@ -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, + query: QueryEngine, +} + +impl DesktopElement { + pub(crate) fn new(id: ElementId, webview: Rc, 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>, + >, + >, + > { + let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + + let fut = self + .query + .new_query::>>(&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>>> { + let script = format!( + "return window.interpreter.ScrollTo({}, {});", + self.id.0, + serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") + ); + + let fut = self + .query + .new_query::(&script, &self.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>>> { + let script = format!( + "return window.interpreter.SetFocus({}, {});", + self.id.0, focus + ); + + let fut = self + .query + .new_query::(&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 {} diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index 7a92bedf5..1e11cad18 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -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, + pub(crate) query: Query, } impl EvalResult { - pub(crate) fn new(sender: tokio::sync::broadcast::Sender) -> Self { - Self { broadcast: sender } + pub(crate) fn new(query: Query) -> Self { + Self { query } } } impl IntoFuture for EvalResult { - type Output = Result; + type Output = Result; - type IntoFuture = Pin>>>; + type IntoFuture = Pin>>>; 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::pin(self.query.resolve()) + as Pin>>> } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 807ebf48e..69161e2a5 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -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(root: Component

, props: P, cfg: Config) } EventData::Ipc(msg) if msg.method() == "user_event" => { - let evt = match serde_json::from_value::(msg.params()) { + let params = msg.params(); + + let evt = match serde_json::from_value::(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::() + .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::(params) { + let view = webviews.get(&event.1).unwrap(); + let query = view + .dom + .base_scope() + .consume_context::() + .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::() - .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") { diff --git a/packages/desktop/src/query.rs b/packages/desktop/src/query.rs new file mode 100644 index 000000000..bd8b2ee2a --- /dev/null +++ b/packages/desktop/src/query.rs @@ -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>>, +} + +/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + /// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id. + pub fn new_query(&self, script: &str, webview: &WebView) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + // start the query + // We embed the return of the eval in a function so we can send it back to the main thread + if let Err(err) = 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 { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, + phantom: std::marker::PhantomData, +} + +impl Query { + /// Resolve the query + pub async fn resolve(mut self) -> Result { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break V::deserialize(result.data).map_err(QueryError::DeserializeError); + } + } + Err(err) => { + break Err(QueryError::RecvError(err)); + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Error, Debug)] +pub enum QueryError { + #[error("Error receiving query result: {0}")] + RecvError(RecvError), + #[error("Error deserializing query result: {0}")] + DeserializeError(serde_json::Error), +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct QueryResult { + id: usize, + data: Value, +} diff --git a/packages/dioxus-tui/src/element.rs b/packages/dioxus-tui/src/element.rs new file mode 100644 index 000000000..152de4a10 --- /dev/null +++ b/packages/dioxus-tui/src/element.rs @@ -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 { + 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, bool)>, + mount_events: impl Iterator, +) { + 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>, + >, + >, + > { + 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 {} diff --git a/packages/dioxus-tui/src/lib.rs b/packages/dioxus-tui/src/lib.rs index 296fd4697..46ada5537 100644 --- a/packages/dioxus-tui/src/lib.rs +++ b/packages/dioxus-tui/src/lib.rs @@ -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(app: Component, 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(app: Component, props: Props struct DioxusRenderer { vdom: VirtualDom, dioxus_state: Rc>, + // Events that are queued up to be sent to the vdom next time the vdom is polled + queued_events: Vec<(ElementId, &'static str, Rc, bool)>, #[cfg(all(feature = "hot-reload", debug_assertions))] hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver, } @@ -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 + '_>> { + // 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(); diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 91917ee4b..3d00495ba 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -41,6 +41,11 @@ features = [ "FocusEvent", "CompositionEvent", "ClipboardEvent", + "Element", + "DomRect", + "ScrollIntoViewOptions", + "ScrollLogicalPosition", + "ScrollBehavior", ] [dev-dependencies] diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 853400c28..85a408d0b 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -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, } } diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs new file mode 100644 index 000000000..293b51b9e --- /dev/null +++ b/packages/html/src/events/mounted.rs @@ -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::pin(async { Err(MountedError::NotSupported) }) + } + + /// Scroll to make the element visible + fn scroll_to( + &self, + _behavior: ScrollBehavior, + ) -> Pin>>> { + Box::pin(async { Err(MountedError::NotSupported) }) + } + + /// Set the focus on the element + fn set_focus(&self, _focus: bool) -> Pin>>> { + 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, +} + +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> { + self.inner.get_client_rect().await + } + + /// Scroll to make the element visible + pub fn scroll_to( + &self, + behavior: ScrollBehavior, + ) -> Pin>>> { + self.inner.scroll_to(behavior) + } + + /// Set the focus on the element + pub fn set_focus(&self, focus: bool) -> Pin>>> { + self.inner.set_focus(focus) + } +} + +use dioxus_core::Event; + +pub type MountedEvent = Event; + +impl_event! [ + MountedData; + + /// mounted + onmounted +]; + +/// The MountedResult type for the MountedData +pub type MountedResult = Result; + +#[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), +} + +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 {} diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index 651b7e4f6..ef0e7c70d 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -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, EventData::Transition(data) => Rc::new(data) as Rc, EventData::Toggle(data) => Rc::new(data) as Rc, + EventData::Mounted => Rc::new(MountedData::new(())) as Rc, } } } diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 196099444..bcbe80dda 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -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>>>> { + 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>>> { + 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>>> { + let result = self + .dyn_ref::() + .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 {} diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 76f9e19ca..cf9a4bc3f 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -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"] diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index e3c8579a4..3ea0a86d4 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -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; diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index b1b82823c..2cb148e89 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -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); } diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 6a7ad6aee..354b0f28c 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -14,6 +14,8 @@ license = "MIT/Apache-2.0" [dependencies] thiserror = "1.0.38" +log = "0.4.14" +slab = "0.4" futures-util = { version = "0.3.25", default-features = false, features = [ "sink", ] } diff --git a/packages/liveview/src/element.rs b/packages/liveview/src/element.rs new file mode 100644 index 000000000..21be23bee --- /dev/null +++ b/packages/liveview/src/element.rs @@ -0,0 +1,125 @@ +use dioxus_core::ElementId; +use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::query::QueryEngine; + +/// A mounted element passed to onmounted events +pub struct LiveviewElement { + id: ElementId, + query_tx: UnboundedSender, + query: QueryEngine, +} + +impl LiveviewElement { + pub(crate) fn new(id: ElementId, tx: UnboundedSender, query: QueryEngine) -> Self { + Self { + id, + query_tx: tx, + query, + } + } +} + +impl RenderedElementBacking for LiveviewElement { + fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { + Ok(self) + } + + fn get_client_rect( + &self, + ) -> std::pin::Pin< + Box< + dyn futures_util::Future< + Output = dioxus_html::MountedResult>, + >, + >, + > { + let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + + let fut = self + .query + .new_query::>>(&script, &self.query_tx) + .resolve(); + Box::pin(async move { + match fut.await { + Ok(Some(rect)) => Ok(rect), + Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } + + fn scroll_to( + &self, + behavior: dioxus_html::ScrollBehavior, + ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.ScrollTo({}, {});", + self.id.0, + serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") + ); + + let fut = self + .query + .new_query::(&script, &self.query_tx) + .resolve(); + Box::pin(async move { + match fut.await { + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } + + fn set_focus( + &self, + focus: bool, + ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.SetFocus({}, {});", + self.id.0, focus + ); + + let fut = self + .query + .new_query::(&script, &self.query_tx) + .resolve(); + + Box::pin(async move { + match fut.await { + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } +} + +#[derive(Debug)] +enum DesktopQueryError { + FailedToQuery, +} + +impl std::fmt::Display for DesktopQueryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), + } + } +} + +impl std::error::Error for DesktopQueryError {} diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index bea21a71b..2bf4ea3bc 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -18,7 +18,9 @@ pub mod adapters { pub use adapters::*; +mod element; pub mod pool; +mod query; use futures_util::{SinkExt, StreamExt}; pub use pool::*; diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 7dd9758d6..594c764c2 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -26,11 +26,19 @@ class IPC { // todo: retry the connection }; - ws.onmessage = (event) => { + ws.onmessage = (message) => { // Ignore pongs - if (event.data != "__pong__") { - let edits = JSON.parse(event.data); - window.interpreter.handleEdits(edits); + if (message.data != "__pong__") { + const event = JSON.parse(message.data); + switch (event.type) { + case "edits": + let edits = event.data; + window.interpreter.handleEdits(edits); + break; + case "query": + Function("Eval", `"use strict";${event.data};`)(); + break; + } } }; diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index e02c8195b..089d0bad4 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -1,8 +1,13 @@ -use crate::LiveViewError; -use dioxus_core::prelude::*; -use dioxus_html::HtmlEvent; +use crate::{ + element::LiveviewElement, + query::{QueryEngine, QueryResult}, + LiveViewError, +}; +use dioxus_core::{prelude::*, Mutations}; +use dioxus_html::{EventData, HtmlEvent, MountedData}; use futures_util::{pin_mut, SinkExt, StreamExt}; -use std::time::Duration; +use serde::Serialize; +use std::{rc::Rc, time::Duration}; use tokio_util::task::LocalPoolHandle; #[derive(Clone)] @@ -115,7 +120,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li }; // todo: use an efficient binary packed format for this - let edits = serde_json::to_string(&vdom.rebuild()).unwrap(); + let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap(); // pin the futures so we can use select! pin_mut!(ws); @@ -123,11 +128,19 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li // send the initial render to the client ws.send(edits).await?; + // Create the a proxy for query engine + let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel(); + let query_engine = QueryEngine::default(); + // desktop uses this wrapper struct thing around the actual event itself // this is sorta driven by tao/wry - #[derive(serde::Deserialize)] - struct IpcMessage { - params: HtmlEvent, + #[derive(serde::Deserialize, Debug)] + #[serde(tag = "method", content = "params")] + enum IpcMessage { + #[serde(rename = "user_event")] + Event(HtmlEvent), + #[serde(rename = "query")] + Query(QueryResult), } loop { @@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li ws.send("__pong__".to_string()).await?; } Some(Ok(evt)) => { - if let Ok(IpcMessage { params }) = serde_json::from_str::(evt) { - vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles); + if let Ok(message) = serde_json::from_str::(evt) { + match message { + IpcMessage::Event(evt) => { + // Intercept the mounted event and insert a custom element type + if let EventData::Mounted = &evt.data { + let element = LiveviewElement::new(evt.element, query_tx.clone(), query_engine.clone()); + vdom.handle_event( + &evt.name, + Rc::new(MountedData::new(element)), + evt.element, + evt.bubbles, + ); + } + else{ + vdom.handle_event( + &evt.name, + evt.data.into_any(), + evt.element, + evt.bubbles, + ); + } + } + IpcMessage::Query(result) => { + query_engine.send(result); + }, + } } } // log this I guess? when would we get an error here? - Some(Err(_e)) => {}, + Some(Err(_e)) => {} None => return Ok(()), } } + // handle any new queries + Some(query) = query_rx.recv() => { + ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?; + } + Some(msg) = hot_reload_wait => { #[cfg(all(feature = "hot-reload", debug_assertions))] match msg{ @@ -176,6 +218,16 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) .await; - ws.send(serde_json::to_string(&edits).unwrap()).await?; + ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap()) + .await?; } } + +#[derive(Serialize)] +#[serde(tag = "type", content = "data")] +enum ClientUpdate<'a> { + #[serde(rename = "edits")] + Edits(Mutations<'a>), + #[serde(rename = "query")] + Query(String), +} diff --git a/packages/liveview/src/query.rs b/packages/liveview/src/query.rs new file mode 100644 index 000000000..04d531a63 --- /dev/null +++ b/packages/liveview/src/query.rs @@ -0,0 +1,113 @@ +use std::{cell::RefCell, rc::Rc}; + +use serde::{de::DeserializeOwned, Deserialize}; +use serde_json::Value; +use slab::Slab; +use thiserror::Error; +use tokio::sync::{broadcast::error::RecvError, mpsc::UnboundedSender}; + +/// Tracks what query ids are currently active +#[derive(Default, Clone)] +struct SharedSlab { + slab: Rc>>, +} + +/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + /// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id. + pub fn new_query( + &self, + script: &str, + tx: &UnboundedSender, + ) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + // start the query + // We embed the return of the eval in a function so we can send it back to the main thread + if let Err(err) = tx.send(format!( + r#"window.ipc.postMessage( + JSON.stringify({{ + "method":"query", + "params": {{ + "id": {request_id}, + "data": (function(){{{script}}})() + }} + }}) + );"# + )) { + log::warn!("Query error: {err}"); + } + + Query { + slab: self.active_requests.clone(), + id: request_id, + reciever: self.sender.subscribe(), + phantom: std::marker::PhantomData, + } + } + + /// Send a query result + pub fn send(&self, data: QueryResult) { + let _ = self.sender.send(data); + } +} + +pub(crate) struct Query { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, + phantom: std::marker::PhantomData, +} + +impl Query { + /// Resolve the query + pub async fn resolve(mut self) -> Result { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break V::deserialize(result.data).map_err(QueryError::DeserializeError); + } + } + Err(err) => { + break Err(QueryError::RecvError(err)); + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Error, Debug)] +pub enum QueryError { + #[error("Error receiving query result: {0}")] + RecvError(RecvError), + #[error("Error deserializing query result: {0}")] + DeserializeError(serde_json::Error), +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct QueryResult { + id: usize, + data: Value, +} diff --git a/packages/rink/src/query.rs b/packages/rink/src/query.rs index 1cf113310..2f4fe02e0 100644 --- a/packages/rink/src/query.rs +++ b/packages/rink/src/query.rs @@ -91,15 +91,7 @@ impl<'a> ElementRef<'a> { pub fn layout(&self) -> Option { let layout = self .stretch - .layout( - self.inner - .get(self.id) - .unwrap() - .get::() - .unwrap() - .node - .ok()?, - ) + .layout(self.inner.get(self.id)?.get::()?.node.ok()?) .ok(); layout.map(|layout| Layout { order: layout.order, diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index b25b7826c..1647b419a 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -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, max_template_id: u32, pub(crate) interpreter: Channel, + event_channel: mpsc::UnboundedSender, } pub struct UiEvent { @@ -35,7 +36,6 @@ pub struct UiEvent { pub bubbles: bool, pub element: ElementId, pub data: Rc, - pub event: Event, } impl WebsysDom { @@ -49,8 +49,9 @@ impl WebsysDom { }; let interpreter = Channel::default(); - let handler: Closure = - Closure::wrap(Box::new(move |event: &web_sys::Event| { + let handler: Closure = 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) { 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::() { + 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, + }); + } + } } }