Merge branch 'master' into events-2
This commit is contained in:
commit
dccfba12d8
209 files changed
+6270
-2789
No files matched your search
@@ -20,5 +20,5 @@ fn transform_rx(message: Result<Message, axum::Error>) -> Result<Vec<u8>, LiveVi
|
||||
}
|
||||
|
||||
async fn transform_tx(message: Vec<u8>) -> Result<Message, axum::Error> {
|
||||
Ok(Message::Text(String::from_utf8_lossy(&message).to_string()))
|
||||
Ok(Message::Binary(message))
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::{LiveViewError, LiveViewSocket};
|
||||
use rocket::futures::{SinkExt, StreamExt};
|
||||
use rocket_ws::{result::Error, stream::DuplexStream, Message};
|
||||
|
||||
/// Convert a rocket websocket into a LiveViewSocket
|
||||
///
|
||||
/// This is required to launch a LiveView app using the rocket web framework
|
||||
pub fn rocket_socket(stream: DuplexStream) -> impl LiveViewSocket {
|
||||
stream
|
||||
.map(transform_rx)
|
||||
.with(transform_tx)
|
||||
.sink_map_err(|_| LiveViewError::SendingFailed)
|
||||
}
|
||||
|
||||
fn transform_rx(message: Result<Message, Error>) -> Result<Vec<u8>, LiveViewError> {
|
||||
message
|
||||
.map_err(|_| LiveViewError::SendingFailed)?
|
||||
.into_text()
|
||||
.map(|s| s.into_bytes())
|
||||
.map_err(|_| LiveViewError::SendingFailed)
|
||||
}
|
||||
|
||||
async fn transform_tx(message: Vec<u8>) -> Result<Message, Error> {
|
||||
Ok(Message::Text(String::from_utf8_lossy(&message).to_string()))
|
||||
}
|
||||
@@ -30,7 +30,7 @@ impl RenderedElementBacking for LiveviewElement {
|
||||
>,
|
||||
>,
|
||||
> {
|
||||
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
|
||||
let script = format!("return window.interpreter.getClientRect({});", self.id.0);
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
@@ -54,7 +54,7 @@ impl RenderedElementBacking for LiveviewElement {
|
||||
behavior: dioxus_html::ScrollBehavior,
|
||||
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
|
||||
let script = format!(
|
||||
"return window.interpreter.ScrollTo({}, {});",
|
||||
"return window.interpreter.scrollTo({}, {});",
|
||||
self.id.0,
|
||||
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
|
||||
);
|
||||
@@ -78,7 +78,7 @@ impl RenderedElementBacking for LiveviewElement {
|
||||
focus: bool,
|
||||
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
|
||||
let script = format!(
|
||||
"return window.interpreter.SetFocus({}, {});",
|
||||
"return window.interpreter.setFocus({}, {});",
|
||||
self.id.0, focus
|
||||
);
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ pub mod adapters {
|
||||
|
||||
#[cfg(feature = "salvo")]
|
||||
pub use salvo_adapter::*;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
pub mod rocket_adapter;
|
||||
#[cfg(feature = "rocket")]
|
||||
pub use rocket_adapter::*;
|
||||
}
|
||||
|
||||
pub use adapters::*;
|
||||
@@ -42,58 +47,7 @@ pub enum LiveViewError {
|
||||
SendingFailed,
|
||||
}
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static INTERPRETER_JS: Lazy<String> = Lazy::new(|| {
|
||||
let interpreter = dioxus_interpreter_js::INTERPRETER_JS;
|
||||
let serialize_file_uploads = r#"if (
|
||||
target.tagName === "INPUT" &&
|
||||
(event.type === "change" || event.type === "input")
|
||||
) {
|
||||
const type = target.getAttribute("type");
|
||||
if (type === "file") {
|
||||
async function read_files() {
|
||||
const files = target.files;
|
||||
const file_contents = {};
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
|
||||
file_contents[file.name] = Array.from(
|
||||
new Uint8Array(await file.arrayBuffer())
|
||||
);
|
||||
}
|
||||
let file_engine = {
|
||||
files: file_contents,
|
||||
};
|
||||
contents.files = file_engine;
|
||||
|
||||
if (realId === null) {
|
||||
return;
|
||||
}
|
||||
const message = serializeIpcMessage("user_event", {
|
||||
name: name,
|
||||
element: parseInt(realId),
|
||||
data: contents,
|
||||
bubbles,
|
||||
});
|
||||
window.ipc.postMessage(message);
|
||||
}
|
||||
read_files();
|
||||
return;
|
||||
}
|
||||
}"#;
|
||||
|
||||
let interpreter = interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads);
|
||||
interpreter.replace("import { setAttributeInner } from \"./common.js\";", "")
|
||||
});
|
||||
|
||||
static COMMON_JS: Lazy<String> = Lazy::new(|| {
|
||||
let common = dioxus_interpreter_js::COMMON_JS;
|
||||
common.replace("export", "")
|
||||
});
|
||||
|
||||
static MAIN_JS: &str = include_str!("./main.js");
|
||||
static MINIFIED: &str = include_str!("./minified.js");
|
||||
|
||||
/// This script that gets injected into your app connects this page to the websocket endpoint
|
||||
///
|
||||
@@ -131,8 +85,6 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
|
||||
"return path;"
|
||||
};
|
||||
|
||||
let js = &*INTERPRETER_JS;
|
||||
let common = &*COMMON_JS;
|
||||
format!(
|
||||
r#"
|
||||
<script>
|
||||
@@ -141,10 +93,7 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
|
||||
}}
|
||||
|
||||
var WS_ADDR = __dioxusGetWsUrl("{url_or_path}");
|
||||
{js}
|
||||
{common}
|
||||
{MAIN_JS}
|
||||
main();
|
||||
{MINIFIED}
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const config = new InterpreterConfig(false);
|
||||
|
||||
function main() {
|
||||
let root = window.document.getElementById("main");
|
||||
if (root != null) {
|
||||
@@ -7,10 +9,9 @@ function main() {
|
||||
|
||||
class IPC {
|
||||
constructor(root) {
|
||||
// connect to the websocket
|
||||
window.interpreter = new Interpreter(root, new InterpreterConfig(false));
|
||||
|
||||
let ws = new WebSocket(WS_ADDR);
|
||||
window.interpreter.initialize(root);
|
||||
const ws = new WebSocket(WS_ADDR);
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
function ping() {
|
||||
ws.send("__ping__");
|
||||
@@ -19,7 +20,7 @@ class IPC {
|
||||
ws.onopen = () => {
|
||||
// we ping every 30 seconds to keep the websocket alive
|
||||
setInterval(ping, 30000);
|
||||
ws.send(serializeIpcMessage("initialize"));
|
||||
ws.send(window.interpreter.serializeIpcMessage("initialize"));
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
@@ -27,17 +28,29 @@ class IPC {
|
||||
};
|
||||
|
||||
ws.onmessage = (message) => {
|
||||
// Ignore pongs
|
||||
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;
|
||||
const u8view = new Uint8Array(message.data);
|
||||
const binaryFrame = u8view[0] == 1;
|
||||
const messageData = message.data.slice(1)
|
||||
// The first byte tells the shim if this is a binary of text frame
|
||||
if (binaryFrame) {
|
||||
// binary frame
|
||||
run_from_bytes(messageData);
|
||||
}
|
||||
else {
|
||||
// text frame
|
||||
|
||||
let decoder = new TextDecoder("utf-8");
|
||||
|
||||
// Using decode method to get string output
|
||||
let str = decoder.decode(messageData);
|
||||
// Ignore pongs
|
||||
if (str != "__pong__") {
|
||||
const event = JSON.parse(str);
|
||||
switch (event.type) {
|
||||
case "query":
|
||||
Function("Eval", `"use strict";${event.data};`)();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -49,3 +62,5 @@ class IPC {
|
||||
this.ws.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
+168
-19
@@ -5,9 +5,11 @@ use crate::{
|
||||
query::{QueryEngine, QueryResult},
|
||||
LiveViewError,
|
||||
};
|
||||
use dioxus_core::{prelude::*, Mutations};
|
||||
use dioxus_html::{EventData, HtmlEvent, MountedData, PlatformEventData};
|
||||
use dioxus_core::{prelude::*, BorrowedAttributeValue, Mutations};
|
||||
use dioxus_html::{event_bubbles,EventData, HtmlEvent, MountedData, PlatformEventData};
|
||||
use dioxus_interpreter_js::binary_protocol::Channel;
|
||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Serialize;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
@@ -111,7 +113,7 @@ impl<S> LiveViewSocket for S where
|
||||
///
|
||||
/// 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.
|
||||
/// As long as your framework can provide a Sink and Stream of Bytes, 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(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), LiveViewError> {
|
||||
@@ -124,20 +126,31 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
||||
rx
|
||||
};
|
||||
|
||||
let mut templates: FxHashMap<String, u16> = Default::default();
|
||||
let mut max_template_count = 0;
|
||||
|
||||
// Create the a proxy for query engine
|
||||
let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let query_engine = QueryEngine::new(query_tx);
|
||||
vdom.base_scope().provide_context(query_engine.clone());
|
||||
init_eval(vdom.base_scope());
|
||||
|
||||
// todo: use an efficient binary packed format for this
|
||||
let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap();
|
||||
|
||||
// pin the futures so we can use select!
|
||||
pin_mut!(ws);
|
||||
|
||||
// send the initial render to the client
|
||||
ws.send(edits.into_bytes()).await?;
|
||||
let mut edit_channel = Channel::default();
|
||||
if let Some(edits) = {
|
||||
let mutations = vdom.rebuild();
|
||||
apply_edits(
|
||||
mutations,
|
||||
&mut edit_channel,
|
||||
&mut templates,
|
||||
&mut max_template_count,
|
||||
)
|
||||
} {
|
||||
// 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
|
||||
@@ -164,7 +177,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
||||
match evt.as_ref().map(|o| o.as_deref()) {
|
||||
// respond with a pong every ping to keep the websocket alive
|
||||
Some(Ok(b"__ping__")) => {
|
||||
ws.send(b"__pong__".to_vec()).await?;
|
||||
ws.send(text_frame("__pong__")).await?;
|
||||
}
|
||||
Some(Ok(evt)) => {
|
||||
if let Ok(message) = serde_json::from_str::<IpcMessage>(&String::from_utf8_lossy(evt)) {
|
||||
@@ -202,7 +215,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
||||
|
||||
// handle any new queries
|
||||
Some(query) = query_rx.recv() => {
|
||||
ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap().into_bytes()).await?;
|
||||
ws.send(text_frame(&serde_json::to_string(&ClientUpdate::Query(query)).unwrap())).await?;
|
||||
}
|
||||
|
||||
Some(msg) = hot_reload_wait => {
|
||||
@@ -224,20 +237,156 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
||||
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
|
||||
.await;
|
||||
|
||||
ws.send(
|
||||
serde_json::to_string(&ClientUpdate::Edits(edits))
|
||||
.unwrap()
|
||||
.into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
if let Some(edits) = {
|
||||
apply_edits(
|
||||
edits,
|
||||
&mut edit_channel,
|
||||
&mut templates,
|
||||
&mut max_template_count,
|
||||
)
|
||||
} {
|
||||
ws.send(edits).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text_frame(text: &str) -> Vec<u8> {
|
||||
let mut bytes = vec![0];
|
||||
bytes.extend(text.as_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn add_template(
|
||||
template: &Template<'static>,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &mut u16,
|
||||
) {
|
||||
for root in template.roots.iter() {
|
||||
create_template_node(channel, root);
|
||||
templates.insert(template.name.to_owned(), *max_template_count);
|
||||
}
|
||||
channel.add_templates(*max_template_count, template.roots.len() as u16);
|
||||
|
||||
*max_template_count += 1
|
||||
}
|
||||
|
||||
fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) {
|
||||
use TemplateNode::*;
|
||||
match v {
|
||||
Element {
|
||||
tag,
|
||||
namespace,
|
||||
attrs,
|
||||
children,
|
||||
..
|
||||
} => {
|
||||
// Push the current node onto the stack
|
||||
match namespace {
|
||||
Some(ns) => channel.create_element_ns(tag, ns),
|
||||
None => channel.create_element(tag),
|
||||
}
|
||||
// Set attributes on the current node
|
||||
for attr in *attrs {
|
||||
if let TemplateAttribute::Static {
|
||||
name,
|
||||
value,
|
||||
namespace,
|
||||
} = attr
|
||||
{
|
||||
channel.set_top_attribute(name, value, namespace.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
// Add each child to the stack
|
||||
for child in *children {
|
||||
create_template_node(channel, child);
|
||||
}
|
||||
// Add all children to the parent
|
||||
channel.append_children_to_top(children.len() as u16);
|
||||
}
|
||||
Text { text } => channel.create_raw_text(text),
|
||||
DynamicText { .. } => channel.create_raw_text("p"),
|
||||
Dynamic { .. } => channel.add_placeholder(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_edits(
|
||||
mutations: Mutations,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &mut u16,
|
||||
) -> Option<Vec<u8>> {
|
||||
use dioxus_core::Mutation::*;
|
||||
if mutations.templates.is_empty() && mutations.edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
for template in mutations.templates {
|
||||
add_template(&template, channel, templates, max_template_count);
|
||||
}
|
||||
for edit in mutations.edits {
|
||||
match edit {
|
||||
AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
|
||||
AssignId { path, id } => channel.assign_id(path, id.0 as u32),
|
||||
CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
|
||||
CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
|
||||
HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
|
||||
LoadTemplate { name, index, id } => {
|
||||
if let Some(tmpl_id) = templates.get(name) {
|
||||
channel.load_template(*tmpl_id, index as u16, id.0 as u32)
|
||||
}
|
||||
}
|
||||
ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
|
||||
ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
|
||||
InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
|
||||
InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
|
||||
SetAttribute {
|
||||
name,
|
||||
value,
|
||||
id,
|
||||
ns,
|
||||
} => match value {
|
||||
BorrowedAttributeValue::Text(txt) => {
|
||||
channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Float(f) => {
|
||||
channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Int(n) => {
|
||||
channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Bool(b) => channel.set_attribute(
|
||||
id.0 as u32,
|
||||
name,
|
||||
if b { "true" } else { "false" },
|
||||
ns.unwrap_or_default(),
|
||||
),
|
||||
BorrowedAttributeValue::None => {
|
||||
channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
SetText { value, id } => channel.set_text(id.0 as u32, value),
|
||||
NewEventListener { name, id, .. } => {
|
||||
channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
}
|
||||
RemoveEventListener { name, id } => {
|
||||
channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
}
|
||||
Remove { id } => channel.remove(id.0 as u32),
|
||||
PushRoot { id } => channel.push_root(id.0 as u32),
|
||||
}
|
||||
}
|
||||
|
||||
// Add an extra one at the beginning to tell the shim this is a binary frame
|
||||
let mut bytes = vec![1];
|
||||
bytes.extend(channel.export_memory());
|
||||
channel.reset();
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
enum ClientUpdate<'a> {
|
||||
#[serde(rename = "edits")]
|
||||
Edits(Mutations<'a>),
|
||||
enum ClientUpdate {
|
||||
#[serde(rename = "query")]
|
||||
Query(String),
|
||||
}
|
||||
@@ -169,10 +169,7 @@ pub(crate) struct Query<V: DeserializeOwned> {
|
||||
impl<V: DeserializeOwned> Query<V> {
|
||||
/// Resolve the query
|
||||
pub async fn resolve(mut self) -> Result<V, QueryError> {
|
||||
match self.receiver.recv().await {
|
||||
Some(result) => V::deserialize(result).map_err(QueryError::Deserialize),
|
||||
None => Err(QueryError::Recv(RecvError::Closed)),
|
||||
}
|
||||
V::deserialize(self.result().await?).map_err(QueryError::Deserialize)
|
||||
}
|
||||
|
||||
/// Send a message to the query
|
||||
|
||||
Reference in new issue
Block a user