Browse Source

Merge pull request #894 from Demonthos/query-system

Create onmounted Event
Jon Kelley 2 years ago
parent
commit
379ea09b12

+ 44 - 0
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<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);
+                    }
+                }
+            }
+        }
+    ))
+}

+ 60 - 0
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#"
+<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"
+        }
+    ))
+}

+ 33 - 0
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"
+            }
+        }
+    ))
+}

+ 7 - 25
packages/desktop/src/desktop_context.rs

@@ -5,6 +5,7 @@ use std::rc::Weak;
 use crate::create_new_window;
 use crate::create_new_window;
 use crate::eval::EvalResult;
 use crate::eval::EvalResult;
 use crate::events::IpcMessage;
 use crate::events::IpcMessage;
+use crate::query::QueryEngine;
 use crate::shortcut::IntoKeyCode;
 use crate::shortcut::IntoKeyCode;
 use crate::shortcut::IntoModifersState;
 use crate::shortcut::IntoModifersState;
 use crate::shortcut::ShortcutId;
 use crate::shortcut::ShortcutId;
@@ -16,7 +17,6 @@ use dioxus_core::ScopeState;
 use dioxus_core::VirtualDom;
 use dioxus_core::VirtualDom;
 #[cfg(all(feature = "hot-reload", debug_assertions))]
 #[cfg(all(feature = "hot-reload", debug_assertions))]
 use dioxus_hot_reload::HotReloadMsg;
 use dioxus_hot_reload::HotReloadMsg;
-use serde_json::Value;
 use slab::Slab;
 use slab::Slab;
 use wry::application::event::Event;
 use wry::application::event::Event;
 use wry::application::event_loop::EventLoopProxy;
 use wry::application::event_loop::EventLoopProxy;
