Merge branch 'master' into jk/update-hooks

This commit is contained in:
Jonathan Kelley committed 2022-01-25 16:19:12 -05:00
commit 5c4bd0881b
80 files changed
+2884 -1265

No files matched your search

+4 -4
View File
@@ -81,7 +81,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
/// // Using an "ID" associated with your data is a good idea.
/// data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" }))
/// }}
///
///
/// // Matching
/// {match true {
/// true => rsx!(h1 {"Top text"}),
@@ -229,18 +229,18 @@ pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
/// #[inline_props]
/// fn app(cx: Scope, bob: String) -> Element {
/// cx.render(rsx!("hello, {bob}"))
/// }
/// }
///
/// // is equivalent to
///
/// #[derive(PartialEq, Props)]
/// struct AppProps {
/// bob: String,
/// }
/// }
///
/// fn app(cx: Scope<AppProps>) -> Element {
/// cx.render(rsx!("hello, {bob}"))
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn inline_props(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
+8
View File
@@ -364,6 +364,14 @@ mod field_info {
Some(syn::parse(quote!(Default::default()).into()).unwrap());
Ok(())
}
"optional" => {
self.default =
Some(syn::parse(quote!(Default::default()).into()).unwrap());
self.strip_option = true;
Ok(())
}
_ => {
macro_rules! handle_fields {
( $( $flag:expr, $field:ident, $already:expr; )* ) => {
+1 -1
View File
@@ -94,5 +94,5 @@ Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinn
If you don't want to use a crate that uses unsafe, then this crate is not for you.
However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous variants required to execute certain features.
However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous invariants required to execute certain features.
+8 -8
View File
@@ -5,7 +5,7 @@ This document is mostly a brain-dump on how things work. A lot of this informati
Main topics covered here:
- Fiber, Concurrency, and Cooperative Scheduling
- Suspense
- Signals
- Signals
- Patches
- Diffing
- Const/Static structures
@@ -25,7 +25,7 @@ During diffing, the "caller" closure is updated if the props are not `static. Th
Hooks are a form of state that's slightly more finicky than structs but more extensible overall. Hooks cannot be used in conditionals, but are portable enough to run on most targets.
The Dioxus hook model uses a Bump arena where user's data lives.
The Dioxus hook model uses a Bump arena where user's data lives.
Initializing hooks:
- The component is created
@@ -38,7 +38,7 @@ Initializing hooks:
Running hooks:
- Each time use_hook is called, the internal hook state is fetched as &mut T
- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies
- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies
- The hook counter is incremented
@@ -62,7 +62,7 @@ The diffing engine in Dioxus expects the RealDom
Dioxus uses patches - not imperative methods - to modify the real dom. This speeds up the diffing operation and makes diffing cancelable which is useful for cooperative scheduling. In general, the RealDom trait exists so renderers can share "Node pointers" across runtime boundaries.
There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself.
There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself.
@@ -70,11 +70,11 @@ There are no contractual obligations between the VirtualDOM and RealDOM. When th
When an EventTrigger enters the queue and "progress" is called (an async function), Dioxus will get to work running scopes and diffing nodes. Scopes are run and nodes are diffed together. Dioxus records which scopes get diffed to track the progress of its work.
While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending.
While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending.
Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom.
Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom.
All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks.
All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks.
```js
// returns a "deadline" object
function idle() {
@@ -83,7 +83,7 @@ function idle() {
```
## Suspense
In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier.
In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier.
In Dioxus, we have similar philosophy, but the use and details of suspense is slightly different. For starters, we don't currently allow using futures in the element structure. Technically, we can allow futures - and we do with "Signals" - but the "suspense" feature itself is meant to be self-contained within a single component. This forces you to handle all the loading states within your component, instead of outside the component, keeping things a bit more containerized.
+1 -1
View File
@@ -18,7 +18,7 @@ impl BubbleState {
}
}
/// User Events are events that are shuttled from the renderer into the VirtualDom trhough the scheduler channel.
/// User Events are events that are shuttled from the renderer into the VirtualDom through the scheduler channel.
///
/// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree
/// where each listener is checked and fired if the event name matches.
+7 -13
View File
@@ -21,7 +21,7 @@ use std::{
/// - the `rsx!` macro
/// - the [`NodeFactory`] API
pub enum VNode<'src> {
/// Text VNodes simply bump-allocated (or static) string slices
/// Text VNodes are simply bump-allocated (or static) string slices
///
/// # Example
///
@@ -351,31 +351,25 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>;
/// }
///
/// ```
#[derive(Default)]
pub struct EventHandler<'bump, T = ()> {
pub callback: &'bump RefCell<Option<ExternalListenerCallback<'bump, T>>>,
pub callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
}
impl<T> EventHandler<'_, T> {
/// Call this event handler with the appropriate event type
pub fn call(&self, event: T) {
if let Some(callback) = self.callback.borrow_mut().as_mut() {
callback(event);
}
}
/// Forcibly drop the internal handler callback, releasing memory
pub fn release(&self) {
self.callback.replace(None);
}
}
impl<T> Copy for EventHandler<'_, T> {}
impl<T> Clone for EventHandler<'_, T> {
fn clone(&self) -> Self {
Self {
callback: self.callback,
}
}
}
/// Virtual Components for custom user-defined components
/// Only supports the functional syntax
pub struct VComponent<'src> {
@@ -405,7 +399,7 @@ impl<P> AnyProps for VComponentProps<P> {
}
// Safety:
// this will downcat the other ptr as our swallowed type!
// this will downcast the other ptr as our swallowed type!
// you *must* make this check *before* calling this method
// if your functions are not the same, then you will downcast a pointer into a different type (UB)
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool {
@@ -677,7 +671,7 @@ impl<'a> NodeFactory<'a> {
pub fn event_handler<T>(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> {
let handler: &mut dyn FnMut(T) = self.bump.alloc(f);
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
let callback = self.bump.alloc(RefCell::new(Some(caller)));
let callback = RefCell::new(Some(caller));
EventHandler { callback }
}
}
+68 -10
View File
@@ -70,7 +70,7 @@ impl ScopeArena {
}
/// Safety:
/// - Obtaining a mutable refernece to any Scope is unsafe
/// - Obtaining a mutable reference to any Scope is unsafe
/// - Scopes use interior mutability when sharing data into components
pub(crate) fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
unsafe { self.scopes.borrow().get(&id).map(|f| &**f) }
@@ -101,7 +101,7 @@ impl ScopeArena {
let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten();
/*
This scopearena aggressively reuse old scopes when possible.
This scopearena aggressively reuses old scopes when possible.
We try to minimize the new allocations for props/arenas.
However, this will probably lead to some sort of fragmentation.
@@ -198,12 +198,9 @@ impl ScopeArena {
// run the hooks (which hold an &mut Reference)
// recursively call ensure_drop_safety on all children
items.borrowed_props.drain(..).for_each(|comp| {
let scope_id = comp
.scope
.get()
.expect("VComponents should be associated with a valid Scope");
self.ensure_drop_safety(scope_id);
if let Some(scope_id) = comp.scope.get() {
self.ensure_drop_safety(scope_id);
}
drop(comp.props.take());
});
@@ -670,6 +667,60 @@ impl ScopeState {
value
}
/// Provide a context for the root component from anywhere in your app.
///
///
/// # Example
///
/// ```rust, ignore
/// struct SharedState(&'static str);
///
/// static App: Component = |cx| {
/// cx.use_hook(|_| cx.provide_root_context(SharedState("world")));
/// rsx!(cx, Child {})
/// }
///
/// static Child: Component = |cx| {
/// let state = cx.consume_state::<SharedState>();
/// rsx!(cx, div { "hello {state.0}" })
/// }
/// ```
pub fn provide_root_context<T: 'static>(&self, value: T) -> Rc<T> {
let value = Rc::new(value);
// if we *are* the root component, then we can just provide the context directly
if self.scope_id() == ScopeId(0) {
self.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), value.clone())
.map(|f| f.downcast::<T>().ok())
.flatten();
return value;
}
let mut search_parent = self.parent_scope;
while let Some(parent) = search_parent.take() {
let parent = unsafe { &*parent };
if parent.scope_id() == ScopeId(0) {
let exists = parent
.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), value.clone());
if exists.is_some() {
log::warn!("Context already provided to parent scope - replacing it");
}
return value;
}
search_parent = parent.parent_scope;
}
unreachable!("all apps have a root scope")
}
/// Try to retrieve a SharedState with type T from the any parent Scope.
pub fn consume_context<T: 'static>(&self) -> Option<Rc<T>> {
if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
@@ -700,6 +751,11 @@ impl ScopeState {
self.tasks.push_fut(fut)
}
/// Spawns the future but does not return the TaskId
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
self.push_future(fut);
}
// todo: attach some state to the future to know if we should poll it
pub fn remove_future(&self, id: TaskId) {
self.tasks.remove_fut(id);
@@ -840,13 +896,15 @@ impl ScopeState {
self.frames[0].reset();
self.frames[1].reset();
// Finally, free up the hook values
self.hook_arena.reset();
// Free up the hook values
self.hook_vals.get_mut().drain(..).for_each(|state| {
let as_mut = unsafe { &mut *state };
let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) };
drop(boxed);
});
// Finally, clear the hook arena
self.hook_arena.reset();
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ use crate::innerlude::*;
pub struct ElementIdIterator<'a> {
vdom: &'a VirtualDom,
// Heuristcally we should never bleed into 5 completely nested fragments/components
// Heuristically we should never bleed into 5 completely nested fragments/components
// Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>,
}
+4 -4
View File
@@ -9,12 +9,12 @@ use fxhash::FxHashSet;
use indexmap::IndexSet;
use std::{collections::VecDeque, iter::FromIterator, task::Poll};
/// A virtual node s ystem that progresses user events and diffs UI trees.
/// A virtual node system that progresses user events and diffs UI trees.
///
///
/// ## Guide
///
/// Components are defined as simple functions that take [`Scope`] and return an [`Element`].
/// Components are defined as simple functions that take [`Scope`] and return an [`Element`].
///
/// ```rust, ignore
/// #[derive(Props, PartialEq)]
@@ -233,7 +233,7 @@ impl VirtualDom {
/// Get the [`Scope`] for the root component.
///
/// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus
/// This is useful for traversing the tree from the root for heuristics or alternative renderers that use Dioxus
/// directly.
///
/// This method is equivalent to calling `get_scope(ScopeId(0))`
@@ -618,7 +618,7 @@ impl VirtualDom {
///
/// let dom = VirtualDom::new(Base);
/// let nodes = dom.render_nodes(rsx!("div"));
/// ```
/// ```
pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> {
let mut machine = DiffState::new(&self.scopes);
machine.stack.push(DiffInstruction::Diff { new, old });
+1
View File
@@ -29,6 +29,7 @@ tokio = { version = "1.12.0", features = [
], optional = true, default-features = false }
dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"}
dioxus-html = { path = "../html", features = ["serialize"], version ="^0.1.4"}
webbrowser = "0.5.5"
[features]
default = ["tokio_runtime"]
+5 -1
View File
@@ -12,7 +12,11 @@
<div id="main">
</div>
</body>
<script type="text/javascript" src="index.js">
<script>
import("./index.js").then(function (module) {
module.main();
});
</script>
</html>
-446
View File
@@ -1,446 +0,0 @@
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,
};
function serialize_event(event) {
switch (event.type) {
case "copy":
case "cut":
case "past":
return {};
case "compositionend":
case "compositionstart":
case "compositionupdate":
return {
data: event.data,
};
case "keydown":
case "keypress":
case "keyup":
return {
char_code: event.charCode,
key: event.key,
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
key_code: event.keyCode,
shift_key: event.shiftKey,
locale: "locale",
location: event.location,
repeat: event.repeat,
which: event.which,
// locale: event.locale,
};
case "focus":
case "blur":
return {};
case "change":
let target = event.target;
let value;
if (target.type === "checkbox" || target.type === "radio") {
value = target.checked ? "true" : "false";
} else {
value = target.value ?? target.textContent;
}
return {
value: value,
};
case "input":
case "invalid":
case "reset":
case "submit": {
let target = event.target;
let value = target.value ?? target.textContent;
if (target.type == "checkbox") {
value = target.checked ? "true" : "false";
}
return {
value: value,
};
}
case "click":
case "contextmenu":
case "doubleclick":
case "drag":
case "dragend":
case "dragenter":
case "dragexit":
case "dragleave":
case "dragover":
case "dragstart":
case "drop":
case "mousedown":
case "mouseenter":
case "mouseleave":
case "mousemove":
case "mouseout":
case "mouseover":
case "mouseup":
return {
alt_key: event.altKey,
button: event.button,
buttons: event.buttons,
client_x: event.clientX,
client_y: event.clientY,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
};
case "pointerdown":
case "pointermove":
case "pointerup":
case "pointercancel":
case "gotpointercapture":
case "lostpointercapture":
case "pointerenter":
case "pointerleave":
case "pointerover":
case "pointerout":
return {
alt_key: event.altKey,
button: event.button,
buttons: event.buttons,
client_x: event.clientX,
client_y: event.clientY,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
pointer_id: event.pointerId,
width: event.width,
height: event.height,
pressure: event.pressure,
tangential_pressure: event.tangentialPressure,
tilt_x: event.tiltX,
tilt_y: event.tiltY,
twist: event.twist,
pointer_type: event.pointerType,
is_primary: event.isPrimary,
};
case "select":
return {};
case "touchcancel":
case "touchend":
case "touchmove":
case "touchstart":
return {
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
shift_key: event.shiftKey,
// changed_touches: event.changedTouches,
// target_touches: event.targetTouches,
// touches: event.touches,
};
case "scroll":
return {};
case "wheel":
return {
delta_x: event.deltaX,
delta_y: event.deltaY,
delta_z: event.deltaZ,
delta_mode: event.deltaMode,
};
case "animationstart":
case "animationend":
case "animationiteration":
return {
animation_name: event.animationName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
};
case "transitionend":
return {
property_name: event.propertyName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
};
case "abort":
case "canplay":
case "canplaythrough":
case "durationchange":
case "emptied":
case "encrypted":
case "ended":
case "error":
case "loadeddata":
case "loadedmetadata":
case "loadstart":
case "pause":
case "play":
case "playing":
case "progress":
case "ratechange":
case "seeked":
case "seeking":
case "stalled":
case "suspend":
case "timeupdate":
case "volumechange":
case "waiting":
return {};
case "toggle":
return {};
default:
return {};
}
}
class Interpreter {
constructor(root) {
this.root = root;
this.stack = [root];
this.listeners = {
onclick: {},
};
this.lastNodeWasText = false;
this.nodes = [root];
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
PushRoot(edit) {
const id = edit.root;
const node = this.nodes[id];
this.stack.push(node);
}
AppendChildren(edit) {
let root = this.stack[this.stack.length - (1 + edit.many)];
let to_add = this.stack.splice(this.stack.length - edit.many);
for (let i = 0; i < edit.many; i++) {
root.appendChild(to_add[i]);
}
}
ReplaceWith(edit) {
let root = this.nodes[edit.root];
let els = this.stack.splice(this.stack.length - edit.m);
root.replaceWith(...els);
}
InsertAfter(edit) {
let old = this.nodes[edit.root];
let new_nodes = this.stack.splice(this.stack.length - edit.n);
old.after(...new_nodes);
}
InsertBefore(edit) {
let old = this.nodes[edit.root];
let new_nodes = this.stack.splice(this.stack.length - edit.n);
old.before(...new_nodes);
}
Remove(edit) {
let node = this.nodes[edit.root];
if (node !== undefined) {
node.remove();
}
}
CreateTextNode(edit) {
const node = document.createTextNode(edit.text);
this.nodes[edit.root] = node;
this.stack.push(node);
}
CreateElement(edit) {
const tagName = edit.tag;
const el = document.createElement(tagName);
this.nodes[edit.root] = el;
el.setAttribute("dioxus-id", edit.root);
this.stack.push(el);
}
CreateElementNs(edit) {
let el = document.createElementNS(edit.ns, edit.tag);
this.stack.push(el);
this.nodes[edit.root] = el;
}
CreatePlaceholder(edit) {
let el = document.createElement("pre");
el.hidden = true;
this.stack.push(el);
this.nodes[edit.root] = el;
}
RemoveEventListener(edit) {}
NewEventListener(edit) {
const event_name = edit.event_name;
const mounted_node_id = edit.root;
const scope = edit.scope;
const element = this.nodes[edit.root];
element.setAttribute(
`dioxus-event-${event_name}`,
`${scope}.${mounted_node_id}`
);
if (this.listeners[event_name] === undefined) {
this.listeners[event_name] = true;
this.root.addEventListener(event_name, (event) => {
const target = event.target;
const real_id = target.getAttribute(`dioxus-id`);
const should_prevent_default = target.getAttribute(
`dioxus-prevent-default`
);
let contents = serialize_event(event);
if (should_prevent_default === `on${event.type}`) {
event.preventDefault();
}
if (real_id == null) {
return;
}
rpc.call("user_event", {
event: event_name,
mounted_dom_id: parseInt(real_id),
contents: contents,
});
});
}
}
SetText(edit) {
this.nodes[edit.root].textContent = edit.text;
}
SetAttribute(edit) {
// console.log("setting attr", edit);
const name = edit.field;
const value = edit.value;
const ns = edit.ns;
const node = this.nodes[edit.root];
if (ns == "style") {
node.style[name] = value;
} else if (ns != null || ns != undefined) {
node.setAttributeNS(ns, name, value);
} else {
switch (name) {
case "value":
if (value != node.value) {
node.value = value;
}
break;
case "checked":
node.checked = value === "true";
break;
case "selected":
node.selected = value === "true";
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 (value == "false" && bool_attrs.hasOwnProperty(name)) {
node.removeAttribute(name);
} else {
node.setAttribute(name, value);
}
}
}
}
RemoveAttribute(edit) {
const name = edit.field;
const node = this.nodes[edit.root];
node.removeAttribute(name);
if (name === "value") {
node.value = null;
}
if (name === "checked") {
node.checked = false;
}
if (name === "selected") {
node.selected = false;
}
}
handleEdits(edits) {
this.stack.push(this.root);
for (let x = 0; x < edits.length; x++) {
let edit = edits[x];
let f = this[edit.type];
f.call(this, edit);
}
}
}
function main() {
let root = window.document.getElementById("main");
window.interpreter = new Interpreter(root);
rpc.call("initialize");
}
main();
+21 -7
View File
@@ -149,7 +149,7 @@ pub fn launch_with_props<P: 'static + Send>(
props: P,
builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig,
) {
let mut cfg = DesktopConfig::new();
let mut cfg = DesktopConfig::default();
builder(&mut cfg);
let event_loop = EventLoop::with_user_event();
@@ -187,11 +187,25 @@ pub fn launch_with_props<P: 'static + Send>(
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
let _ = proxy.send_event(UserWindowEvent::Update);
}
"browser_open" => {
let data = req.params.unwrap();
log::trace!("Open browser: {:?}", data);
if let Some(arr) = data.as_array() {
if let Some(temp) = arr[0].as_object() {
if temp.contains_key("href") {
let url = temp.get("href").unwrap().as_str().unwrap();
if let Err(e) = webbrowser::open(url) {
log::error!("Open Browser error: {:?}", e);
}
}
}
}
}
_ => {}
}
None
})
.with_custom_protocol("dioxus".into(), move |request| {
.with_custom_protocol(String::from("dioxus"), move |request| {
// Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case"
// For now, we only serve two pieces of content which get included as bytes into the final binary.
let path = request.uri().replace("dioxus://", "");
@@ -203,7 +217,7 @@ pub fn launch_with_props<P: 'static + Send>(
} else if path.trim_end_matches('/') == "index.html/index.js" {
wry::http::ResponseBuilder::new()
.mimetype("text/javascript")
.body(include_bytes!("./index.js").to_vec())
.body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec())
} else {
wry::http::ResponseBuilder::new()
.status(wry::http::status::StatusCode::NOT_FOUND)
@@ -211,10 +225,10 @@ pub fn launch_with_props<P: 'static + Send>(
}
})
.with_file_drop_handler(move |window, evet| {
if let Some(handler) = file_handler.as_ref() {
return handler(window, evet);
}
false
file_handler
.as_ref()
.map(|handler| handler(window, evet))
.unwrap_or_default()
});
for (name, handler) in cfg.protocos.drain(..) {
+9 -9
View File
@@ -42,7 +42,7 @@ The HTML namespace is defined mostly with macros. However, the expanded form wou
struct base;
impl DioxusElement for base {
const TAG_NAME: &'static str = "base";
const NAME_SPACE: Option<&'static str> = None;
const NAME_SPACE: Option<&'static str> = None;
}
impl base {
#[inline]
@@ -60,7 +60,7 @@ Because attributes are defined as methods on the unit struct, they guard the att
## How to extend it:
Whenever the rsx! macro is called, it relies on a module `dioxus_elements` to be in scope. When you enable the `html` feature in dioxus, this module gets imported in the prelude. However, you can extend this with your own set of custom elements by making your own `dioxus_elements` module and re-exporting the html namespace.
Whenever the rsx! macro is called, it relies on a module `dioxus_elements` to be in scope. When you enable the `html` feature in dioxus, this module gets imported in the prelude. However, you can extend this with your own set of custom elements by making your own `dioxus_elements` module and re-exporting the html namespace.
```rust
mod dioxus_elements {
@@ -68,13 +68,13 @@ mod dioxus_elements {
struct my_element;
impl DioxusElement for my_element {
const TAG_NAME: &'static str = "base";
const NAME_SPACE: Option<&'static str> = None;
const NAME_SPACE: Option<&'static str> = None;
}
}
```
## Limitations:
-
-
## How to work around it:
If an attribute in Dioxus is invalid (defined incorrectly) - first, make an issue - but then, you can work around it. The raw builder API is actually somewhat ergonomic to work with, and the NodeFactory type exposes a bunch of methods to make any type of tree - even invalid ones! So obviously, be careful, but there's basically anything you can do.
@@ -87,19 +87,19 @@ cx.render(rsx!{
{LazyNodes::new(move |f| {
f.raw_element(
// tag name
"custom_element",
"custom_element",
// attributes
&[f.attr("billy", format_args!("goat"))],
&[f.attr("billy", format_args!("goat"))],
// listeners
&[f.listener(onclick(move |_| {}))],
&[f.listener(onclick(move |_| {}))],
// children
&[cx.render(rsx!(div {} ))],
&[cx.render(rsx!(div {} ))],
// key
None
None
)
})}
}
+6
View File
@@ -399,6 +399,9 @@ pub trait GlobalAttributes {
/// Specify the font weight of the text.
font_weight: "font-weight",
/// Sets gaps (gutters) between rows and columns. Shorthand for row_gap and column_gap.
gap: "gap",
/// Specify the height of an element.
height: "height",
@@ -531,6 +534,9 @@ pub trait GlobalAttributes {
/// Specify the location of the right edge of the positioned element.
right: "right",
/// Specifies the gap between the rows in a multi_column element.
row_gap: "row-gap",
/// Specifies the length of the tab character.
tab_size: "tab-size",
+8
View File
@@ -0,0 +1,8 @@
# JS Interpreter
After diffing old and new trees, the Dioxus VirtualDom produces patches that are used to modify the existing Dom. We can send these patches anywhere - including targets without WASM support.
In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets.
For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks.
+499
View File
@@ -0,0 +1,499 @@
export function main() {
let root = window.document.getElementById("main");
if (root != null) {
window.interpreter = new Interpreter(root);
window.rpc.call("initialize");
}
}
export class Interpreter {
root;
stack;
listeners;
handlers;
lastNodeWasText;
nodes;
constructor(root) {
this.root = root;
this.stack = [root];
this.listeners = {};
this.handlers = {};
this.lastNodeWasText = false;
this.nodes = [root];
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
PushRoot(root) {
const node = this.nodes[root];
this.stack.push(node);
}
AppendChildren(many) {
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);
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) {
node.remove();
}
}
CreateTextNode(text, root) {
// todo: make it so the types are okay
const node = document.createTextNode(text);
this.nodes[root] = node;
this.stack.push(node);
}
CreateElement(tag, root) {
const el = document.createElement(tag);
// el.setAttribute("data-dioxus-id", `${root}`);
this.nodes[root] = el;
this.stack.push(el);
}
CreateElementNs(tag, root, ns) {
let el = document.createElementNS(ns, tag);
this.stack.push(el);
this.nodes[root] = el;
}
CreatePlaceholder(root) {
let el = document.createElement("pre");
el.hidden = true;
this.stack.push(el);
this.nodes[root] = el;
}
NewEventListener(event_name, root, handler) {
const element = this.nodes[root];
element.setAttribute("data-dioxus-id", `${root}`);
if (this.listeners[event_name] === undefined) {
this.listeners[event_name] = 0;
this.handlers[event_name] = handler;
this.root.addEventListener(event_name, handler);
}
else {
this.listeners[event_name]++;
}
}
RemoveEventListener(root, event_name) {
const element = this.nodes[root];
element.removeAttribute(`data-dioxus-id`);
this.listeners[event_name]--;
if (this.listeners[event_name] === 0) {
this.root.removeEventListener(event_name, this.handlers[event_name]);
delete this.listeners[event_name];
delete this.handlers[event_name];
}
}
SetText(root, text) {
this.nodes[root].textContent = text;
}
SetAttribute(root, field, value, ns) {
const name = field;
const node = this.nodes[root];
if (ns == "style") {
// @ts-ignore
node.style[name] = value;
}
else if (ns != null || ns != undefined) {
node.setAttributeNS(ns, name, value);
}
else {
switch (name) {
case "value":
if (value != node.value) {
node.value = value;
}
break;
case "checked":
node.checked = value === "true";
break;
case "selected":
node.selected = value === "true";
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 (value == "false" && bool_attrs.hasOwnProperty(name)) {
node.removeAttribute(name);
}
else {
node.setAttribute(name, value);
}
}
}
}
RemoveAttribute(root, name) {
const node = this.nodes[root];
node.removeAttribute(name);
if (name === "value") {
node.value = "";
}
if (name === "checked") {
node.checked = false;
}
if (name === "selected") {
node.selected = false;
}
}
handleEdits(edits) {
this.stack.push(this.root);
for (let edit of edits) {
this.handleEdit(edit);
}
}
handleEdit(edit) {
switch (edit.type) {
case "PushRoot":
this.PushRoot(edit.root);
break;
case "AppendChildren":
this.AppendChildren(edit.many);
break;
case "ReplaceWith":
this.ReplaceWith(edit.root, edit.m);
break;
case "InsertAfter":
this.InsertAfter(edit.root, edit.n);
break;
case "InsertBefore":
this.InsertBefore(edit.root, edit.n);
break;
case "Remove":
this.Remove(edit.root);
break;
case "CreateTextNode":
this.CreateTextNode(edit.text, edit.root);
break;
case "CreateElement":
this.CreateElement(edit.tag, edit.root);
break;
case "CreateElementNs":
this.CreateElementNs(edit.tag, edit.root, edit.ns);
break;
case "CreatePlaceholder":
this.CreatePlaceholder(edit.root);
break;
case "RemoveEventListener":
this.RemoveEventListener(edit.root, edit.event_name);
break;
case "NewEventListener":
// this handler is only provided on desktop implementations since this
// method is not used by the web implementation
let handler = (event) => {
let target = event.target;
if (target != null) {
let realId = target.getAttribute(`data-dioxus-id`);
// walk the tree to find the real element
while (realId == null && target.parentElement != null) {
target = target.parentElement;
realId = target.getAttribute(`data-dioxus-id`);
}
const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
let contents = serialize_event(event);
if (shouldPreventDefault === `on${event.type}`) {
event.preventDefault();
}
if (event.type == "submit") {
event.preventDefault();
}
if (event.type == "click") {
event.preventDefault();
if (shouldPreventDefault !== `onclick`) {
if (target.tagName == "A") {
const href = target.getAttribute("href");
if (href !== "" && href !== null && href !== undefined && realId != null) {
window.rpc.call("browser_open", {
mounted_dom_id: parseInt(realId),
href
});
}
}
}
}
if (realId == null) {
return;
}
window.rpc.call("user_event", {
event: edit.event_name,
mounted_dom_id: parseInt(realId),
contents: contents,
});
}
};
this.NewEventListener(edit.event_name, edit.root, handler);
break;
case "SetText":
this.SetText(edit.root, edit.text);
break;
case "SetAttribute":
this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
break;
case "RemoveAttribute":
this.RemoveAttribute(edit.root, edit.name);
break;
}
}
}
function serialize_event(event) {
switch (event.type) {
case "copy":
case "cut":
case "past": {
return {};
}
case "compositionend":
case "compositionstart":
case "compositionupdate": {
let { data } = event;
return {
data,
};
}
case "keydown":
case "keypress":
case "keyup": {
let { charCode, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event;
return {
char_code: charCode,
key: key,
alt_key: altKey,
ctrl_key: ctrlKey,
meta_key: metaKey,
key_code: keyCode,
shift_key: shiftKey,
location: location,
repeat: repeat,
which: which,
locale: "locale",
};
}
case "focus":
case "blur": {
return {};
}
case "change": {
let target = event.target;
let value;
if (target.type === "checkbox" || target.type === "radio") {
value = target.checked ? "true" : "false";
}
else {
value = target.value ?? target.textContent;
}
return {
value: value,
};
}
case "input":
case "invalid":
case "reset":
case "submit": {
let target = event.target;
let value = target.value ?? target.textContent;
if (target.type == "checkbox") {
value = target.checked ? "true" : "false";
}
return {
value: value,
};
}
case "click":
case "contextmenu":
case "doubleclick":
case "drag":
case "dragend":
case "dragenter":
case "dragexit":
case "dragleave":
case "dragover":
case "dragstart":
case "drop":
case "mousedown":
case "mouseenter":
case "mouseleave":
case "mousemove":
case "mouseout":
case "mouseover":
case "mouseup": {
const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, } = event;
return {
alt_key: altKey,
button: button,
buttons: buttons,
client_x: clientX,
client_y: clientY,
ctrl_key: ctrlKey,
meta_key: metaKey,
page_x: pageX,
page_y: pageY,
screen_x: screenX,
screen_y: screenY,
shift_key: shiftKey,
};
}
case "pointerdown":
case "pointermove":
case "pointerup":
case "pointercancel":
case "gotpointercapture":
case "lostpointercapture":
case "pointerenter":
case "pointerleave":
case "pointerover":
case "pointerout": {
const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, pointerId, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, pointerType, isPrimary, } = event;
return {
alt_key: altKey,
button: button,
buttons: buttons,
client_x: clientX,
client_y: clientY,
ctrl_key: ctrlKey,
meta_key: metaKey,
page_x: pageX,
page_y: pageY,
screen_x: screenX,
screen_y: screenY,
shift_key: shiftKey,
pointer_id: pointerId,
width: width,
height: height,
pressure: pressure,
tangential_pressure: tangentialPressure,
tilt_x: tiltX,
tilt_y: tiltY,
twist: twist,
pointer_type: pointerType,
is_primary: isPrimary,
};
}
case "select": {
return {};
}
case "touchcancel":
case "touchend":
case "touchmove":
case "touchstart": {
const { altKey, ctrlKey, metaKey, shiftKey, } = event;
return {
// changed_touches: event.changedTouches,
// target_touches: event.targetTouches,
// touches: event.touches,
alt_key: altKey,
ctrl_key: ctrlKey,
meta_key: metaKey,
shift_key: shiftKey,
};
}
case "scroll": {
return {};
}
case "wheel": {
const { deltaX, deltaY, deltaZ, deltaMode, } = event;
return {
delta_x: deltaX,
delta_y: deltaY,
delta_z: deltaZ,
delta_mode: deltaMode,
};
}
case "animationstart":
case "animationend":
case "animationiteration": {
const { animationName, elapsedTime, pseudoElement, } = event;
return {
animation_name: animationName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "transitionend": {
const { propertyName, elapsedTime, pseudoElement, } = event;
return {
property_name: propertyName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "abort":
case "canplay":
case "canplaythrough":
case "durationchange":
case "emptied":
case "encrypted":
case "ended":
case "error":
case "loadeddata":
case "loadedmetadata":
case "loadstart":
case "pause":
case "play":
case "playing":
case "progress":
case "ratechange":
case "seeked":
case "seeking":
case "stalled":
case "suspend":
case "timeupdate":
case "volumechange":
case "waiting": {
return {};
}
case "toggle": {
return {};
}
default: {
return {};
}
}
}
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,
};
+680
View File
@@ -0,0 +1,680 @@
export function main() {
let root = window.document.getElementById("main");
if (root != null) {
window.interpreter = new Interpreter(root);
window.rpc.call("initialize");
}
}
declare global {
interface Window {
interpreter: Interpreter;
rpc: { call: (method: string, args?: any) => void };
}
}
export class Interpreter {
root: Element;
stack: Element[];
listeners: { [key: string]: number };
handlers: { [key: string]: (evt: Event) => void };
lastNodeWasText: boolean;
nodes: Element[];
constructor(root: Element) {
this.root = root;
this.stack = [root];
this.listeners = {};
this.handlers = {};
this.lastNodeWasText = false;
this.nodes = [root];
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
PushRoot(root: number) {
const node = this.nodes[root];
this.stack.push(node);
}
AppendChildren(many: number) {
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: number, m: number) {
let root = this.nodes[root_id] as Element;
let els = this.stack.splice(this.stack.length - m);
root.replaceWith(...els);
}
InsertAfter(root: number, n: number) {
let old = this.nodes[root] as Element;
let new_nodes = this.stack.splice(this.stack.length - n);
old.after(...new_nodes);
}
InsertBefore(root: number, n: number) {
let old = this.nodes[root] as Element;
let new_nodes = this.stack.splice(this.stack.length - n);
old.before(...new_nodes);
}
Remove(root: number) {
let node = this.nodes[root] as Element;
if (node !== undefined) {
node.remove();
}
}
CreateTextNode(text: string, root: number) {
// todo: make it so the types are okay
const node = document.createTextNode(text) as any as Element;
this.nodes[root] = node;
this.stack.push(node);
}
CreateElement(tag: string, root: number) {
const el = document.createElement(tag);
// el.setAttribute("data-dioxus-id", `${root}`);
this.nodes[root] = el;
this.stack.push(el);
}
CreateElementNs(tag: string, root: number, ns: string) {
let el = document.createElementNS(ns, tag);
this.stack.push(el);
this.nodes[root] = el;
}
CreatePlaceholder(root: number) {
let el = document.createElement("pre");
el.hidden = true;
this.stack.push(el);
this.nodes[root] = el;
}
NewEventListener(event_name: string, root: number, handler: (evt: Event) => void) {
const element = this.nodes[root];
element.setAttribute("data-dioxus-id", `${root}`);
if (this.listeners[event_name] === undefined) {
this.listeners[event_name] = 0;
this.handlers[event_name] = handler;
this.root.addEventListener(event_name, handler);
} else {
this.listeners[event_name]++;
}
}
RemoveEventListener(root: number, event_name: string) {
const element = this.nodes[root];
element.removeAttribute(`data-dioxus-id`);
this.listeners[event_name]--;
if (this.listeners[event_name] === 0) {
this.root.removeEventListener(event_name, this.handlers[event_name]);
delete this.listeners[event_name];
delete this.handlers[event_name];
}
}
SetText(root: number, text: string) {
this.nodes[root].textContent = text;
}
SetAttribute(root: number, field: string, value: string, ns: string | undefined) {
const name = field;
const node = this.nodes[root];
if (ns == "style") {
// @ts-ignore
(node as HTMLElement).style[name] = value;
} else if (ns != null || ns != undefined) {
node.setAttributeNS(ns, name, value);
} else {
switch (name) {
case "value":
if (value != (node as HTMLInputElement).value) {
(node as HTMLInputElement).value = value;
}
break;
case "checked":
(node as HTMLInputElement).checked = value === "true";
break;
case "selected":
(node as HTMLOptionElement).selected = value === "true";
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 (value == "false" && bool_attrs.hasOwnProperty(name)) {
node.removeAttribute(name);
} else {
node.setAttribute(name, value);
}
}
}
}
RemoveAttribute(root: number, name: string) {
const node = this.nodes[root];
node.removeAttribute(name);
if (name === "value") {
(node as HTMLInputElement).value = "";
}
if (name === "checked") {
(node as HTMLInputElement).checked = false;
}
if (name === "selected") {
(node as HTMLOptionElement).selected = false;
}
}
handleEdits(edits: DomEdit[]) {
this.stack.push(this.root);
for (let edit of edits) {
this.handleEdit(edit);
}
}
handleEdit(edit: DomEdit) {
switch (edit.type) {
case "PushRoot":
this.PushRoot(edit.root);
break;
case "AppendChildren":
this.AppendChildren(edit.many);
break;
case "ReplaceWith":
this.ReplaceWith(edit.root, edit.m);
break;
case "InsertAfter":
this.InsertAfter(edit.root, edit.n);
break;
case "InsertBefore":
this.InsertBefore(edit.root, edit.n);
break;
case "Remove":
this.Remove(edit.root);
break;
case "CreateTextNode":
this.CreateTextNode(edit.text, edit.root);
break;
case "CreateElement":
this.CreateElement(edit.tag, edit.root);
break;
case "CreateElementNs":
this.CreateElementNs(edit.tag, edit.root, edit.ns);
break;
case "CreatePlaceholder":
this.CreatePlaceholder(edit.root);
break;
case "RemoveEventListener":
this.RemoveEventListener(edit.root, edit.event_name);
break;
case "NewEventListener":
// this handler is only provided on desktop implementations since this
// method is not used by the web implementation
let handler = (event: Event) => {
let target = event.target as Element | null;
if (target != null) {
let realId = target.getAttribute(`data-dioxus-id`);
// walk the tree to find the real element
while (realId == null && target.parentElement != null) {
target = target.parentElement;
realId = target.getAttribute(`data-dioxus-id`);
}
const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
let contents = serialize_event(event);
if (shouldPreventDefault === `on${event.type}`) {
event.preventDefault();
}
if (event.type == "submit") {
event.preventDefault();
}
if (event.type == "click") {
event.preventDefault();
if (shouldPreventDefault !== `onclick`) {
if (target.tagName == "A") {
const href = target.getAttribute("href")
if (href !== "" && href !== null && href !== undefined && realId != null) {
window.rpc.call("browser_open", {
mounted_dom_id: parseInt(realId),
href
});
}
}
}
}
if (realId == null) {
return;
}
window.rpc.call("user_event", {
event: (edit as NewEventListener).event_name,
mounted_dom_id: parseInt(realId),
contents: contents,
});
}
};
this.NewEventListener(edit.event_name, edit.root, handler);
break;
case "SetText":
this.SetText(edit.root, edit.text);
break;
case "SetAttribute":
this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
break;
case "RemoveAttribute":
this.RemoveAttribute(edit.root, edit.name);
break;
}
}
}
function serialize_event(event: Event) {
switch (event.type) {
case "copy":
case "cut":
case "past": {
return {};
}
case "compositionend":
case "compositionstart":
case "compositionupdate": {
let { data } = (event as CompositionEvent);
return {
data,
};
}
case "keydown":
case "keypress":
case "keyup": {
let {
charCode,
key,
altKey,
ctrlKey,
metaKey,
keyCode,
shiftKey,
location,
repeat,
which,
} = (event as KeyboardEvent);
return {
char_code: charCode,
key: key,
alt_key: altKey,
ctrl_key: ctrlKey,
meta_key: metaKey,
key_code: keyCode,
shift_key: shiftKey,
location: location,
repeat: repeat,
which: which,
locale: "locale",
};
}
case "focus":
case "blur": {
return {};
}
case "change": {
let target = event.target as HTMLInputElement;
let value;
if (target.type === "checkbox" || target.type === "radio") {
value = target.checked ? "true" : "false";
} else {
value = target.value ?? target.textContent;
}
return {
value: value,
};
}
case "input":
case "invalid":
case "reset":
case "submit": {
let target = event.target as HTMLFormElement;
let value = target.value ?? target.textContent;
if (target.type == "checkbox") {
value = target.checked ? "true" : "false";
}
return {
value: value,
};
}
case "click":
case "contextmenu":
case "doubleclick":
case "drag":
case "dragend":
case "dragenter":
case "dragexit":
case "dragleave":
case "dragover":
case "dragstart":
case "drop":
case "mousedown":
case "mouseenter":
case "mouseleave":
case "mousemove":
case "mouseout":
case "mouseover":
case "mouseup": {
const {
altKey,
button,
buttons,
clientX,
clientY,
ctrlKey,
metaKey,
pageX,
pageY,
screenX,
screenY,
shiftKey,
} = event as MouseEvent;
return {
alt_key: altKey,
button: button,
buttons: buttons,
client_x: clientX,
client_y: clientY,
ctrl_key: ctrlKey,
meta_key: metaKey,
page_x: pageX,
page_y: pageY,
screen_x: screenX,
screen_y: screenY,
shift_key: shiftKey,
};
}
case "pointerdown":
case "pointermove":
case "pointerup":
case "pointercancel":
case "gotpointercapture":
case "lostpointercapture":
case "pointerenter":
case "pointerleave":
case "pointerover":
case "pointerout": {
const {
altKey,
button,
buttons,
clientX,
clientY,
ctrlKey,
metaKey,
pageX,
pageY,
screenX,
screenY,
shiftKey,
pointerId,
width,
height,
pressure,
tangentialPressure,
tiltX,
tiltY,
twist,
pointerType,
isPrimary,
} = event as PointerEvent;
return {
alt_key: altKey,
button: button,
buttons: buttons,
client_x: clientX,
client_y: clientY,
ctrl_key: ctrlKey,
meta_key: metaKey,
page_x: pageX,
page_y: pageY,
screen_x: screenX,
screen_y: screenY,
shift_key: shiftKey,
pointer_id: pointerId,
width: width,
height: height,
pressure: pressure,
tangential_pressure: tangentialPressure,
tilt_x: tiltX,
tilt_y: tiltY,
twist: twist,
pointer_type: pointerType,
is_primary: isPrimary,
};
}
case "select": {
return {};
}
case "touchcancel":
case "touchend":
case "touchmove":
case "touchstart": {
const {
altKey,
ctrlKey,
metaKey,
shiftKey,
} = event as TouchEvent;
return {
// changed_touches: event.changedTouches,
// target_touches: event.targetTouches,
// touches: event.touches,
alt_key: altKey,
ctrl_key: ctrlKey,
meta_key: metaKey,
shift_key: shiftKey,
};
}
case "scroll": {
return {};
}
case "wheel": {
const {
deltaX,
deltaY,
deltaZ,
deltaMode,
} = event as WheelEvent;
return {
delta_x: deltaX,
delta_y: deltaY,
delta_z: deltaZ,
delta_mode: deltaMode,
};
}
case "animationstart":
case "animationend":
case "animationiteration": {
const {
animationName,
elapsedTime,
pseudoElement,
} = event as AnimationEvent;
return {
animation_name: animationName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "transitionend": {
const {
propertyName,
elapsedTime,
pseudoElement,
} = event as TransitionEvent;
return {
property_name: propertyName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "abort":
case "canplay":
case "canplaythrough":
case "durationchange":
case "emptied":
case "encrypted":
case "ended":
case "error":
case "loadeddata":
case "loadedmetadata":
case "loadstart":
case "pause":
case "play":
case "playing":
case "progress":
case "ratechange":
case "seeked":
case "seeking":
case "stalled":
case "suspend":
case "timeupdate":
case "volumechange":
case "waiting": {
return {};
}
case "toggle": {
return {};
}
default: {
return {};
}
}
}
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,
};
type PushRoot = { type: "PushRoot", root: number };
type AppendChildren = { type: "AppendChildren", many: number };
type ReplaceWith = { type: "ReplaceWith", root: number, m: number };
type InsertAfter = { type: "InsertAfter", root: number, n: number };
type InsertBefore = { type: "InsertBefore", root: number, n: number };
type Remove = { type: "Remove", root: number };
type CreateTextNode = { type: "CreateTextNode", text: string, root: number };
type CreateElement = { type: "CreateElement", tag: string, root: number };
type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string };
type CreatePlaceholder = { type: "CreatePlaceholder", root: number };
type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number };
type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number };
type SetText = { type: "SetText", root: number, text: string };
type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined };
type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string };
type DomEdit =
PushRoot |
AppendChildren |
ReplaceWith |
InsertAfter |
InsertBefore |
Remove |
CreateTextNode |
CreateElement |
CreateElementNs |
CreatePlaceholder |
NewEventListener |
RemoveEventListener |
SetText |
SetAttribute |
RemoveAttribute;
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ES2015",
"lib": [
"es2015",
"es5",
"es6",
"dom"
],
"strict": true,
}
}
+2 -2
View File
@@ -20,7 +20,7 @@ async fn main() -> tide::Result<()> {
}
```
Dioxus LiveView runs your Dioxus apps on the server
Dioxus LiveView runs your Dioxus apps on the server
@@ -36,7 +36,7 @@ async fn main() {
async fn order_shoes(mut req: WebsocketRequest) -> Response {
let stream = req.upgrade();
dioxus::liveview::launch(App, stream).await;
dioxus::liveview::launch(App, stream).await;
}
fn App(cx: Scope) -> Element {
+1 -1
View File
@@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile
And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile.
```shell
$ cargo mobile init
$ cargo mobile init
```
We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`.
+1 -1
View File
@@ -16,7 +16,7 @@ fn app() {
Then, in your route, you can choose to parse the Route any way you want through `use_route`.
```rust
let id: usize = use_route(&cx).path("id")?;
let id: usize = use_route(&cx).segment("id")?;
let state: CustomState = use_route(&cx).parse()?;
```
+23 -12
View File
@@ -11,7 +11,7 @@ pub struct LinkProps<'a> {
/// The url that gets pushed to the history stack
///
/// You can either put it your own inline method or just autoderive the route using `derive(Routable)`
/// You can either put in your own inline method or just autoderive the route using `derive(Routable)`
///
/// ```rust, ignore
///
@@ -31,6 +31,9 @@ pub struct LinkProps<'a> {
#[props(default, strip_option)]
id: Option<&'a str>,
#[props(default, strip_option)]
title: Option<&'a str>,
children: Element<'a>,
#[props(default)]
@@ -38,17 +41,25 @@ pub struct LinkProps<'a> {
}
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
let service = cx.consume_context::<RouterService>()?;
cx.render(rsx! {
a {
href: "{cx.props.to}",
class: format_args!("{}", cx.props.class.unwrap_or("")),
id: format_args!("{}", cx.props.id.unwrap_or("")),
log::debug!("render Link to {}", cx.props.to);
if let Some(service) = cx.consume_context::<RouterService>() {
return cx.render(rsx! {
a {
href: "{cx.props.to}",
class: format_args!("{}", cx.props.class.unwrap_or("")),
id: format_args!("{}", cx.props.id.unwrap_or("")),
title: format_args!("{}", cx.props.title.unwrap_or("")),
prevent_default: "onclick",
onclick: move |_| service.push_route(cx.props.to),
prevent_default: "onclick",
onclick: move |_| service.push_route(cx.props.to),
&cx.props.children
}
})
&cx.props.children
}
});
}
log::warn!(
"Attempted to create a Link to {} outside of a Router context",
cx.props.to,
);
None
}
+1
View File
@@ -30,6 +30,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
Some(ctx) => ctx.total_route.to_string(),
None => cx.props.to.to_string(),
};
log::trace!("total route for {} is {}", cx.props.to, total_route);
// provide our route context
let route_context = cx.provide_context(RouteContext {
+1 -1
View File
@@ -12,7 +12,7 @@ pub struct RouterProps<'a> {
children: Element<'a>,
#[props(default, strip_option)]
onchange: Option<&'a Fn(&'a str)>,
onchange: Option<&'a dyn Fn(&'a str)>,
}
#[allow(non_snake_case)]
+69 -16
View File
@@ -1,30 +1,83 @@
use dioxus_core::ScopeState;
use gloo::history::{HistoryResult, Location};
use serde::de::DeserializeOwned;
use std::{rc::Rc, str::FromStr};
pub struct UseRoute<'a> {
cur_route: String,
cx: &'a ScopeState,
use crate::RouterService;
/// This struct provides is a wrapper around the internal router
/// implementation, with methods for getting information about the current
/// route.
pub struct UseRoute {
router: Rc<RouterService>,
}
impl<'a> UseRoute<'a> {
/// Parse the query part of the URL
pub fn param<T>(&self, param: &str) -> Option<&T> {
todo!()
impl UseRoute {
/// This method simply calls the [`Location::query`] method.
pub fn query<T>(&self) -> HistoryResult<T>
where
T: DeserializeOwned,
{
self.current_location().query::<T>()
}
pub fn nth_segment(&self, n: usize) -> Option<&str> {
todo!()
/// Returns the nth segment in the path. Paths that end with a slash have
/// the slash removed before determining the segments. If the path has
/// fewer segments than `n` then this method returns `None`.
pub fn nth_segment(&self, n: usize) -> Option<String> {
let mut segments = self.path_segments();
let len = segments.len();
if len - 1 < n {
return None;
}
Some(segments.remove(n))
}
pub fn last_segment(&self) -> Option<&'a str> {
todo!()
/// Returns the last segment in the path. Paths that end with a slash have
/// the slash removed before determining the segments. The root path, `/`,
/// will return an empty string.
pub fn last_segment(&self) -> String {
let mut segments = self.path_segments();
segments.remove(segments.len() - 1)
}
/// Parse the segments of the URL, using named parameters (defined in your router)
pub fn segment<T>(&self, name: &str) -> Option<&T> {
todo!()
/// Get the named parameter from the path, as defined in your router. The
/// value will be parsed into the type specified by `T` by calling
/// `value.parse::<T>()`. This method returns `None` if the named
/// parameter does not exist in the current path.
pub fn segment<T>(&self, name: &str) -> Option<Result<T, T::Err>>
where
T: FromStr,
{
self.router
.current_path_params()
.get(name)
.and_then(|v| Some(v.parse::<T>()))
}
/// Returns the [Location] for the current route.
pub fn current_location(&self) -> Location {
self.router.current_location()
}
fn path_segments(&self) -> Vec<String> {
let location = self.router.current_location();
let path = location.path();
if path == "/" {
return vec![String::new()];
}
let stripped = &location.path()[1..];
stripped.split('/').map(str::to_string).collect::<Vec<_>>()
}
}
pub fn use_route<'a>(cx: &'a ScopeState) -> UseRoute<'a> {
todo!()
/// This hook provides access to information about the current location in the
/// context of a [`Router`]. If this function is called outside of a `Router`
/// component it will panic.
pub fn use_route(cx: &ScopeState) -> UseRoute {
let router = cx
.consume_context::<RouterService>()
.expect("Cannot call use_route outside the scope of a Router component")
.clone();
UseRoute { router }
}
-2
View File
@@ -1,5 +1,3 @@
use url::Url;
pub trait RouterProvider {
fn get_current_route(&self) -> String;
fn subscribe_to_route_changes(&self, callback: Box<dyn Fn(String)>);
+99 -21
View File
@@ -1,6 +1,6 @@
use gloo::history::{BrowserHistory, History, HistoryListener};
use gloo::history::{BrowserHistory, History, HistoryListener, Location};
use std::{
cell::{Cell, RefCell},
cell::{Cell, Ref, RefCell},
collections::HashMap,
rc::Rc,
};
@@ -10,10 +10,9 @@ use dioxus_core::ScopeId;
pub struct RouterService {
pub(crate) regen_route: Rc<dyn Fn(ScopeId)>,
history: Rc<RefCell<BrowserHistory>>,
registerd_routes: RefCell<RouteSlot>,
slots: Rc<RefCell<Vec<(ScopeId, String)>>>,
root_found: Rc<Cell<bool>>,
cur_root: RefCell<String>,
root_found: Rc<Cell<Option<ScopeId>>>,
cur_path_params: Rc<RefCell<HashMap<String, String>>>,
listener: HistoryListener,
}
@@ -40,48 +39,54 @@ impl RouterService {
let _slots = slots.clone();
let root_found = Rc::new(Cell::new(false));
let root_found = Rc::new(Cell::new(None));
let regen = regen_route.clone();
let _root_found = root_found.clone();
let listener = history.listen(move || {
_root_found.set(false);
_root_found.set(None);
// checking if the route is valid is cheap, so we do it
for (slot, _) in _slots.borrow_mut().iter().rev() {
log::trace!("regenerating slot {:?}", slot);
for (slot, root) in _slots.borrow_mut().iter().rev() {
log::trace!("regenerating slot {:?} for root '{}'", slot, root);
regen(*slot);
}
});
Self {
registerd_routes: RefCell::new(RouteSlot::Routes {
partial: String::from("/"),
total: String::from("/"),
rest: Vec::new(),
}),
root_found,
history: Rc::new(RefCell::new(history)),
regen_route,
slots,
cur_root: RefCell::new(path.to_string()),
cur_path_params: Rc::new(RefCell::new(HashMap::new())),
listener,
}
}
pub fn push_route(&self, route: &str) {
log::trace!("Pushing route: {}", route);
self.history.borrow_mut().push(route);
}
pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) {
self.slots.borrow_mut().push((scope, route));
let clean = clean_route(route);
log::trace!("Registered route '{}' with scope id {:?}", clean, scope);
self.slots.borrow_mut().push((scope, clean));
}
pub fn should_render(&self, scope: ScopeId) -> bool {
if self.root_found.get() {
log::trace!("Should render scope id {:?}?", scope);
if let Some(root_id) = self.root_found.get() {
log::trace!(" we already found a root with scope id {:?}", root_id);
if root_id == scope {
log::trace!(" yes - it's a match");
return true;
}
log::trace!(" no - it's not a match");
return false;
}
let location = self.history.borrow().location();
let path = location.path();
log::trace!(" current path is '{}'", path);
let roots = self.slots.borrow();
@@ -89,15 +94,24 @@ impl RouterService {
// fallback logic
match root {
Some((_id, route)) => {
if route == path {
self.root_found.set(true);
Some((id, route)) => {
log::trace!(
" matched given scope id {:?} with route root '{}'",
scope,
route,
);
if let Some(params) = route_matches_path(route, path) {
log::trace!(" and it matches the current path '{}'", path);
self.root_found.set(Some(*id));
*self.cur_path_params.borrow_mut() = params;
true
} else {
if route == "" {
self.root_found.set(true);
log::trace!(" and the route is the root, so we will use that without a better match");
self.root_found.set(Some(*id));
true
} else {
log::trace!(" and the route '{}' is not the root nor does it match the current path", route);
false
}
}
@@ -105,6 +119,70 @@ impl RouterService {
None => false,
}
}
pub fn current_location(&self) -> Location {
self.history.borrow().location().clone()
}
pub fn current_path_params(&self) -> Ref<HashMap<String, String>> {
self.cur_path_params.borrow()
}
}
fn clean_route(route: String) -> String {
if route.as_str() == "/" {
return route;
}
route.trim_end_matches('/').to_string()
}
fn clean_path(path: &str) -> &str {
if path == "/" {
return path;
}
path.trim_end_matches('/')
}
fn route_matches_path(route: &str, path: &str) -> Option<HashMap<String, String>> {
let route_pieces = route.split('/').collect::<Vec<_>>();
let path_pieces = clean_path(path).split('/').collect::<Vec<_>>();
log::trace!(
" checking route pieces {:?} vs path pieces {:?}",
route_pieces,
path_pieces,
);
if route_pieces.len() != path_pieces.len() {
log::trace!(" the routes are different lengths");
return None;
}
let mut matches = HashMap::new();
for (i, r) in route_pieces.iter().enumerate() {
log::trace!(" checking route piece '{}' vs path", r);
// If this is a parameter then it matches as long as there's
// _any_thing in that spot in the path.
if r.starts_with(':') {
log::trace!(
" route piece '{}' starts with a colon so it matches anything",
r,
);
let param = &r[1..];
matches.insert(param.to_string(), path_pieces[i].to_string());
continue;
}
log::trace!(
" route piece '{}' must be an exact match for path piece '{}'",
r,
path_pieces[i],
);
if path_pieces[i] != *r {
return None;
}
}
Some(matches)
}
pub struct RouterCfg {
+2 -2
View File
@@ -52,7 +52,7 @@ let content = dioxus::ssr::render_vdom(&dom);
```
## Configuring output
It's possible to configure the output of the generated HTML.
It's possible to configure the output of the generated HTML.
```rust, ignore
let content = dioxus::ssr::render_vdom(&dom, |config| config.pretty(true).prerender(true));
@@ -82,7 +82,7 @@ buf.write_fmt!(format_args!("{}", args));
## Usage in pre-rendering
## Usage in pre-rendering
This crate is particularly useful in pre-generating pages server-side and then selectively loading dioxus client-side to pick up the reactive elements.
+6 -17
View File
@@ -11,8 +11,8 @@ documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies]
dioxus-core = { path = "../core", version ="^0.1.7"}
dioxus-html = { path = "../html", version ="^0.1.4"}
dioxus-core = { path = "../core", version = "^0.1.7" }
dioxus-html = { path = "../html", version = "^0.1.4" }
js-sys = "0.3"
wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] }
lazy_static = "1.4.0"
@@ -20,7 +20,7 @@ wasm-bindgen-futures = "0.4.20"
log = { version = "0.4.14", features = ["release_max_level_off"] }
fxhash = "0.2.1"
wasm-logger = "0.2.0"
console_error_panic_hook = "0.1.6"
console_error_panic_hook = { version = "0.1.7", optional = true }
wasm-bindgen-test = "0.3.21"
once_cell = "1.8"
async-channel = "1.6.1"
@@ -69,22 +69,11 @@ features = [
"IdleDeadline",
]
# [lib]
# crate-type = ["cdylib", "rlib"]
[features]
default = ["panic_hook"]
panic_hook = ["console_error_panic_hook"]
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro" }
wasm-bindgen-test = "0.3.28"
dioxus-ssr = { path = "../ssr" }
# im-rc = "15.0.0"
# separator = "0.4.1"
# uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] }
# serde = { version = "1.0.126", features = ["derive"] }
# reqwest = { version = "0.11", features = ["json"] }
# dioxus-hooks = { path = "../hooks" }
# rand = { version = "0.8.4", features = ["small_rng"] }
# [dev-dependencies.getrandom]
# version = "0.2"
# features = ["js"]
+59
View File
@@ -0,0 +1,59 @@
use js_sys::Function;
use wasm_bindgen::prelude::*;
use web_sys::{Element, Node};
#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")]
extern "C" {
pub type Interpreter;
#[wasm_bindgen(constructor)]
pub fn new(arg: Element) -> Interpreter;
#[wasm_bindgen(method)]
pub fn set_node(this: &Interpreter, id: usize, node: Node);
#[wasm_bindgen(method)]
pub fn PushRoot(this: &Interpreter, root: u64);
#[wasm_bindgen(method)]
pub fn AppendChildren(this: &Interpreter, many: u32);
#[wasm_bindgen(method)]
pub fn ReplaceWith(this: &Interpreter, root: u64, m: u32);
#[wasm_bindgen(method)]
pub fn InsertAfter(this: &Interpreter, root: u64, n: u32);
#[wasm_bindgen(method)]
pub fn InsertBefore(this: &Interpreter, root: u64, n: u32);
#[wasm_bindgen(method)]
pub fn Remove(this: &Interpreter, root: u64);
#[wasm_bindgen(method)]
pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64);
#[wasm_bindgen(method)]
pub fn CreateElement(this: &Interpreter, tag: &str, root: u64);
#[wasm_bindgen(method)]
pub fn CreateElementNs(this: &Interpreter, tag: &str, root: u64, ns: &str);
#[wasm_bindgen(method)]
pub fn CreatePlaceholder(this: &Interpreter, root: u64);
#[wasm_bindgen(method)]
pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function);
#[wasm_bindgen(method)]
pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str);
#[wasm_bindgen(method)]
pub fn SetText(this: &Interpreter, root: u64, text: &str);
#[wasm_bindgen(method)]
pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>);
#[wasm_bindgen(method)]
pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str);
}
+1 -1
View File
@@ -1,4 +1,4 @@
pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[
pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[
// Important tags to dioxus
"dioxus-id",
"dioxus",
+5 -6
View File
@@ -3,10 +3,9 @@
/// This struct helps configure the specifics of hydration and render destination for WebSys.
///
/// # Example
///
/// ```rust, ignore
/// fn main() {
/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot"))
/// }
/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot"))
/// ```
pub struct WebConfig {
pub(crate) hydrate: bool,
@@ -31,7 +30,7 @@ impl WebConfig {
/// work and suspended nodes.
///
/// Dioxus will load up all the elements with the `dio_el` data attribute into memory when the page is loaded.
pub fn hydrate(mut self, f: bool) -> Self {
pub fn hydrate(&mut self, f: bool) -> &mut Self {
self.hydrate = f;
self
}
@@ -39,7 +38,7 @@ impl WebConfig {
/// Set the name of the element that Dioxus will use as the root.
///
/// This is akint to calling React.render() on the element with the specified name.
pub fn rootname(mut self, name: impl Into<String>) -> Self {
pub fn rootname(&mut self, name: impl Into<String>) -> &mut Self {
self.rootname = name.into();
self
}
@@ -47,7 +46,7 @@ impl WebConfig {
/// Set the name of the element that Dioxus will use as the root.
///
/// This is akint to calling React.render() on the element with the specified name.
pub fn with_string_cache(mut self, cache: Vec<String>) -> Self {
pub fn with_string_cache(&mut self, cache: Vec<String>) -> &mut Self {
self.cached_strings = cache;
self
}
+87 -464
View File
@@ -7,56 +7,51 @@
//! - tests to ensure dyn_into works for various event types.
//! - Partial delegation?>
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent};
use fxhash::FxHashMap;
use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc};
use crate::bindings::Interpreter;
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
use js_sys::Function;
use std::{any::Any, rc::Rc, sync::Arc};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
HtmlOptionElement, HtmlTextAreaElement, Node,
};
use web_sys::{Document, Element, Event, HtmlElement};
use crate::{nodeslab::NodeSlab, WebConfig};
use crate::WebConfig;
pub struct WebsysDom {
stack: Stack,
/// A map from ElementID (index) to Node
pub(crate) nodes: NodeSlab,
document: Document,
pub interpreter: Interpreter,
pub(crate) root: Element,
sender_callback: Rc<dyn Fn(SchedulerMsg)>,
// map of listener types to number of those listeners
// This is roughly a delegater
// TODO: check how infero delegates its events - some are more performant
listeners: FxHashMap<&'static str, ListenerEntry>,
pub handler: Closure<dyn FnMut(&Event)>,
}
type ListenerEntry = (usize, Closure<dyn FnMut(&Event)>);
impl WebsysDom {
pub fn new(cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
let document = load_document();
// eventually, we just want to let the interpreter do all the work of decoding events into our event type
let callback: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
if let Ok(synthetic_event) = decode_trigger(event) {
// Try to prevent default if the attribute is set
if let Some(target) = event.target() {
if let Some(node) = target.dyn_ref::<HtmlElement>() {
if let Some(name) = node.get_attribute("dioxus-prevent-default") {
if name == synthetic_event.name
|| name.trim_start_matches("on") == synthetic_event.name
{
log::trace!("Preventing default");
event.prevent_default();
}
}
}
}
let nodes = NodeSlab::new(2000);
let listeners = FxHashMap::default();
let mut stack = Stack::with_capacity(10);
sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event))
}
});
let root = load_document().get_element_by_id(&cfg.rootname).unwrap();
let root_node = root.clone().dyn_into::<Node>().unwrap();
stack.push(root_node);
Self {
stack,
nodes,
listeners,
document,
sender_callback,
interpreter: Interpreter::new(root.clone()),
handler: Closure::wrap(callback),
root,
}
}
@@ -64,438 +59,49 @@ impl WebsysDom {
pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
for edit in edits.drain(..) {
match edit {
DomEdit::PushRoot { root } => self.push(root),
DomEdit::AppendChildren { many } => self.append_children(many),
DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
DomEdit::Remove { root } => self.remove(root),
DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id),
DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id),
DomEdit::CreateElementNs { tag, root: id, ns } => {
self.create_element(tag, Some(ns), id)
DomEdit::PushRoot { root } => self.interpreter.PushRoot(root),
DomEdit::AppendChildren { many } => self.interpreter.AppendChildren(many),
DomEdit::ReplaceWith { root, m } => self.interpreter.ReplaceWith(root, m),
DomEdit::InsertAfter { root, n } => self.interpreter.InsertAfter(root, n),
DomEdit::InsertBefore { root, n } => self.interpreter.InsertBefore(root, n),
DomEdit::Remove { root } => self.interpreter.Remove(root),
DomEdit::CreateTextNode { text, root } => {
self.interpreter.CreateTextNode(text, root)
}
DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id),
DomEdit::CreateElement { tag, root } => self.interpreter.CreateElement(tag, root),
DomEdit::CreateElementNs { tag, root, ns } => {
self.interpreter.CreateElementNs(tag, root, ns)
}
DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root),
DomEdit::NewEventListener {
event_name,
scope,
root: mounted_node_id,
} => self.new_event_listener(event_name, scope, mounted_node_id),
DomEdit::RemoveEventListener { event, root } => {
self.remove_event_listener(event, root)
event_name, root, ..
} => {
let handler: &Function = self.handler.as_ref().unchecked_ref();
self.interpreter.NewEventListener(event_name, root, handler);
}
DomEdit::SetText { text, root } => self.set_text(text, root),
DomEdit::RemoveEventListener { root, event } => {
self.interpreter.RemoveEventListener(root, event)
}
DomEdit::SetText { root, text } => self.interpreter.SetText(root, text),
DomEdit::SetAttribute {
root,
field,
value,
ns,
root,
} => self.set_attribute(field, value, ns, root),
DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root),
DomEdit::InsertAfter { n, root } => self.insert_after(n, root),
DomEdit::InsertBefore { n, root } => self.insert_before(n, root),
}
}
}
fn push(&mut self, root: u64) {
let key = root as usize;
let domnode = &self.nodes[key];
let real_node: Node = match domnode {
Some(n) => n.clone(),
None => todo!(),
};
self.stack.push(real_node);
}
fn append_children(&mut self, many: u32) {
let root: Node = self
.stack
.list
.get(self.stack.list.len() - (1 + many as usize))
.unwrap()
.clone();
// We need to make sure to add comments between text nodes
// We ensure that the text siblings are patched by preventing the browser from merging
// neighboring text nodes. Originally inspired by some of React's work from 2016.
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
// -> https://github.com/facebook/react/pull/5753
/*
todo: we need to track this for replacing/insert after/etc
*/
let mut last_node_was_text = false;
for child in self
.stack
.list
.drain((self.stack.list.len() - many as usize)..)
{
if child.dyn_ref::<web_sys::Text>().is_some() {
if last_node_was_text {
let comment_node = self
.document
.create_comment("dioxus")
.dyn_into::<Node>()
.unwrap();
root.append_child(&comment_node).unwrap();
}
last_node_was_text = true;
} else {
last_node_was_text = false;
}
root.append_child(&child).unwrap();
}
}
fn replace_with(&mut self, m: u32, root: u64) {
let old = self.nodes[root as usize].as_ref().unwrap();
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - m as usize)..)
.collect();
if let Some(el) = old.dyn_ref::<Element>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
el.replace_with_with_node(&arr).unwrap();
}
}
fn remove(&mut self, root: u64) {
let node = self.nodes[root as usize].as_ref().unwrap();
if let Some(element) = node.dyn_ref::<Element>() {
element.remove();
} else {
if let Some(parent) = node.parent_node() {
parent.remove_child(&node).unwrap();
}
}
}
fn create_placeholder(&mut self, id: u64) {
self.create_element("pre", None, id);
self.set_attribute("hidden", "", None, id);
}
fn create_text_node(&mut self, text: &str, id: u64) {
let textnode = self
.document
.create_text_node(text)
.dyn_into::<Node>()
.unwrap();
self.stack.push(textnode.clone());
self.nodes[(id as usize)] = Some(textnode);
}
fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
let tag = wasm_bindgen::intern(tag);
let el = match ns {
Some(ns) => self
.document
.create_element_ns(Some(ns), tag)
.unwrap()
.dyn_into::<Node>()
.unwrap(),
None => self
.document
.create_element(tag)
.unwrap()
.dyn_into::<Node>()
.unwrap(),
};
use smallstr::SmallString;
use std::fmt::Write;
let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new();
write!(s, "{}", id).unwrap();
let el2 = el.dyn_ref::<Element>().unwrap();
el2.set_attribute("dioxus-id", s.as_str()).unwrap();
self.stack.push(el.clone());
self.nodes[(id as usize)] = Some(el);
}
fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) {
let event = wasm_bindgen::intern(event);
// attach the correct attributes to the element
// these will be used by accessing the event's target
// This ensures we only ever have one handler attached to the root, but decide
// dynamically when we want to call a listener.
let el = self.stack.top();
let el = el.dyn_ref::<Element>().unwrap();
el.set_attribute("dioxus-event", event).unwrap();
// Register the callback to decode
if let Some(entry) = self.listeners.get_mut(event) {
entry.0 += 1;
} else {
let trigger = self.sender_callback.clone();
let c: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
// "Result" cannot be received from JS
// Instead, we just build and immediately execute a closure that returns result
match decode_trigger(event) {
Ok(synthetic_event) => {
let target = event.target().unwrap();
if let Some(node) = target.dyn_ref::<HtmlElement>() {
if let Some(name) = node.get_attribute("dioxus-prevent-default") {
if name == synthetic_event.name
|| name.trim_start_matches("on") == synthetic_event.name
{
log::trace!("Preventing default");
event.prevent_default();
}
}
}
trigger.as_ref()(SchedulerMsg::Event(synthetic_event))
}
Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
};
});
let handler = Closure::wrap(c);
self.root
.add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref())
.unwrap();
// Increment the listeners
self.listeners.insert(event.into(), (1, handler));
}
}
fn remove_event_listener(&mut self, _event: &str, _root: u64) {
todo!()
}
fn set_text(&mut self, text: &str, root: u64) {
let el = self.nodes[root as usize].as_ref().unwrap();
el.set_text_content(Some(text))
}
fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) {
let node = self.nodes[root as usize].as_ref().unwrap();
if ns == Some("style") {
if let Some(el) = node.dyn_ref::<Element>() {
let el = el.dyn_ref::<HtmlElement>().unwrap();
let style_dc: CssStyleDeclaration = el.style();
style_dc.set_property(name, value).unwrap();
}
} else {
let fallback = || {
let el = node.dyn_ref::<Element>().unwrap();
el.set_attribute(name, value).unwrap()
};
match name {
"dangerous_inner_html" => {
if let Some(el) = node.dyn_ref::<Element>() {
el.set_inner_html(value);
}
}
"value" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
/*
if the attribute being set is the same as the value of the input, then don't bother setting it.
This is used in controlled components to keep the cursor in the right spot.
this logic should be moved into the virtualdom since we have the notion of "volatile"
*/
if input.value() != value {
input.set_value(value);
}
} else if let Some(node) = node.dyn_ref::<HtmlTextAreaElement>() {
if name == "value" {
node.set_value(value);
}
} else {
fallback();
}
}
"checked" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
match value {
"true" => input.set_checked(true),
"false" => input.set_checked(false),
_ => fallback(),
}
} else {
fallback();
}
}
"selected" => {
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
node.set_selected(true);
} else {
fallback();
}
}
_ => {
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
if value == "false" {
if let Some(el) = node.dyn_ref::<Element>() {
match name {
"allowfullscreen"
| "allowpaymentrequest"
| "async"
| "autofocus"
| "autoplay"
| "checked"
| "controls"
| "default"
| "defer"
| "disabled"
| "formnovalidate"
| "hidden"
| "ismap"
| "itemscope"
| "loop"
| "multiple"
| "muted"
| "nomodule"
| "novalidate"
| "open"
| "playsinline"
| "readonly"
| "required"
| "reversed"
| "selected"
| "truespeed" => {
let _ = el.remove_attribute(name);
}
_ => {
let _ = el.set_attribute(name, value);
}
};
}
} else {
fallback();
}
} => self.interpreter.SetAttribute(root, field, value, ns),
DomEdit::RemoveAttribute { root, name } => {
self.interpreter.RemoveAttribute(root, name)
}
}
}
}
fn remove_attribute(&mut self, name: &str, root: u64) {
let node = self.nodes[root as usize].as_ref().unwrap();
if let Some(node) = node.dyn_ref::<web_sys::Element>() {
node.remove_attribute(name).unwrap();
}
if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
// Some attributes are "volatile" and don't work through `removeAttribute`.
if name == "value" {
node.set_value("");
}
if name == "checked" {
node.set_checked(false);
}
}
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
if name == "selected" {
node.set_selected(true);
}
}
}
fn insert_after(&mut self, n: u32, root: u64) {
let old = self.nodes[root as usize].as_ref().unwrap();
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - n as usize)..)
.collect();
if let Some(el) = old.dyn_ref::<Element>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
el.after_with_node(&arr).unwrap();
}
}
fn insert_before(&mut self, n: u32, root: u64) {
let anchor = self.nodes[root as usize].as_ref().unwrap();
if n == 1 {
let before = self.stack.pop();
anchor
.parent_node()
.unwrap()
.insert_before(&before, Some(&anchor))
.unwrap();
} else {
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - n as usize)..)
.collect();
if let Some(el) = anchor.dyn_ref::<Element>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = anchor.dyn_ref::<web_sys::CharacterData>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = anchor.dyn_ref::<web_sys::DocumentType>() {
el.before_with_node(&arr).unwrap();
}
}
}
}
#[derive(Debug, Default)]
struct Stack {
list: Vec<Node>,
}
impl Stack {
#[inline]
fn with_capacity(cap: usize) -> Self {
Stack {
list: Vec::with_capacity(cap),
}
}
#[inline]
fn push(&mut self, node: Node) {
self.list.push(node);
}
#[inline]
fn pop(&mut self) -> Node {
self.list.pop().unwrap()
}
fn top(&self) -> &Node {
match self.list.last() {
Some(a) => a,
None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
}
}
}
pub struct DioxusWebsysEvent(web_sys::Event);
// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread
#[allow(clippy::non_send_fields_in_send_ty)]
unsafe impl Send for DioxusWebsysEvent {}
unsafe impl Sync for DioxusWebsysEvent {}
@@ -632,6 +238,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send
shift_key: evt.shift_key(),
})
}
"scroll" => Arc::new(()),
"wheel" => {
let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap();
@@ -670,9 +277,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send
/// This function decodes a websys event and produces an EventTrigger
/// With the websys implementation, we attach a unique key to the nodes
fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
use anyhow::Context;
let target = event
let mut target = event
.target()
.expect("missing target")
.dyn_into::<Element>()
@@ -680,18 +285,36 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
let typ = event.type_();
let element_id = target
.get_attribute("dioxus-id")
.context("Could not find element id on event target")?
.parse()?;
Ok(UserEvent {
name: event_name_from_typ(&typ),
data: virtual_event_from_websys_event(event.clone()),
element: Some(ElementId(element_id)),
scope_id: None,
priority: dioxus_core::EventPriority::Medium,
})
loop {
match target.get_attribute("data-dioxus-id").map(|f| f.parse()) {
Some(Ok(id)) => {
return Ok(UserEvent {
name: event_name_from_typ(&typ),
data: virtual_event_from_websys_event(event.clone()),
element: Some(ElementId(id)),
scope_id: None,
priority: dioxus_core::EventPriority::Medium,
});
}
Some(Err(e)) => {
return Err(e.into());
}
None => {
// walk the tree upwards until we actually find an event target
if let Some(parent) = target.parent_element() {
target = parent;
} else {
return Ok(UserEvent {
name: event_name_from_typ(&typ),
data: virtual_event_from_websys_event(event.clone()),
element: None,
scope_id: None,
priority: dioxus_core::EventPriority::Low,
});
}
}
}
}
}
pub(crate) fn load_document() -> Document {
+37 -6
View File
@@ -61,10 +61,10 @@ pub use dioxus_core as dioxus;
use dioxus_core::prelude::Component;
use futures_util::FutureExt;
pub(crate) mod bindings;
mod cache;
mod cfg;
mod dom;
mod nodeslab;
mod rehydrate;
mod ric_raf;
@@ -92,6 +92,29 @@ pub fn launch(root_component: Component) {
launch_with_props(root_component, (), |c| c);
}
/// Launch your app and run the event loop, with configuration.
///
/// This function will start your web app on the main web thread.
///
/// You can configure the WebView window with a configuration closure
///
/// ```rust
/// use dioxus::prelude::*;
///
/// fn main() {
/// dioxus_web::launch_with_props(App, |config| config.pre_render(true));
/// }
///
/// fn app(cx: Scope) -> Element {
/// cx.render(rsx!{
/// h1 {"hello world!"}
/// })
/// }
/// ```
pub fn launch_cfg(root: Component, config_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig) {
launch_with_props(root, (), config_builder)
}
/// Launches the VirtualDOM from the specified component function and props.
///
/// This method will block the thread with `spawn_local`
@@ -100,7 +123,11 @@ pub fn launch(root_component: Component) {
///
/// ```rust, ignore
/// fn main() {
/// dioxus_web::launch_with_props(App, RootProps { name: String::from("joe") });
/// dioxus_web::launch_with_props(
/// App,
/// RootProps { name: String::from("joe") },
/// |config| config
/// );
/// }
///
/// #[derive(ParitalEq, Props)]
@@ -112,15 +139,19 @@ pub fn launch(root_component: Component) {
/// rsx!(cx, div {"hello {cx.props.name}"})
/// }
/// ```
pub fn launch_with_props<T, F>(
pub fn launch_with_props<T>(
root_component: Component<T>,
root_properties: T,
configuration_builder: F,
configuration_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig,
) where
T: Send + 'static,
F: FnOnce(WebConfig) -> WebConfig,
{
let config = configuration_builder(WebConfig::default());
if cfg!(feature = "panic_hook") {
console_error_panic_hook::set_once();
}
let mut config = WebConfig::default();
configuration_builder(&mut config);
wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config));
}
-34
View File
@@ -1,34 +0,0 @@
//! This module provides a mirror of the VirtualDOM Element Slab using a Vector.
use std::ops::{Index, IndexMut};
use web_sys::Node;
pub(crate) struct NodeSlab {
nodes: Vec<Option<Node>>,
}
impl NodeSlab {
pub fn new(capacity: usize) -> NodeSlab {
let nodes = Vec::with_capacity(capacity);
NodeSlab { nodes }
}
}
impl Index<usize> for NodeSlab {
type Output = Option<Node>;
fn index(&self, index: usize) -> &Self::Output {
&self.nodes[index]
}
}
impl IndexMut<usize> for NodeSlab {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
if index >= self.nodes.capacity() * 3 {
panic!("Trying to mutate an element way too far out of bounds");
}
if index + 1 > self.nodes.len() {
self.nodes.resize_with(index + 1, || None);
}
&mut self.nodes[index]
}
}
+857
View File
@@ -0,0 +1,857 @@
//! Implementation of a renderer for Dioxus on the web.
//!
//! Oustanding todos:
//! - Removing event listeners (delegation)
//! - Passive event listeners
//! - no-op event listener patch for safari
//! - tests to ensure dyn_into works for various event types.
//! - Partial delegation?>
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent};
use fxhash::FxHashMap;
use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
HtmlOptionElement, HtmlTextAreaElement, Node,
};
use crate::{nodeslab::NodeSlab, WebConfig};
pub struct WebsysDom {
stack: Stack,
/// A map from ElementID (index) to Node
pub(crate) nodes: NodeSlab,
document: Document,
pub(crate) root: Element,
sender_callback: Rc<dyn Fn(SchedulerMsg)>,
// map of listener types to number of those listeners
// This is roughly a delegater
// TODO: check how infero delegates its events - some are more performant
listeners: FxHashMap<&'static str, ListenerEntry>,
}
type ListenerEntry = (usize, Closure<dyn FnMut(&Event)>);
impl WebsysDom {
pub fn new(cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
let document = load_document();
let nodes = NodeSlab::new(2000);
let listeners = FxHashMap::default();
let mut stack = Stack::with_capacity(10);
let root = load_document().get_element_by_id(&cfg.rootname).unwrap();
let root_node = root.clone().dyn_into::<Node>().unwrap();
stack.push(root_node);
Self {
stack,
nodes,
listeners,
document,
sender_callback,
root,
}
}
pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
for edit in edits.drain(..) {
match edit {
DomEdit::PushRoot { root } => self.push(root),
DomEdit::AppendChildren { many } => self.append_children(many),
DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
DomEdit::Remove { root } => self.remove(root),
DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id),
DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id),
DomEdit::CreateElementNs { tag, root: id, ns } => {
self.create_element(tag, Some(ns), id)
}
DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id),
DomEdit::NewEventListener {
event_name,
scope,
root: mounted_node_id,
} => self.new_event_listener(event_name, scope, mounted_node_id),
DomEdit::RemoveEventListener { event, root } => {
self.remove_event_listener(event, root)
}
DomEdit::SetText { text, root } => self.set_text(text, root),
DomEdit::SetAttribute {
field,
value,
ns,
root,
} => self.set_attribute(field, value, ns, root),
DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root),
DomEdit::InsertAfter { n, root } => self.insert_after(n, root),
DomEdit::InsertBefore { n, root } => self.insert_before(n, root),
}
}
}
fn push(&mut self, root: u64) {
let key = root as usize;
let domnode = &self.nodes[key];
let real_node: Node = match domnode {
Some(n) => n.clone(),
None => todo!(),
};
self.stack.push(real_node);
}
fn append_children(&mut self, many: u32) {
let root: Node = self
.stack
.list
.get(self.stack.list.len() - (1 + many as usize))
.unwrap()
.clone();
// We need to make sure to add comments between text nodes
// We ensure that the text siblings are patched by preventing the browser from merging
// neighboring text nodes. Originally inspired by some of React's work from 2016.
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
// -> https://github.com/facebook/react/pull/5753
/*
todo: we need to track this for replacing/insert after/etc
*/
let mut last_node_was_text = false;
for child in self
.stack
.list
.drain((self.stack.list.len() - many as usize)..)
{
if child.dyn_ref::<web_sys::Text>().is_some() {
if last_node_was_text {
let comment_node = self
.document
.create_comment("dioxus")
.dyn_into::<Node>()
.unwrap();
root.append_child(&comment_node).unwrap();
}
last_node_was_text = true;
} else {
last_node_was_text = false;
}
root.append_child(&child).unwrap();
}
}
fn replace_with(&mut self, m: u32, root: u64) {
let old = self.nodes[root as usize].as_ref().unwrap();
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - m as usize)..)
.collect();
if let Some(el) = old.dyn_ref::<Element>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
el.replace_with_with_node(&arr).unwrap();
}
}
fn remove(&mut self, root: u64) {
let node = self.nodes[root as usize].as_ref().unwrap();
if let Some(element) = node.dyn_ref::<Element>() {
element.remove();
} else {
if let Some(parent) = node.parent_node() {
parent.remove_child(&node).unwrap();
}
}
}
fn create_placeholder(&mut self, id: u64) {
self.create_element("pre", None, id);
self.set_attribute("hidden", "", None, id);
}
fn create_text_node(&mut self, text: &str, id: u64) {
let textnode = self
.document
.create_text_node(text)
.dyn_into::<Node>()
.unwrap();
self.stack.push(textnode.clone());
self.nodes[(id as usize)] = Some(textnode);
}
fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
let tag = wasm_bindgen::intern(tag);
let el = match ns {
Some(ns) => self
.document
.create_element_ns(Some(ns), tag)
.unwrap()
.dyn_into::<Node>()
.unwrap(),
None => self
.document
.create_element(tag)
.unwrap()
.dyn_into::<Node>()
.unwrap(),
};
use smallstr::SmallString;
use std::fmt::Write;
let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new();
write!(s, "{}", id).unwrap();
let el2 = el.dyn_ref::<Element>().unwrap();
el2.set_attribute("dioxus-id", s.as_str()).unwrap();
self.stack.push(el.clone());
self.nodes[(id as usize)] = Some(el);
}
fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) {
let event = wasm_bindgen::intern(event);
// attach the correct attributes to the element
// these will be used by accessing the event's target
// This ensures we only ever have one handler attached to the root, but decide
// dynamically when we want to call a listener.
let el = self.stack.top();
let el = el.dyn_ref::<Element>().unwrap();
el.set_attribute("dioxus-event", event).unwrap();
// Register the callback to decode
if let Some(entry) = self.listeners.get_mut(event) {
entry.0 += 1;
} else {
let trigger = self.sender_callback.clone();
let c: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
// "Result" cannot be received from JS
// Instead, we just build and immediately execute a closure that returns result
match decode_trigger(event) {
Ok(synthetic_event) => {
let target = event.target().unwrap();
if let Some(node) = target.dyn_ref::<HtmlElement>() {
if let Some(name) = node.get_attribute("dioxus-prevent-default") {
if name == synthetic_event.name
|| name.trim_start_matches("on") == synthetic_event.name
{
log::trace!("Preventing default");
event.prevent_default();
}
}
}
trigger.as_ref()(SchedulerMsg::Event(synthetic_event))
}
Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
};
});
let handler = Closure::wrap(c);
self.root
.add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref())
.unwrap();
// Increment the listeners
self.listeners.insert(event.into(), (1, handler));
}
}
fn remove_event_listener(&mut self, _event: &str, _root: u64) {
todo!()
}
fn set_text(&mut self, text: &str, root: u64) {
let el = self.nodes[root as usize].as_ref().unwrap();
el.set_text_content(Some(text))
}
fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) {
let node = self.nodes[root as usize].as_ref().unwrap();
if ns == Some("style") {
if let Some(el) = node.dyn_ref::<Element>() {
let el = el.dyn_ref::<HtmlElement>().unwrap();
let style_dc: CssStyleDeclaration = el.style();
style_dc.set_property(name, value).unwrap();
}
} else {
let fallback = || {
let el = node.dyn_ref::<Element>().unwrap();
el.set_attribute(name, value).unwrap()
};
match name {
"dangerous_inner_html" => {
if let Some(el) = node.dyn_ref::<Element>() {
el.set_inner_html(value);
}
}
"value" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
/*
if the attribute being set is the same as the value of the input, then don't bother setting it.
This is used in controlled components to keep the cursor in the right spot.
this logic should be moved into the virtualdom since we have the notion of "volatile"
*/
if input.value() != value {
input.set_value(value);
}
} else if let Some(node) = node.dyn_ref::<HtmlTextAreaElement>() {
if name == "value" {
node.set_value(value);
}
} else {
fallback();
}
}
"checked" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
match value {
"true" => input.set_checked(true),
"false" => input.set_checked(false),
_ => fallback(),
}
} else {
fallback();
}
}
"selected" => {
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
node.set_selected(true);
} else {
fallback();
}
}
_ => {
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
if value == "false" {
if let Some(el) = node.dyn_ref::<Element>() {
match name {
"allowfullscreen"
| "allowpaymentrequest"
| "async"
| "autofocus"
| "autoplay"
| "checked"
| "controls"
| "default"
| "defer"
| "disabled"
| "formnovalidate"
| "hidden"
| "ismap"
| "itemscope"
| "loop"
| "multiple"
| "muted"
| "nomodule"
| "novalidate"
| "open"
| "playsinline"
| "readonly"
| "required"
| "reversed"
| "selected"
| "truespeed" => {
let _ = el.remove_attribute(name);
}
_ => {
let _ = el.set_attribute(name, value);
}
};
}
} else {
fallback();
}
}
}
}
}
fn remove_attribute(&mut self, name: &str, root: u64) {
let node = self.nodes[root as usize].as_ref().unwrap();
if let Some(node) = node.dyn_ref::<web_sys::Element>() {
node.remove_attribute(name).unwrap();
}
if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
// Some attributes are "volatile" and don't work through `removeAttribute`.
if name == "value" {
node.set_value("");
}
if name == "checked" {
node.set_checked(false);
}
}
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
if name == "selected" {
node.set_selected(true);
}
}
}
fn insert_after(&mut self, n: u32, root: u64) {
let old = self.nodes[root as usize].as_ref().unwrap();
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - n as usize)..)
.collect();
if let Some(el) = old.dyn_ref::<Element>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
el.after_with_node(&arr).unwrap();
}
}
fn insert_before(&mut self, n: u32, root: u64) {
let anchor = self.nodes[root as usize].as_ref().unwrap();
if n == 1 {
let before = self.stack.pop();
anchor
.parent_node()
.unwrap()
.insert_before(&before, Some(&anchor))
.unwrap();
} else {
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - n as usize)..)
.collect();
if let Some(el) = anchor.dyn_ref::<Element>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = anchor.dyn_ref::<web_sys::CharacterData>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = anchor.dyn_ref::<web_sys::DocumentType>() {
el.before_with_node(&arr).unwrap();
}
}
}
}
#[derive(Debug, Default)]
struct Stack {
list: Vec<Node>,
}
impl Stack {
#[inline]
fn with_capacity(cap: usize) -> Self {
Stack {
list: Vec::with_capacity(cap),
}
}
#[inline]
fn push(&mut self, node: Node) {
self.list.push(node);
}
#[inline]
fn pop(&mut self) -> Node {
self.list.pop().unwrap()
}
fn top(&self) -> &Node {
match self.list.last() {
Some(a) => a,
None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
}
}
}
pub struct DioxusWebsysEvent(web_sys::Event);
// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread
unsafe impl Send for DioxusWebsysEvent {}
unsafe impl Sync for DioxusWebsysEvent {}
// todo: some of these events are being casted to the wrong event type.
// We need tests that simulate clicks/etc and make sure every event type works.
fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send + Sync> {
use dioxus_html::on::*;
use dioxus_html::KeyCode;
match event.type_().as_str() {
"copy" | "cut" | "paste" => Arc::new(ClipboardData {}),
"compositionend" | "compositionstart" | "compositionupdate" => {
let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap();
Arc::new(CompositionData {
data: evt.data().unwrap_or_default(),
})
}
"keydown" | "keypress" | "keyup" => {
let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap();
Arc::new(KeyboardData {
alt_key: evt.alt_key(),
char_code: evt.char_code(),
key: evt.key(),
key_code: KeyCode::from_raw_code(evt.key_code() as u8),
ctrl_key: evt.ctrl_key(),
locale: "not implemented".to_string(),
location: evt.location() as usize,
meta_key: evt.meta_key(),
repeat: evt.repeat(),
shift_key: evt.shift_key(),
which: evt.which() as usize,
})
}
"focus" | "blur" => Arc::new(FocusData {}),
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
// don't have a good solution with the serialized event problem
"change" | "input" | "invalid" | "reset" | "submit" => {
let evt: &web_sys::Event = event.dyn_ref().unwrap();
let target: web_sys::EventTarget = evt.target().unwrap();
let value: String = (&target)
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| {
// todo: special case more input types
match input.type_().as_str() {
"checkbox" => {
match input.checked() {
true => "true".to_string(),
false => "false".to_string(),
}
},
_ => {
input.value()
}
}
})
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
})
// select elements are NOT input events - because - why woudn't they be??
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlSelectElement| input.value())
})
.or_else(|| {
target
.dyn_ref::<web_sys::HtmlElement>()
.unwrap()
.text_content()
})
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
Arc::new(FormData { value })
}
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap();
Arc::new(MouseData {
alt_key: evt.alt_key(),
button: evt.button(),
buttons: evt.buttons(),
client_x: evt.client_x(),
client_y: evt.client_y(),
ctrl_key: evt.ctrl_key(),
meta_key: evt.meta_key(),
screen_x: evt.screen_x(),
screen_y: evt.screen_y(),
shift_key: evt.shift_key(),
page_x: evt.page_x(),
page_y: evt.page_y(),
})
}
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap();
Arc::new(PointerData {
alt_key: evt.alt_key(),
button: evt.button(),
buttons: evt.buttons(),
client_x: evt.client_x(),
client_y: evt.client_y(),
ctrl_key: evt.ctrl_key(),
meta_key: evt.meta_key(),
page_x: evt.page_x(),
page_y: evt.page_y(),
screen_x: evt.screen_x(),
screen_y: evt.screen_y(),
shift_key: evt.shift_key(),
pointer_id: evt.pointer_id(),
width: evt.width(),
height: evt.height(),
pressure: evt.pressure(),
tangential_pressure: evt.tangential_pressure(),
tilt_x: evt.tilt_x(),
tilt_y: evt.tilt_y(),
twist: evt.twist(),
pointer_type: evt.pointer_type(),
is_primary: evt.is_primary(),
// get_modifier_state: evt.get_modifier_state(),
})
}
"select" => Arc::new(SelectionData {}),
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap();
Arc::new(TouchData {
alt_key: evt.alt_key(),
ctrl_key: evt.ctrl_key(),
meta_key: evt.meta_key(),
shift_key: evt.shift_key(),
})
}
"scroll" => Arc::new(()),
"wheel" => {
let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap();
Arc::new(WheelData {
delta_x: evt.delta_x(),
delta_y: evt.delta_y(),
delta_z: evt.delta_z(),
delta_mode: evt.delta_mode(),
})
}
"animationstart" | "animationend" | "animationiteration" => {
let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap();
Arc::new(AnimationData {
elapsed_time: evt.elapsed_time(),
animation_name: evt.animation_name(),
pseudo_element: evt.pseudo_element(),
})
}
"transitionend" => {
let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap();
Arc::new(TransitionData {
elapsed_time: evt.elapsed_time(),
property_name: evt.property_name(),
pseudo_element: evt.pseudo_element(),
})
}
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}),
"toggle" => Arc::new(ToggleData {}),
_ => Arc::new(()),
}
}
/// This function decodes a websys event and produces an EventTrigger
/// With the websys implementation, we attach a unique key to the nodes
fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
use anyhow::Context;
let target = event
.target()
.expect("missing target")
.dyn_into::<Element>()
.expect("not a valid element");
let typ = event.type_();
let element_id = target
.get_attribute("dioxus-id")
.context("Could not find element id on event target")?
.parse()?;
Ok(UserEvent {
name: event_name_from_typ(&typ),
data: virtual_event_from_websys_event(event.clone()),
element: Some(ElementId(element_id)),
scope_id: None,
priority: dioxus_core::EventPriority::Medium,
})
}
pub(crate) fn load_document() -> Document {
web_sys::window()
.expect("should have access to the Window")
.document()
.expect("should have access to the Document")
}
fn event_name_from_typ(typ: &str) -> &'static str {
match typ {
"copy" => "copy",
"cut" => "cut",
"paste" => "paste",
"compositionend" => "compositionend",
"compositionstart" => "compositionstart",
"compositionupdate" => "compositionupdate",
"keydown" => "keydown",
"keypress" => "keypress",
"keyup" => "keyup",
"focus" => "focus",
"blur" => "blur",
"change" => "change",
"input" => "input",
"invalid" => "invalid",
"reset" => "reset",
"submit" => "submit",
"click" => "click",
"contextmenu" => "contextmenu",
"doubleclick" => "doubleclick",
"drag" => "drag",
"dragend" => "dragend",
"dragenter" => "dragenter",
"dragexit" => "dragexit",
"dragleave" => "dragleave",
"dragover" => "dragover",
"dragstart" => "dragstart",
"drop" => "drop",
"mousedown" => "mousedown",
"mouseenter" => "mouseenter",
"mouseleave" => "mouseleave",
"mousemove" => "mousemove",
"mouseout" => "mouseout",
"mouseover" => "mouseover",
"mouseup" => "mouseup",
"pointerdown" => "pointerdown",
"pointermove" => "pointermove",
"pointerup" => "pointerup",
"pointercancel" => "pointercancel",
"gotpointercapture" => "gotpointercapture",
"lostpointercapture" => "lostpointercapture",
"pointerenter" => "pointerenter",
"pointerleave" => "pointerleave",
"pointerover" => "pointerover",
"pointerout" => "pointerout",
"select" => "select",
"touchcancel" => "touchcancel",
"touchend" => "touchend",
"touchmove" => "touchmove",
"touchstart" => "touchstart",
"scroll" => "scroll",
"wheel" => "wheel",
"animationstart" => "animationstart",
"animationend" => "animationend",
"animationiteration" => "animationiteration",
"transitionend" => "transitionend",
"abort" => "abort",
"canplay" => "canplay",
"canplaythrough" => "canplaythrough",
"durationchange" => "durationchange",
"emptied" => "emptied",
"encrypted" => "encrypted",
"ended" => "ended",
"error" => "error",
"loadeddata" => "loadeddata",
"loadedmetadata" => "loadedmetadata",
"loadstart" => "loadstart",
"pause" => "pause",
"play" => "play",
"playing" => "playing",
"progress" => "progress",
"ratechange" => "ratechange",
"seeked" => "seeked",
"seeking" => "seeking",
"stalled" => "stalled",
"suspend" => "suspend",
"timeupdate" => "timeupdate",
"volumechange" => "volumechange",
"waiting" => "waiting",
"toggle" => "toggle",
_ => {
panic!("unsupported event type")
}
}
}
//! This module provides a mirror of the VirtualDOM Element Slab using a Vector.
use std::ops::{Index, IndexMut};
use web_sys::Node;
pub(crate) struct NodeSlab {
nodes: Vec<Option<Node>>,
}
impl NodeSlab {
pub fn new(capacity: usize) -> NodeSlab {
let nodes = Vec::with_capacity(capacity);
NodeSlab { nodes }
}
}
impl Index<usize> for NodeSlab {
type Output = Option<Node>;
fn index(&self, index: usize) -> &Self::Output {
&self.nodes[index]
}
}
impl IndexMut<usize> for NodeSlab {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
if index >= self.nodes.capacity() * 3 {
panic!("Trying to mutate an element way too far out of bounds");
}
if index + 1 > self.nodes.len() {
self.nodes.resize_with(index + 1, || None);
}
&mut self.nodes[index]
}
}
#[derive(Debug, Default)]
struct Stack {
list: Vec<Node>,
}
impl Stack {
#[inline]
fn with_capacity(cap: usize) -> Self {
Stack {
list: Vec::with_capacity(cap),
}
}
#[inline]
fn push(&mut self, node: Node) {
self.list.push(node);
}
#[inline]
fn pop(&mut self) -> Node {
self.list.pop().unwrap()
}
fn top(&self) -> &Node {
match self.list.last() {
Some(a) => a,
None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
}
}
}
+17 -5
View File
@@ -80,7 +80,8 @@ impl WebsysDom {
*last_node_was_text = true;
self.nodes[node_id.0] = Some(node);
self.interpreter.set_node(node_id.0, node);
// self.nodes[node_id.0] = Some(node);
*cur_place += 1;
}
@@ -105,7 +106,8 @@ impl WebsysDom {
.set_attribute("dioxus-id", s.as_str())
.unwrap();
self.nodes[node_id.0] = Some(node.clone());
self.interpreter.set_node(node_id.0, node.clone());
// self.nodes[node_id.0] = Some(node.clone());
*cur_place += 1;
@@ -116,7 +118,15 @@ impl WebsysDom {
// we cant have the last node be text
let mut last_node_was_text = false;
for child in vel.children {
self.rehydrate_single(nodes, place, dom, &child, &mut last_node_was_text)?;
self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?;
}
for listener in vel.listeners {
self.interpreter.NewEventListener(
listener.event,
listener.mounted_node.get().unwrap().as_u64(),
self.handler.as_ref().unchecked_ref(),
);
}
place.pop();
@@ -135,14 +145,16 @@ impl WebsysDom {
let cur_place = place.last_mut().unwrap();
let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap();
self.nodes[node_id.0] = Some(node);
self.interpreter.set_node(node_id.0, node);
// self.nodes[node_id.0] = Some(node);
*cur_place += 1;
}
VNode::Fragment(el) => {
for el in el.children {
self.rehydrate_single(nodes, place, dom, &el, last_node_was_text)?;
self.rehydrate_single(nodes, place, dom, el, last_node_was_text)?;
}
}
+1 -2
View File
@@ -68,8 +68,7 @@ impl RafLoop {
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.recv().await.unwrap();
let deadline = TimeoutFuture::new(deadline);
deadline
TimeoutFuture::new(deadline)
}
pub async fn wait_for_raf(&self) {