Switch to bun, clean up web implementation

This commit is contained in:
Jonathan Kelley 2024-03-04 15:23:48 -08:00
parent 199173a409
commit 22266cc560
No known key found for this signature in database
GPG key ID: 1FBB50F7EB0A08BE
23 changed files with 240 additions and 173 deletions

View file

@ -1,3 +1,5 @@
#![allow(unused)] // for whatever reason, the compiler is not recognizing the use of these functions
use dioxus::prelude::*;
use dioxus_core::Element;

View file

@ -11,7 +11,7 @@
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 });
const message = window.interpreter.serializeIpcMessage("file_dialog", { 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");

View file

@ -49,7 +49,7 @@ impl IpcMessage {
pub(crate) fn method(&self) -> IpcMethod {
match self.method.as_str() {
// todo: this is a misspelling, needs to be fixed
"file_diolog" => IpcMethod::FileDialog,
"file_dialog" => IpcMethod::FileDialog,
"user_event" => IpcMethod::UserEvent,
"query" => IpcMethod::Query,
"browser_open" => IpcMethod::BrowserOpen,

View file

@ -1,5 +1,5 @@
use crate::{assets::*, edits::EditQueue};
use dioxus_interpreter_js::unified_bindings::{native_js, SLEDGEHAMMER_JS};
use dioxus_interpreter_js::unified_bindings::native_js;
use std::path::{Path, PathBuf};
use wry::{
http::{status::StatusCode, Request, Response},

View file

@ -1 +1 @@
4429706825984325407
3599957386864841107

View file

@ -11,13 +11,16 @@ class Interpreter {
this.handler = handler;
this.initialize(root);
}
initialize(root) {
initialize(root, handler = null) {
this.global = {};
this.local = {};
this.root = root;
this.nodes = [root];
this.stack = [root];
this.templates = {};
if (handler) {
this.handler = handler;
}
}
createListener(event_name, element, bubbles) {
if (bubbles) {

View file

@ -11,13 +11,16 @@ class Interpreter {
this.handler = handler;
this.initialize(root);
}
initialize(root) {
initialize(root, handler = null) {
this.global = {};
this.local = {};
this.root = root;
this.nodes = [root];
this.stack = [root];
this.templates = {};
if (handler) {
this.handler = handler;
}
}
createListener(event_name, element, bubbles) {
if (bubbles) {
@ -73,6 +76,86 @@ class Interpreter {
}
}
// src/ts/set_attribute.ts
function setAttributeInner(node, field, value, ns) {
if (ns === "style") {
node.style.setProperty(field, value);
return;
}
if (!!ns) {
node.setAttributeNS(ns, field, value);
return;
}
switch (field) {
case "value":
if (node.value !== 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:
if (!truthy(value) && isBoolAttr(field)) {
node.removeAttribute(field);
} else {
node.setAttribute(field, value);
}
}
}
var truthy = function(val) {
return val === "true" || val === true;
};
var isBoolAttr = function(field) {
switch (field) {
case "allowfullscreen":
case "allowpaymentrequest":
case "async":
case "autofocus":
case "autoplay":
case "checked":
case "controls":
case "default":
case "defer":
case "disabled":
case "formnovalidate":
case "hidden":
case "ismap":
case "itemscope":
case "loop":
case "multiple":
case "muted":
case "nomodule":
case "novalidate":
case "open":
case "playsinline":
case "readonly":
case "required":
case "reversed":
case "selected":
case "truespeed":
case "webkitdirectory":
return true;
default:
return false;
}
};
// src/ts/interpreter_web.ts
class PlatformInterpreter extends Interpreter {
m;
@ -126,5 +209,6 @@ class PlatformInterpreter extends Interpreter {
}
}
export {
setAttributeInner,
PlatformInterpreter
};

View file

@ -11,6 +11,12 @@ mod write_native_mutations;
#[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))]
pub use write_native_mutations::*;
#[cfg(feature = "sledgehammer")]
pub mod unified_bindings;
#[cfg(feature = "sledgehammer")]
pub use unified_bindings::*;
// Common bindings for minimal usage.
#[cfg(all(feature = "minimal_bindings", feature = "webonly"))]
pub mod minimal_bindings {
@ -28,9 +34,3 @@ pub mod minimal_bindings {
pub fn collectFormValues(node: JsValue) -> JsValue;
}
}
#[cfg(feature = "sledgehammer")]
pub mod unified_bindings;
#[cfg(feature = "sledgehammer")]
pub use unified_bindings::*;

View file

@ -28,7 +28,7 @@ export class Interpreter {
this.initialize(root);
}
initialize(root: HTMLElement) {
initialize(root: HTMLElement, handler: EventListener | null = null) {
this.global = {};
this.local = {};
this.root = root;
@ -36,6 +36,10 @@ export class Interpreter {
this.nodes = [root];
this.stack = [root];
this.templates = {};
if (handler) {
this.handler = handler;
}
}
createListener(event_name: string, element: HTMLElement, bubbles: boolean) {

View file

@ -5,6 +5,7 @@
// We're using sledgehammer directly
import { Interpreter } from "./interpreter_core";
export { setAttributeInner } from "./set_attribute";
export class PlatformInterpreter extends Interpreter {
m: any;

View file

@ -6,40 +6,13 @@ use wasm_bindgen::prelude::wasm_bindgen;
use sledgehammer_bindgen::bindgen;
/// Combine the interpreter class with the sledgehammer_bindgen generated methods.
pub fn native_js() -> String {
format!("{}\n{}", include_str!("./js/native.js"), GENERATED_JS,)
}
pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
/// Extensions to the interpreter that are specific to the web platform.
#[cfg(feature = "webonly")]
#[wasm_bindgen(module = "src/js/web.js")]
extern "C" {
pub type WebInterpreter;
#[wasm_bindgen(method, js_name = "saveTemplate")]
pub fn save_template(this: &WebInterpreter, nodes: Vec<Node>, tmpl_id: u16);
#[wasm_bindgen(method)]
pub fn hydrate(this: &WebInterpreter, ids: Vec<u32>);
#[wasm_bindgen(method, js_name = "getNode")]
pub fn get_node(this: &WebInterpreter, id: u32) -> Node;
}
#[cfg(feature = "webonly")]
type PlatformInterpreter = WebInterpreter;
#[cfg(feature = "webonly")]
impl Interpreter {
/// Convert the interpreter to a web interpreter, enabling methods like hydrate and save_template.
pub fn as_web(&self) -> &WebInterpreter {
use wasm_bindgen::prelude::JsCast;
&self.js_channel().unchecked_ref()
}
}
#[bindgen(module)]
mod js {
/// The interpreter extends the core interpreter which contains the state for the interpreter along with some functions that all platforms use like `AppendChildren`.
@ -222,3 +195,34 @@ mod js {
"{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...this.els);}"
}
}
/// Extensions to the interpreter that are specific to the web platform.
#[cfg(feature = "webonly")]
#[wasm_bindgen(module = "src/js/web.js")]
extern "C" {
pub type WebInterpreter;
#[wasm_bindgen(method)]
pub fn initialize(this: &WebInterpreter, root: Node, handler: &js_sys::Function);
#[wasm_bindgen(method, js_name = "saveTemplate")]
pub fn save_template(this: &WebInterpreter, nodes: Vec<Node>, tmpl_id: u16);
#[wasm_bindgen(method)]
pub fn hydrate(this: &WebInterpreter, ids: Vec<u32>);
#[wasm_bindgen(method, js_name = "getNode")]
pub fn get_node(this: &WebInterpreter, id: u32) -> Node;
}
#[cfg(feature = "webonly")]
type PlatformInterpreter = WebInterpreter;
#[cfg(feature = "webonly")]
impl Interpreter {
/// Convert the interpreter to a web interpreter, enabling methods like hydrate and save_template.
pub fn as_web(&self) -> &WebInterpreter {
use wasm_bindgen::prelude::JsCast;
&self.js_channel().unchecked_ref()
}
}

View file

@ -1,10 +0,0 @@
{
// extends the base
"extends": "./tsconfig.json",
"compilerOptions": {
"outFile": "src/js/native.js"
},
"files": [
"src/ts/interpreter_native.ts"
],
}

View file

@ -1,10 +0,0 @@
{
// extends the base
"extends": "./tsconfig.json",
"compilerOptions": {
"outFile": "src/js/web.js"
},
"files": [
"src/ts/interpreter_web.ts"
],
}

View file

@ -31,7 +31,7 @@ pub enum LiveViewError {
}
fn handle_edits_code() -> String {
use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS;
use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
use minify_js::{minify, Session, TopLevelMode};
let serialize_file_uploads = r#"if (

View file

@ -18,7 +18,6 @@ use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers};
use dioxus_html::input_data::{
MouseButton as DioxusMouseButton, MouseButtonSet as DioxusMouseButtons,
};
use dioxus_html::FormValue;
use dioxus_html::{event_bubbles, prelude::*};
use std::any::Any;
use std::collections::HashMap;
@ -67,7 +66,7 @@ impl EventData {
pub struct FormData {
pub(crate) value: String,
pub values: HashMap<String, FormValue>,
pub values: HashMap<String, String>,
pub(crate) files: Option<Files>,
}
@ -77,7 +76,7 @@ impl HasFormData for FormData {
self.value.clone()
}
fn values(&self) -> HashMap<String, FormValue> {
fn values(&self) -> HashMap<String, String> {
self.values.clone()
}

View file

@ -0,0 +1,3 @@
requestIdleCallback and requestAnimationFrame implemenation
These currently actually slow down our DOM patching and thus are temporarily removed. Technically we can schedule around rIC and rAF but choose not to.

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(),

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

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),

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) {

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
}