@@ -59,8 +59,8 @@ pub struct DesktopContext {
     /// The proxy to the event loop
     /// The proxy to the event loop
     pub proxy: ProxyType,
     pub proxy: ProxyType,
 
 
-    /// The receiver for eval results since eval is async
-    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,
     pub(super) pending_windows: WebviewQueue,
 
 
@@ -96,7 +96,7 @@ impl DesktopContext {
             webview,
             webview,
             proxy,
             proxy,
             event_loop,
             event_loop,
-            eval: tokio::sync::broadcast::channel(8).0,
+            query: Default::default(),
             pending_windows: webviews,
             pending_windows: webviews,
             event_handlers,
             event_handlers,
             shortcut_manager,
             shortcut_manager,
@@ -210,28 +210,10 @@ impl DesktopContext {
 
 
     /// Evaluate a javascript expression
     /// Evaluate a javascript expression
     pub fn eval(&self, code: &str) -> EvalResult {
     pub fn eval(&self, code: &str) -> EvalResult {
-        // Embed the return of the eval in a function so we can send it back to the main thread
-        let script = format!(
-            r#"
-            window.ipc.postMessage(
-                JSON.stringify({{
-                    "method":"eval_result",
-                    "params": (
-                        function(){{
-                            {code}
-                        }}
-                    )()
-                }})
-            );
-            "#
-        );
-
-        if let Err(e) = self.webview.evaluate_script(&script) {
-            // send an error to the eval receiver
-            log::warn!("Eval script error: {e}");
-        }
+        // 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);
 
 
-        EvalResult::new(self.eval.clone())
+        EvalResult::new(query)
     }
     }
 
 
     /// Create a wry event handler that listens for wry events.
     /// Create a wry event handler that listens for wry events.

+ 123 - 0
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<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 {}

+ 9 - 13
packages/desktop/src/eval.rs

@@ -1,36 +1,32 @@
 use std::rc::Rc;
 use std::rc::Rc;
 
 
+use crate::query::Query;
+use crate::query::QueryError;
 use crate::use_window;
 use crate::use_window;
 use dioxus_core::ScopeState;
 use dioxus_core::ScopeState;
-use serde::de::Error;
 use std::future::Future;
 use std::future::Future;
 use std::future::IntoFuture;
 use std::future::IntoFuture;
 use std::pin::Pin;
 use std::pin::Pin;
 
 
 /// A future that resolves to the result of a JavaScript evaluation.
 /// A future that resolves to the result of a JavaScript evaluation.
 pub struct EvalResult {
 pub struct EvalResult {
-    pub(crate) broadcast: tokio::sync::broadcast::Sender<serde_json::Value>,
+    pub(crate) query: Query<serde_json::Value>,
 }
 }
 
 
 impl EvalResult {
 impl EvalResult {
-    pub(crate) fn new(sender: tokio::sync::broadcast::Sender<serde_json::Value>) -> Self {
-        Self { broadcast: sender }
+    pub(crate) fn new(query: Query<serde_json::Value>) -> Self {
+        Self { query }
     }
     }
 }
 }
 
 
 impl IntoFuture for EvalResult {
 impl IntoFuture for EvalResult {
-    type Output = Result<serde_json::Value, serde_json::Error>;
+    type Output = Result<serde_json::Value, QueryError>;
 
 
-    type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
+    type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>;
 
 
     fn into_future(self) -> Self::IntoFuture {
     fn into_future(self) -> Self::IntoFuture {
-        Box::pin(async move {
-            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>>>>
     }
     }
 }
 }
 
 

+ 49 - 18
packages/desktop/src/lib.rs

@@ -5,22 +5,27 @@
 
 
 mod cfg;
 mod cfg;
 mod desktop_context;
 mod desktop_context;
+mod element;
 mod escape;
 mod escape;
 mod eval;
 mod eval;
 mod events;
 mod events;
 mod file_upload;
 mod file_upload;
 mod protocol;
 mod protocol;
+mod query;
 mod shortcut;
 mod shortcut;
 mod waker;
 mod waker;
 mod webview;
 mod webview;
 
 
+use crate::query::QueryResult;
 pub use cfg::Config;
 pub use cfg::Config;
 pub use desktop_context::{
 pub use desktop_context::{
     use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
     use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
 };
 };
 use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
 use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
 use dioxus_core::*;
 use dioxus_core::*;
+use dioxus_html::MountedData;
 use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
 use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
+use element::DesktopElement;
 pub use eval::{use_eval, EvalResult};
 pub use eval::{use_eval, EvalResult};
 use futures_util::{pin_mut, FutureExt};
 use futures_util::{pin_mut, FutureExt};
 use shortcut::ShortcutRegistry;
 use shortcut::ShortcutRegistry;
@@ -221,39 +226,65 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
                 }
                 }
 
 
                 EventData::Ipc(msg) if msg.method() == "user_event" => {
                 EventData::Ipc(msg) if msg.method() == "user_event" => {
-                    let evt = match serde_json::from_value::<HtmlEvent>(msg.params()) {
+                    let params = msg.params();
+
+                    let evt = match serde_json::from_value::<HtmlEvent>(params) {
                         Ok(value) => value,
                         Ok(value) => value,
                         Err(_) => return,
                         Err(_) => return,
                     };
                     };
 
 
+                    let HtmlEvent {
+                        element,
+                        name,
+                        bubbles,
+                        data,
+                    } = evt;
+
                     let view = webviews.get_mut(&event.1).unwrap();
                     let view = webviews.get_mut(&event.1).unwrap();
 
 
-                    view.dom
-                        .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);
                     send_edits(view.dom.render_immediate(), &view.webview);
                 }
                 }
 
 
+                // When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query
+                EventData::Ipc(msg) if msg.method() == "query" => {
+                    let params = msg.params();
+
+                    if let Ok(result) = serde_json::from_value::<QueryResult>(params) {
+                        let view = webviews.get(&event.1).unwrap();
+                        let query = view
+                            .dom
+                            .base_scope()
+                            .consume_context::<DesktopContext>()
+                            .unwrap()
+                            .query;
+
+                        query.send(result);
+                    }
+                }
+
                 EventData::Ipc(msg) if msg.method() == "initialize" => {
                 EventData::Ipc(msg) if msg.method() == "initialize" => {
                     let view = webviews.get_mut(&event.1).unwrap();
                     let view = webviews.get_mut(&event.1).unwrap();
                     send_edits(view.dom.rebuild(), &view.webview);
                     send_edits(view.dom.rebuild(), &view.webview);
                 }
                 }
 
 
-                // When the webview chirps back with the result of the eval, we send it to the active receiver
-                //
-                // This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once,
-                // you might the wrong result. This should be fixed
-                EventData::Ipc(msg) if msg.method() == "eval_result" => {
-                    webviews[&event.1]
-                        .dom
-                        .base_scope()
-                        .consume_context::<DesktopContext>()
-                        .unwrap()
-                        .eval
-                        .send(msg.params())
-                        .unwrap();
-                }
-
                 EventData::Ipc(msg) if msg.method() == "browser_open" => {
                 EventData::Ipc(msg) if msg.method() == "browser_open" => {
                     if let Some(temp) = msg.params().as_object() {
                     if let Some(temp) = msg.params().as_object() {
                         if temp.contains_key("href") {
                         if temp.contains_key("href") {

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

+ 99 - 0
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<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 {}

+ 50 - 9
packages/dioxus-tui/src/lib.rs

@@ -1,7 +1,9 @@
+mod element;
 pub mod prelude;
 pub mod prelude;
 pub mod widgets;
 pub mod widgets;
 
 
 use std::{
 use std::{
+    any::Any,
     ops::Deref,
     ops::Deref,
     rc::Rc,
     rc::Rc,
     sync::{Arc, RwLock},
     sync::{Arc, RwLock},
@@ -12,6 +14,7 @@ use dioxus_html::EventData;
 use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
 use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
 use dioxus_native_core::prelude::*;
 use dioxus_native_core::prelude::*;
 
 
+use element::{create_mounted_events, find_mount_events};
 pub use rink::{query::Query, Config, RenderingMode, Size, TuiContext};
 pub use rink::{query::Query, Config, RenderingMode, Size, TuiContext};
 use rink::{render, Driver};
 use rink::{render, Driver};
 
 
@@ -37,14 +40,32 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
                 mapping: dioxus_state.clone(),
                 mapping: dioxus_state.clone(),
             });
             });
         let muts = vdom.rebuild();
         let muts = vdom.rebuild();
-        let mut rdom = rdom.write().unwrap();
-        dioxus_state
-            .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 {
         DioxusRenderer {
             vdom,
             vdom,
             dioxus_state,
             dioxus_state,
+            queued_events,
             #[cfg(all(feature = "hot-reload", debug_assertions))]
             #[cfg(all(feature = "hot-reload", debug_assertions))]
             hot_reload_rx: {
             hot_reload_rx: {
                 let (hot_reload_tx, hot_reload_rx) =
                 let (hot_reload_tx, hot_reload_rx) =
@@ -62,6 +83,8 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
 struct DioxusRenderer {
 struct DioxusRenderer {
     vdom: VirtualDom,
     vdom: VirtualDom,
     dioxus_state: Rc<RwLock<DioxusState>>,
     dioxus_state: Rc<RwLock<DioxusState>>,
+    // Events that are queued up to be sent to the vdom next time the vdom is polled
+    queued_events: Vec<(ElementId, &'static str, Rc<dyn Any>, bool)>,
     #[cfg(all(feature = "hot-reload", debug_assertions))]
     #[cfg(all(feature = "hot-reload", debug_assertions))]
     hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
     hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
 }
 }
@@ -71,10 +94,23 @@ impl Driver for DioxusRenderer {
         let muts = self.vdom.render_immediate();
         let muts = self.vdom.render_immediate();
         {
         {
             let mut rdom = rdom.write().unwrap();
             let mut rdom = rdom.write().unwrap();
-            self.dioxus_state
-                .write()
-                .unwrap()
-                .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 = ()> + '_>> {
     fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
+        // Add any queued events
+        for (id, event, value, bubbles) in self.queued_events.drain(..) {
+            self.vdom.handle_event(event, value, id, bubbles);
+        }
+
         #[cfg(all(feature = "hot-reload", debug_assertions))]
         #[cfg(all(feature = "hot-reload", debug_assertions))]
         return Box::pin(async {
         return Box::pin(async {
             let hot_reload_wait = self.hot_reload_rx.recv();
             let hot_reload_wait = self.hot_reload_rx.recv();

+ 5 - 0
packages/html/Cargo.toml

@@ -41,6 +41,11 @@ features = [
     "FocusEvent",
     "FocusEvent",
     "CompositionEvent",
     "CompositionEvent",
     "ClipboardEvent",
     "ClipboardEvent",
+    "Element",
+    "DomRect",
+    "ScrollIntoViewOptions",
+    "ScrollLogicalPosition",
+    "ScrollBehavior",
 ]
 ]
 
 
 [dev-dependencies]
 [dev-dependencies]

+ 3 - 0
packages/html/src/events.rs

@@ -33,6 +33,7 @@ mod form;
 mod image;
 mod image;
 mod keyboard;
 mod keyboard;
 mod media;
 mod media;
+mod mounted;
 mod mouse;
 mod mouse;
 mod pointer;
 mod pointer;
 mod scroll;
 mod scroll;
@@ -51,6 +52,7 @@ pub use form::*;
 pub use image::*;
 pub use image::*;
 pub use keyboard::*;
 pub use keyboard::*;
 pub use media::*;
 pub use media::*;
+pub use mounted::*;
 pub use mouse::*;
 pub use mouse::*;
 pub use pointer::*;
 pub use pointer::*;
 pub use scroll::*;
 pub use scroll::*;
@@ -144,6 +146,7 @@ pub fn event_bubbles(evt: &str) -> bool {
         "animationiteration" => true,
         "animationiteration" => true,
         "transitionend" => true,
         "transitionend" => true,
         "toggle" => true,
         "toggle" => true,
+        "mounted" => false,
         _ => true,
         _ => true,
     }
     }
 }
 }

+ 131 - 0
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<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 {}

+ 5 - 0
packages/html/src/transit.rs

@@ -113,6 +113,9 @@ fn fun_name(
         // Toggle
         // Toggle
         "toggle" => Toggle(de(data)?),
         "toggle" => Toggle(de(data)?),
 
 
+        // Mounted
+        "mounted" => Mounted,
+
         // ImageData => "load" | "error";
         // ImageData => "load" | "error";
         // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
         // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
         other => {
         other => {
@@ -151,6 +154,7 @@ pub enum EventData {
     Animation(AnimationData),
     Animation(AnimationData),
     Transition(TransitionData),
     Transition(TransitionData),
     Toggle(ToggleData),
     Toggle(ToggleData),
+    Mounted,
 }
 }
 
 
 impl EventData {
 impl EventData {
@@ -172,6 +176,7 @@ impl EventData {
             EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
             EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
             EventData::Transition(data) => Rc::new(data) as Rc<dyn Any>,
             EventData::Transition(data) => Rc::new(data) as Rc<dyn Any>,
             EventData::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
             EventData::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Mounted => Rc::new(MountedData::new(())) as Rc<dyn Any>,
         }
         }
     }
     }
 }
 }

+ 69 - 4
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::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
 use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
 use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
-use crate::DragData;
+use crate::{
+    DragData, MountedData, MountedError, MountedResult, RenderedElementBacking, ScrollBehavior,
+};
 use keyboard_types::{Code, Key, Modifiers};
 use keyboard_types::{Code, Key, Modifiers};
 use std::convert::TryInto;
 use std::convert::TryInto;
+use std::future::Future;
+use std::pin::Pin;
 use std::str::FromStr;
 use std::str::FromStr;
-use wasm_bindgen::JsCast;
+use wasm_bindgen::{JsCast, JsValue};
 use web_sys::{
 use web_sys::{
-    AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent,
-    TransitionEvent, WheelEvent,
+    AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent,
+    ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent,
 };
 };
 
 
 macro_rules! uncheck_convert {
 macro_rules! uncheck_convert {
@@ -193,3 +197,64 @@ impl From<&TransitionEvent> for TransitionData {
         }
         }
     }
     }
 }
 }
+
+impl From<&web_sys::Element> for MountedData {
+    fn from(e: &web_sys::Element) -> Self {
+        MountedData::new(e.clone())
+    }
+}
+
+impl RenderedElementBacking for web_sys::Element {
+    fn get_client_rect(
+        &self,
+    ) -> Pin<Box<dyn Future<Output = MountedResult<euclid::Rect<f64, f64>>>>> {
+        let rect = self.get_bounding_client_rect();
+        let result = Ok(euclid::Rect::new(
+            euclid::Point2D::new(rect.left(), rect.top()),
+            euclid::Size2D::new(rect.width(), rect.height()),
+        ));
+        Box::pin(async { result })
+    }
+
+    fn get_raw_element(&self) -> MountedResult<&dyn std::any::Any> {
+        Ok(self)
+    }
+
+    fn scroll_to(
+        &self,
+        behavior: ScrollBehavior,
+    ) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
+        match behavior {
+            ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options(
+                ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant),
+            ),
+            ScrollBehavior::Smooth => self.scroll_into_view_with_scroll_into_view_options(
+                ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Smooth),
+            ),
+        }
+
+        Box::pin(async { Ok(()) })
+    }
+
+    fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
+        let result = self
+            .dyn_ref::<web_sys::HtmlElement>()
+            .ok_or_else(|| MountedError::OperationFailed(Box::new(FocusError(self.into()))))
+            .and_then(|e| {
+                (if focus { e.focus() } else { e.blur() })
+                    .map_err(|err| MountedError::OperationFailed(Box::new(FocusError(err))))
+            });
+        Box::pin(async { result })
+    }
+}
+
+#[derive(Debug)]
+struct FocusError(JsValue);
+
+impl std::fmt::Display for FocusError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "failed to focus element {:?}", self.0)
+    }
+}
+
+impl std::error::Error for FocusError {}

+ 2 - 0
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"] }
 web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
 sledgehammer_bindgen = { version = "0.2.1", optional = true }
 sledgehammer_bindgen = { version = "0.2.1", optional = true }
 sledgehammer_utils = { version = "0.1.1", optional = true }
 sledgehammer_utils = { version = "0.1.1", optional = true }
+serde = { version = "1.0", features = ["derive"], optional = true }
 
 
 [features]
 [features]
 default = []
 default = []
+serialize = ["serde"]
 web = ["wasm-bindgen", "js-sys", "web-sys"]
 web = ["wasm-bindgen", "js-sys", "web-sys"]
 sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
 sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]

+ 56 - 3
packages/interpreter/src/interpreter.js

@@ -204,6 +204,45 @@ class Interpreter {
       node.removeAttribute(name);
       node.removeAttribute(name);
     }
     }
   }
   }
