diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 8b41ad951..8c309ccec 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -280,9 +280,6 @@ impl VirtualDom { /// Whenever the VirtualDom "works", it will re-render this scope pub fn mark_dirty(&mut self, id: ScopeId) { let height = self.scopes[id.0].height; - - println!("marking scope {} dirty with height {}", id.0, height); - self.dirty_scopes.insert(DirtyScope { height, id }); } diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index 498feaeae..72ce93f1a 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -27,6 +27,7 @@ pub struct HtmlEvent { pub element: ElementId, pub name: String, pub data: EventData, + pub bubbles: bool, } impl HtmlEvent { @@ -85,6 +86,7 @@ fn test_back_and_forth() { element: ElementId(0), data: EventData::Mouse(MouseData::default()), name: "click".to_string(), + bubbles: true, }; println!("{}", serde_json::to_string_pretty(&data).unwrap()); diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 7e5395809..ada1bed64 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -345,7 +345,10 @@ class Interpreter { break; case "NewEventListener": // this handler is only provided on desktop implementations since this - // method is not used by the web implementation + // method is not used by the web implementationa + + let bubbles = event_bubbles(edit.name); + let handler = (event) => { let target = event.target; if (target != null) { @@ -430,11 +433,12 @@ class Interpreter { name: edit.name, element: parseInt(realId), data: contents, + bubbles: bubbles, }) ); } }; - this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler); + this.NewEventListener(edit.name, edit.id, bubbles, handler); break; } } diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index 5c72d2010..65f5c7193 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -45,7 +45,9 @@ async fn main() { .and(warp::any().map(move || view.clone())) .map(move |ws: Ws, view: LiveView| { println!("Got a connection!"); - ws.on_upgrade(|ws| view.upgrade_warp(ws, app)) + ws.on_upgrade(|ws| async move { + let _ = view.upgrade_warp(ws, app).await; + }) }); println!("Listening on http://{}", addr); diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index 2b9870959..f42e2f9a2 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -1,12 +1,14 @@ -use crate::{LiveView, LiveViewError}; +use crate::{liveview_eventloop, LiveView, LiveViewError}; use dioxus_core::prelude::*; -use dioxus_html::HtmlEvent; use futures_util::{SinkExt, StreamExt}; -use std::time::Duration; use warp::ws::{Message, WebSocket}; impl LiveView { - pub async fn upgrade_warp(self, ws: WebSocket, app: fn(Scope<()>) -> Element) { + pub async fn upgrade_warp( + self, + ws: WebSocket, + app: fn(Scope<()>) -> Element, + ) -> Result<(), LiveViewError> { self.upgrade_warp_with_props(ws, app, ()).await } @@ -15,60 +17,37 @@ impl LiveView { ws: WebSocket, app: fn(Scope) -> Element, props: T, - ) { - self.pool - .spawn_pinned(move || liveview_eventloop(app, props, ws)) - .await; - } -} + ) -> Result<(), LiveViewError> { + let (ws_tx, ws_rx) = ws.split(); -async fn liveview_eventloop( - app: Component, - props: T, - mut ws: WebSocket, -) -> Result<(), LiveViewError> -where - T: Send + 'static, -{ - let mut vdom = VirtualDom::new_with_props(app, props); - let edits = serde_json::to_string(&vdom.rebuild()).unwrap(); - ws.send(Message::text(edits)).await.unwrap(); + let ws_tx = ws_tx + .with(transform_warp) + .sink_map_err(|_| LiveViewError::SendingFailed); - loop { - tokio::select! { - // poll any futures or suspense - _ = vdom.wait_for_work() => {} + let ws_rx = ws_rx.map(transform_warp_rx); - evt = ws.next() => { - match evt { - Some(Ok(evt)) => { - if let Ok(evt) = evt.to_str() { - // desktop uses this wrapper struct thing - #[derive(serde::Deserialize)] - struct IpcMessage { - params: HtmlEvent - } - - let event: IpcMessage = serde_json::from_str(evt).unwrap(); - let event = event.params; - let bubbles = event.bubbles(); - vdom.handle_event(&event.name, event.data.into_any(), event.element, bubbles); - } - } - Some(Err(_e)) => { - // log this I guess? - // when would we get an error here? - } - None => return Ok(()), - } - } + match self + .pool + .spawn_pinned(move || liveview_eventloop(app, props, ws_tx, ws_rx)) + .await + { + Ok(Ok(_)) => Ok(()), + Ok(Err(e)) => Err(e), + Err(_) => Err(LiveViewError::SendingFailed), } - - let edits = vdom - .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) - .await; - - ws.send(Message::text(serde_json::to_string(&edits).unwrap())) - .await?; } } + +fn transform_warp_rx(f: Result) -> Result { + // destructure the message into the buffer we got from warp + let msg = f.map_err(|_| LiveViewError::SendingFailed)?.into_bytes(); + + // transform it back into a string, saving us the allocation + let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?; + + Ok(msg) +} + +async fn transform_warp(message: String) -> Result { + Ok(Message::text(message)) +} diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 5d111e319..79b9eb06d 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -1,3 +1,30 @@ +pub mod adapters { + #[cfg(feature = "warp")] + pub mod warp_adapter; + + #[cfg(feature = "axum")] + pub mod axum_adapter; + + #[cfg(feature = "salvo")] + pub mod salvo_adapter; +} + +pub mod pool; +use futures_util::{SinkExt, StreamExt}; +pub use pool::*; + +pub trait WebsocketTx: SinkExt {} +impl WebsocketTx for T where T: SinkExt {} + +pub trait WebsocketRx: StreamExt> {} +impl WebsocketRx for T where T: StreamExt> {} + +#[derive(Debug, thiserror::Error)] +pub enum LiveViewError { + #[error("warp error")] + SendingFailed, +} + use dioxus_interpreter_js::INTERPRETER_JS; static MAIN_JS: &str = include_str!("./main.js"); @@ -13,23 +40,3 @@ pub fn interpreter_glue(url: &str) -> String { "# ) } - -pub mod adapters { - #[cfg(feature = "warp")] - pub mod warp_adapter; - - #[cfg(feature = "axum")] - pub mod axum_adapter; - - #[cfg(feature = "salvo")] - pub mod salvo_adapter; -} - -pub mod pool; -pub use pool::*; - -#[derive(Debug, thiserror::Error)] -pub enum LiveViewError { - #[error("Connection Failed")] - Warp(#[from] warp::Error), -} diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 9b0cf5c38..e37ac7b67 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -4,7 +4,7 @@ function main() { if (root != null) { // create a new ipc window.ipc = new IPC(root); - window.ipc.send(serializeIpcMessage("initialize")); + window.ipc.postMessage(serializeIpcMessage("initialize")); } } @@ -24,6 +24,7 @@ class IPC { }; this.ws.onmessage = (event) => { + console.log("Received message: ", event.data); let edits = JSON.parse(event.data); window.interpreter.handleEdits(edits); }; diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index 05d7fe98e..124843220 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -1,3 +1,8 @@ +use crate::LiveViewError; +use dioxus_core::prelude::*; +use dioxus_html::HtmlEvent; +use futures_util::{pin_mut, SinkExt, StreamExt}; +use std::time::Duration; use tokio_util::task::LocalPoolHandle; #[derive(Clone)] @@ -18,3 +23,66 @@ impl LiveView { } } } + +/// The primary event loop for the VirtualDom waiting for user input +/// +/// This function makes it easy to integrate Dioxus LiveView with any socket-based framework. +/// +/// As long as your framework can provide a Sink and Stream of Strings, you can use this function. +/// +/// You might need to transform the error types of the web backend into the LiveView error type. +pub async fn liveview_eventloop( + app: Component, + props: T, + ws_tx: impl SinkExt, + ws_rx: impl StreamExt>, +) -> Result<(), LiveViewError> +where + T: Send + 'static, +{ + let mut vdom = VirtualDom::new_with_props(app, props); + + // todo: use an efficient binary packed format for this + let edits = serde_json::to_string(&vdom.rebuild()).unwrap(); + + // pin the futures so we can use select! + pin_mut!(ws_tx); + pin_mut!(ws_rx); + + ws_tx.send(edits).await?; + + // 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, + } + + loop { + tokio::select! { + // poll any futures or suspense + _ = vdom.wait_for_work() => {} + + evt = ws_rx.next() => { + match evt { + Some(Ok(evt)) => { + let event: IpcMessage = serde_json::from_str(&evt).unwrap(); + let event = event.params; + vdom.handle_event(&event.name, event.data.into_any(), event.element, event.bubbles); + } + Some(Err(_e)) => { + // log this I guess? + // when would we get an error here? + } + None => return Ok(()), + } + } + } + + let edits = vdom + .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) + .await; + + ws_tx.send(serde_json::to_string(&edits).unwrap()).await?; + } +}