Merge pull request #1534 from ealmloff/binary-protocal

Binary protocol for desktop and liveview
This commit is contained in:
Jonathan Kelley 2024-01-04 10:17:16 -08:00 committed by GitHub
commit 70ff508163
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1199 additions and 837 deletions

View file

@ -1,20 +1,20 @@
use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Discovery error: {0}")]
OpenIdConnect(
#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>,
),
#[error("Parsing error: {0}")]
Parse(#[from] url::ParseError),
#[error("Request token error: {0}")]
RequestToken(
#[from]
RequestTokenError<
openidconnect::reqwest::Error<reqwest::Error>,
StandardErrorResponse<CoreErrorResponseType>,
>,
),
}
use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Discovery error: {0}")]
OpenIdConnect(
#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>,
),
#[error("Parsing error: {0}")]
Parse(#[from] url::ParseError),
#[error("Request token error: {0}")]
RequestToken(
#[from]
RequestTokenError<
openidconnect::reqwest::Error<reqwest::Error>,
StandardErrorResponse<CoreErrorResponseType>,
>,
),
}

View file

@ -26,7 +26,7 @@ pub struct Mutations<'a> {
/// Any templates encountered while diffing the DOM.
///
/// These must be loaded into a cache before applying the edits
pub templates: Vec<Template<'a>>,
pub templates: Vec<Template<'static>>,
/// Any mutations required to patch the renderer to match the layout of the VirtualDom
pub edits: Vec<Mutation<'a>>,

1
packages/desktop/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/src/minified.js

View file

@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true, features = ["serialize", "native-bind"] }
dioxus-interpreter-js = { workspace = true }
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
dioxus-hot-reload = { workspace = true, optional = true }
serde = "1.0.136"
@ -33,6 +33,7 @@ webbrowser = "0.8.0"
infer = "0.11.0"
dunce = "1.0.2"
slab = { workspace = true }
rustc-hash = { workspace = true }
futures-util = { workspace = true }
urlencoding = "2.1.2"
@ -58,6 +59,7 @@ tokio_runtime = ["tokio"]
fullscreen = ["wry/fullscreen"]
transparent = ["wry/transparent"]
devtools = ["wry/devtools"]
dox = ["wry/dox"]
hot-reload = ["dioxus-hot-reload"]
gnu = []
@ -72,6 +74,10 @@ dioxus = { workspace = true }
exitcode = "1.1.2"
scraper = "0.16.0"
[build-dependencies]
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
minify-js = "0.5.6"
# These tests need to be run on the main thread, so they cannot use rust's test harness.
[[test]]
name = "check_events"

View file

@ -1,4 +1,19 @@
fn main() {
use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS;
use std::io::Write;
const EDITS_PATH: &str = {
#[cfg(any(target_os = "android", target_os = "windows"))]
{
"http://dioxus.index.html/edits"
}
#[cfg(not(any(target_os = "android", target_os = "windows")))]
{
"dioxus://index.html/edits"
}
};
fn check_gnu() {
// WARN about wry support on windows gnu targets. GNU windows targets don't work well in wry currently
if std::env::var("CARGO_CFG_WINDOWS").is_ok()
&& std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu"
@ -7,3 +22,73 @@ fn main() {
println!("cargo:warning=GNU windows targets have some limitations within Wry. Using the MSVC windows toolchain is recommended. If you would like to use continue using GNU, you can read https://github.com/wravery/webview2-rs#cross-compilation and disable this warning by adding the gnu feature to dioxus-desktop in your Cargo.toml")
}
}
fn main() {
check_gnu();
let prevent_file_upload = r#"// Prevent file inputs from opening the file dialog on click
let inputs = document.querySelectorAll("input");
for (let input of inputs) {
if (!input.getAttribute("data-dioxus-file-listener")) {
// prevent file inputs from opening the file dialog on click
const type = input.getAttribute("type");
if (type === "file") {
input.setAttribute("data-dioxus-file-listener", true);
input.addEventListener("click", (event) => {
let target = event.target;
let target_id = find_real_id(target);
if (target_id !== null) {
const send = (event_name) => {
const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
window.ipc.postMessage(message);
};
send("change&input");
}
event.preventDefault();
});
}
}
}"#;
let polling_request = format!(
r#"// Poll for requests
window.interpreter.wait_for_request = (headless) => {{
fetch(new Request("{EDITS_PATH}"))
.then(response => {{
response.arrayBuffer()
.then(bytes => {{
// In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly
if (headless) {{
run_from_bytes(bytes);
}}
else {{
requestAnimationFrame(() => {{
run_from_bytes(bytes);
}});
}}
window.interpreter.wait_for_request(headless);
}});
}})
}}"#
);
let mut interpreter = SLEDGEHAMMER_JS
.replace("/*POST_HANDLE_EDITS*/", prevent_file_upload)
.replace("export", "")
+ &polling_request;
while let Some(import_start) = interpreter.find("import") {
let import_end = interpreter[import_start..]
.find(|c| c == ';' || c == '\n')
.map(|i| i + import_start)
.unwrap_or_else(|| interpreter.len());
interpreter.replace_range(import_start..import_end, "");
}
let js = format!("{interpreter}\nconst config = new InterpreterConfig(false);");
use minify_js::*;
let session = Session::new();
let mut out = Vec::new();
minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap();
let minified = String::from_utf8(out).unwrap();
let mut file = std::fs::File::create("src/minified.js").unwrap();
file.write_all(minified.as_bytes()).unwrap();
}

View file

@ -0,0 +1,32 @@
use dioxus::prelude::*;
fn app(cx: Scope) -> Element {
let state = use_state(cx, || 0);
use_future(cx, (), |_| {
to_owned![state];
async move {
loop {
state += 1;
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
}
}
});
cx.render(rsx! {
button {
onclick: move |_| {
state.set(0);
},
"reset"
}
for _ in 0..10000 {
div {
"hello desktop! {state}"
}
}
})
}
fn main() {
dioxus_desktop::launch(app);
}

View file

@ -52,7 +52,7 @@ fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) {
#[allow(deprecated)]
fn app(cx: Scope) -> Element {
let desktop_context: DesktopContext = cx.consume_context().unwrap();
let recieved_events = use_state(cx, || 0);
let received_events = use_state(cx, || 0);
// button
mock_event(
@ -216,7 +216,7 @@ fn app(cx: Scope) -> Element {
r#"new FocusEvent("focusout",{bubbles: true})"#,
);
if **recieved_events == 12 {
if **received_events == 12 {
println!("all events recieved");
desktop_context.close();
}
@ -229,6 +229,9 @@ fn app(cx: Scope) -> Element {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().is_empty());
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
received_events.modify(|x| *x + 1)
},
assert_eq!(
event.data.trigger_button(),
Some(dioxus_html::input_data::MouseButton::Primary),
@ -241,13 +244,8 @@ fn app(cx: Scope) -> Element {
onmousemove: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(
event
.data
.held_buttons()
.contains(dioxus_html::input_data::MouseButton::Secondary),
);
recieved_events.modify(|x| *x + 1)
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
received_events.modify(|x| *x + 1)
}
}
div {
@ -255,17 +253,9 @@ fn app(cx: Scope) -> Element {
onclick: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(
event
.data
.held_buttons()
.contains(dioxus_html::input_data::MouseButton::Secondary),
);
assert_eq!(
event.data.trigger_button(),
Some(dioxus_html::input_data::MouseButton::Secondary),
);
recieved_events.modify(|x| *x + 1)
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
received_events.modify(|x| *x + 1)
}
}
div {
@ -273,20 +263,10 @@ fn app(cx: Scope) -> Element {
ondoubleclick: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(
event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary),
);
assert!(
event
.data
.held_buttons()
.contains(dioxus_html::input_data::MouseButton::Secondary),
);
assert_eq!(
event.data.trigger_button(),
Some(dioxus_html::input_data::MouseButton::Secondary),
);
recieved_events.modify(|x| *x + 1)
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
received_events.modify(|x| *x + 1)
}
}
div {
@ -294,17 +274,9 @@ fn app(cx: Scope) -> Element {
onmousedown: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(
event
.data
.held_buttons()
.contains(dioxus_html::input_data::MouseButton::Secondary),
);
assert_eq!(
event.data.trigger_button(),
Some(dioxus_html::input_data::MouseButton::Secondary),
);
recieved_events.modify(|x| *x + 1)
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
received_events.modify(|x| *x + 1)
}
}
div {
@ -313,11 +285,8 @@ fn app(cx: Scope) -> Element {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().is_empty());
assert_eq!(
event.data.trigger_button(),
Some(dioxus_html::input_data::MouseButton::Primary),
);
recieved_events.modify(|x| *x + 1)
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
received_events.modify(|x| *x + 1)
}
}
div {
@ -330,7 +299,7 @@ fn app(cx: Scope) -> Element {
let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else {
panic!("Expected delta to be in pixels") };
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
recieved_events.modify(|x| *x + 1)
received_events.modify(|x| *x + 1)
}
}
input {
@ -342,7 +311,8 @@ fn app(cx: Scope) -> Element {
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(event.data.is_auto_repeating());
recieved_events.modify(|x| *x + 1)
received_events.modify(|x| *x + 1)
}
}
input {
@ -354,7 +324,7 @@ fn app(cx: Scope) -> Element {
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(!event.data.is_auto_repeating());
recieved_events.modify(|x| *x + 1)
received_events.modify(|x| *x + 1)
}
}
input {
@ -366,21 +336,21 @@ fn app(cx: Scope) -> Element {
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(!event.data.is_auto_repeating());
recieved_events.modify(|x| *x + 1)
received_events.modify(|x| *x + 1)
}
}
input {
id: "focus_in_div",
onfocusin: move |event| {
println!("{:?}", event.data);
recieved_events.modify(|x| *x + 1)
received_events.modify(|x| *x + 1)
}
}
input {
id: "focus_out_div",
onfocusout: move |event| {
println!("{:?}", event.data);
recieved_events.modify(|x| *x + 1)
received_events.modify(|x| *x + 1)
}
}
}