+
+  GetClientRect(id) {
+    const node = this.nodes[id];
+    if (!node) {
+      return;
+    }
+    const rect = node.getBoundingClientRect();
+    return {
+      type: "GetClientRect",
+      origin: [rect.x, rect.y],
+      size: [rect.width, rect.height],
+    };
+  }
+
+  ScrollTo(id, behavior) {
+    const node = this.nodes[id];
+    if (!node) {
+      return false;
+    }
+    node.scrollIntoView({
+      behavior: behavior,
+    });
+    return true;
+  }
+
+  /// Set the focus on the element
+  SetFocus(id, focus) {
+    const node = this.nodes[id];
+    if (!node) {
+      return false;
+    }
+    if (focus) {
+      node.focus();
+    } else {
+      node.blur();
+    }
+    return true;
+  }
+
   handleEdits(edits) {
   handleEdits(edits) {
     for (let template of edits.templates) {
     for (let template of edits.templates) {
       this.SaveTemplate(template);
       this.SaveTemplate(template);
@@ -346,9 +385,21 @@ class Interpreter {
       case "NewEventListener":
       case "NewEventListener":
         let bubbles = event_bubbles(edit.name);
         let bubbles = event_bubbles(edit.name);
 
 
-        this.NewEventListener(edit.name, edit.id, bubbles, (event) => {
-          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;
         break;
     }
     }
   }
   }
@@ -933,6 +984,8 @@ function event_bubbles(event) {
       return true;
       return true;
     case "toggle":
     case "toggle":
       return true;
       return true;
+    case "mounted":
+      return false;
   }
   }
 
 
   return true;
   return true;

+ 6 - 0
packages/interpreter/src/sledgehammer_bindings.rs

@@ -117,6 +117,9 @@ mod js {
     export function set_node(id, node) {
     export function set_node(id, node) {
         nodes[id] = node;
         nodes[id] = node;
     }
     }
+    export function get_node(id) {
+        return nodes[id];
+    }
     export function initilize(root, handler) {
     export function initilize(root, handler) {
         listeners.handler = handler;
         listeners.handler = handler;
         nodes = [root];
         nodes = [root];
@@ -167,6 +170,9 @@ mod js {
         #[wasm_bindgen]
         #[wasm_bindgen]
         pub fn set_node(id: u32, node: Node);
         pub fn set_node(id: u32, node: Node);
 
 
+        #[wasm_bindgen]
+        pub fn get_node(id: u32) -> Node;
+
         #[wasm_bindgen]
         #[wasm_bindgen]
         pub fn initilize(root: Node, handler: &Function);
         pub fn initilize(root: Node, handler: &Function);
     }
     }

+ 2 - 0
packages/liveview/Cargo.toml

@@ -14,6 +14,8 @@ license = "MIT/Apache-2.0"
 
 
 [dependencies]
 [dependencies]
 thiserror = "1.0.38"
 thiserror = "1.0.38"
+log = "0.4.14"
+slab = "0.4"
 futures-util = { version = "0.3.25", default-features = false, features = [
 futures-util = { version = "0.3.25", default-features = false, features = [
     "sink",
     "sink",
 ] }
 ] }

+ 125 - 0
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<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 {}

+ 2 - 0
packages/liveview/src/lib.rs

@@ -18,7 +18,9 @@ pub mod adapters {
 
 
 pub use adapters::*;
 pub use adapters::*;
 
 
+mod element;
 pub mod pool;
 pub mod pool;
+mod query;
 use futures_util::{SinkExt, StreamExt};
 use futures_util::{SinkExt, StreamExt};
 pub use pool::*;
 pub use pool::*;
 
 

+ 12 - 4
packages/liveview/src/main.js

@@ -26,11 +26,19 @@ class IPC {
       // todo: retry the connection
       // todo: retry the connection
     };
     };
 
 
-    ws.onmessage = (event) => {
+    ws.onmessage = (message) => {
       // Ignore pongs
       // Ignore pongs
-      if (event.data != "__pong__") {
-        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;
+        }
       }
       }
     };
     };
 
 

+ 64 - 12
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 futures_util::{pin_mut, SinkExt, StreamExt};
-use std::time::Duration;
+use serde::Serialize;
+use std::{rc::Rc, time::Duration};
 use tokio_util::task::LocalPoolHandle;
 use tokio_util::task::LocalPoolHandle;
 
 
 #[derive(Clone)]
 #[derive(Clone)]
@@ -115,7 +120,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
     };
     };
 
 
     // todo: use an efficient binary packed format for this
     // todo: use an efficient binary packed format for this
