diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml
index 90f956289..ee75f71de 100644
--- a/packages/liveview/Cargo.toml
+++ b/packages/liveview/Cargo.toml
@@ -10,7 +10,6 @@ description = "Build server-side apps with Dioxus"
license = "MIT/Apache-2.0"
-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
@@ -28,7 +27,7 @@ tokio-util = { version = "0.7.0", features = ["full"] }
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" }
-
+dioxus-interpreter-js = { path = "../interpreter" }
# warp
warp = { version = "0.3", optional = true }
@@ -39,6 +38,9 @@ tower = { version = "0.4.12", optional = true }
# salvo
salvo = { version = "0.32.0", optional = true, features = ["ws"] }
+thiserror = "1.0.37"
+uuid = { version = "1.2.2", features = ["v4"] }
+anyhow = "1.0.66"
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
@@ -49,4 +51,16 @@ salvo = { version = "0.32.0", features = ["affix", "ws"] }
tower = "0.4.12"
[features]
-default = []
\ No newline at end of file
+default = ["salvo"]
+
+[[example]]
+name = "axum"
+required-features = ["axum"]
+
+[[example]]
+name = "salvo"
+required-features = ["salvo"]
+
+[[example]]
+name = "warp"
+required-features = ["warp"]
diff --git a/packages/liveview/examples/axum.rs b/packages/liveview/examples/axum.rs
index 450af30ef..410c0e9a4 100644
--- a/packages/liveview/examples/axum.rs
+++ b/packages/liveview/examples/axum.rs
@@ -1,32 +1,53 @@
-#[cfg(not(feature = "axum"))]
-fn main() {}
+use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
+use dioxus::prelude::*;
+
+fn app(cx: Scope) -> Element {
+ let mut num = use_state(cx, || 0);
+
+ cx.render(rsx! {
+ div {
+ "hello axum! {num}"
+ button { onclick: move |_| num += 1, "Increment" }
+ }
+ })
+}
-#[cfg(feature = "axum")]
#[tokio::main]
async fn main() {
- use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
- use dioxus_core::{Element, LazyNodes, Scope};
pretty_env_logger::init();
- fn app(cx: Scope) -> Element {
- cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
- }
-
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
- let view = dioxus_liveview::new(addr);
- let body = view.body("
Dioxus Liveview");
+ let view = dioxus_liveview::LiveViewPool::new();
let app = Router::new()
- .route("/", get(move || async { Html(body) }))
.route(
- "/app",
+ "/",
+ get(move || async move {
+ Html(format!(
+ r#"
+
+
+ Dioxus LiveView with Warp
+
+ {glue}
+
+ "#,
+ glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
+ ))
+ }),
+ )
+ .route(
+ "/ws",
get(move |ws: WebSocketUpgrade| async move {
ws.on_upgrade(move |socket| async move {
- view.upgrade_axum(socket, app).await;
+ _ = view.launch(dioxus_liveview::axum_socket(socket), app).await;
})
}),
);
+
+ println!("Listening on http://{}", addr);
+
axum::Server::bind(&addr.to_string().parse().unwrap())
.serve(app.into_make_service())
.await
diff --git a/packages/liveview/examples/salvo.rs b/packages/liveview/examples/salvo.rs
index 132a62633..cfe7a7ffd 100644
--- a/packages/liveview/examples/salvo.rs
+++ b/packages/liveview/examples/salvo.rs
@@ -1,55 +1,70 @@
-#[cfg(not(feature = "salvo"))]
-fn main() {}
+use dioxus::prelude::*;
+use dioxus_liveview::LiveViewPool;
+use salvo::extra::affix;
+use salvo::extra::ws::WsHandler;
+use salvo::prelude::*;
+use std::net::SocketAddr;
+use std::sync::Arc;
+
+fn app(cx: Scope) -> Element {
+ let mut num = use_state(cx, || 0);
+
+ cx.render(rsx! {
+ div {
+ "hello salvo! {num}"
+ button { onclick: move |_| num += 1, "Increment" }
+ }
+ })
+}
-#[cfg(feature = "salvo")]
#[tokio::main]
async fn main() {
- use std::sync::Arc;
-
- use dioxus_core::{Element, LazyNodes, Scope};
- use dioxus_liveview as liveview;
- use dioxus_liveview::Liveview;
- use salvo::extra::affix;
- use salvo::extra::ws::WsHandler;
- use salvo::prelude::*;
-
- fn app(cx: Scope) -> Element {
- cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
- }
-
pretty_env_logger::init();
- let addr = ([127, 0, 0, 1], 3030);
+ let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
+
+ let view = LiveViewPool::new();
- // todo: compactify this routing under one liveview::app method
- let view = liveview::new(addr);
let router = Router::new()
.hoop(affix::inject(Arc::new(view)))
.get(index)
- .push(Router::with_path("app").get(connect));
+ .push(Router::with_path("ws").get(connect));
+
+ println!("Listening on http://{}", addr);
+
Server::new(TcpListener::bind(addr)).serve(router).await;
+}
- #[handler]
- fn index(depot: &mut Depot, res: &mut Response) {
- let view = depot.obtain::>().unwrap();
- let body = view.body("Dioxus LiveView");
- res.render(Text::Html(body));
- }
+#[handler]
+fn index(_depot: &mut Depot, res: &mut Response) {
+ let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
+ res.render(Text::Html(format!(
+ r#"
+
+
+ Dioxus LiveView with Warp
+
+ {glue}
+
+ "#,
+ glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
+ )));
+}
- #[handler]
- async fn connect(
- req: &mut Request,
- depot: &mut Depot,
- res: &mut Response,
- ) -> Result<(), StatusError> {
- let view = depot.obtain::>().unwrap().clone();
- let fut = WsHandler::new().handle(req, res)?;
- let fut = async move {
- if let Some(ws) = fut.await {
- view.upgrade_salvo(ws, app).await;
- }
- };
- tokio::task::spawn(fut);
- Ok(())
- }
+#[handler]
+async fn connect(
+ req: &mut Request,
+ depot: &mut Depot,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ let view = depot.obtain::>().unwrap().clone();
+ let fut = WsHandler::new().handle(req, res)?;
+
+ tokio::spawn(async move {
+ if let Some(ws) = fut.await {
+ _ = view.launch(dioxus_liveview::salvo_socket(ws), app).await;
+ }
+ });
+
+ Ok(())
}
diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs
index 7b2ddfef5..7e759ea90 100644
--- a/packages/liveview/examples/warp.rs
+++ b/packages/liveview/examples/warp.rs
@@ -1,35 +1,56 @@
-#[cfg(not(feature = "warp"))]
-fn main() {}
+use dioxus::prelude::*;
+use dioxus_liveview::adapters::warp_adapter::warp_socket;
+use dioxus_liveview::LiveViewPool;
+use std::net::SocketAddr;
+use warp::ws::Ws;
+use warp::Filter;
+
+fn app(cx: Scope) -> Element {
+ let mut num = use_state(cx, || 0);
+
+ cx.render(rsx! {
+ div {
+ "hello warp! {num}"
+ button {
+ onclick: move |_| num += 1,
+ "Increment"
+ }
+ }
+ })
+}
-#[cfg(feature = "warp")]
#[tokio::main]
async fn main() {
- use dioxus_core::{Element, LazyNodes, Scope};
- use dioxus_liveview as liveview;
- use warp::ws::Ws;
- use warp::Filter;
-
- fn app(cx: Scope) -> Element {
- cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
- }
-
pretty_env_logger::init();
- let addr = ([127, 0, 0, 1], 3030);
+ let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
- // todo: compactify this routing under one liveview::app method
- let view = liveview::new(addr);
- let body = view.body("Dioxus LiveView");
+ let index = warp::path::end().map(move || {
+ warp::reply::html(format!(
+ r#"
+
+
+ Dioxus LiveView with Warp
+
+ {glue}
+
+ "#,
+ glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws/"))
+ ))
+ });
- let routes = warp::path::end()
- .map(move || warp::reply::html(body.clone()))
- .or(warp::path("app")
- .and(warp::ws())
- .and(warp::any().map(move || view.clone()))
- .map(|ws: Ws, view: liveview::Liveview| {
- ws.on_upgrade(|socket| async move {
- view.upgrade_warp(socket, app).await;
- })
- }));
- warp::serve(routes).run(addr).await;
+ let pool = LiveViewPool::new();
+
+ let ws = warp::path("ws")
+ .and(warp::ws())
+ .and(warp::any().map(move || pool.clone()))
+ .map(move |ws: Ws, pool: LiveViewPool| {
+ ws.on_upgrade(|ws| async move {
+ let _ = pool.launch(warp_socket(ws), app).await;
+ })
+ });
+
+ println!("Listening on http://{}", addr);
+
+ warp::serve(index.or(ws)).run(addr).await;
}
diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs
index c60b267ea..646cde38e 100644
--- a/packages/liveview/src/adapters/axum_adapter.rs
+++ b/packages/liveview/src/adapters/axum_adapter.rs
@@ -1,94 +1,23 @@
-use crate::events;
+use crate::{LiveViewError, LiveViewSocket};
use axum::extract::ws::{Message, WebSocket};
-use dioxus_core::prelude::*;
-use futures_util::{
- future::{select, Either},
- pin_mut, SinkExt, StreamExt,
-};
-use tokio::sync::mpsc;
-use tokio_stream::wrappers::UnboundedReceiverStream;
-use tokio_util::task::LocalPoolHandle;
+use futures_util::{SinkExt, StreamExt};
-impl crate::Liveview {
- pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) {
- connect(ws, self.pool.clone(), app, ()).await;
- }
-
- pub async fn upgrade_axum_with_props(
- &self,
- ws: WebSocket,
- app: fn(Scope) -> Element,
- props: T,
- ) where
- T: Send + Sync + 'static,
- {
- connect(ws, self.pool.clone(), app, props).await;
- }
+/// Convert a warp websocket into a LiveViewSocket
+///
+/// This is required to launch a LiveView app using the warp web framework
+pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket {
+ ws.map(transform_rx)
+ .with(transform_tx)
+ .sink_map_err(|_| LiveViewError::SendingFailed)
}
-pub async fn connect(
- socket: WebSocket,
- pool: LocalPoolHandle,
- app: fn(Scope) -> Element,
- props: T,
-) where
- T: Send + Sync + 'static,
-{
- let (mut user_ws_tx, mut user_ws_rx) = socket.split();
- let (event_tx, event_rx) = mpsc::unbounded_channel();
- let (edits_tx, edits_rx) = mpsc::unbounded_channel();
- let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
- let mut event_rx = UnboundedReceiverStream::new(event_rx);
- let vdom_fut = pool.clone().spawn_pinned(move || async move {
- let mut vdom = VirtualDom::new_with_props(app, props);
- let edits = vdom.rebuild();
- let serialized = serde_json::to_string(&edits.edits).unwrap();
- edits_tx.send(serialized).unwrap();
- loop {
- let new_event = {
- let vdom_fut = vdom.wait_for_work();
- pin_mut!(vdom_fut);
- match select(event_rx.next(), vdom_fut).await {
- Either::Left((l, _)) => l,
- Either::Right((_, _)) => None,
- }
- };
- if let Some(new_event) = new_event {
- vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event));
- } else {
- let mutations = vdom.work_with_deadline(|| false);
- for mutation in mutations {
- let edits = serde_json::to_string(&mutation.edits).unwrap();
- edits_tx.send(edits).unwrap();
- }
- }
- }
- });
- loop {
- match select(user_ws_rx.next(), edits_rx.next()).await {
- Either::Left((l, _)) => {
- if let Some(Ok(msg)) = l {
- if let Ok(Some(msg)) = msg.to_text().map(events::parse_ipc_message) {
- let user_event = events::trigger_from_serialized(msg.params);
- event_tx.send(user_event).unwrap();
- } else {
- break;
- }
- } else {
- break;
- }
- }
- Either::Right((edits, _)) => {
- if let Some(edits) = edits {
- // send the edits to the client
- if user_ws_tx.send(Message::Text(edits)).await.is_err() {
- break;
- }
- } else {
- break;
- }
- }
- }
- }
- vdom_fut.abort();
+fn transform_rx(message: Result) -> Result {
+ message
+ .map_err(|_| LiveViewError::SendingFailed)?
+ .into_text()
+ .map_err(|_| LiveViewError::SendingFailed)
+}
+
+async fn transform_tx(message: String) -> Result {
+ Ok(Message::Text(message))
}
diff --git a/packages/liveview/src/adapters/salvo_adapter.rs b/packages/liveview/src/adapters/salvo_adapter.rs
index 8a28784b8..19cb528c7 100644
--- a/packages/liveview/src/adapters/salvo_adapter.rs
+++ b/packages/liveview/src/adapters/salvo_adapter.rs
@@ -1,110 +1,25 @@
-use crate::events;
-use dioxus_core::prelude::*;
-use futures_util::{pin_mut, SinkExt, StreamExt};
+use futures_util::{SinkExt, StreamExt};
use salvo::extra::ws::{Message, WebSocket};
-use tokio::sync::mpsc;
-use tokio_stream::wrappers::UnboundedReceiverStream;
-use tokio_util::task::LocalPoolHandle;
-impl crate::Liveview {
- pub async fn upgrade_salvo(&self, ws: salvo::extra::ws::WebSocket, app: fn(Scope) -> Element) {
- connect(ws, self.pool.clone(), app, ()).await;
- }
- pub async fn upgrade_salvo_with_props(
- &self,
- ws: salvo::extra::ws::WebSocket,
- app: fn(Scope) -> Element,
- props: T,
- ) where
- T: Send + Sync + 'static,
- {
- connect(ws, self.pool.clone(), app, props).await;
- }
+use crate::{LiveViewError, LiveViewSocket};
+
+/// Convert a salvo websocket into a LiveViewSocket
+///
+/// This is required to launch a LiveView app using the warp web framework
+pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket {
+ ws.map(transform_rx)
+ .with(transform_tx)
+ .sink_map_err(|_| LiveViewError::SendingFailed)
}
-pub async fn connect(
- ws: WebSocket,
- pool: LocalPoolHandle,
- app: fn(Scope) -> Element,
- props: T,
-) where
- T: Send + Sync + 'static,
-{
- // Use a counter to assign a new unique ID for this user.
+fn transform_rx(message: Result) -> Result {
+ let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?;
- // Split the socket into a sender and receive of messages.
- let (mut user_ws_tx, mut user_ws_rx) = ws.split();
+ let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?;
- let (event_tx, event_rx) = mpsc::unbounded_channel();
- let (edits_tx, edits_rx) = mpsc::unbounded_channel();
+ Ok(msg)
+}
- let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
- let mut event_rx = UnboundedReceiverStream::new(event_rx);
-
- let vdom_fut = pool.spawn_pinned(move || async move {
- let mut vdom = VirtualDom::new_with_props(app, props);
-
- let edits = vdom.rebuild();
-
- let serialized = serde_json::to_string(&edits.edits).unwrap();
- edits_tx.send(serialized).unwrap();
-
- loop {
- use futures_util::future::{select, Either};
-
- let new_event = {
- let vdom_fut = vdom.wait_for_work();
-
- pin_mut!(vdom_fut);
-
- match select(event_rx.next(), vdom_fut).await {
- Either::Left((l, _)) => l,
- Either::Right((_, _)) => None,
- }
- };
-
- if let Some(new_event) = new_event {
- vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event));
- } else {
- let mutations = vdom.work_with_deadline(|| false);
- for mutation in mutations {
- let edits = serde_json::to_string(&mutation.edits).unwrap();
- edits_tx.send(edits).unwrap();
- }
- }
- }
- });
-
- loop {
- use futures_util::future::{select, Either};
-
- match select(user_ws_rx.next(), edits_rx.next()).await {
- Either::Left((l, _)) => {
- if let Some(Ok(msg)) = l {
- if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) {
- if msg.method == "user_event" {
- let user_event = events::trigger_from_serialized(msg.params);
- event_tx.send(user_event).unwrap();
- }
- } else {
- break;
- }
- } else {
- break;
- }
- }
- Either::Right((edits, _)) => {
- if let Some(edits) = edits {
- // send the edits to the client
- if user_ws_tx.send(Message::text(edits)).await.is_err() {
- break;
- }
- } else {
- break;
- }
- }
- }
- }
-
- vdom_fut.abort();
+async fn transform_tx(message: String) -> Result {
+ Ok(Message::text(message))
}
diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs
index 398c7cdd9..e5c821ce3 100644
--- a/packages/liveview/src/adapters/warp_adapter.rs
+++ b/packages/liveview/src/adapters/warp_adapter.rs
@@ -1,110 +1,28 @@
-use crate::events;
-use dioxus_core::prelude::*;
-use futures_util::{pin_mut, SinkExt, StreamExt};
-use tokio::sync::mpsc;
-use tokio_stream::wrappers::UnboundedReceiverStream;
-use tokio_util::task::LocalPoolHandle;
+use crate::{LiveViewError, LiveViewSocket};
+use futures_util::{SinkExt, StreamExt};
use warp::ws::{Message, WebSocket};
-impl crate::Liveview {
- pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) {
- connect(ws, self.pool.clone(), app, ()).await;
- }
- pub async fn upgrade_warp_with_props(
- &self,
- ws: warp::ws::WebSocket,
- app: fn(Scope) -> Element,
- props: T,
- ) where
- T: Send + Sync + 'static,
- {
- connect(ws, self.pool.clone(), app, props).await;
- }
+/// Convert a warp websocket into a LiveViewSocket
+///
+/// This is required to launch a LiveView app using the warp web framework
+pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket {
+ ws.map(transform_rx)
+ .with(transform_tx)
+ .sink_map_err(|_| LiveViewError::SendingFailed)
}
-pub async fn connect(
- ws: WebSocket,
- pool: LocalPoolHandle,
- app: fn(Scope) -> Element,
- props: T,
-) where
- T: Send + Sync + 'static,
-{
- // Use a counter to assign a new unique ID for this user.
+fn transform_rx(message: Result) -> Result {
+ // destructure the message into the buffer we got from warp
+ let msg = message
+ .map_err(|_| LiveViewError::SendingFailed)?
+ .into_bytes();
- // Split the socket into a sender and receive of messages.
- let (mut user_ws_tx, mut user_ws_rx) = ws.split();
+ // transform it back into a string, saving us the allocation
+ let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?;
- let (event_tx, event_rx) = mpsc::unbounded_channel();
- let (edits_tx, edits_rx) = mpsc::unbounded_channel();
+ Ok(msg)
+}
- let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
- let mut event_rx = UnboundedReceiverStream::new(event_rx);
-
- let vdom_fut = pool.spawn_pinned(move || async move {
- let mut vdom = VirtualDom::new_with_props(app, props);
-
- let edits = vdom.rebuild();
-
- let serialized = serde_json::to_string(&edits.edits).unwrap();
- edits_tx.send(serialized).unwrap();
-
- loop {
- use futures_util::future::{select, Either};
-
- let new_event = {
- let vdom_fut = vdom.wait_for_work();
-
- pin_mut!(vdom_fut);
-
- match select(event_rx.next(), vdom_fut).await {
- Either::Left((l, _)) => l,
- Either::Right((_, _)) => None,
- }
- };
-
- if let Some(new_event) = new_event {
- vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event));
- } else {
- let mutations = vdom.work_with_deadline(|| false);
- for mutation in mutations {
- let edits = serde_json::to_string(&mutation.edits).unwrap();
- edits_tx.send(edits).unwrap();
- }
- }
- }
- });
-
- loop {
- use futures_util::future::{select, Either};
-
- match select(user_ws_rx.next(), edits_rx.next()).await {
- Either::Left((l, _)) => {
- if let Some(Ok(msg)) = l {
- if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) {
- if msg.method == "user_event" {
- let user_event = events::trigger_from_serialized(msg.params);
- event_tx.send(user_event).unwrap();
- }
- } else {
- break;
- }
- } else {
- break;
- }
- }
- Either::Right((edits, _)) => {
- if let Some(edits) = edits {
- // send the edits to the client
- if user_ws_tx.send(Message::text(edits)).await.is_err() {
- break;
- }
- } else {
- break;
- }
- }
- }
- }
-
- vdom_fut.abort();
+async fn transform_tx(message: String) -> Result {
+ Ok(Message::text(message))
}
diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs
deleted file mode 100644
index 2c5f97cc7..000000000
--- a/packages/liveview/src/events.rs
+++ /dev/null
@@ -1,207 +0,0 @@
-#![allow(dead_code)]
-
-//! Convert a serialized event to an event trigger
-
-use std::any::Any;
-use std::sync::Arc;
-
-use dioxus_core::ElementId;
-// use dioxus_html::event_bubbles;
-use dioxus_html::events::*;
-
-#[derive(serde::Serialize, serde::Deserialize)]
-pub(crate) struct IpcMessage {
- pub method: String,
- pub params: serde_json::Value,
-}
-
-pub(crate) fn parse_ipc_message(payload: &str) -> Option {
- match serde_json::from_str(payload) {
- Ok(message) => Some(message),
- Err(_) => None,
- }
-}
-
-#[derive(serde::Serialize, serde::Deserialize)]
-struct ImEvent {
- event: String,
- mounted_dom_id: ElementId,
- contents: serde_json::Value,
-}
-
-pub fn trigger_from_serialized(_val: serde_json::Value) {
- todo!()
- // let ImEvent {
- // event,
- // mounted_dom_id,
- // contents,
- // } = serde_json::from_value(val).unwrap();
-
- // let mounted_dom_id = Some(mounted_dom_id);
-
- // let name = event_name_from_type(&event);
- // let event = make_synthetic_event(&event, contents);
-
- // UserEvent {
- // name,
- // scope_id: None,
- // element: mounted_dom_id,
- // data: event,
- // bubbles: event_bubbles(name),
- // }
-}
-
-fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc {
- match name {
- "copy" | "cut" | "paste" => {
- //
- Arc::new(ClipboardData {})
- }
- "compositionend" | "compositionstart" | "compositionupdate" => {
- Arc::new(serde_json::from_value::(val).unwrap())
- }
- "keydown" | "keypress" | "keyup" => {
- let evt = serde_json::from_value::(val).unwrap();
- Arc::new(evt)
- }
- "focus" | "blur" | "focusout" | "focusin" => {
- //
- Arc::new(FocusData {})
- }
-
- // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
- // don't have a good solution with the serialized event problem
- "change" | "input" | "invalid" | "reset" | "submit" => {
- Arc::new(serde_json::from_value::(val).unwrap())
- }
-
- "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
- | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
- | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
- Arc::new(serde_json::from_value::(val).unwrap())
- }
- "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
- | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
- Arc::new(serde_json::from_value::(val).unwrap())
- }
- "select" => {
- //
- Arc::new(serde_json::from_value::(val).unwrap())
- }
-
- "touchcancel" | "touchend" | "touchmove" | "touchstart" => {
- Arc::new(serde_json::from_value::(val).unwrap())
- }
-
- "scroll" => Arc::new(()),
-
- "wheel" => Arc::new(serde_json::from_value::(val).unwrap()),
-
- "animationstart" | "animationend" | "animationiteration" => {
- Arc::new(serde_json::from_value::(val).unwrap())
- }
-
- "transitionend" => Arc::new(serde_json::from_value::(val).unwrap()),
-
- "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
- | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
- | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
- | "timeupdate" | "volumechange" | "waiting" => {
- //
- Arc::new(MediaData {})
- }
-
- "toggle" => Arc::new(ToggleData {}),
-
- _ => Arc::new(()),
- }
-}
-
-fn event_name_from_type(typ: &str) -> &'static str {
- match typ {
- "copy" => "copy",
- "cut" => "cut",
- "paste" => "paste",
- "compositionend" => "compositionend",
- "compositionstart" => "compositionstart",
- "compositionupdate" => "compositionupdate",
- "keydown" => "keydown",
- "keypress" => "keypress",
- "keyup" => "keyup",
- "focus" => "focus",
- "focusout" => "focusout",
- "focusin" => "focusin",
- "blur" => "blur",
- "change" => "change",
- "input" => "input",
- "invalid" => "invalid",
- "reset" => "reset",
- "submit" => "submit",
- "click" => "click",
- "contextmenu" => "contextmenu",
- "doubleclick" => "doubleclick",
- "drag" => "drag",
- "dragend" => "dragend",
- "dragenter" => "dragenter",
- "dragexit" => "dragexit",
- "dragleave" => "dragleave",
- "dragover" => "dragover",
- "dragstart" => "dragstart",
- "drop" => "drop",
- "mousedown" => "mousedown",
- "mouseenter" => "mouseenter",
- "mouseleave" => "mouseleave",
- "mousemove" => "mousemove",
- "mouseout" => "mouseout",
- "mouseover" => "mouseover",
- "mouseup" => "mouseup",
- "pointerdown" => "pointerdown",
- "pointermove" => "pointermove",
- "pointerup" => "pointerup",
- "pointercancel" => "pointercancel",
- "gotpointercapture" => "gotpointercapture",
- "lostpointercapture" => "lostpointercapture",
- "pointerenter" => "pointerenter",
- "pointerleave" => "pointerleave",
- "pointerover" => "pointerover",
- "pointerout" => "pointerout",
- "select" => "select",
- "touchcancel" => "touchcancel",
- "touchend" => "touchend",
- "touchmove" => "touchmove",
- "touchstart" => "touchstart",
- "scroll" => "scroll",
- "wheel" => "wheel",
- "animationstart" => "animationstart",
- "animationend" => "animationend",
- "animationiteration" => "animationiteration",
- "transitionend" => "transitionend",
- "abort" => "abort",
- "canplay" => "canplay",
- "canplaythrough" => "canplaythrough",
- "durationchange" => "durationchange",
- "emptied" => "emptied",
- "encrypted" => "encrypted",
- "ended" => "ended",
- "error" => "error",
- "loadeddata" => "loadeddata",
- "loadedmetadata" => "loadedmetadata",
- "loadstart" => "loadstart",
- "pause" => "pause",
- "play" => "play",
- "playing" => "playing",
- "progress" => "progress",
- "ratechange" => "ratechange",
- "seeked" => "seeked",
- "seeking" => "seeking",
- "stalled" => "stalled",
- "suspend" => "suspend",
- "timeupdate" => "timeupdate",
- "volumechange" => "volumechange",
- "waiting" => "waiting",
- "toggle" => "toggle",
- _ => {
- panic!("unsupported event type")
- }
- }
-}
diff --git a/packages/liveview/src/interpreter.js b/packages/liveview/src/interpreter.js
deleted file mode 100644
index 7e99745f2..000000000
--- a/packages/liveview/src/interpreter.js
+++ /dev/null
@@ -1,973 +0,0 @@
-function main() {
- let root = window.document.getElementById("main");
-
- if (root != null) {
- // create a new ipc
- window.ipc = new IPC(root);
-
- window.ipc.send(serializeIpcMessage("initialize"));
- }
-}
-
-class IPC {
- constructor(root) {
- // connect to the websocket
- window.interpreter = new Interpreter(root);
-
- this.ws = new WebSocket(WS_ADDR);
-
- this.ws.onopen = () => {
- console.log("Connected to the websocket");
- };
-
- this.ws.onerror = (err) => {
- console.error("Error: ", err);
- };
-
- this.ws.onmessage = (event) => {
- let edits = JSON.parse(event.data);
- window.interpreter.handleEdits(edits);
- };
- }
-
- send(msg) {
- this.ws.send(msg);
- }
-}
-
-class ListenerMap {
- constructor(root) {
- // bubbling events can listen at the root element
- this.global = {};
- // non bubbling events listen at the element the listener was created at
- this.local = {};
- this.root = root;
- }
-
- create(event_name, element, handler, bubbles) {
- if (bubbles) {
- if (this.global[event_name] === undefined) {
- this.global[event_name] = {};
- this.global[event_name].active = 1;
- this.global[event_name].callback = handler;
- this.root.addEventListener(event_name, handler);
- } else {
- this.global[event_name].active++;
- }
- }
- else {
- const id = element.getAttribute("data-dioxus-id");
- if (!this.local[id]) {
- this.local[id] = {};
- }
- this.local[id][event_name] = handler;
- element.addEventListener(event_name, handler);
- }
- }
-
- remove(element, event_name, bubbles) {
- if (bubbles) {
- this.global[event_name].active--;
- if (this.global[event_name].active === 0) {
- this.root.removeEventListener(event_name, this.global[event_name].callback);
- delete this.global[event_name];
- }
- }
- else {
- const id = element.getAttribute("data-dioxus-id");
- delete this.local[id][event_name];
- if (this.local[id].length === 0) {
- delete this.local[id];
- }
- element.removeEventListener(event_name, handler);
- }
- }
-}
-
-class Interpreter {
- constructor(root) {
- this.root = root;
- this.lastNode = root;
- this.listeners = new ListenerMap(root);
- this.handlers = {};
- this.nodes = [root];
- this.parents = [];
- }
- checkAppendParent() {
- if (this.parents.length > 0) {
- const lastParent = this.parents[this.parents.length - 1];
- lastParent[1]--;
- if (lastParent[1] === 0) {
- this.parents.pop();
- }
- lastParent[0].appendChild(this.lastNode);
- }
- }
- AppendChildren(root, children) {
- let node;
- if (root == null) {
- node = this.lastNode;
- } else {
- node = this.nodes[root];
- }
- for (let i = 0; i < children.length; i++) {
- node.appendChild(this.nodes[children[i]]);
- }
- }
- ReplaceWith(root, nodes) {
- let node;
- if (root == null) {
- node = this.lastNode;
- } else {
- node = this.nodes[root];
- }
- let els = [];
- for (let i = 0; i < nodes.length; i++) {
- els.push(this.nodes[nodes[i]])
- }
- node.replaceWith(...els);
- }
- InsertAfter(root, nodes) {
- let node;
- if (root == null) {
- node = this.lastNode;
- } else {
- node = this.nodes[root];
- }
- let els = [];
- for (let i = 0; i < nodes.length; i++) {
- els.push(this.nodes[nodes[i]])
- }
- node.after(...els);
- }
- InsertBefore(root, nodes) {
- let node;
- if (root == null) {
- node = this.lastNode;
- } else {
- node = this.nodes[root];
- }
- let els = [];
- for (let i = 0; i < nodes.length; i++) {
- els.push(this.nodes[nodes[i]])
- }
- node.before(...els);
- }
- Remove(root) {
- let node;
- if (root == null) {
- node = this.lastNode;
- } else {
- node = this.nodes[root];
- }
- if (node !== undefined) {
- node.remove();
- }
- }
- CreateTextNode(text, root) {
- this.lastNode = document.createTextNode(text);
- this.checkAppendParent();
- if (root != null) {
- this.nodes[root] = this.lastNode;
- }
- }
- CreateElement(tag, root, children) {
- this.lastNode = document.createElement(tag);
- this.checkAppendParent();
- if (root != null) {
- this.nodes[root] = this.lastNode;
- }
- if (children > 0) {
- this.parents.push([this.lastNode, children]);
- }
- }
- CreateElementNs(tag, root, ns, children) {
- this.lastNode = document.createElementNS(ns, tag);
- this.checkAppendParent();
- if (root != null) {
- this.nodes[root] = this.lastNode;
- }
- if (children > 0) {
- this.parents.push([this.lastNode, children]);
- }
- }
- CreatePlaceholder(root) {
- this.lastNode = document.createElement("pre");
- this.lastNode.hidden = true;
- this.checkAppendParent();
- if (root != null) {
- this.nodes[root] = this.lastNode;
- }
- }
- NewEventListener(event_name, root, handler, bubbles) {
- let node;
- if (root == null) {
- node = this.lastNode;
- } else {
- node = this.nodes[root];
- }
- node.setAttribute("data-dioxus-id", `${root}`);
- this.listeners.create(event_name, node, handler, bubbles);
- }
- RemoveEventListener(root, event_name, bubbles) {
- let node;
- if (root == null) {
- node = this.lastNode;
- } else {
- node = this.nodes[root];
- }
- node.removeAttribute(`data-dioxus-id`);
- this.listeners.remove(node, event_name, bubbles);
- }
- SetText(root, text) {
- let node;
- if (root == null) {
- node = this.lastNode;
- } else {
- node = this.nodes[root];
- }
- node.data = text;
- }
- SetAttribute(root, field, value, ns) {
- const name = field;
- let node;
- if (root == null) {
- node = this.lastNode;
- } else {
- node = this.nodes[root];
- }
- if (ns === "style") {
- // @ts-ignore
- node.style[name] = value;
- } else if (ns != null || ns != undefined) {
- node.setAttributeNS(ns, name, value);
- } else {
- switch (name) {
- case "value":
- if (value !== node.value) {
- node.value = value;
- }
- break;
- case "checked":
- node.checked = value === "true";
- break;
- case "selected":
- node.selected = value === "true";
- break;
- case "dangerous_inner_html":
- node.innerHTML = value;
- break;
- default:
- // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
- if (value === "false" && bool_attrs.hasOwnProperty(name)) {
- node.removeAttribute(name);
- } else {
- node.setAttribute(name, value);
- }
- }
- }
- }
- RemoveAttribute(root, field, ns) {
- const name = field;
- let node;
- if (root == null) {
- node = this.lastNode;
- } else {
- node = this.nodes[root];
- }
- if (ns == "style") {
- node.style.removeProperty(name);
- } else if (ns !== null || ns !== undefined) {
- node.removeAttributeNS(ns, name);
- } else if (name === "value") {
- node.value = "";
- } else if (name === "checked") {
- node.checked = false;
- } else if (name === "selected") {
- node.selected = false;
- } else if (name === "dangerous_inner_html") {
- node.innerHTML = "";
- } else {
- node.removeAttribute(name);
- }
- }
- CloneNode(old, new_id) {
- let node;
- if (old === null) {
- node = this.lastNode;
- } else {
- node = this.nodes[old];
- }
- this.nodes[new_id] = node.cloneNode(true);
- }
- CloneNodeChildren(old, new_ids) {
- let node;
- if (old === null) {
- node = this.lastNode;
- } else {
- node = this.nodes[old];
- }
- const old_node = node.cloneNode(true);
- let i = 0;
- for (let node = old_node.firstChild; i < new_ids.length; node = node.nextSibling) {
- this.nodes[new_ids[i++]] = node;
- }
- }
- FirstChild() {
- this.lastNode = this.lastNode.firstChild;
- }
- NextSibling() {
- this.lastNode = this.lastNode.nextSibling;
- }
- ParentNode() {
- this.lastNode = this.lastNode.parentNode;
- }
- StoreWithId(id) {
- this.nodes[id] = this.lastNode;
- }
- SetLastNode(root) {
- this.lastNode = this.nodes[root];
- }
- handleEdits(edits) {
- for (let edit of edits) {
- this.handleEdit(edit);
- }
- }
- handleEdit(edit) {
- switch (edit.type) {
- case "PushRoot":
- this.PushRoot(edit.root);
- break;
- case "AppendChildren":
- this.AppendChildren(edit.root, edit.children);
- break;
- case "ReplaceWith":
- this.ReplaceWith(edit.root, edit.nodes);
- break;
- case "InsertAfter":
- this.InsertAfter(edit.root, edit.nodes);
- break;
- case "InsertBefore":
- this.InsertBefore(edit.root, edit.nodes);
- break;
- case "Remove":
- this.Remove(edit.root);
- break;
- case "CreateTextNode":
- this.CreateTextNode(edit.text, edit.root);
- break;
- case "CreateElement":
- this.CreateElement(edit.tag, edit.root, edit.children);
- break;
- case "CreateElementNs":
- this.CreateElementNs(edit.tag, edit.root, edit.ns, edit.children);
- break;
- case "CreatePlaceholder":
- this.CreatePlaceholder(edit.root);
- break;
- case "RemoveEventListener":
- this.RemoveEventListener(edit.root, edit.event_name);
- break;
- case "NewEventListener":
- // this handler is only provided on desktop implementations since this
- // method is not used by the web implementation
- let handler = (event) => {
- let target = event.target;
- if (target != null) {
- let realId = target.getAttribute(`data-dioxus-id`);
- let shouldPreventDefault = target.getAttribute(
- `dioxus-prevent-default`
- );
-
- if (event.type === "click") {
- // todo call prevent default if it's the right type of event
- if (shouldPreventDefault !== `onclick`) {
- if (target.tagName === "A") {
- event.preventDefault();
- const href = target.getAttribute("href");
- if (href !== "" && href !== null && href !== undefined) {
- window.ipc.postMessage(
- serializeIpcMessage("browser_open", { href })
- );
- }
- }
- }
-
- // also prevent buttons from submitting
- if (target.tagName === "BUTTON" && event.type == "submit") {
- event.preventDefault();
- }
- }
- // walk the tree to find the real element
- while (realId == null) {
- // we've reached the root we don't want to send an event
- if (target.parentElement === null) {
- return;
- }
-
- target = target.parentElement;
- realId = target.getAttribute(`data-dioxus-id`);
- }
-
- shouldPreventDefault = target.getAttribute(
- `dioxus-prevent-default`
- );
-
- let contents = serialize_event(event);
-
- if (shouldPreventDefault === `on${event.type}`) {
- event.preventDefault();
- }
-
- if (event.type === "submit") {
- event.preventDefault();
- }
-
- if (
- target.tagName === "FORM" &&
- (event.type === "submit" || event.type === "input")
- ) {
- for (let x = 0; x < target.elements.length; x++) {
- let element = target.elements[x];
- let name = element.getAttribute("name");
- if (name != null) {
- if (element.getAttribute("type") === "checkbox") {
- // @ts-ignore
- contents.values[name] = element.checked ? "true" : "false";
- } else if (element.getAttribute("type") === "radio") {
- if (element.checked) {
- contents.values[name] = element.value;
- }
- } else {
- // @ts-ignore
- contents.values[name] =
- element.value ?? element.textContent;
- }
- }
- }
- }
-
- if (realId === null) {
- return;
- }
- realId = parseInt(realId);
- window.ipc.send(
- serializeIpcMessage("user_event", {
- event: edit.event_name,
- mounted_dom_id: realId,
- contents: contents,
- })
- );
- }
- };
- this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name));
-
- break;
- case "SetText":
- this.SetText(edit.root, edit.text);
- break;
- case "SetAttribute":
- this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
- break;
- case "RemoveAttribute":
- this.RemoveAttribute(edit.root, edit.name, edit.ns);
- break;
- case "CloneNode":
- this.CloneNode(edit.id, edit.new_id);
- break;
- case "CloneNodeChildren":
- this.CloneNodeChildren(edit.id, edit.new_ids);
- break;
- case "FirstChild":
- this.FirstChild();
- break;
- case "NextSibling":
- this.NextSibling();
- break;
- case "ParentNode":
- this.ParentNode();
- break;
- case "StoreWithId":
- this.StoreWithId(BigInt(edit.id));
- break;
- case "SetLastNode":
- this.SetLastNode(BigInt(edit.id));
- break;
- }
- }
-}
-
-function serialize_event(event) {
- switch (event.type) {
- case "copy":
- case "cut":
- case "past": {
- return {};
- }
- case "compositionend":
- case "compositionstart":
- case "compositionupdate": {
- let { data } = event;
- return {
- data,
- };
- }
- case "keydown":
- case "keypress":
- case "keyup": {
- let {
- charCode,
- key,
- altKey,
- ctrlKey,
- metaKey,
- keyCode,
- shiftKey,
- location,
- repeat,
- which,
- code,
- } = event;
- return {
- char_code: charCode,
- key: key,
- alt_key: altKey,
- ctrl_key: ctrlKey,
- meta_key: metaKey,
- key_code: keyCode,
- shift_key: shiftKey,
- location: location,
- repeat: repeat,
- which: which,
- code,
- };
- }
- case "focus":
- case "blur": {
- return {};
- }
- case "change": {
- let target = event.target;
- let value;
- if (target.type === "checkbox" || target.type === "radio") {
- value = target.checked ? "true" : "false";
- } else {
- value = target.value ?? target.textContent;
- }
- return {
- value: value,
- values: {},
- };
- }
- case "input":
- case "invalid":
- case "reset":
- case "submit": {
- let target = event.target;
- let value = target.value ?? target.textContent;
-
- if (target.type === "checkbox") {
- value = target.checked ? "true" : "false";
- }
-
- return {
- value: value,
- values: {},
- };
- }
- case "click":
- case "contextmenu":
- case "doubleclick":
- case "dblclick":
- case "drag":
- case "dragend":
- case "dragenter":
- case "dragexit":
- case "dragleave":
- case "dragover":
- case "dragstart":
- case "drop":
- case "mousedown":
- case "mouseenter":
- case "mouseleave":
- case "mousemove":
- case "mouseout":
- case "mouseover":
- case "mouseup": {
- const {
- altKey,
- button,
- buttons,
- clientX,
- clientY,
- ctrlKey,
- metaKey,
- offsetX,
- offsetY,
- pageX,
- pageY,
- screenX,
- screenY,
- shiftKey,
- } = event;
- return {
- alt_key: altKey,
- button: button,
- buttons: buttons,
- client_x: clientX,
- client_y: clientY,
- ctrl_key: ctrlKey,
- meta_key: metaKey,
- offset_x: offsetX,
- offset_y: offsetY,
- page_x: pageX,
- page_y: pageY,
- screen_x: screenX,
- screen_y: screenY,
- shift_key: shiftKey,
- };
- }
- case "pointerdown":
- case "pointermove":
- case "pointerup":
- case "pointercancel":
- case "gotpointercapture":
- case "lostpointercapture":
- case "pointerenter":
- case "pointerleave":
- case "pointerover":
- case "pointerout": {
- const {
- altKey,
- button,
- buttons,
- clientX,
- clientY,
- ctrlKey,
- metaKey,
- pageX,
- pageY,
- screenX,
- screenY,
- shiftKey,
- pointerId,
- width,
- height,
- pressure,
- tangentialPressure,
- tiltX,
- tiltY,
- twist,
- pointerType,
- isPrimary,
- } = event;
- return {
- alt_key: altKey,
- button: button,
- buttons: buttons,
- client_x: clientX,
- client_y: clientY,
- ctrl_key: ctrlKey,
- meta_key: metaKey,
- page_x: pageX,
- page_y: pageY,
- screen_x: screenX,
- screen_y: screenY,
- shift_key: shiftKey,
- pointer_id: pointerId,
- width: width,
- height: height,
- pressure: pressure,
- tangential_pressure: tangentialPressure,
- tilt_x: tiltX,
- tilt_y: tiltY,
- twist: twist,
- pointer_type: pointerType,
- is_primary: isPrimary,
- };
- }
- case "select": {
- return {};
- }
- case "touchcancel":
- case "touchend":
- case "touchmove":
- case "touchstart": {
- const { altKey, ctrlKey, metaKey, shiftKey } = event;
- return {
- // changed_touches: event.changedTouches,
- // target_touches: event.targetTouches,
- // touches: event.touches,
- alt_key: altKey,
- ctrl_key: ctrlKey,
- meta_key: metaKey,
- shift_key: shiftKey,
- };
- }
- case "scroll": {
- return {};
- }
- case "wheel": {
- const { deltaX, deltaY, deltaZ, deltaMode } = event;
- return {
- delta_x: deltaX,
- delta_y: deltaY,
- delta_z: deltaZ,
- delta_mode: deltaMode,
- };
- }
- case "animationstart":
- case "animationend":
- case "animationiteration": {
- const { animationName, elapsedTime, pseudoElement } = event;
- return {
- animation_name: animationName,
- elapsed_time: elapsedTime,
- pseudo_element: pseudoElement,
- };
- }
- case "transitionend": {
- const { propertyName, elapsedTime, pseudoElement } = event;
- return {
- property_name: propertyName,
- elapsed_time: elapsedTime,
- pseudo_element: pseudoElement,
- };
- }
- case "abort":
- case "canplay":
- case "canplaythrough":
- case "durationchange":
- case "emptied":
- case "encrypted":
- case "ended":
- case "error":
- case "loadeddata":
- case "loadedmetadata":
- case "loadstart":
- case "pause":
- case "play":
- case "playing":
- case "progress":
- case "ratechange":
- case "seeked":
- case "seeking":
- case "stalled":
- case "suspend":
- case "timeupdate":
- case "volumechange":
- case "waiting": {
- return {};
- }
- case "toggle": {
- return {};
- }
- default: {
- return {};
- }
- }
-}
-function serializeIpcMessage(method, params = {}) {
- return JSON.stringify({ method, params });
-}
-const bool_attrs = {
- allowfullscreen: true,
- allowpaymentrequest: true,
- async: true,
- autofocus: true,
- autoplay: true,
- checked: true,
- controls: true,
- default: true,
- defer: true,
- disabled: true,
- formnovalidate: true,
- hidden: true,
- ismap: true,
- itemscope: true,
- loop: true,
- multiple: true,
- muted: true,
- nomodule: true,
- novalidate: true,
- open: true,
- playsinline: true,
- readonly: true,
- required: true,
- reversed: true,
- selected: true,
- truespeed: true,
-};
-
-function is_element_node(node) {
- return node.nodeType == 1;
-}
-
-function event_bubbles(event) {
- switch (event) {
- case "copy":
- return true;
- case "cut":
- return true;
- case "paste":
- return true;
- case "compositionend":
- return true;
- case "compositionstart":
- return true;
- case "compositionupdate":
- return true;
- case "keydown":
- return true;
- case "keypress":
- return true;
- case "keyup":
- return true;
- case "focus":
- return false;
- case "focusout":
- return true;
- case "focusin":
- return true;
- case "blur":
- return false;
- case "change":
- return true;
- case "input":
- return true;
- case "invalid":
- return true;
- case "reset":
- return true;
- case "submit":
- return true;
- case "click":
- return true;
- case "contextmenu":
- return true;
- case "doubleclick":
- return true;
- case "dblclick":
- return true;
- case "drag":
- return true;
- case "dragend":
- return true;
- case "dragenter":
- return false;
- case "dragexit":
- return false;
- case "dragleave":
- return true;
- case "dragover":
- return true;
- case "dragstart":
- return true;
- case "drop":
- return true;
- case "mousedown":
- return true;
- case "mouseenter":
- return false;
- case "mouseleave":
- return false;
- case "mousemove":
- return true;
- case "mouseout":
- return true;
- case "scroll":
- return false;
- case "mouseover":
- return true;
- case "mouseup":
- return true;
- case "pointerdown":
- return true;
- case "pointermove":
- return true;
- case "pointerup":
- return true;
- case "pointercancel":
- return true;
- case "gotpointercapture":
- return true;
- case "lostpointercapture":
- return true;
- case "pointerenter":
- return false;
- case "pointerleave":
- return false;
- case "pointerover":
- return true;
- case "pointerout":
- return true;
- case "select":
- return true;
- case "touchcancel":
- return true;
- case "touchend":
- return true;
- case "touchmove":
- return true;
- case "touchstart":
- return true;
- case "wheel":
- return true;
- case "abort":
- return false;
- case "canplay":
- return false;
- case "canplaythrough":
- return false;
- case "durationchange":
- return false;
- case "emptied":
- return false;
- case "encrypted":
- return true;
- case "ended":
- return false;
- case "error":
- return false;
- case "loadeddata":
- return false;
- case "loadedmetadata":
- return false;
- case "loadstart":
- return false;
- case "pause":
- return false;
- case "play":
- return false;
- case "playing":
- return false;
- case "progress":
- return false;
- case "ratechange":
- return false;
- case "seeked":
- return false;
- case "seeking":
- return false;
- case "stalled":
- return false;
- case "suspend":
- return false;
- case "timeupdate":
- return false;
- case "volumechange":
- return false;
- case "waiting":
- return false;
- case "animationstart":
- return true;
- case "animationend":
- return true;
- case "animationiteration":
- return true;
- case "transitionend":
- return true;
- case "toggle":
- return true;
- }
-}
\ No newline at end of file
diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs
index 540d29974..ffddf0272 100644
--- a/packages/liveview/src/lib.rs
+++ b/packages/liveview/src/lib.rs
@@ -1,56 +1,55 @@
-#![allow(dead_code)]
-
-pub(crate) mod events;
pub mod adapters {
#[cfg(feature = "warp")]
pub mod warp_adapter;
+ #[cfg(feature = "warp")]
+ pub use warp_adapter::*;
#[cfg(feature = "axum")]
pub mod axum_adapter;
+ #[cfg(feature = "axum")]
+ pub use axum_adapter::*;
#[cfg(feature = "salvo")]
pub mod salvo_adapter;
+
+ #[cfg(feature = "salvo")]
+ pub use salvo_adapter::*;
}
-use std::net::SocketAddr;
+pub use adapters::*;
-use tokio_util::task::LocalPoolHandle;
+pub mod pool;
+use futures_util::{SinkExt, StreamExt};
+pub use pool::*;
-#[derive(Clone)]
-pub struct Liveview {
- pool: LocalPoolHandle,
- addr: String,
+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,
}
-impl Liveview {
- pub fn body(&self, header: &str) -> String {
- format!(
- r#"
-
-
-
- {header}
-
-
-
-
-
-"#,
- addr = self.addr,
- interpreter = include_str!("../src/interpreter.js")
- )
- }
-}
+use dioxus_interpreter_js::INTERPRETER_JS;
+static MAIN_JS: &str = include_str!("./main.js");
-pub fn new(addr: impl Into) -> Liveview {
- let addr: SocketAddr = addr.into();
-
- Liveview {
- pool: LocalPoolHandle::new(16),
- addr: addr.to_string(),
- }
+/// This script that gets injected into your app connects this page to the websocket endpoint
+///
+/// Once the endpoint is connected, it will send the initial state of the app, and then start
+/// processing user events and returning edits to the liveview instance
+pub fn interpreter_glue(url: &str) -> String {
+ format!(
+ r#"
+
+ "#
+ )
}
diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js
new file mode 100644
index 000000000..e37ac7b67
--- /dev/null
+++ b/packages/liveview/src/main.js
@@ -0,0 +1,36 @@
+function main() {
+ let root = window.document.getElementById("main");
+
+ if (root != null) {
+ // create a new ipc
+ window.ipc = new IPC(root);
+ window.ipc.postMessage(serializeIpcMessage("initialize"));
+ }
+}
+
+class IPC {
+ constructor(root) {
+ // connect to the websocket
+ window.interpreter = new Interpreter(root);
+
+ this.ws = new WebSocket(WS_ADDR);
+
+ this.ws.onopen = () => {
+ console.log("Connected to the websocket");
+ };
+
+ this.ws.onerror = (err) => {
+ console.error("Error: ", err);
+ };
+
+ this.ws.onmessage = (event) => {
+ console.log("Received message: ", event.data);
+ let edits = JSON.parse(event.data);
+ window.interpreter.handleEdits(edits);
+ };
+ }
+
+ postMessage(msg) {
+ this.ws.send(msg);
+ }
+}
diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs
new file mode 100644
index 000000000..c769e1371
--- /dev/null
+++ b/packages/liveview/src/pool.rs
@@ -0,0 +1,123 @@
+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)]
+pub struct LiveViewPool {
+ pub(crate) pool: LocalPoolHandle,
+}
+
+impl Default for LiveViewPool {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl LiveViewPool {
+ pub fn new() -> Self {
+ LiveViewPool {
+ pool: LocalPoolHandle::new(16),
+ }
+ }
+
+ pub async fn launch(
+ &self,
+ ws: impl LiveViewSocket,
+ app: fn(Scope<()>) -> Element,
+ ) -> Result<(), LiveViewError> {
+ self.launch_with_props(ws, app, ()).await
+ }
+
+ pub async fn launch_with_props(
+ &self,
+ ws: impl LiveViewSocket,
+ app: fn(Scope) -> Element,
+ props: T,
+ ) -> Result<(), LiveViewError> {
+ match self.pool.spawn_pinned(move || run(app, props, ws)).await {
+ Ok(Ok(_)) => Ok(()),
+ Ok(Err(e)) => Err(e),
+ Err(_) => Err(LiveViewError::SendingFailed),
+ }
+ }
+}
+
+/// A LiveViewSocket is a Sink and Stream of Strings that Dioxus uses to communicate with the client
+pub trait LiveViewSocket:
+ SinkExt
+ + StreamExt
- >
+ + Send
+ + 'static
+{
+}
+
+impl
LiveViewSocket for S where
+ S: SinkExt
+ + StreamExt- >
+ + Send
+ + 'static
+{
+}
+
+/// 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 run(
+ app: Component,
+ props: T,
+ ws: impl LiveViewSocket,
+) -> 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);
+
+ // send the initial render to the client
+ ws.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.next() => {
+ match evt {
+ 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);
+ }
+ }
+ // log this I guess? when would we get an error here?
+ Some(Err(_e)) => {},
+ None => return Ok(()),
+ }
+ }
+ }
+
+ let edits = vdom
+ .render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
+ .await;
+
+ ws.send(serde_json::to_string(&edits).unwrap()).await?;
+ }
+}