View file

@ -6,7 +6,6 @@ use wry::{
application::window::{Window, WindowBuilder},
http::{Request as HttpRequest, Response as HttpResponse},
webview::FileDropEvent,
Result as WryResult,
};
// pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
@ -43,7 +42,7 @@ type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
pub(crate) type WryProtocol = (
String,
Box<dyn Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static>,
Box<dyn Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static>,
);
impl Config {
@ -130,7 +129,7 @@ impl Config {
/// Set a custom protocol
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
where
F: Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static,
F: Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static,
{
self.protocols.push((name, Box::new(handler)));
self

View file

@ -1,7 +1,3 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::rc::Weak;
use crate::create_new_window;
use crate::events::IpcMessage;
use crate::protocol::AssetFuture;
@ -15,7 +11,17 @@ use dioxus_core::ScopeState;
use dioxus_core::VirtualDom;
#[cfg(all(feature = "hot-reload", debug_assertions))]
use dioxus_hot_reload::HotReloadMsg;
use dioxus_interpreter_js::binary_protocol::Channel;
use rustc_hash::FxHashMap;
use slab::Slab;
use std::cell::RefCell;
use std::fmt::Debug;
use std::fmt::Formatter;
use std::rc::Rc;
use std::rc::Weak;
use std::sync::atomic::AtomicU16;
use std::sync::Arc;
use std::sync::Mutex;
use wry::application::event::Event;
use wry::application::event_loop::EventLoopProxy;
use wry::application::event_loop::EventLoopWindowTarget;
@ -35,6 +41,45 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
.unwrap()
}
/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events.
/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made.
#[derive(Default, Clone)]
pub(crate) struct EditQueue {
queue: Arc<Mutex<Vec<Vec<u8>>>>,
responder: Arc<Mutex<Option<wry::webview::RequestAsyncResponder>>>,
}
impl Debug for EditQueue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EditQueue")
.field("queue", &self.queue)
.field("responder", {
&self.responder.lock().unwrap().as_ref().map(|_| ())
})
.finish()
}
}
impl EditQueue {
pub fn handle_request(&self, responder: wry::webview::RequestAsyncResponder) {
let mut queue = self.queue.lock().unwrap();
if let Some(bytes) = queue.pop() {
responder.respond(wry::http::Response::new(bytes));
} else {
*self.responder.lock().unwrap() = Some(responder);
}
}
pub fn add_edits(&self, edits: Vec<u8>) {
let mut responder = self.responder.lock().unwrap();
if let Some(responder) = responder.take() {
responder.respond(wry::http::Response::new(edits));
} else {
self.queue.lock().unwrap().push(edits);
}
}
}
pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
/// An imperative interface to the current window.
@ -67,6 +112,11 @@ pub struct DesktopService {
pub(crate) shortcut_manager: ShortcutRegistry,
pub(crate) edit_queue: EditQueue,
pub(crate) templates: RefCell<FxHashMap<String, u16>>,
pub(crate) max_template_count: AtomicU16,
pub(crate) channel: RefCell<Channel>,
pub(crate) asset_handlers: AssetHandlerRegistry,
#[cfg(target_os = "ios")]
@ -93,6 +143,7 @@ impl DesktopService {
webviews: WebviewQueue,
event_handlers: WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
edit_queue: EditQueue,
asset_handlers: AssetHandlerRegistry,
) -> Self {
Self {
@ -103,6 +154,10 @@ impl DesktopService {
pending_windows: webviews,
event_handlers,
shortcut_manager,
edit_queue,
templates: Default::default(),
max_template_count: Default::default(),
channel: Default::default(),
asset_handlers,
#[cfg(target_os = "ios")]
views: Default::default(),

View file

@ -30,7 +30,7 @@ impl RenderedElementBacking for DesktopElement {
>,
>,
> {
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 DesktopElement {
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")
);
@ -81,7 +81,7 @@ impl RenderedElementBacking for DesktopElement {
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
);

View file

@ -27,16 +27,19 @@ pub use desktop_context::{
};
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
use dioxus_core::*;
use dioxus_html::MountedData;
use dioxus_html::{event_bubbles, MountedData};
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
use dioxus_interpreter_js::binary_protocol::Channel;
use element::DesktopElement;
use eval::init_eval;
use futures_util::{pin_mut, FutureExt};
use rustc_hash::FxHashMap;
pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse};
use shortcut::ShortcutRegistry;
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::atomic::AtomicU16;
use std::task::Waker;
use std::{collections::HashMap, sync::Arc};
pub use tao::dpi::{LogicalSize, PhysicalSize};
@ -280,7 +283,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
let evt = match serde_json::from_value::<HtmlEvent>(params) {
Ok(value) => value,
Err(_) => return,
Err(err) => {
tracing::error!("Error parsing user_event: {:?}", err);
return;
}
};
let HtmlEvent {
@ -312,7 +318,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
view.dom.handle_event(&name, as_any, element, bubbles);
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
send_edits(view.dom.render_immediate(), &view.desktop_context);
}
// 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
@ -335,7 +341,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
EventData::Ipc(msg) if msg.method() == "initialize" => {
let view = webviews.get_mut(&event.1).unwrap();
send_edits(view.dom.rebuild(), &view.desktop_context.webview);
send_edits(view.dom.rebuild(), &view.desktop_context);
view.desktop_context
.webview
.window()
@ -377,7 +383,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
view.dom.handle_event(event_name, data, id, event_bubbles);
}
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
send_edits(view.dom.render_immediate(), &view.desktop_context);
}
}
@ -397,7 +403,7 @@ fn create_new_window(
event_handlers: &WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
) -> WebviewHandler {
let (webview, web_context, asset_handlers) =
let (webview, web_context, asset_handlers, edit_queue) =
webview::build(&mut cfg, event_loop, proxy.clone());
let desktop_context = Rc::from(DesktopService::new(
webview,
@ -407,6 +413,7 @@ fn create_new_window(
event_handlers.clone(),
shortcut_manager,
asset_handlers,
edit_queue,
));
let cx = dom.base_scope();
@ -453,16 +460,149 @@ fn poll_vdom(view: &mut WebviewHandler) {
}
}
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
send_edits(view.dom.render_immediate(), &view.desktop_context);
}
}
/// Send a list of mutations to the webview
fn send_edits(edits: Mutations, webview: &WebView) {
let serialized = serde_json::to_string(&edits).unwrap();
fn send_edits(edits: Mutations, desktop_context: &DesktopContext) {
let mut channel = desktop_context.channel.borrow_mut();
let mut templates = desktop_context.templates.borrow_mut();
if let Some(bytes) = apply_edits(
edits,
&mut channel,
&mut templates,
&desktop_context.max_template_count,
) {
desktop_context.edit_queue.add_edits(bytes)
}
}
// todo: use SSE and binary data to send the edits with lower overhead
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
fn apply_edits(
mutations: Mutations,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) -> 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),
}
}
let bytes: Vec<_> = channel.export_memory().collect();
channel.reset();
Some(bytes)
}
fn add_template(
template: &Template<'static>,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) {
let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed);
for root in template.roots.iter() {
create_template_node(channel, root);
templates.insert(template.name.to_owned(), current_max_template_count);
}
channel.add_templates(current_max_template_count, template.roots.len() as u16);
max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
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(),
}
}
/// Different hide implementations per platform