-    let edits = serde_json::to_string(&vdom.rebuild()).unwrap();
+    let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap();
 
 
     // pin the futures so we can use select!
     // pin the futures so we can use select!
     pin_mut!(ws);
     pin_mut!(ws);
@@ -123,11 +128,19 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
     // send the initial render to the client
     // send the initial render to the client
     ws.send(edits).await?;
     ws.send(edits).await?;
 
 
+    // Create the a proxy for query engine
+    let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
+    let query_engine = QueryEngine::default();
+
     // desktop uses this wrapper struct thing around the actual event itself
     // desktop uses this wrapper struct thing around the actual event itself
     // this is sorta driven by tao/wry
     // this is sorta driven by tao/wry
-    #[derive(serde::Deserialize)]
-    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 {
     loop {
@@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
                         ws.send("__pong__".to_string()).await?;
                         ws.send("__pong__".to_string()).await?;
                     }
                     }
                     Some(Ok(evt)) => {
                     Some(Ok(evt)) => {
-                        if let Ok(IpcMessage { params }) = serde_json::from_str::<IpcMessage>(evt) {
-                            vdom.handle_event(&params.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?
                     // log this I guess? when would we get an error here?
-                    Some(Err(_e)) => {},
+                    Some(Err(_e)) => {}
                     None => return Ok(()),
                     None => return Ok(()),
                 }
                 }
             }
             }
 
 
