Switch to bun, clean up web implementation

This commit is contained in:
Jonathan Kelley committed 2024-03-04 15:23:48 -08:00
1 parent 199173a409
commit 22266cc560
23 files changed
+240 -173

No files matched your search

+38 -33
View File
@@ -8,7 +8,7 @@
use dioxus_core::ElementId;
use dioxus_html::PlatformEventData;
use dioxus_interpreter_js::unified_bindings::{Interpreter, InterpreterInterface};
use dioxus_interpreter_js::{unified_bindings::Interpreter, WebInterpreter};
use futures_channel::mpsc;
use rustc_hash::FxHashMap;
use wasm_bindgen::{closure::Closure, JsCast};
@@ -17,14 +17,16 @@ use web_sys::{Document, Element, Event};
use crate::{load_document, virtual_event_from_websys_event, Config, WebEventConverter};
pub struct WebsysDom {
pub(crate) document: Document,
#[allow(dead_code)]
pub(crate) root: Element,
pub(crate) document: Document,
pub(crate) templates: FxHashMap<String, u16>,
pub(crate) max_template_id: u16,
pub(crate) interpreter: InterpreterInterface,
pub(crate) interpreter: Interpreter,
#[cfg(feature = "mounted")]
pub(crate) event_channel: mpsc::UnboundedSender<UiEvent>,
#[cfg(feature = "mounted")]
pub(crate) queued_mounted_events: Vec<ElementId>,
}
@@ -64,7 +66,7 @@ impl WebsysDom {
}
};
let interpreter = InterpreterInterface::default();
let interpreter = Interpreter::default();
let handler: Closure<dyn FnMut(&Event)> = Closure::wrap(Box::new({
let event_channel = event_channel.clone();
@@ -72,42 +74,45 @@ impl WebsysDom {
let name = event.type_();
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())
{
prevent_event = prevent_requests
.map(|f| f.trim_start_matches("on"))
.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 {
let Some((element, target)) = element else {
return;
};
let prevent_event;
if let Some(prevent_requests) = target
.get_attribute("dioxus-prevent-default")
.as_deref()
.map(|f| f.split_whitespace())
{
prevent_event = prevent_requests
.map(|f| f.trim_start_matches("on"))
.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();
}
let data = virtual_event_from_websys_event(event.clone(), target);
let _ = event_channel.unbounded_send(UiEvent {
name,
bubbles,
element,
data,
});
} else if prevent_event {
event.prevent_default();
}
let data = virtual_event_from_websys_event(event.clone(), target);
let _ = event_channel.unbounded_send(UiEvent {
name,
bubbles,
element,
data,
});
}
}));
let _interpreter: &Interpreter = interpreter.as_ref();
let _interpreter: &WebInterpreter = interpreter.as_web();
_interpreter.initialize(
root.clone().unchecked_into(),
handler.as_ref().unchecked_ref(),
+8 -19
View File
@@ -4,7 +4,6 @@ use dioxus_html::{
point_interaction::{
InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
},
prelude::FormValue,
DragData, FileEngine, FormData, HasDragData, HasFileData, HasFormData, HasImageData,
HasMouseData, HtmlEventConverter, ImageData, MountedData, PlatformEventData, ScrollData,
};
@@ -385,24 +384,14 @@ impl HasFormData for WebFormData {
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener")
}
fn values(&self) -> HashMap<String, FormValue> {
fn values(&self) -> HashMap<String, String> {
let mut values = HashMap::new();
fn insert_value(map: &mut HashMap<String, FormValue>, key: String, new_value: String) {
match map.entry(key) {
std::collections::hash_map::Entry::Occupied(mut o) => {
let first_value = match o.get_mut() {
FormValue::Text(data) => std::mem::take(data),
FormValue::VecText(vec) => {
vec.push(new_value);
return;
}
};
let _ = o.insert(FormValue::VecText(vec![first_value, new_value]));
}
std::collections::hash_map::Entry::Vacant(v) => {
let _ = v.insert(FormValue::Text(new_value));
}
fn insert_value(map: &mut HashMap<String, String>, key: String, new_value: String) {
if let Some(value) = map.get(&key) {
map.insert(key, format!("{},{}", value, new_value));
} else {
map.insert(key, new_value);
}
}
@@ -425,8 +414,8 @@ impl HasFormData for WebFormData {
}
} else if let Some(select) = self.element.dyn_ref::<web_sys::HtmlSelectElement>() {
// try to fill in select element values
let options = get_select_data(select);
values.insert("options".to_string(), FormValue::VecText(options));
let options = get_select_data(select).join(",");
values.insert("options".to_string(), options);
}
values
+11 -11
View File
@@ -30,16 +30,21 @@ use futures_util::{pin_mut, select, FutureExt, StreamExt};
mod cfg;
mod dom;
#[cfg(feature = "eval")]
mod eval;
mod event;
pub mod launch;
mod mutations;
pub use event::*;
#[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;
@@ -49,7 +54,7 @@ mod rehydrate;
///
/// # Example
///
/// ```ignore
/// ```rust, ignore
/// fn main() {
/// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("joe") });
/// wasm_bindgen_futures::spawn_local(app_fut);
@@ -61,12 +66,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) {
let mut dom = virtual_dom;
#[cfg(feature = "eval")]
{
// Eval
dom.in_runtime(|| {
eval::init_eval();
});
}
dom.in_runtime(|| eval::init_eval());
#[cfg(feature = "panic_hook")]
if web_config.default_panic_hook {
@@ -109,13 +109,12 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) {
websys_dom.mount();
loop {
tracing::trace!("waiting for work");
// if virtual dom has nothing, wait for it to have something before requesting idle time
// if there is work then this future resolves immediately.
let (mut res, template) = {
let work = dom.wait_for_work().fuse();
pin_mut!(work);
let mut rx_next = rx.select_next_some();
#[cfg(all(feature = "hot_reload", debug_assertions))]
@@ -127,6 +126,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) {
evt = rx_next => (Some(evt), None),
}
}
#[cfg(not(all(feature = "hot_reload", debug_assertions)))]
select! {
_ = work => (None, None),
+32 -36
View File
@@ -5,9 +5,7 @@ use dioxus_core::WriteMutations;
use dioxus_core::{AttributeValue, ElementId};
use dioxus_html::event_bubbles;
use dioxus_html::PlatformEventData;
use dioxus_interpreter_js::get_node;
use dioxus_interpreter_js::minimal_bindings;
use dioxus_interpreter_js::save_template;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
@@ -58,19 +56,23 @@ impl WebsysDom {
pub fn flush_edits(&mut self) {
self.interpreter.flush();
#[cfg(feature = "mounted")]
// Now that we've flushed the edits and the dom nodes exist, we can send the mounted events.
{
for id in self.queued_mounted_events.drain(..) {
let node = get_node(self.interpreter.js_channel(), id.0 as u32);
if let Some(element) = node.dyn_ref::<web_sys::Element>() {
let _ = self.event_channel.unbounded_send(UiEvent {
name: "mounted".to_string(),
bubbles: false,
element: id,
data: PlatformEventData::new(Box::new(element.clone())),
});
}
#[cfg(feature = "mounted")]
self.flush_queued_mounted_events();
}
#[cfg(feature = "mounted")]
fn flush_queued_mounted_events(&mut self) {
for id in self.queued_mounted_events.drain(..) {
let node = self.interpreter.as_web().get_node(id.0 as u32);
if let Some(element) = node.dyn_ref::<web_sys::Element>() {
let _ = self.event_channel.unbounded_send(UiEvent {
name: "mounted".to_string(),
bubbles: false,
element: id,
data: PlatformEventData::new(Box::new(element.clone())),
});
}
}
}
@@ -84,14 +86,14 @@ impl WebsysDom {
impl WriteMutations for WebsysDom {
fn register_template(&mut self, template: Template) {
let mut roots = vec![];
for root in template.roots {
roots.push(self.create_template_node(root))
}
self.templates
.insert(template.name.to_owned(), self.max_template_id);
save_template(self.interpreter.js_channel(), roots, self.max_template_id);
self.interpreter
.as_web()
.save_template(roots, self.max_template_id);
self.max_template_id += 1
}
@@ -184,30 +186,24 @@ impl WriteMutations for WebsysDom {
}
fn create_event_listener(&mut self, name: &'static str, id: ElementId) {
match name {
// mounted events are fired immediately after the element is mounted.
"mounted" => {
#[cfg(feature = "mounted")]
self.send_mount_event(id);
}
_ => {
self.interpreter
.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}
// mounted events are fired immediately after the element is mounted.
if name == "mounted" {
#[cfg(feature = "mounted")]
self.send_mount_event(id);
return;
}
self.interpreter
.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}
fn remove_event_listener(&mut self, name: &'static str, id: ElementId) {
match name {
"mounted" => {}
_ => {
self.interpreter.remove_event_listener(
name,
id.0 as u32,
event_bubbles(name) as u8,
);
}
if name == "mounted" {
return;
}
self.interpreter
.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}
fn remove_node(&mut self, id: ElementId) {
+6 -9
View File
@@ -3,7 +3,6 @@ use dioxus_core::prelude::*;
use dioxus_core::AttributeValue;
use dioxus_core::WriteMutations;
use dioxus_core::{DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VirtualDom};
use dioxus_interpreter_js::save_template;
#[derive(Debug)]
pub enum RehydrationError {
@@ -23,7 +22,7 @@ impl WebsysDom {
// Recursively rehydrate the dom from the VirtualDom
self.rehydrate_scope(root_scope, dom, &mut ids, &mut to_mount)?;
dioxus_interpreter_js::hydrate(self.interpreter.js_channel(), ids);
self.interpreter.as_web().hydrate(ids);
#[cfg(feature = "mounted")]
for id in to_mount {
@@ -40,8 +39,7 @@ impl WebsysDom {
ids: &mut Vec<u32>,
to_mount: &mut Vec<ElementId>,
) -> Result<(), RehydrationError> {
let vnode = scope.root_node();
self.rehydrate_vnode(dom, vnode, ids, to_mount)
self.rehydrate_vnode(dom, scope.root_node(), ids, to_mount)
}
fn rehydrate_vnode(
@@ -168,11 +166,10 @@ impl WriteMutations for OnlyWriteTemplates<'_> {
self.0
.templates
.insert(template.name.to_owned(), self.0.max_template_id);
save_template(
self.0.interpreter.js_channel(),
roots,
self.0.max_template_id,
);
self.0
.interpreter
.as_web()
.save_template(roots, self.0.max_template_id);
self.0.max_template_id += 1
}
-80
View File
@@ -1,80 +0,0 @@
//! This module provides some utilities around scheduling tasks on the main thread of the browser.
//!
//! The ultimate goal here is to not block the main thread during animation frames, so our animations don't result in "jank".
//!
//! Hence, this module provides Dioxus "Jank Free Rendering" on the web.
//!
//! Because RIC doesn't work on Safari, we polyfill using the "ricpolyfill.js" file and use some basic detection to see
//! if RIC is available.
use futures_util::StreamExt;
use gloo_timers::future::TimeoutFuture;
use js_sys::Function;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
use web_sys::{window, Window};
pub(crate) struct RafLoop {
window: Window,
ric_receiver: futures_channel::mpsc::UnboundedReceiver<u32>,
raf_receiver: futures_channel::mpsc::UnboundedReceiver<()>,
ric_closure: Closure<dyn Fn(JsValue)>,
raf_closure: Closure<dyn Fn(JsValue)>,
}
impl RafLoop {
pub fn new() -> Self {
let (raf_sender, raf_receiver) = futures_channel::mpsc::unbounded();
let raf_closure: Closure<dyn Fn(JsValue)> = Closure::wrap(Box::new(move |_v: JsValue| {
raf_sender.unbounded_send(()).unwrap()
}));
let (ric_sender, ric_receiver) = futures_channel::mpsc::unbounded();
let has_idle_callback = {
let bo = window().unwrap().dyn_into::<js_sys::Object>().unwrap();
bo.has_own_property(&JsValue::from_str("requestIdleCallback"))
};
let ric_closure: Closure<dyn Fn(JsValue)> = Closure::wrap(Box::new(move |v: JsValue| {
let time_remaining = if has_idle_callback {
if let Ok(deadline) = v.dyn_into::<web_sys::IdleDeadline>() {
deadline.time_remaining() as u32
} else {
10
}
} else {
10
};
ric_sender.unbounded_send(time_remaining).unwrap()
}));
// execute the polyfill for safari
Function::new_no_args(include_str!("./ricpolyfill.js"))
.call0(&JsValue::NULL)
.unwrap();
let window = web_sys::window().unwrap();
Self {
window,
raf_receiver,
raf_closure,
ric_receiver,
ric_closure,
}
}
/// waits for some idle time and returns a timeout future that expires after the idle time has passed
pub async fn wait_for_idle_time(&mut self) -> TimeoutFuture {
let ric_fn = self.ric_closure.as_ref().dyn_ref::<Function>().unwrap();
let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap();
let deadline = self.ric_receiver.next().await.unwrap();
TimeoutFuture::new(deadline)
}
pub async fn wait_for_raf(&mut self) {
let raf_fn = self.raf_closure.as_ref().dyn_ref::<Function>().unwrap();
let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap();
self.raf_receiver.next().await.unwrap();
}
}
-28
View File
@@ -1,28 +0,0 @@
const requestIdleCallback =
(typeof self !== 'undefined' &&
self.requestIdleCallback &&
self.requestIdleCallback.bind(window)) ||
function (cb) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
},
});
}, 1);
};
const cancelIdleCallback =
(typeof self !== 'undefined' &&
self.cancelIdleCallback &&
self.cancelIdleCallback.bind(window)) ||
function (id) {
return clearTimeout(id);
};
if (typeof window !== 'undefined') {
window.requestIdleCallback = requestIdleCallback;
window.cancelIdleCallback = cancelIdleCallback;
}