View file

@ -16,12 +16,16 @@ use tokio::{
};
use wry::{
http::{status::StatusCode, Request, Response},
webview::RequestAsyncResponder,
Result,
};
use crate::{use_window, DesktopContext};
fn module_loader(root_name: &str) -> String {
use crate::desktop_context::EditQueue;
static MINIFIED: &str = include_str!("./minified.js");
fn module_loader(root_name: &str, headless: bool) -> String {
let js = INTERPRETER_JS.replace(
"/*POST_HANDLE_EDITS*/",
r#"// Prevent file inputs from opening the file dialog on click
@ -48,16 +52,20 @@ fn module_loader(root_name: &str) -> String {
}
}"#,
);
format!(
r#"
<script type="module">
{js}
let rootname = "{root_name}";
let root = window.document.getElementById(rootname);
if (root != null) {{
window.interpreter = new Interpreter(root, new InterpreterConfig(true));
window.ipc.postMessage(serializeIpcMessage("initialize"));
{MINIFIED}
// Wait for the page to load
window.onload = function() {{
let rootname = "{root_name}";
let root_element = window.document.getElementById(rootname);
if (root_element != null) {{
window.interpreter.initialize(root_element);
window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
}}
window.interpreter.wait_for_request({headless});
}}
</script>
"#
@ -211,6 +219,8 @@ pub(super) async fn desktop_handler(
custom_index: Option<String>,
root_name: &str,
asset_handlers: &AssetHandlerRegistry,
edit_queue: &EditQueue,
headless: bool,
) -> Result<AssetResponse> {
let request = AssetRequest::from(request);
@ -220,7 +230,10 @@ pub(super) async fn desktop_handler(
// we'll look for the closing </body> tag and insert our little module loader there.
let body = match custom_index {
Some(custom_index) => custom_index
.replace("</body>", &format!("{}</body>", module_loader(root_name)))
.replace(
"</body>",
&format!("{}</body>", module_loader(root_name, headless)),
)
.into_bytes(),
None => {
@ -232,20 +245,28 @@ pub(super) async fn desktop_handler(
}
template
.replace("<!-- MODULE LOADER -->", &module_loader(root_name))
.replace(
"<!-- MODULE LOADER -->",
&module_loader(root_name, headless),
)
.into_bytes()
}
};
return Response::builder()
match Response::builder()
.header("Content-Type", "text/html")
.header("Access-Control-Allow-Origin", "*")
.body(Cow::from(body))
.map_err(From::from);
} else if request.uri().path() == "/common.js" {
return Response::builder()
.header("Content-Type", "text/javascript")
.body(Cow::from(COMMON_JS.as_bytes()))
.map_err(From::from);
{
Ok(response) => {
responder.respond(response);
return;
}
Err(err) => tracing::error!("error building response: {}", err),
}
} else if request.uri().path().trim_matches('/') == "edits" {
edit_queue.handle_request(responder);
return;
}
// If the user provided a custom asset handler, then call it and return the response
@ -266,16 +287,41 @@ pub(super) async fn desktop_handler(
}
if asset.exists() {
return Response::builder()
.header("Content-Type", get_mime_from_path(&asset)?)
.body(Cow::from(std::fs::read(asset)?))
.map_err(From::from);
let content_type = match get_mime_from_path(&asset) {
Ok(content_type) => content_type,
Err(err) => {
tracing::error!("error getting mime type: {}", err);
return;
}
};
let asset = match std::fs::read(asset) {
Ok(asset) => asset,
Err(err) => {
tracing::error!("error reading asset: {}", err);
return;
}
};
match Response::builder()
.header("Content-Type", content_type)
.body(Cow::from(asset))
{
Ok(response) => {
responder.respond(response);
return;
}
Err(err) => tracing::error!("error building response: {}", err),
}
}
Response::builder()
match Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Cow::from(String::from("Not Found").into_bytes()))
.map_err(From::from)
{
Ok(response) => {
responder.respond(response);
}
Err(err) => tracing::error!("error building response: {}", err),
}
}
#[allow(unreachable_code)]

View file

@ -1,4 +1,4 @@
use crate::desktop_context::EventData;
use crate::desktop_context::{EditQueue, EventData};
use crate::protocol::{self, AssetHandlerRegistry};
use crate::{desktop_context::UserWindowEvent, Config};
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
@ -9,11 +9,12 @@ use wry::application::window::Window;
use wry::http::Response;
use wry::webview::{WebContext, WebView, WebViewBuilder};
pub fn build(
pub(crate) fn build(
cfg: &mut Config,
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
proxy: EventLoopProxy<UserWindowEvent>,
) -> (WebView, WebContext, AssetHandlerRegistry) {
) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue) {
let builder = cfg.window.clone();
let window = builder.with_visible(false).build(event_loop).unwrap();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
@ -39,6 +40,8 @@ pub fn build(
}
let mut web_context = WebContext::new(cfg.data_dir.clone());
let edit_queue = EditQueue::default();
let headless = !cfg.window.window.visible;
let asset_handlers = AssetHandlerRegistry::new();
let asset_handlers_ref = asset_handlers.clone();
@ -126,7 +129,7 @@ pub fn build(
webview = webview.with_devtools(true);
}
(webview.build().unwrap(), web_context, asset_handlers)
(webview.build().unwrap(), web_context, asset_handlers, edit_queue)
}
/// Builds a standard menu bar depending on the users platform. It may be used as a starting point

View file

@ -14,13 +14,14 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
wasm-bindgen = { workspace = true, optional = true }
js-sys = { version = "0.3.56", optional = true }
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
sledgehammer_bindgen = { version = "0.2.1", optional = true }
sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true }
sledgehammer_utils = { version = "0.2", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
[features]
default = []
serialize = ["serde"]
web = ["wasm-bindgen", "js-sys", "web-sys"]
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
sledgehammer = ["sledgehammer_bindgen", "sledgehammer_utils"]
web = ["sledgehammer", "wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen/web"]
binary-protocol = ["sledgehammer", "wasm-bindgen"]
minimal_bindings = []

View file

@ -1,83 +0,0 @@
#![allow(clippy::unused_unit, non_upper_case_globals)]
use js_sys::Function;
use wasm_bindgen::prelude::*;
use web_sys::Element;
#[wasm_bindgen(module = "/src/interpreter.js")]
extern "C" {
pub type InterpreterConfig;
#[wasm_bindgen(constructor)]
pub fn new(intercept_link_redirects: bool) -> InterpreterConfig;
pub type Interpreter;
#[wasm_bindgen(constructor)]
pub fn new(arg: Element, config: InterpreterConfig) -> Interpreter;
#[wasm_bindgen(method)]
pub fn SaveTemplate(this: &Interpreter, template: JsValue);
#[wasm_bindgen(method)]
pub fn MountToRoot(this: &Interpreter);
#[wasm_bindgen(method)]
pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
#[wasm_bindgen(method)]
pub fn CreatePlaceholder(this: &Interpreter, id: u32);
#[wasm_bindgen(method)]
pub fn CreateTextNode(this: &Interpreter, value: JsValue, id: u32);
#[wasm_bindgen(method)]
pub fn HydrateText(this: &Interpreter, path: &[u8], value: &str, id: u32);
#[wasm_bindgen(method)]
pub fn LoadTemplate(this: &Interpreter, name: &str, index: u32, id: u32);
#[wasm_bindgen(method)]
pub fn ReplaceWith(this: &Interpreter, id: u32, m: u32);
#[wasm_bindgen(method)]
pub fn ReplacePlaceholder(this: &Interpreter, path: &[u8], m: u32);
#[wasm_bindgen(method)]
pub fn InsertAfter(this: &Interpreter, id: u32, n: u32);
#[wasm_bindgen(method)]
pub fn InsertBefore(this: &Interpreter, id: u32, n: u32);
#[wasm_bindgen(method)]
pub fn SetAttribute(this: &Interpreter, id: u32, name: &str, value: JsValue, ns: Option<&str>);
#[wasm_bindgen(method)]
pub fn SetBoolAttribute(this: &Interpreter, id: u32, name: &str, value: bool);
#[wasm_bindgen(method)]
pub fn SetText(this: &Interpreter, id: u32, text: JsValue);
#[wasm_bindgen(method)]
pub fn NewEventListener(
this: &Interpreter,
name: &str,
id: u32,
bubbles: bool,
handler: &Function,
);
#[wasm_bindgen(method)]
pub fn RemoveEventListener(this: &Interpreter, name: &str, id: u32);
#[wasm_bindgen(method)]
pub fn RemoveAttribute(this: &Interpreter, id: u32, field: &str, ns: Option<&str>);
#[wasm_bindgen(method)]
pub fn Remove(this: &Interpreter, id: u32);
#[wasm_bindgen(method)]
pub fn PushRoot(this: &Interpreter, id: u32);
#[wasm_bindgen(method)]
pub fn AppendChildren(this: &Interpreter, id: u32, m: u32);
}

View file

@ -1,33 +1,3 @@
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,
webkitdirectory: true,
};
export function setAttributeInner(node, field, value, ns) {
const name = field;
if (ns === "style") {
@ -36,7 +6,7 @@ export function setAttributeInner(node, field, value, ns) {
node.style = {};
}
node.style[name] = value;
} else if (ns != null && ns != undefined) {
} else if (!!ns) {
node.setAttributeNS(ns, name, value);
} else {
switch (name) {
@ -74,6 +44,36 @@ export function setAttributeInner(node, field, value, ns) {
}
}
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,
webkitdirectory: true,
};
function truthy(val) {
return val === "true" || val === true;
}

View file

@ -1,390 +1,9 @@
import { setAttributeInner } from "./common.js";
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);
}
}
removeAllNonBubbling(element) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id];
}
}
class InterpreterConfig {
constructor(intercept_link_redirects) {
this.intercept_link_redirects = intercept_link_redirects;
}
}
class Interpreter {
constructor(root, config) {
this.config = config;
this.root = root;
this.listeners = new ListenerMap(root);
this.nodes = [root];
this.stack = [root];
this.handlers = {};
this.templates = {};
this.lastNodeWasText = false;
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
MountToRoot() {
this.AppendChildren(this.stack.length - 1);
}
SetNode(id, node) {
this.nodes[id] = node;
}
PushRoot(root) {
const node = this.nodes[root];
this.stack.push(node);
}
PopRoot() {
this.stack.pop();
}
AppendChildren(many) {
// let root = this.nodes[id];
let root = this.stack[this.stack.length - 1 - many];
let to_add = this.stack.splice(this.stack.length - many);
for (let i = 0; i < many; i++) {
root.appendChild(to_add[i]);
}
}
ReplaceWith(root_id, m) {
let root = this.nodes[root_id];
let els = this.stack.splice(this.stack.length - m);
if (is_element_node(root.nodeType)) {
this.listeners.removeAllNonBubbling(root);
}
root.replaceWith(...els);
}
InsertAfter(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.after(...new_nodes);
}
InsertBefore(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.before(...new_nodes);
}
Remove(root) {
let node = this.nodes[root];
if (node !== undefined) {
if (is_element_node(node)) {
this.listeners.removeAllNonBubbling(node);
}
node.remove();
}
}
CreateTextNode(text, root) {
const node = document.createTextNode(text);
this.nodes[root] = node;
this.stack.push(node);
}
CreatePlaceholder(root) {
let el = document.createElement("pre");
el.hidden = true;
this.stack.push(el);
this.nodes[root] = el;
}
NewEventListener(event_name, root, bubbles, handler) {
const element = this.nodes[root];
element.setAttribute("data-dioxus-id", `${root}`);
this.listeners.create(event_name, element, handler, bubbles);
}
RemoveEventListener(root, event_name, bubbles) {
const element = this.nodes[root];
element.removeAttribute(`data-dioxus-id`);
this.listeners.remove(element, event_name, bubbles);
}
SetText(root, text) {
this.nodes[root].textContent = text;
}
SetAttribute(id, field, value, ns) {
if (value === null) {
this.RemoveAttribute(id, field, ns);
} else {
const node = this.nodes[id];
setAttributeInner(node, field, value, ns);
}
}
RemoveAttribute(root, field, ns) {
const node = this.nodes[root];
if (!ns) {
switch (field) {
case "value":
node.value = "";
break;
case "checked":
node.checked = false;
break;
case "selected":
node.selected = false;
break;
case "dangerous_inner_html":
node.innerHTML = "";
break;
default:
node.removeAttribute(field);
break;
}
} else if (ns == "style") {
node.style.removeProperty(name);
} else {
node.removeAttributeNS(ns, field);
}
}
GetClientRect(id) {
const node = this.nodes[id];
if (!node) {
return;
}
const rect = node.getBoundingClientRect();
return {
type: "GetClientRect",
origin: [rect.x, rect.y],
size: [rect.width, rect.height],
};
}
ScrollTo(id, behavior) {
const node = this.nodes[id];
if (!node) {
return false;
}
node.scrollIntoView({
behavior: behavior,
});
return true;
}
/// Set the focus on the element
SetFocus(id, focus) {
const node = this.nodes[id];
if (!node) {
return false;
}
if (focus) {
node.focus();
} else {
node.blur();
}
return true;
}
handleEdits(edits) {
for (let template of edits.templates) {
this.SaveTemplate(template);
}
for (let edit of edits.edits) {
this.handleEdit(edit);
}
/*POST_HANDLE_EDITS*/
}
SaveTemplate(template) {
let roots = [];
for (let root of template.roots) {
roots.push(this.MakeTemplateNode(root));
}
this.templates[template.name] = roots;
}
MakeTemplateNode(node) {
switch (node.type) {
case "Text":
return document.createTextNode(node.text);
case "Dynamic":
let dyn = document.createElement("pre");
dyn.hidden = true;
return dyn;
case "DynamicText":
return document.createTextNode("placeholder");
case "Element":
let el;
if (node.namespace != null) {
el = document.createElementNS(node.namespace, node.tag);
} else {
el = document.createElement(node.tag);
}
for (let attr of node.attrs) {
if (attr.type == "Static") {
setAttributeInner(el, attr.name, attr.value, attr.namespace);
}
}
for (let child of node.children) {
el.appendChild(this.MakeTemplateNode(child));
}
return el;
}
}
AssignId(path, id) {
this.nodes[id] = this.LoadChild(path);
}
LoadChild(path) {
// iterate through each number and get that child
let node = this.stack[this.stack.length - 1];
for (let i = 0; i < path.length; i++) {
node = node.childNodes[path[i]];
}
return node;
}
HydrateText(path, value, id) {
let node = this.LoadChild(path);
if (node.nodeType == Node.TEXT_NODE) {
node.textContent = value;
} else {
// replace with a textnode
let text = document.createTextNode(value);
node.replaceWith(text);
node = text;
}
this.nodes[id] = node;
}
ReplacePlaceholder(path, m) {
let els = this.stack.splice(this.stack.length - m);
let node = this.LoadChild(path);
node.replaceWith(...els);
}
LoadTemplate(name, index, id) {
let node = this.templates[name][index].cloneNode(true);
this.nodes[id] = node;
this.stack.push(node);
}
handleEdit(edit) {
switch (edit.type) {
case "AppendChildren":
this.AppendChildren(edit.m);
break;
case "AssignId":
this.AssignId(edit.path, edit.id);
break;
case "CreatePlaceholder":
this.CreatePlaceholder(edit.id);
break;
case "CreateTextNode":
this.CreateTextNode(edit.value, edit.id);
break;
case "HydrateText":
this.HydrateText(edit.path, edit.value, edit.id);
break;
case "LoadTemplate":
this.LoadTemplate(edit.name, edit.index, edit.id);
break;
case "PushRoot":
this.PushRoot(edit.id);
break;
case "ReplaceWith":
this.ReplaceWith(edit.id, edit.m);
break;
case "ReplacePlaceholder":
this.ReplacePlaceholder(edit.path, edit.m);
break;
case "InsertAfter":
this.InsertAfter(edit.id, edit.m);
break;
case "InsertBefore":
this.InsertBefore(edit.id, edit.m);
break;
case "Remove":
this.Remove(edit.id);
break;
case "SetText":
this.SetText(edit.id, edit.value);
break;
case "SetAttribute":
this.SetAttribute(edit.id, edit.name, edit.value, edit.ns);
break;
case "RemoveAttribute":
this.RemoveAttribute(edit.id, edit.name, edit.ns);
break;
case "RemoveEventListener":
this.RemoveEventListener(edit.id, edit.name);
break;
case "NewEventListener":
let bubbles = event_bubbles(edit.name);
// 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, this.config);
});
}
break;
}
}
}
// this handler is only provided on the desktop and liveview implementations since this
// method is not used by the web implementation
function handler(event, name, bubbles, config) {
@ -416,7 +35,7 @@ function handler(event, name, bubbles, config) {
const href = a_element.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.ipc.postMessage(
serializeIpcMessage("browser_open", { href })
window.interpreter.serializeIpcMessage("browser_open", { href })
);
}
}
@ -444,7 +63,44 @@ function handler(event, name, bubbles, config) {
let contents = serialize_event(event);
/*POST_EVENT_SERIALIZATION*/
// TODO: this should be liveview only
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 = window.interpreter.serializeIpcMessage("user_event", {
name: name,
element: parseInt(realId),
data: contents,
bubbles,
});
window.ipc.postMessage(message);
}
read_files();
return;
}
}
if (
target.tagName === "FORM" &&
@ -476,7 +132,7 @@ function handler(event, name, bubbles, config) {
return;
}
window.ipc.postMessage(
serializeIpcMessage("user_event", {
window.interpreter.serializeIpcMessage("user_event", {
name: name,
element: parseInt(realId),
data: contents,
@ -506,6 +162,130 @@ function find_real_id(target) {
return realId;
}
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 = null;
}
create(event_name, element, bubbles, handler) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = {};
this.global[event_name].active = 1;
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] = {};
}
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, this.global[event_name].callback);
}
}
removeAllNonBubbling(element) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id];
}
}
function LoadChild(array) {
// iterate through each number and get that child
node = stack[stack.length - 1];
for (let i = 0; i < array.length; i++) {
end = array[i];
for (node = node.firstChild; end > 0; end--) {
node = node.nextSibling;
}
}
return node;
}
const listeners = new ListenerMap();
let nodes = [];
let stack = [];
let root;
const templates = {};
let node, els, end, k;
function AppendChildren(id, many) {
root = nodes[id];
els = stack.splice(stack.length - many);
for (k = 0; k < many; k++) {
root.appendChild(els[k]);
}
}
window.interpreter = {}
window.interpreter.initialize = function (root) {
nodes = [root];
stack = [root];
listeners.root = root;
}
window.interpreter.getClientRect = function (id) {
const node = nodes[id];
if (!node) {
return;
}
const rect = node.getBoundingClientRect();
return {
type: "GetClientRect",
origin: [rect.x, rect.y],
size: [rect.width, rect.height],
};
}
window.interpreter.scrollTo = function (id, behavior) {
const node = nodes[id];
if (!node) {
return false;
}
node.scrollIntoView({
behavior: behavior,
});
return true;
}
/// Set the focus on the element
window.interpreter.setFocus = function (id, focus) {
const node = nodes[id];
if (!node) {
return false;
}
if (focus) {
node.focus();
} else {
node.blur();
}
return true;
}
function get_mouse_data(event) {
const {
altKey,
@ -782,7 +562,7 @@ function serialize_event(event) {
}
}
}
function serializeIpcMessage(method, params = {}) {
window.interpreter.serializeIpcMessage = function (method, params = {}) {
return JSON.stringify({ method, params });
}

View file

@ -10,14 +10,8 @@ mod sledgehammer_bindings;
#[cfg(feature = "sledgehammer")]
pub use sledgehammer_bindings::*;
#[cfg(feature = "web")]
mod bindings;
#[cfg(feature = "web")]
pub use bindings::Interpreter;
// Common bindings for minimal usage.
#[cfg(feature = "minimal_bindings")]
#[cfg(all(feature = "minimal_bindings", feature = "web"))]
pub mod minimal_bindings {
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
#[wasm_bindgen(module = "/src/common.js")]

View file

@ -1,9 +1,17 @@
#[cfg(feature = "web")]
use js_sys::Function;
#[cfg(feature = "web")]
use sledgehammer_bindgen::bindgen;
#[cfg(feature = "web")]
use web_sys::Node;
#[bindgen]
#[cfg(feature = "web")]
pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
#[cfg(feature = "web")]
#[bindgen(module)]
mod js {
const JS_FILE: &str = "./packages/interpreter/src/common.js";
const JS: &str = r#"
class ListenerMap {
constructor(root) {
@ -57,51 +65,6 @@ mod js {
delete this.local[id];
}
}
function SetAttributeInner(node, field, value, ns) {
const name = field;
if (ns === "style") {
// ????? why do we need to do this
if (node.style === undefined) {
node.style = {};
}
node.style[name] = value;
} else if (ns !== null && ns !== undefined && ns !== "") {
node.setAttributeNS(ns, name, value);
} else {
switch (name) {
case "value":
if (value !== node.value) {
node.value = value;
}
break;
case "initial_value":
node.defaultValue = value;
break;
case "checked":
node.checked = truthy(value);
break;
case "initial_checked":
node.defaultChecked = truthy(value);
break;
case "selected":
node.selected = truthy(value);
break;
case "initial_selected":
node.defaultSelected = truthy(value);
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 (!truthy(value) && bool_attrs.hasOwnProperty(name)) {
node.removeAttribute(name);
} else {
node.setAttribute(name, value);
}
}
}
}
function LoadChild(ptr, len) {
// iterate through each number and get that child
node = stack[stack.length - 1];
@ -129,7 +92,7 @@ mod js {
export function get_node(id) {
return nodes[id];
}
export function initilize(root, handler) {
export function initialize(root, handler) {
listeners.handler = handler;
nodes = [root];
stack = [root];
@ -142,43 +105,11 @@ mod js {
root.appendChild(els[k]);
}
}
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,
webkitdirectory: true,
};
function truthy(val) {
return val === "true" || val === true;
}
"#;
extern "C" {
#[wasm_bindgen]
pub fn save_template(nodes: Vec<Node>, tmpl_id: u32);
pub fn save_template(nodes: Vec<Node>, tmpl_id: u16);
#[wasm_bindgen]
pub fn set_node(id: u32, node: Node);
@ -187,7 +118,7 @@ mod js {
pub fn get_node(id: u32) -> Node;
#[wasm_bindgen]
pub fn initilize(root: Node, handler: &Function);
pub fn initialize(root: Node, handler: &Function);
}
fn mount_to_root() {
@ -196,19 +127,19 @@ mod js {
fn push_root(root: u32) {
"{stack.push(nodes[$root$]);}"
}
fn append_children(id: u32, many: u32) {
fn append_children(id: u32, many: u16) {
"{AppendChildren($id$, $many$);}"
}
fn pop_root() {
"{stack.pop();}"
}
fn replace_with(id: u32, n: u32) {
fn replace_with(id: u32, n: u16) {
"{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
}
fn insert_after(id: u32, n: u32) {
fn insert_after(id: u32, n: u16) {
"{nodes[$id$].after(...stack.splice(stack.length-$n$));}"
}
fn insert_before(id: u32, n: u32) {
fn insert_before(id: u32, n: u16) {
"{nodes[$id$].before(...stack.splice(stack.length-$n$));}"
}
fn remove(id: u32) {
@ -233,7 +164,7 @@ mod js {
"{nodes[$id$].textContent = $text$;}"
}
fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
"{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}"
"{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}"
}
fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
r#"{
@ -279,10 +210,167 @@ mod js {
nodes[$id$] = node;
}"#
}
fn replace_placeholder(ptr: u32, len: u8, n: u32) {
fn replace_placeholder(ptr: u32, len: u8, n: u16) {
"{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}"
}
fn load_template(tmpl_id: u32, index: u32, id: u32) {
fn load_template(tmpl_id: u16, index: u16, id: u32) {
"{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}"
}
}
#[cfg(feature = "binary-protocol")]
pub mod binary_protocol {
use sledgehammer_bindgen::bindgen;
pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
#[bindgen]
mod protocol_js {
const JS_FILE: &str = "./packages/interpreter/src/interpreter.js";
const JS_FILE: &str = "./packages/interpreter/src/common.js";
fn mount_to_root() {
"{AppendChildren(root, stack.length-1);}"
}
fn push_root(root: u32) {
"{stack.push(nodes[$root$]);}"
}
fn append_children(id: u32, many: u16) {
"{AppendChildren($id$, $many$);}"
}
fn append_children_to_top(many: u16) {
"{
root = stack[stack.length-many-1];
els = stack.splice(stack.length-many);
for (k = 0; k < many; k++) {
root.appendChild(els[k]);
}
}"
}
fn pop_root() {
"{stack.pop();}"
}
fn replace_with(id: u32, n: u16) {
"{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
}
fn insert_after(id: u32, n: u16) {
"{nodes[$id$].after(...stack.splice(stack.length-$n$));}"
}
fn insert_before(id: u32, n: u16) {
"{nodes[$id$].before(...stack.splice(stack.length-$n$));}"
}
fn remove(id: u32) {
"{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}"
}
fn create_raw_text(text: &str) {
"{stack.push(document.createTextNode($text$));}"
}
fn create_text_node(text: &str, id: u32) {
"{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}"
}
fn create_element(element: &'static str<u8, el>) {
"{stack.push(document.createElement($element$))}"
}
fn create_element_ns(element: &'static str<u8, el>, ns: &'static str<u8, namespace>) {
"{stack.push(document.createElementNS($ns$, $element$))}"
}
fn create_placeholder(id: u32) {
"{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}"
}
fn add_placeholder() {
"{node = document.createElement('pre'); node.hidden = true; stack.push(node);}"
}
fn new_event_listener(event: &str<u8, evt>, id: u32, bubbles: u8) {
r#"
bubbles = bubbles == 1;
node = nodes[id];
if(node.listening){
node.listening += 1;
} else {
node.listening = 1;
}
node.setAttribute('data-dioxus-id', `\${id}`);
const event_name = $event$;
// if this is a mounted listener, we send the event immediately
if (event_name === "mounted") {
window.ipc.postMessage(
window.interpreter.serializeIpcMessage("user_event", {
name: event_name,
element: id,
data: null,
bubbles,
})
);
} else {
listeners.create(event_name, node, bubbles, (event) => {
handler(event, event_name, bubbles, config);
});
}"#
}
fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
"{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}"
}
fn set_text(id: u32, text: &str) {
"{nodes[$id$].textContent = $text$;}"
}
fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
"{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}"
}
fn set_top_attribute(field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
"{setAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}"
}
fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
r#"{
node = nodes[$id$];
if (!ns) {
switch (field) {
case "value":
node.value = "";
break;
case "checked":
node.checked = false;
break;
case "selected":
node.selected = false;
break;
case "dangerous_inner_html":
node.innerHTML = "";
break;
default:
node.removeAttribute(field);
break;
}
} else if (ns == "style") {
node.style.removeProperty(name);
} else {
node.removeAttributeNS(ns, field);
}
}"#
}
fn assign_id(array: &[u8], id: u32) {
"{nodes[$id$] = LoadChild($array$);}"
}
fn hydrate_text(array: &[u8], value: &str, id: u32) {
r#"{
node = LoadChild($array$);
if (node.nodeType == Node.TEXT_NODE) {
node.textContent = value;
} else {
let text = document.createTextNode(value);
node.replaceWith(text);
node = text;
}
nodes[$id$] = node;
}"#
}
fn replace_placeholder(array: &[u8], n: u16) {
"{els = stack.splice(stack.length - $n$); node = LoadChild($array$); node.replaceWith(...els);}"
}
fn load_template(tmpl_id: u16, index: u16, id: u32) {
"{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}"
}
fn add_templates(tmpl_id: u16, len: u16) {
"{templates[$tmpl_id$] = stack.splice(stack.length-$len$);}"
}
}
}

1
packages/liveview/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/src/minified.js

View file

@ -22,9 +22,10 @@ tokio-stream = { version = "0.1.11", features = ["net"] }
tokio-util = { version = "0.7.4", features = ["rt"] }
serde = { version = "1.0.151", features = ["derive"] }
serde_json = "1.0.91"
rustc-hash = { workspace = true }
dioxus-html = { workspace = true, features = ["serialize"] }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-interpreter-js = { workspace = true }
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
dioxus-hot-reload = { workspace = true, optional = true }
# warp
@ -58,6 +59,10 @@ rocket = "0.5.0"
rocket_ws = "0.1.0"
tower = "0.4.13"
[build-dependencies]
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
minify-js = "0.5.6"
[features]
default = ["hot-reload"]
# actix = ["actix-files", "actix-web", "actix-ws"]
@ -68,6 +73,10 @@ rocket = ["dep:rocket", "dep:rocket_ws"]
name = "axum"
required-features = ["axum"]
[[example]]
name = "axum_stress"
required-features = ["axum"]
[[example]]
name = "salvo"
required-features = ["salvo"]

View file

@ -0,0 +1,64 @@
use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS;
use minify_js::*;
use std::io::Write;
fn main() {
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 = window.interpreter.serializeIpcMessage("user_event", {
name: name,
element: parseInt(realId),
data: contents,
bubbles,
});
window.ipc.postMessage(message);
}
read_files();
return;
}
}"#;
let mut interpreter = SLEDGEHAMMER_JS
.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads)
.replace("export", "");
while let Some(import_start) = interpreter.find("import") {
let import_end = interpreter[import_start..]
.find(|c| c == ';' || c == '\n')
.map(|i| i + import_start)
.unwrap_or_else(|| interpreter.len());
interpreter.replace_range(import_start..import_end, "");
}
let main_js = std::fs::read_to_string("src/main.js").unwrap();
let js = format!("{interpreter}\n{main_js}");
let session = Session::new();
let mut out = Vec::new();
minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap();
let minified = String::from_utf8(out).unwrap();
let mut file = std::fs::File::create("src/minified.js").unwrap();
file.write_all(minified.as_bytes()).unwrap();
}

View file

@ -0,0 +1,65 @@
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
use dioxus::prelude::*;
fn app(cx: Scope) -> Element {
let state = use_state(cx, || 0);
use_future(cx, (), |_| {
to_owned![state];
async move {
loop {
state += 1;
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
}
}
});
cx.render(rsx! {
for _ in 0..10000 {
div {
"hello axum! {state}"
}
}
})
}
#[tokio::main]
async fn main() {
pretty_env_logger::init();
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
let view = dioxus_liveview::LiveViewPool::new();
let app = Router::new()
.route(
"/",
get(move || async move {
Html(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with axum</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
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.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
.unwrap();
}

View file

@ -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))
}

View file

@ -29,7 +29,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
@ -53,7 +53,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")
);
@ -77,7 +77,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
);

View file

@ -46,58 +46,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
///
@ -135,8 +84,6 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
"return path;"
};
let js = &*INTERPRETER_JS;
let common = &*COMMON_JS;
format!(
r#"
<script>
@ -145,10 +92,7 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
}}
var WS_ADDR = __dioxusGetWsUrl("{url_or_path}");
{js}
{common}
{MAIN_JS}
main();
{MINIFIED}
</script>
"#
)