+            // handle any new queries
+            Some(query) = query_rx.recv() => {
+                ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?;
+            }
+
             Some(msg) = hot_reload_wait => {
             Some(msg) = hot_reload_wait => {
                 #[cfg(all(feature = "hot-reload", debug_assertions))]
                 #[cfg(all(feature = "hot-reload", debug_assertions))]
                 match msg{
                 match msg{
@@ -176,6 +218,16 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
             .render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
             .render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
             .await;
             .await;
 
 
-        ws.send(serde_json::to_string(&edits).unwrap()).await?;
+        ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap())
+            .await?;
     }
     }
 }
 }
+
+#[derive(Serialize)]
+#[serde(tag = "type", content = "data")]
+enum ClientUpdate<'a> {
+    #[serde(rename = "edits")]
+    Edits(Mutations<'a>),
+    #[serde(rename = "query")]
+    Query(String),
+}

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

+ 1 - 9
packages/rink/src/query.rs

@@ -91,15 +91,7 @@ impl<'a> ElementRef<'a> {
     pub fn layout(&self) -> Option<Layout> {
     pub fn layout(&self) -> Option<Layout> {
         let layout = self
         let layout = self
             .stretch
             .stretch
-            .layout(
-                self.inner
-                    .get(self.id)
-                    .unwrap()
-                    .get::<TaffyLayout>()
-                    .unwrap()
-                    .node
-                    .ok()?,
-            )
+            .layout(self.inner.get(self.id)?.get::<TaffyLayout>()?.node.ok()?)
             .ok();
             .ok();
         layout.map(|layout| Layout {
         layout.map(|layout| Layout {
             order: layout.order,
             order: layout.order,

+ 41 - 12
packages/web/src/dom.rs

@@ -10,8 +10,8 @@
 use dioxus_core::{
 use dioxus_core::{
     BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
     BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
 };
 };
-use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData};
-use dioxus_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 futures_channel::mpsc;
 use js_sys::Array;
 use js_sys::Array;
 use rustc_hash::FxHashMap;
 use rustc_hash::FxHashMap;
@@ -28,6 +28,7 @@ pub struct WebsysDom {
     templates: FxHashMap<String, u32>,
     templates: FxHashMap<String, u32>,
     max_template_id: u32,
     max_template_id: u32,
     pub(crate) interpreter: Channel,
     pub(crate) interpreter: Channel,
+    event_channel: mpsc::UnboundedSender<UiEvent>,
 }
 }
 
 
 pub struct UiEvent {
 pub struct UiEvent {
@@ -35,7 +36,6 @@ pub struct UiEvent {
     pub bubbles: bool,
     pub bubbles: bool,
     pub element: ElementId,
     pub element: ElementId,
     pub data: Rc<dyn Any>,
     pub data: Rc<dyn Any>,
-    pub event: Event,
 }
 }
 
 
 impl WebsysDom {
 impl WebsysDom {
@@ -49,8 +49,9 @@ impl WebsysDom {
         };
         };
         let interpreter = Channel::default();
         let interpreter = Channel::default();
 
 
-        let handler: Closure<dyn FnMut(&Event)> =
-            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 name = event.type_();
                 let element = walk_event_for_id(event);
                 let element = walk_event_for_id(event);
                 let bubbles = dioxus_html::event_bubbles(name.as_str());
                 let bubbles = dioxus_html::event_bubbles(name.as_str());
@@ -74,10 +75,10 @@ impl WebsysDom {
                         bubbles,
                         bubbles,
                         element,
                         element,
                         data,
                         data,
-                        event: event.clone(),
                     });
                     });
                 }
                 }
-            }));
+            }
+        }));
 
 
         dioxus_interpreter_js::initilize(
         dioxus_interpreter_js::initilize(
             root.clone().unchecked_into(),
             root.clone().unchecked_into(),
@@ -90,6 +91,7 @@ impl WebsysDom {
             interpreter,
             interpreter,
             templates: FxHashMap::default(),
             templates: FxHashMap::default(),
             max_template_id: 0,
             max_template_id: 0,
+            event_channel,
         }
         }
     }
     }
 
 
