Merge branch 'master' into add-file-data-drag-event

This commit is contained in:
Evan Almloff committed 2023-09-17 18:56:59 -05:00
commit 847801908f
575 files changed
+12880 -17751

No files matched your search

+29 -48
View File
@@ -1,12 +1,12 @@
[package]
name = "dioxus-web"
version = "0.3.2"
version = { workspace = true }
authors = ["Jonathan Kelley"]
edition = "2018"
description = "Web renderer for Dioxus using websys"
license = "MIT/Apache-2.0"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com/docs/0.3/guide/en/getting_started/web.html"
homepage = "https://dioxuslabs.com/learn/0.4/getting_started/wasm"
keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies]
@@ -20,73 +20,53 @@ dioxus-interpreter-js = { workspace = true, features = [
js-sys = "0.3.56"
wasm-bindgen = { workspace = true, features = ["enable-interning"] }
wasm-bindgen-futures = "0.4.29"
log = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }
console_error_panic_hook = { version = "0.1.7", optional = true }
once_cell = "1.9.0"
anyhow = "1.0.53"
gloo-timers = { version = "0.2.3", features = ["futures"] }
futures-util = { workspace = true, features = ["std", "async-await", "async-await-macro"] }
smallstr = "0.2.0"
futures-channel = { workspace = true }
serde_json = { version = "1.0" }
serde = { version = "1.0" }
serde-wasm-bindgen = "0.4.5"
serde-wasm-bindgen = "0.5.0"
async-trait = "0.1.58"
async-channel = "1.8.0"
[dependencies.web-sys]
version = "0.3.56"
features = [
"Comment",
"Attr",
"Document",
"Element",
"CssStyleDeclaration",
"HtmlElement",
"HtmlInputElement",
"HtmlSelectElement",
"HtmlTextAreaElement",
"HtmlFormElement",
"EventTarget",
"HtmlCollection",
"Node",
"NodeList",
"Text",
"Window",
"Event",
"MouseEvent",
"InputEvent",
"ClipboardEvent",
"NamedNodeMap",
"KeyboardEvent",
"TouchEvent",
"WheelEvent",
"AnimationEvent",
"TransitionEvent",
"PointerEvent",
"FocusEvent",
"CompositionEvent",
"ClipboardEvent",
"DocumentType",
"CharacterData",
"SvgElement",
"SvgAnimatedString",
"HtmlOptionElement",
"IdleDeadline",
"WebSocket",
"Location",
"MessageEvent",
"console",
"FileList",
"File",
"FileReader",
"DataTransfer"
"DataTransfer",
]
[features]
default = ["panic_hook"]
default = ["panic_hook", "mounted", "file_engine", "hot_reload", "eval"]
panic_hook = ["console_error_panic_hook"]
hydrate = []
hydrate = [
"web-sys/Comment",
"web-sys/console",
]
mounted = [
"web-sys/Element",
"dioxus-html/mounted"
]
file_engine = [
"web-sys/File",
"web-sys/FileList",
"web-sys/FileReader",
]
hot_reload = [
"web-sys/MessageEvent",
"web-sys/WebSocket",
"web-sys/Location",
]
eval = []
[dev-dependencies]
dioxus = { workspace = true }
@@ -95,4 +75,5 @@ dioxus-ssr = { workspace = true}
wasm-logger = "0.2.0"
gloo-timers = "0.2.3"
gloo-dialogs = "0.1.1"
dioxus-web = { workspace = true, features = ["hydrate"] }
dioxus-web = { path = ".", features = ["hydrate"] }
tracing-wasm = "0.2.1"
+2 -5
View File
@@ -7,18 +7,15 @@
[crates-badge]: https://img.shields.io/crates/v/dioxus-web.svg
[crates-url]: https://crates.io/crates/dioxus-web
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[Guides](https://dioxuslabs.com/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-web/latest/dioxus_web) |
[Chat](https://discord.gg/XgGxMSkvUM)
@@ -36,6 +33,7 @@ Run Dioxus in the browser using WebAssembly.
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
@@ -43,4 +41,3 @@ This project is licensed under the [MIT license].
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.
+2 -2
View File
@@ -43,13 +43,13 @@ fn Bapp(cx: Scope) -> Element {
fn main() {
console_error_panic_hook::set_once();
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
tracing_wasm::set_as_global_default();
let mut dom = VirtualDom::new(app);
let _ = dom.rebuild();
let pre = dioxus_ssr::pre_render(&dom);
log::trace!("{}", pre);
tracing::trace!("{}", pre);
// set the inner content of main to the pre-rendered content
window()
+24 -8
View File
@@ -7,19 +7,21 @@
//! - tests to ensure dyn_into works for various event types.
//! - Partial delegation?>
use crate::file_engine::WebFileEngine;
use dioxus_core::{
BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
};
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData, MountedData};
use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData};
use dioxus_interpreter_js::{get_node, minimal_bindings, save_template, Channel};
use futures_channel::mpsc;
use js_sys::Array;
use rustc_hash::FxHashMap;
use std::{any::Any, rc::Rc, sync::Arc};
use std::sync::Arc;
use std::{any::Any, rc::Rc};
use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast, JsValue};
use web_sys::{Document, Element, Event};
use crate::{file_engine::WebFileEngine, Config};
use crate::Config;
pub struct WebsysDom {
document: Document,
@@ -56,17 +58,27 @@ impl WebsysDom {
let element = walk_event_for_id(event);
let bubbles = dioxus_html::event_bubbles(name.as_str());
if let Some((element, target)) = element {
let prevent_event;
if let Some(prevent_requests) = target
.get_attribute("dioxus-prevent-default")
.as_deref()
.map(|f| f.split_whitespace())
{
if prevent_requests
prevent_event = prevent_requests
.map(|f| f.trim_start_matches("on"))
.any(|f| f == name)
{
.any(|f| f == name);
} else {
prevent_event = false;
}
// Prevent forms from submitting and redirecting
if name == "submit" {
// On forms the default behavior is not to submit, if prevent default is set then we submit the form
if !prevent_event {
event.prevent_default();
}
} else if prevent_event {
event.prevent_default();
}
let data = virtual_event_from_websys_event(event.clone(), target);
@@ -239,7 +251,6 @@ impl WebsysDom {
for id in to_mount {
let node = get_node(id.0 as u32);
if let Some(element) = node.dyn_ref::<Element>() {
log::info!("mounted event fired: {}", id.0);
let data: MountedData = element.into();
let data = Rc::new(data);
let _ = self.event_channel.unbounded_send(UiEvent {
@@ -384,11 +395,16 @@ fn read_input_to_data(target: Element) -> Rc<FormData> {
}
}
#[cfg(not(feature = "file_engine"))]
let files = None;
#[cfg(feature = "file_engine")]
let files = target
.dyn_ref()
.and_then(|input: &web_sys::HtmlInputElement| {
input.files().and_then(|files| {
WebFileEngine::new(files).map(|f| Arc::new(f) as Arc<dyn FileEngine>)
#[allow(clippy::arc_with_non_send_sync)]
crate::file_engine::WebFileEngine::new(files)
.map(|f| std::sync::Arc::new(f) as std::sync::Arc<dyn dioxus_html::FileEngine>)
})
});
+41
View File
@@ -0,0 +1,41 @@
export class Dioxus {
constructor(sendCallback, returnCallback) {
this.sendCallback = sendCallback;
this.returnCallback = returnCallback;
this.promiseResolve = null;
this.received = [];
}
// Receive message from Rust
recv() {
return new Promise((resolve, _reject) => {
// If data already exists, resolve immediately
let data = this.received.shift();
if (data) {
resolve(data);
return;
}
// Otherwise set a resolve callback
this.promiseResolve = resolve;
});
}
// Send message to rust.
send(data) {
this.sendCallback(data);
}
// Internal rust send
rustSend(data) {
// If a promise is waiting for data, resolve it, and clear the resolve callback
if (this.promiseResolve) {
this.promiseResolve(data);
this.promiseResolve = null;
return;
}
// Otherwise add the data to a queue
this.received.push(data);
}
}
+130
View File
@@ -0,0 +1,130 @@
use async_trait::async_trait;
use dioxus_core::ScopeState;
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
use js_sys::Function;
use serde_json::Value;
use std::{cell::RefCell, rc::Rc, str::FromStr};
use wasm_bindgen::prelude::*;
/// Provides the WebEvalProvider through [`cx.provide_context`].
pub fn init_eval(cx: &ScopeState) {
let provider: Rc<dyn EvalProvider> = Rc::new(WebEvalProvider {});
cx.provide_context(provider);
}
/// Reprents the web-target's provider of evaluators.
pub struct WebEvalProvider;
impl EvalProvider for WebEvalProvider {
fn new_evaluator(&self, js: String) -> Result<Rc<dyn Evaluator>, EvalError> {
WebEvaluator::new(js).map(|eval| Rc::new(eval) as Rc<dyn Evaluator + 'static>)
}
}
/// Required to avoid blocking the Rust WASM thread.
const PROMISE_WRAPPER: &str = r#"
return new Promise(async (resolve, _reject) => {
{JS_CODE}
resolve(null);
});
"#;
/// Reprents a web-target's JavaScript evaluator.
pub struct WebEvaluator {
dioxus: Dioxus,
channel_receiver: async_channel::Receiver<serde_json::Value>,
result: RefCell<Option<serde_json::Value>>,
}
impl WebEvaluator {
/// Creates a new evaluator for web-based targets.
pub fn new(js: String) -> Result<Self, EvalError> {
let (channel_sender, channel_receiver) = async_channel::unbounded();
// This Rc cloning mess hurts but it seems to work..
let recv_value = Closure::<dyn FnMut(JsValue)>::new(move |data| {
match serde_wasm_bindgen::from_value::<serde_json::Value>(data) {
Ok(data) => _ = channel_sender.send_blocking(data),
Err(e) => {
// Can't really do much here.
tracing::error!("failed to serialize JsValue to serde_json::Value (eval communication) - {}", e);
}
}
});
let dioxus = Dioxus::new(recv_value.as_ref().unchecked_ref());
recv_value.forget();
// Wrap the evaluated JS in a promise so that wasm can continue running (send/receive data from js)
let code = PROMISE_WRAPPER.replace("{JS_CODE}", &js);
let result = match Function::new_with_args("dioxus", &code).call1(&JsValue::NULL, &dioxus) {
Ok(result) => {
if let Ok(stringified) = js_sys::JSON::stringify(&result) {
if !stringified.is_undefined() && stringified.is_valid_utf16() {
let string: String = stringified.into();
Value::from_str(&string).map_err(|e| {
EvalError::Communication(format!("Failed to parse result - {}", e))
})?
} else {
return Err(EvalError::Communication(
"Failed to stringify result".into(),
));
}
} else {
return Err(EvalError::Communication(
"Failed to stringify result".into(),
));
}
}
Err(err) => {
return Err(EvalError::InvalidJs(
err.as_string().unwrap_or("unknown".to_string()),
));
}
};
Ok(Self {
dioxus,
channel_receiver,
result: RefCell::new(Some(result)),
})
}
}
#[async_trait(?Send)]
impl Evaluator for WebEvaluator {
/// Runs the evaluated JavaScript.
async fn join(&self) -> Result<serde_json::Value, EvalError> {
self.result.take().ok_or(EvalError::Finished)
}
/// Sends a message to the evaluated JavaScript.
fn send(&self, data: serde_json::Value) -> Result<(), EvalError> {
let data = match serde_wasm_bindgen::to_value::<serde_json::Value>(&data) {
Ok(d) => d,
Err(e) => return Err(EvalError::Communication(e.to_string())),
};
self.dioxus.rustSend(data);
Ok(())
}
/// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript.
async fn recv(&self) -> Result<serde_json::Value, EvalError> {
self.channel_receiver
.recv()
.await
.map_err(|_| EvalError::Communication("failed to receive data from js".to_string()))
}
}
#[wasm_bindgen(module = "/src/eval.js")]
extern "C" {
pub type Dioxus;
#[wasm_bindgen(constructor)]
pub fn new(recv_callback: &Function) -> Dioxus;
#[wasm_bindgen(method)]
pub fn rustSend(this: &Dioxus, data: JsValue);
}
+23
View File
@@ -1,3 +1,5 @@
use std::any::Any;
use dioxus_html::FileEngine;
use futures_channel::oneshot;
use js_sys::Uint8Array;
@@ -100,4 +102,25 @@ impl FileEngine for WebFileEngine {
None
}
}
async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
let file = self.find(file)?;
Some(Box::new(file))
}
}
/// Helper trait for WebFileEngine
#[async_trait::async_trait(?Send)]
pub trait WebFileEngineExt {
/// returns web_sys::File
async fn get_web_file(&self, file: &str) -> Option<web_sys::File>;
}
#[async_trait::async_trait(?Send)]
impl WebFileEngineExt for std::sync::Arc<dyn FileEngine> {
async fn get_web_file(&self, file: &str) -> Option<web_sys::File> {
let native_file = self.get_native_file(file).await?;
let ret = native_file.downcast::<web_sys::File>().ok()?;
Some(*ret)
}
}
+3 -13
View File
@@ -3,22 +3,12 @@
use futures_channel::mpsc::UnboundedReceiver;
use dioxus_core::Template;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use web_sys::{MessageEvent, WebSocket};
#[cfg(not(debug_assertions))]
pub(crate) fn init() -> UnboundedReceiver<Template<'static>> {
let (tx, rx) = futures_channel::mpsc::unbounded();
std::mem::forget(tx);
rx
}
#[cfg(debug_assertions)]
pub(crate) fn init() -> UnboundedReceiver<Template<'static>> {
use std::convert::TryInto;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use web_sys::{MessageEvent, WebSocket};
use serde::Deserialize;
+38 -13
View File
@@ -54,18 +54,24 @@
// - Do DOM work in the next requestAnimationFrame callback
pub use crate::cfg::Config;
pub use crate::util::{use_eval, EvalResult};
pub use crate::file_engine::WebFileEngineExt;
use dioxus_core::{Element, Scope, VirtualDom};
use futures_util::{pin_mut, FutureExt, StreamExt};
use futures_util::{
future::{select, Either},
pin_mut, FutureExt, StreamExt,
};
mod cache;
mod cfg;
mod dom;
#[cfg(feature = "eval")]
mod eval;
#[cfg(feature = "file_engine")]
mod file_engine;
#[cfg(all(feature = "hot_reload", debug_assertions))]
mod hot_reload;
#[cfg(feature = "hydrate")]
mod rehydrate;
mod util;
// Currently disabled since it actually slows down immediate rendering
// todo: only schedule non-immediate renders through ric/raf
@@ -164,15 +170,23 @@ pub fn launch_with_props<T: 'static>(
/// }
/// ```
pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_props: T, cfg: Config) {
log::info!("Starting up");
tracing::info!("Starting up");
let mut dom = VirtualDom::new_with_props(root, root_props);
#[cfg(feature = "eval")]
{
// Eval
let cx = dom.base_scope();
eval::init_eval(cx);
}
#[cfg(feature = "panic_hook")]
if cfg.default_panic_hook {
console_error_panic_hook::set_once();
}
#[cfg(all(feature = "hot_reload", debug_assertions))]
let mut hotreload_rx = hot_reload::init();
for s in crate::cache::BUILTIN_INTERNED_STRINGS {
@@ -191,7 +205,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
let mut websys_dom = dom::WebsysDom::new(cfg, tx);
log::info!("rebuilding app");
tracing::info!("rebuilding app");
if should_hydrate {
#[cfg(feature = "hydrate")]
@@ -203,7 +217,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
websys_dom.load_templates(&templates);
if let Err(err) = websys_dom.rehydrate(&dom) {
log::error!(
tracing::error!(
"Rehydration failed {:?}. Rebuild DOM into element from scratch",
&err
);
@@ -226,7 +240,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
websys_dom.mount();
loop {
log::trace!("waiting for work");
tracing::trace!("waiting for work");
// if virtualdom has nothing, wait for it to have something before requesting idle time
// if there is work then this future resolves immediately.
@@ -234,12 +248,23 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
let work = dom.wait_for_work().fuse();
pin_mut!(work);
futures_util::select! {
_ = work => (None, None),
new_template = hotreload_rx.next() => {
(None, new_template)
}
evt = rx.next() => (evt, None)
#[cfg(all(feature = "hot_reload", debug_assertions))]
// futures_util::select! {
// _ = work => (None, None),
// new_template = hotreload_rx.next() => {
// (None, new_template)
// }
// evt = rx.next() =>
// }
match select(work, select(hotreload_rx.next(), rx.next())).await {
Either::Left((_, _)) => (None, None),
Either::Right((Either::Left((new_template, _)), _)) => (None, new_template),
Either::Right((Either::Right((evt, _)), _)) => (evt, None),
}
#[cfg(not(all(feature = "hot_reload", debug_assertions)))]
match select(work, rx.next()).await {
Either::Left((_, _)) => (None, None),
Either::Right((evt, _)) => (evt, None),
}
};
+12 -12
View File
@@ -1,7 +1,6 @@
use crate::dom::WebsysDom;
use dioxus_core::{
AttributeValue, DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VPlaceholder, VText,
VirtualDom,
AttributeValue, DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VirtualDom,
};
use dioxus_html::event_bubbles;
use wasm_bindgen::JsCast;
@@ -115,9 +114,9 @@ impl WebsysDom {
node: &TemplateNode,
last_node_was_static_text: &mut bool,
) -> Result<(), RehydrationError> {
log::trace!("rehydrate template node: {:?}", node);
tracing::trace!("rehydrate template node: {:?}", node);
if let Ok(current_child) = current_child {
if log::log_enabled!(log::Level::Trace) {
if tracing::event_enabled!(tracing::Level::TRACE) {
web_sys::console::log_1(&current_child.clone().into());
}
}
@@ -130,7 +129,7 @@ impl WebsysDom {
if let dioxus_core::TemplateAttribute::Dynamic { id } = attr {
let attribute = &vnode.dynamic_attrs[*id];
let value = &attribute.value;
let id = attribute.mounted_element.get();
let id = attribute.mounted_element();
mounted_id = Some(id);
let name = attribute.name;
if let AttributeValue::Listener(_) = value {
@@ -204,14 +203,15 @@ impl WebsysDom {
dynamic: &DynamicNode,
last_node_was_static_text: &mut bool,
) -> Result<(), RehydrationError> {
log::trace!("rehydrate dynamic node: {:?}", dynamic);
tracing::trace!("rehydrate dynamic node: {:?}", dynamic);
if let Ok(current_child) = current_child {
if log::log_enabled!(log::Level::Trace) {
if tracing::event_enabled!(tracing::Level::TRACE) {
web_sys::console::log_1(&current_child.clone().into());
}
}
match dynamic {
dioxus_core::DynamicNode::Text(VText { id, .. }) => {
dioxus_core::DynamicNode::Text(text) => {
let id = text.mounted_element();
// skip comment separator before node
if cfg!(debug_assertions) {
assert!(current_child
@@ -227,7 +227,7 @@ impl WebsysDom {
set_node(
hydrated,
id.get().ok_or(VNodeNotInitialized)?,
id.ok_or(VNodeNotInitialized)?,
current_child.clone()?,
);
*current_child = current_child
@@ -251,10 +251,10 @@ impl WebsysDom {
*last_node_was_static_text = false;
}
dioxus_core::DynamicNode::Placeholder(VPlaceholder { id, .. }) => {
dioxus_core::DynamicNode::Placeholder(placeholder) => {
set_node(
hydrated,
id.get().ok_or(VNodeNotInitialized)?,
placeholder.mounted_element().ok_or(VNodeNotInitialized)?,
current_child.clone()?,
);
*current_child = current_child
@@ -265,7 +265,7 @@ impl WebsysDom {
*last_node_was_static_text = false;
}
dioxus_core::DynamicNode::Component(comp) => {
let scope = comp.scope.get().ok_or(VNodeNotInitialized)?;
let scope = comp.mounted_scope().ok_or(VNodeNotInitialized)?;
self.rehydrate_scope(
dom.get_scope(scope).unwrap(),
current_child,
-70
View File
@@ -1,70 +0,0 @@
//! Utilities specific to websys
use std::{
future::{IntoFuture, Ready},
rc::Rc,
str::FromStr,
};
use dioxus_core::*;
use serde::de::Error;
use serde_json::Value;
/// Get a closure that executes any JavaScript in the webpage.
///
/// # Safety
///
/// Please be very careful with this function. A script with too many dynamic
/// parts is practically asking for a hacker to find an XSS vulnerability in
/// it. **This applies especially to web targets, where the JavaScript context
/// has access to most, if not all of your application data.**
///
/// # Panics
///
/// The closure will panic if the provided script is not valid JavaScript code
/// or if it returns an uncaught error.
pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String) -> EvalResult> {
cx.use_hook(|| {
Rc::new(|script: String| EvalResult {
value: if let Ok(value) =
js_sys::Function::new_no_args(&script).call0(&wasm_bindgen::JsValue::NULL)
{
if let Ok(stringified) = js_sys::JSON::stringify(&value) {
if !stringified.is_undefined() && stringified.is_valid_utf16() {
let string: String = stringified.into();
Value::from_str(&string)
} else {
Err(serde_json::Error::custom("Failed to stringify result"))
}
} else {
Err(serde_json::Error::custom("Failed to stringify result"))
}
} else {
Err(serde_json::Error::custom("Failed to execute script"))
},
}) as Rc<dyn Fn(String) -> EvalResult>
})
}
/// A wrapper around the result of a JavaScript evaluation.
/// This implements IntoFuture to be compatible with the desktop renderer's EvalResult.
pub struct EvalResult {
value: Result<Value, serde_json::Error>,
}
impl EvalResult {
/// Get the result of the Javascript execution.
pub fn get(self) -> Result<Value, serde_json::Error> {
self.value
}
}
impl IntoFuture for EvalResult {
type Output = Result<Value, serde_json::Error>;
type IntoFuture = Ready<Result<Value, serde_json::Error>>;
fn into_future(self) -> Self::IntoFuture {
std::future::ready(self.value)
}
}