View file

@ -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();

View file

@ -4,9 +4,11 @@ use crate::{
query::{QueryEngine, QueryResult},
LiveViewError,
};
use dioxus_core::{prelude::*, Mutations};
use dioxus_html::{EventData, HtmlEvent, MountedData};
use dioxus_core::{prelude::*, BorrowedAttributeValue, Mutations};
use dioxus_html::{event_bubbles, EventData, HtmlEvent, MountedData};
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;
@ -107,7 +109,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> {
@ -120,20 +122,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
@ -160,7 +173,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)) {
@ -199,7 +212,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 => {
@ -221,20 +234,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),
}

View file

@ -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

View file

@ -13,8 +13,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true, features = ["wasm-bind"] }
dioxus-interpreter-js = { workspace = true, features = [
"sledgehammer",
"minimal_bindings",
"web",
] }
js-sys = "0.3.56"

View file

@ -25,8 +25,8 @@ pub struct WebsysDom {
document: Document,
#[allow(dead_code)]
pub(crate) root: Element,
templates: FxHashMap<String, u32>,
max_template_id: u32,
templates: FxHashMap<String, u16>,
max_template_id: u16,
pub(crate) interpreter: Channel,
event_channel: mpsc::UnboundedSender<UiEvent>,
}
@ -99,7 +99,7 @@ impl WebsysDom {
}
}));
dioxus_interpreter_js::initilize(
dioxus_interpreter_js::initialize(
root.clone().unchecked_into(),
handler.as_ref().unchecked_ref(),
);
@ -184,7 +184,7 @@ impl WebsysDom {
let mut to_mount = Vec::new();
for edit in &edits {
match edit {
AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u16),
AssignId { path, id } => {
i.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32)
}
@ -195,15 +195,15 @@ impl WebsysDom {
}
LoadTemplate { name, index, id } => {
if let Some(tmpl_id) = self.templates.get(*name) {
i.load_template(*tmpl_id, *index as u32, id.0 as u32)
i.load_template(*tmpl_id, *index as u16, id.0 as u32)
}
}
ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u32),
ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u16),
ReplacePlaceholder { path, m } => {
i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u32)
i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u16)
}
InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u32),
InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u32),
InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u16),
InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u16),
SetAttribute {
name,
value,

View file

@ -56,6 +56,7 @@
// - Do DOM work in the next requestAnimationFrame callback
pub use crate::cfg::Config;
#[cfg(feature = "file_engine")]
pub use crate::file_engine::WebFileEngineExt;
use dioxus_core::{Element, Scope, VirtualDom};
use futures_util::{