@@ -161,6 +163,8 @@ impl WebsysDom {
     pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
     pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
         use Mutation::*;
         use Mutation::*;
         let i = &mut self.interpreter;
         let i = &mut self.interpreter;
+        // we need to apply the mount events last, so we collect them here
+        let mut to_mount = Vec::new();
         for edit in &edits {
         for edit in &edits {
             match edit {
             match edit {
                 AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
                 AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
@@ -211,18 +215,43 @@ impl WebsysDom {
                 },
                 },
                 SetText { value, id } => i.set_text(id.0 as u32, value),
                 SetText { value, id } => i.set_text(id.0 as u32, value),
                 NewEventListener { name, id, .. } => {
                 NewEventListener { name, id, .. } => {
-                    console::log_1(&format!("new event listener: {}", name).into());
-                    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),
                 Remove { id } => i.remove(id.0 as u32),
                 PushRoot { id } => i.push_root(id.0 as u32),
                 PushRoot { id } => i.push_root(id.0 as u32),
             }
             }
         }
         }
         edits.clear();
         edits.clear();
         i.flush();
         i.flush();
+
+        for id in to_mount {
+            let node = get_node(id.0 as u32);
+            if let Some(element) = node.dyn_ref::<Element>() {
+                log::info!("mounted event fired: {}", id.0);
+                let data: MountedData = element.into();
+                let data = Rc::new(data);
+                let _ = self.event_channel.unbounded_send(UiEvent {
+                    name: "mounted".to_string(),
+                    bubbles: false,
+                    element: id,
+                    data,
+                });
+            }
+        }
     }
     }
 }
 }