wip: enable more diffing

This commit is contained in:
Jonathan Kelley 2021-07-05 01:11:49 -04:00
parent b5e5ef171a
commit e8f29a8f8a
26 changed files with 1326 additions and 367 deletions

View file

@ -60,7 +60,7 @@ members = [
"packages/html-namespace",
"packages/web",
# "packages/webview"
# "packages/cli",
"packages/cli",
# "packages/atoms",
# "packages/ssr",
# "packages/docsite",

View file

@ -0,0 +1,136 @@
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin: 0;
font: 100 14px 'Roboto';
}
button {
display: block;
background: none;
border: none;
padding: 0;
font-family: inherit;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
button:active {
box-shadow: inset 0px 0px 80px 0px rgba(0,0,0,0.25);
}
#wrapper {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
#app {
width: 320px;
height: 520px;
position: relative;
}
.calculator {
width: 100%;
height: 100%;
background: black;
display: flex;
flex-direction: column;
}
#wrapper .calculator {
box-shadow: 0px 0px 20px 0px #aaa;
}
.calculator-display {
color: white;
background: #1c191c;
line-height: 130px;
font-size: 6em;
flex: 1;
}
.auto-scaling-text {
display: inline-block;
}
.calculator-display .auto-scaling-text {
padding: 0 30px;
position: absolute;
right: 0;
transform-origin: right;
}
.calculator-keypad {
height: 400px;
display: flex;
}
.calculator .input-keys {
width: 240px;
}
.calculator .function-keys {
display: flex;
}
.calculator .digit-keys {
background: #e0e0e7;
display: flex;
flex-direction: row;
flex-wrap: wrap-reverse;
}
.calculator-key {
width: 80px;
height: 80px;
border-top: 1px solid #777;
border-right: 1px solid #666;
text-align: center;
line-height: 80px;
}
.calculator .function-keys .calculator-key {
font-size: 2em;
}
.calculator .function-keys .key-multiply {
line-height: 50px;
}
.calculator .digit-keys .calculator-key {
font-size: 2.25em;
}
.calculator .digit-keys .key-0 {
width: 160px;
text-align: left;
padding-left: 32px;
}
.calculator .digit-keys .key-dot {
padding-top: 1em;
font-size: 0.75em;
}
.calculator .operator-keys .calculator-key {
color: white;
border-right: 0;
font-size: 3em;
}
.calculator .function-keys {
background: linear-gradient(to bottom, rgba(202,202,204,1) 0%, rgba(196,194,204,1) 100%);
}
.calculator .operator-keys {
background: linear-gradient(to bottom, rgba(252,156,23,1) 0%, rgba(247,126,27,1) 100%);
}

View file

@ -98,16 +98,16 @@ static App: FC<()> = |cx| {
CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" }
CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "" }
{(1..9).map(|k| rsx!{
{(1..9).map(move |k| rsx!{
CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" }
})}
}
div { class: "operator-keys"
CalculatorKey { name:"key-divide", onclick: move |_| operator.set(Some(Operator::Div)) "÷" }
CalculatorKey { name:"key-multiply", onclick: move |_| operator.set(Some(Operator::Mul)) "×" }
CalculatorKey { name:"key-subtract", onclick: move |_| operator.set(Some(Operator::Sub)) "" }
CalculatorKey { name:"key-add", onclick: move |_| operator.set(Some(Operator::Add)) "+" }
CalculatorKey { name:"key-equals", onclick: move |_| perform_operation() "=" }
CalculatorKey { name: "key-divide", onclick: move |_| operator.set(Some(Operator::Div)) "÷" }
CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some(Operator::Mul)) "×" }
CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some(Operator::Sub)) "" }
CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some(Operator::Add)) "+" }
CalculatorKey { name: "key-equals", onclick: move |_| perform_operation() "=" }
}
}
}
@ -116,7 +116,10 @@ static App: FC<()> = |cx| {
#[derive(Props)]
struct CalculatorKeyProps<'a> {
/// Name!
name: &'static str,
/// Click!
onclick: &'a dyn Fn(MouseEvent),
}

60
examples/compose.rs Normal file
View file

@ -0,0 +1,60 @@
fn main() {}
struct Title(String);
struct Position([f32; 3]);
struct Velocity([f32; 3]);
type Batch<T> = fn(&mut T) -> ();
static Atom: Batch<(Title, Position, Velocity)> = |_| {};
enum VNode<'a> {
El(El<'a>),
Text(&'a str),
Fragment(&'a [VNode<'a>]),
}
struct El<'a> {
name: &'static str,
key: Option<&'a str>,
attrs: &'a [(&'static str, AttrType<'a>)],
children: &'a [El<'a>],
}
enum AttrType<'a> {
Numeric(usize),
Text(&'a str),
}
fn example() {
use AttrType::Numeric;
let el = El {
name: "div",
attrs: &[("type", Numeric(10)), ("type", Numeric(10))],
key: None,
children: &[],
};
}
use dioxus::prelude::bumpalo::Bump;
trait IntoVnode {
fn into_vnode<'a>(self, b: &'a Bump) -> VNode<'a>;
}
impl<'a> IntoIterator for VNode<'a> {
type Item = VNode<'a>;
type IntoIter = std::iter::Once<VNode<'a>>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
std::iter::once(self)
}
}
fn take_iterable<F: IntoVnode>(f: impl IntoIterator<Item = F>) {
let iter = f.into_iter();
let b = Bump::new();
for f in iter {
let v = f.into_vnode(&b);
}
}

View file

@ -0,0 +1,16 @@
use dioxus::prelude::*;
fn main() {}
static Example: FC<()> = |cx| {
let (g, set_g) = use_state(&cx, || 0);
let v = (0..10).map(move |f| {
rsx!(li {
onclick: move |_| set_g(10)
})
});
cx.render(rsx! {
div {
{v}
}
})
};

View file

@ -4,51 +4,54 @@
//! Some components benefit through the use of "Models". Models are a single block of encapsulated state that allow mutative
//! methods to be performed on them. Dioxus exposes the ability to use the model pattern through the "use_model" hook.
//!
//! Models are commonly used in the "Model-View-Component" approach for building UI state.
//!
//! `use_model` is basically just a fancy wrapper around set_state, but saves a "working copy" of the new state behind a
//! RefCell. To modify the working copy, you need to call "modify" which returns the RefMut. This makes it easy to write
//! RefCell. To modify the working copy, you need to call "get_mut" which returns the RefMut. This makes it easy to write
//! fully encapsulated apps that retain a certain feel of native Rusty-ness. A calculator app is a good example of when this
//! is useful.
//!
//! Do note that "get_mut" returns a `RefMut` (a lock over a RefCell). If two `RefMut`s are held at the same time (ie in a loop)
//! the RefCell will panic and crash. You can use `try_get_mut` or `.modify` to avoid this problem, or just not hold two
//! RefMuts at the same time.
use dioxus::events::on::*;
use dioxus::prelude::*;
use std::{
cell::RefCell,
ops::{Deref, DerefMut},
};
fn main() {
dioxus::webview::launch(App)
}
static App: FC<()> = |cx| {
let state = use_model(&cx, || CalculatorState::new());
let calc = use_model(&cx, || Calculator::new());
let clear_display = state.display_value.eq("0");
let clear_display = calc.display_value.eq("0");
let clear_text = if clear_display { "C" } else { "AC" };
let formatted = state.formatted();
let formatted = calc.formatted();
cx.render(rsx! {
div { class: "calculator", onkeydown: move |evt| state.modify().handle_keydown(evt),
div { class: "calculator-display", "{formatted}"}
div { class: "calculator", onkeydown: move |evt| calc.get_mut().handle_keydown(evt),
div { class: "calculator-display","{formatted}"}
div { class: "input-keys"
div { class: "function-keys"
CalculatorKey { name: "key-clear", onclick: move |_| state.modify().clear_display() "{clear_text}" }
CalculatorKey { name: "key-sign", onclick: move |_| state.modify().toggle_sign(), "±"}
CalculatorKey { name: "key-percent", onclick: move |_| state.modify().toggle_percent() "%"}
// All there ways that the calculator may be modified:
CalculatorKey { name: "key-clear", onclick: move |_| calc.get_mut().clear_display() "{clear_text}" }
CalculatorKey { name: "key-sign", onclick: move |_| calc.modify(Calculator::toggle_sign), "±"}
CalculatorKey { name: "key-percent", onclick: move |_| calc.modify(|f| f.toggle_percent()) "%"}
}
div { class: "digit-keys"
CalculatorKey { name: "key-0", onclick: move |_| state.modify().input_digit(0), "0" }
CalculatorKey { name: "key-dot", onclick: move |_| state.modify().input_dot(), "" }
{(1..9).map(|k| rsx!{
CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| state.modify().input_digit(k), "{k}" }
CalculatorKey { name: "key-0", onclick: move |_| calc.get_mut().input_digit(0), "0" }
CalculatorKey { name: "key-dot", onclick: move |_| calc.get_mut().input_dot(), "" }
{(1..9).map(move |k| rsx!{
CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| calc.get_mut().input_digit(k), "{k}" }
})}
}
div { class: "operator-keys"
CalculatorKey { name:"key-divide", onclick: move |_| state.modify().set_operator(Operator::Div) "÷" }
CalculatorKey { name:"key-multiply", onclick: move |_| state.modify().set_operator(Operator::Mul) "×" }
CalculatorKey { name:"key-subtract", onclick: move |_| state.modify().set_operator(Operator::Sub) "" }
CalculatorKey { name:"key-add", onclick: move |_| state.modify().set_operator(Operator::Add) "+" }
CalculatorKey { name:"key-equals", onclick: move |_| state.modify().perform_operation() "=" }
CalculatorKey { name:"key-divide", onclick: move |_| calc.get_mut().set_operator(Operator::Div) "÷" }
CalculatorKey { name:"key-multiply", onclick: move |_| calc.get_mut().set_operator(Operator::Mul) "×" }
CalculatorKey { name:"key-subtract", onclick: move |_| calc.get_mut().set_operator(Operator::Sub) "" }
CalculatorKey { name:"key-add", onclick: move |_| calc.get_mut().set_operator(Operator::Add) "+" }
CalculatorKey { name:"key-equals", onclick: move |_| calc.get_mut().perform_operation() "=" }
}
}
}
@ -56,7 +59,7 @@ static App: FC<()> = |cx| {
};
#[derive(Clone)]
struct CalculatorState {
struct Calculator {
display_value: String,
operator: Option<Operator>,
cur_val: f64,
@ -68,9 +71,10 @@ enum Operator {
Mul,
Div,
}
impl CalculatorState {
impl Calculator {
fn new() -> Self {
CalculatorState {
Calculator {
display_value: "0".to_string(),
operator: None,
cur_val: 0.0,
@ -118,7 +122,9 @@ impl CalculatorState {
self.display_value.pop();
}
}
fn set_operator(&mut self, operator: Operator) {}
fn set_operator(&mut self, operator: Operator) {
self.operator = Some(operator)
}
fn handle_keydown(&mut self, evt: KeyboardEvent) {
match evt.key_code() {
KeyCode::Backspace => self.backspace(),
@ -160,6 +166,10 @@ fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> {
use odl::use_model;
mod odl {
use super::*;
use std::{
cell::RefCell,
ops::{Deref, DerefMut},
};
/// Use model makes it easy to use "models" as state for components. To modify the model, call "modify" and a clone of the
/// current model will be made, with a RefMut lock on it. Dioxus will never run your components multithreaded, so you can
/// be relatively sure that this won't fail in practice
@ -208,9 +218,14 @@ mod odl {
wip: RefCell::new(t),
}
}
pub fn modify(&self) -> RefMut<'_, T> {
pub fn get_mut(&self) -> RefMut<'_, T> {
self.wip.borrow_mut()
}
pub fn modify(&self, f: impl FnOnce(&mut T)) {
let mut g = self.get_mut();
let r = g.deref_mut();
todo!()
}
}
#[derive(Clone, Copy)]

View file

@ -2,9 +2,17 @@ use dioxus::prelude::*;
fn main() {}
static Example: FC<()> = |cx| {
let (g, set_g) = use_state(&cx, || 0);
let v = (0..10).map(|f| {
rsx! {
li {
onclick: move |_| set_g(10)
}
}
});
cx.render(rsx! {
div {
{v}
}
})
};

View file

@ -0,0 +1,7 @@
// mod iterator;
// mod memo;
mod iterators;
mod listener;
mod memo;
fn main() {}

View file

@ -1,2 +0,0 @@
mod iterator;
mod memo;

View file

@ -70,15 +70,20 @@ pub fn build(config: &Config, _build_config: &BuildConfig) -> Result<()> {
// [3] Bindgen the final binary for use easy linking
let mut bindgen_builder = Bindgen::new();
let release_type = match config.release {
true => "release",
false => "debug",
};
let input_path = match executable {
ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir
// .join("wasm32-unknown-unknown/release")
.join("wasm32-unknown-unknown/debug")
.join(format!("wasm32-unknown-unknown/{}", release_type))
.join(format!("{}.wasm", name)),
ExecutableType::Example(name) => target_dir
// .join("wasm32-unknown-unknown/release/examples")
.join("wasm32-unknown-unknown/debug/examples")
.join(format!("wasm32-unknown-unknown/{}/examples", release_type))
.join(format!("{}.wasm", name)),
};

View file

@ -68,19 +68,43 @@ impl ToTokens for Element {
let name = &self.name.to_string();
tokens.append_all(quote! {
dioxus::builder::ElementBuilder::new(__cx, #name)
__cx.element(#name)
});
// dioxus::builder::ElementBuilder::new(__cx, #name)
// dioxus::builder::ElementBuilder::new(__cx, #name)
// Add attributes
// TODO: conver to the "attrs" syntax for compile-time known sizes
for attr in self.attrs.iter() {
attr.to_tokens(tokens);
}
let mut children = self.children.iter();
while let Some(child) = children.next() {
let inner_toks = child.to_token_stream();
// let mut children = self.children.iter();
// while let Some(child) = children.next() {
// let inner_toks = child.to_token_stream();
// tokens.append_all(quote! {
// .iter_child(#inner_toks)
// })
// }
let mut childs = quote! {};
for child in &self.children {
match child {
Node::Text(e) => e.to_tokens(&mut childs),
Node::Element(e) => e.to_tokens(&mut childs),
Node::RawExpr(e) => quote! {
__cx.fragment_from_iter(#e)
}
.to_tokens(&mut childs),
}
childs.append_all(quote! {,})
}
if self.children.len() > 0 {
tokens.append_all(quote! {
.iter_child(#inner_toks)
})
.children([
#childs
])
});
}
tokens.append_all(quote! {

View file

@ -99,14 +99,14 @@ impl ToTokens for RsxRender {
// The `in cx` pattern allows directly rendering
Some(ident) => out_tokens.append_all(quote! {
#ident.render(dioxus::prelude::LazyNodes::new(move |__cx|{
let bump = &__cx.bump();
// let bump = &__cx.bump();
#inner
}))
}),
// Otherwise we just build the LazyNode wrapper
None => out_tokens.append_all(quote! {
dioxus::prelude::LazyNodes::new(move |__cx: &NodeFactory|{
let bump = &__cx.bump();
// let bump = &__cx.bump();
#inner
})
}),

View file

@ -61,13 +61,7 @@ impl ToTokens for TextNode {
// todo: use heuristics to see if we can promote to &static str
let token_stream = &self.0.to_token_stream();
tokens.append_all(quote! {
{
// use bumpalo::core_alloc::fmt::Write;
// let mut s = bumpalo::collections::String::new_in(bump);
// s.write_fmt(format_args_f!(#token_stream)).unwrap();
dioxus::builder::text3(bump, format_args_f!(#token_stream))
// dioxus::builder::text2(s)
}
__cx.text(format_args_f!(#token_stream))
});
}
}

View file

@ -105,7 +105,7 @@ static HTML_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
"source",
"span",
"strong",
// "style",
"style",
"sub",
"summary",
"sup",

View file

@ -135,197 +135,357 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
Then, we diff and queue an edit event (via chagelist). s single trees - when components show up, we save that traversal and then re-enter later.
When re-entering, we reuse the EditList in DiffState
*/
match old_node {
VNode::Element(old) => match new_node {
// New node is an element, old node was en element, need to investiage more deeply
VNode::Element(new) => {
// If the element type is completely different, the element needs to be re-rendered completely
// This is an optimization React makes due to how users structure their code
if new.tag_name != old.tag_name || new.namespace != old.namespace {
self.create(new_node);
self.dom.replace_with();
return;
}
new.dom_id.set(old.dom_id.get());
self.diff_listeners(old.listeners, new.listeners);
self.diff_attr(old.attributes, new.attributes, new.namespace);
self.diff_children(old.children, new.children);
}
// New node is a text element, need to replace the element with a simple text node
VNode::Text(_) => {
self.create(new_node);
self.dom.replace_with();
}
// New node is a component
// Make the component and replace our element on the stack with it
VNode::Component(_) => {
self.create(new_node);
self.dom.replace_with();
}
// New node is actually a sequence of nodes.
// We need to replace this one node with a sequence of nodes
// Not yet implement because it's kinda hairy
VNode::Fragment(new) => {
match new.children.len() {
0 => {
// remove
}
1 => {
// replace
self.create(&new.children[0]);
self.dom.replace_with();
}
_ => {
todo!()
// remove and mount many
// self.remove_self_and_next_siblings(old)
//
// let iter = ChildIterator::new(new_node, self.components).rev();
// for child in iter {}
// for child in new.children.iter().skip(1).rev() {
// self.dom.remove();
// }
}
}
}
// New Node is actually suspended. Todo
VNode::Suspended { real } => todo!(),
},
// Old element was text
VNode::Text(old) => match new_node {
VNode::Text(new) => {
if old.text != new.text {
log::debug!("Text has changed {}, {}", old.text, new.text);
self.dom.set_text(new.text);
}
new.dom_id.set(old.dom_id.get());
}
VNode::Element(_) | VNode::Component(_) => {
self.create(new_node);
self.dom.replace_with();
}
// TODO on handling these types
VNode::Fragment(frag) => {
if frag.children.len() == 0 {
// do nothing
} else {
self.create(&frag.children[0]);
self.dom.replace_with();
for child in frag.children.iter().skip(1) {
self.create(child);
self.dom.append_child();
}
}
}
VNode::Suspended { real } => todo!(),
},
// Old element was a component
VNode::Component(old) => {
match new_node {
// It's something entirely different
VNode::Element(_) | VNode::Text(_) => {
self.create(new_node);
self.dom.replace_with();
}
// It's also a component
VNode::Component(new) => {
if old.user_fc == new.user_fc {
// Make sure we're dealing with the same component (by function pointer)
// Make sure the new component vnode is referencing the right scope id
let scope_id = old.ass_scope.get();
new.ass_scope.set(scope_id);
new.mounted_root.set(old.mounted_root.get());
// make sure the component's caller function is up to date
self.components
.with_scope(scope_id.unwrap(), |scope| {
scope.caller = Rc::downgrade(&new.caller)
})
.unwrap();
// React doesn't automatically memoize, but we do.
// The cost is low enough to make it worth checking
// Rust produces fairly performant comparison methods, sometimes SIMD accelerated
let should_render = match old.comparator {
Some(comparator) => comparator(new),
None => true,
};
if should_render {
// // self.dom.commit_traversal();
self.components
.with_scope(scope_id.unwrap(), |f| {
f.run_scope().unwrap();
})
.unwrap();
// diff_machine.change_list.load_known_root(root_id);
// run the scope
//
} else {
// Component has memoized itself and doesn't need to be re-rendered.
// We still need to make sure the child's props are up-to-date.
// Don't commit traversal
}
} else {
// It's an entirely different component
// A new component has shown up! We need to destroy the old node
// Wipe the old one and plant the new one
// self.dom.commit_traversal();
// self.dom.replace_node_with(old.dom_id, new.dom_id);
// self.create(new_node);
// self.dom.replace_with();
self.create(new_node);
// self.create_and_repalce(new_node, old.mounted_root.get());
// Now we need to remove the old scope and all of its descendents
let old_scope = old.ass_scope.get().unwrap();
self.destroy_scopes(old_scope);
}
}
VNode::Fragment(_) => todo!(),
VNode::Suspended { real } => todo!(),
log::debug!("diffing...");
match (old_node, new_node) {
// Handle the "sane" cases first.
// The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment".
// So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable.
(VNode::Text(old), VNode::Text(new)) => {
if old.text != new.text {
self.dom.push_root(old.dom_id.get());
log::debug!("Text has changed {}, {}", old.text, new.text);
self.dom.set_text(new.text);
}
new.dom_id.set(old.dom_id.get());
}
VNode::Fragment(old) => {
(VNode::Element(old), VNode::Element(new)) => {
// If the element type is completely different, the element needs to be re-rendered completely
// This is an optimization React makes due to how users structure their code
//
match new_node {
VNode::Fragment(new) => todo!(),
// In Dioxus, this is less likely to occur unless through a fragment
if new.tag_name != old.tag_name || new.namespace != old.namespace {
log::debug!("things have changed....?");
self.dom.push_root(old.dom_id.get());
self.create(new_node);
self.dom.replace_with();
return;
}
log::debug!("Important Things haven't changed!");
new.dom_id.set(old.dom_id.get());
// going from fragment to element means we're going from many (or potentially none) to one
VNode::Element(new) => {}
VNode::Text(_) => todo!(),
VNode::Suspended { real } => todo!(),
VNode::Component(_) => todo!(),
self.diff_listeners(old.listeners, new.listeners);
self.diff_attr(old.attributes, new.attributes, new.namespace);
self.diff_children(old.children, new.children);
}
(VNode::Component(old), VNode::Component(new)) => {
log::warn!("diffing components? {:#?}", new.user_fc);
if old.user_fc == new.user_fc {
// Make sure we're dealing with the same component (by function pointer)
// Make sure the new component vnode is referencing the right scope id
let scope_id = old.ass_scope.get();
new.ass_scope.set(scope_id);
// new.mounted_root.set(old.mounted_root.get());
// make sure the component's caller function is up to date
let scope = self.components.try_get_mut(scope_id.unwrap()).unwrap();
// .with_scope(scope_id.unwrap(), |scope| {
scope.caller = Rc::downgrade(&new.caller);
scope.update_children(new.children);
// })
// .unwrap();
// React doesn't automatically memoize, but we do.
// The cost is low enough to make it worth checking
// Rust produces fairly performant comparison methods, sometimes SIMD accelerated
// let should_render = match old.comparator {
// Some(comparator) => comparator(new),
// None => true,
// };
// if should_render {
// // self.dom.commit_traversal();
// let f = self.components.try_get_mut(scope_id.unwrap()).unwrap();
// self.components
// .with_scope(scope_id.unwrap(), |f| {
log::debug!("running scope during diff {:#?}", scope_id);
scope.run_scope().unwrap();
self.diff_node(scope.old_frame(), scope.next_frame());
log::debug!("scope completed {:#?}", scope_id);
self.seen_nodes.insert(scope_id.unwrap());
// })
// .unwrap();
// diff_machine.change_list.load_known_root(root_id);
// run the scope
//
// } else {
// log::error!("Memoized componented");
// // Component has memoized itself and doesn't need to be re-rendered.
// // We still need to make sure the child's props are up-to-date.
// // Don't commit traversal
// }
} else {
// It's an entirely different component
// A new component has shown up! We need to destroy the old node
// Wipe the old one and plant the new one
// self.dom.commit_traversal();
// self.dom.replace_node_with(old.dom_id, new.dom_id);
// self.create(new_node);
log::warn!("creating and replacing...");
self.create(new_node);
// self.dom.replace_with();
// self.create_and_repalce(new_node, old.mounted_root.get());
// Now we need to remove the old scope and all of its descendents
let old_scope = old.ass_scope.get().unwrap();
self.destroy_scopes(old_scope);
}
}
// a suspended node will perform a mem-copy of the previous elements until it is ready
// this means that event listeners will need to be disabled and removed
// it also means that props will need to disabled - IE if the node "came out of hibernation" any props should be considered outdated
VNode::Suspended { real: old_real } => {
(VNode::Fragment(old), VNode::Fragment(new)) => {
log::debug!("diffing as fragment");
// This is the case where options or direct vnodes might be used.
// In this case, it's faster to just skip ahead to their diff
if old.children.len() == 1 && new.children.len() == 1 {
self.diff_node(old.children.get(0).unwrap(), new.children.get(0).unwrap());
return;
}
// Diff using the approach where we're looking for added or removed nodes.
if old.children.len() != new.children.len() {}
// Diff where we think the elements are the same
if old.children.len() == new.children.len() {}
self.diff_children(old.children, new.children);
// todo!()
}
// Okay - these are the "insane" cases where the structure is entirely different.
// The factory and rsx! APIs don't really produce structures like this, so we don't take any too complicated
// code paths.
// in the case where the old node was a fragment but the new nodes are text,
(VNode::Fragment(_) | VNode::Component(_), VNode::Element(_) | VNode::Text(_)) => {
// find the first real element int the old node
let mut iter = RealChildIterator::new(old_node, self.components);
if let Some(first) = iter.next() {
// replace the very first node with the creation of the element or text
} else {
// there are no real elements in the old fragment...
// We need to load up the next real
}
if let VNode::Component(old) = old_node {
// schedule this component's destructor to be run
todo!()
}
}
// In the case where real nodes are being replaced by potentially
(VNode::Element(_) | VNode::Text(_), VNode::Fragment(new)) => {
//
match new_node {
VNode::Suspended { real: new_real } => {
//
}
VNode::Element(_) => todo!(),
VNode::Text(_) => todo!(),
VNode::Fragment(_) => todo!(),
VNode::Component(_) => todo!(),
}
}
(VNode::Text(_), VNode::Element(_)) => {
self.create(new_node);
self.dom.replace_with();
}
(VNode::Element(_), VNode::Text(_)) => {
self.create(new_node);
self.dom.replace_with();
}
_ => {
//
} /*
*/
//
// VNode::Element(old) => match new_node {
// // New node is an element, old node was en element, need to investiage more deeply
// VNode::Element(new) => {
// // If the element type is completely different, the element needs to be re-rendered completely
// // This is an optimization React makes due to how users structure their code
// if new.tag_name != old.tag_name || new.namespace != old.namespace {
// self.create(new_node);
// self.dom.replace_with();
// return;
// }
// new.dom_id.set(old.dom_id.get());
// self.diff_listeners(old.listeners, new.listeners);
// self.diff_attr(old.attributes, new.attributes, new.namespace);
// self.diff_children(old.children, new.children);
// }
// // New node is a text element, need to replace the element with a simple text node
// VNode::Text(_) => {
// self.create(new_node);
// self.dom.replace_with();
// }
// // New node is a component
// // Make the component and replace our element on the stack with it
// VNode::Component(_) => {
// self.create(new_node);
// self.dom.replace_with();
// }
// // New node is actually a sequence of nodes.
// // We need to replace this one node with a sequence of nodes
// // Not yet implement because it's kinda hairy
// VNode::Fragment(new) => {
// match new.children.len() {
// 0 => {
// // remove
// }
// 1 => {
// // replace
// self.create(&new.children[0]);
// self.dom.replace_with();
// }
// _ => {
// todo!()
// // remove and mount many
// // self.remove_self_and_next_siblings(old)
// //
// // let iter = ChildIterator::new(new_node, self.components).rev();
// // for child in iter {}
// // for child in new.children.iter().skip(1).rev() {
// // self.dom.remove();
// // }
// }
// }
// }
// // New Node is actually suspended. Todo
// VNode::Suspended { real } => todo!(),
// },
// // Old element was text
// VNode::Text(old) => match new_node {
// VNode::Text(new) => {
// if old.text != new.text {
// log::debug!("Text has changed {}, {}", old.text, new.text);
// self.dom.set_text(new.text);
// }
// new.dom_id.set(old.dom_id.get());
// }
// VNode::Element(_) | VNode::Component(_) => {
// self.create(new_node);
// self.dom.replace_with();
// }
// // TODO on handling these types
// VNode::Fragment(frag) => {
// if frag.children.len() == 0 {
// // do nothing
// } else {
// self.create(&frag.children[0]);
// self.dom.replace_with();
// for child in frag.children.iter().skip(1) {
// self.create(child);
// self.dom.append_child();
// }
// }
// }
// VNode::Suspended { real } => todo!(),
// },
// // Old element was a component
// VNode::Component(old) => {
// match new_node {
// // It's something entirely different
// VNode::Element(_) | VNode::Text(_) => {
// self.create(new_node);
// self.dom.replace_with();
// }
// // It's also a component
// VNode::Component(new) => {
// if old.user_fc == new.user_fc {
// // Make sure we're dealing with the same component (by function pointer)
// // Make sure the new component vnode is referencing the right scope id
// let scope_id = old.ass_scope.get();
// new.ass_scope.set(scope_id);
// new.mounted_root.set(old.mounted_root.get());
// // make sure the component's caller function is up to date
// self.components
// .with_scope(scope_id.unwrap(), |scope| {
// scope.caller = Rc::downgrade(&new.caller)
// })
// .unwrap();
// // React doesn't automatically memoize, but we do.
// // The cost is low enough to make it worth checking
// // Rust produces fairly performant comparison methods, sometimes SIMD accelerated
// let should_render = match old.comparator {
// Some(comparator) => comparator(new),
// None => true,
// };
// if should_render {
// // // self.dom.commit_traversal();
// self.components
// .with_scope(scope_id.unwrap(), |f| {
// f.run_scope().unwrap();
// })
// .unwrap();
// // diff_machine.change_list.load_known_root(root_id);
// // run the scope
// //
// } else {
// // Component has memoized itself and doesn't need to be re-rendered.
// // We still need to make sure the child's props are up-to-date.
// // Don't commit traversal
// }
// } else {
// // It's an entirely different component
// // A new component has shown up! We need to destroy the old node
// // Wipe the old one and plant the new one
// // self.dom.commit_traversal();
// // self.dom.replace_node_with(old.dom_id, new.dom_id);
// // self.create(new_node);
// // self.dom.replace_with();
// self.create(new_node);
// // self.create_and_repalce(new_node, old.mounted_root.get());
// // Now we need to remove the old scope and all of its descendents
// let old_scope = old.ass_scope.get().unwrap();
// self.destroy_scopes(old_scope);
// }
// }
// VNode::Fragment(_) => todo!(),
// VNode::Suspended { real } => todo!(),
// }
// }
// VNode::Fragment(old) => {
// //
// match new_node {
// VNode::Fragment(new) => todo!(),
// // going from fragment to element means we're going from many (or potentially none) to one
// VNode::Element(new) => {}
// VNode::Text(_) => todo!(),
// VNode::Suspended { real } => todo!(),
// VNode::Component(_) => todo!(),
// }
// }
// // a suspended node will perform a mem-copy of the previous elements until it is ready
// // this means that event listeners will need to be disabled and removed
// // it also means that props will need to disabled - IE if the node "came out of hibernation" any props should be considered outdated
// VNode::Suspended { real: old_real } => {
// //
// match new_node {
// VNode::Suspended { real: new_real } => {
// //
// }
// VNode::Element(_) => todo!(),
// VNode::Text(_) => todo!(),
// VNode::Fragment(_) => todo!(),
// VNode::Component(_) => todo!(),
// }
// }
}
}
@ -340,6 +500,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
// [... node]
fn create(&mut self, node: &'bump VNode<'bump>) {
// debug_assert!(self.dom.traversal_is_committed());
log::warn!("Creating node!");
match node {
VNode::Text(text) => {
let real_id = self.dom.create_text_node(text.text);
@ -402,7 +563,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
}
VNode::Component(component) => {
let real_id = self.dom.create_placeholder();
// let real_id = self.dom.create_placeholder();
// let root_id = next_id();
// self.dom.save_known_root(root_id);
@ -455,6 +616,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
new_component.run_scope().unwrap();
// And then run the diff algorithm
let _real_id = self.dom.create_placeholder();
self.diff_node(new_component.old_frame(), new_component.next_frame());
// Finally, insert this node as a seen node.
@ -675,11 +837,14 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
);
if new_is_keyed && old_is_keyed {
todo!("Not yet implemented a migration away from temporaries");
log::warn!("using the wrong approach");
self.diff_non_keyed_children(old, new);
// todo!("Not yet implemented a migration away from temporaries");
// let t = self.dom.next_temporary();
// self.diff_keyed_children(old, new);
// self.dom.set_next_temporary(t);
} else {
log::debug!("diffing non keyed children");
self.diff_non_keyed_children(old, new);
}
}
@ -1120,24 +1285,24 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
// self.dom.go_to_sibling(i);
// [... parent this_child]
let did = old_child.get_mounted_id(self.components).unwrap();
if did.0 == 0 {
log::debug!("Root is bad: {:#?}", old_child);
}
self.dom.push_root(did);
// let did = old_child.get_mounted_id(self.components).unwrap();
// if did.0 == 0 {
// log::debug!("Root is bad: {:#?}", old_child);
// }
// self.dom.push_root(did);
self.diff_node(old_child, new_child);
let old_id = old_child.get_mounted_id(self.components).unwrap();
let new_id = new_child.get_mounted_id(self.components).unwrap();
// let old_id = old_child.get_mounted_id(self.components).unwrap();
// let new_id = new_child.get_mounted_id(self.components).unwrap();
log::debug!(
"pushed root. {:?}, {:?}",
old_child.get_mounted_id(self.components).unwrap(),
new_child.get_mounted_id(self.components).unwrap()
);
if old_id != new_id {
log::debug!("Mismatch: {:?}", new_child);
}
// log::debug!(
// "pushed root. {:?}, {:?}",
// old_child.get_mounted_id(self.components).unwrap(),
// new_child.get_mounted_id(self.components).unwrap()
// );
// if old_id != new_id {
// log::debug!("Mismatch: {:?}", new_child);
// }
}
// match old.len().cmp(&new.len()) {
@ -1256,7 +1421,11 @@ enum KeyedPrefixResult {
MoreWorkToDo(usize),
}
struct ChildIterator<'a> {
/// This iterator iterates through a list of virtual children and only returns real children (Elements or Text).
///
/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like
/// "InsertBefore".
struct RealChildIterator<'a> {
scopes: &'a ScopeArena,
// Heuristcally we should never bleed into 5 completely nested fragments/components
@ -1264,7 +1433,7 @@ struct ChildIterator<'a> {
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>,
}
impl<'a> ChildIterator<'a> {
impl<'a> RealChildIterator<'a> {
fn new(starter: &'a VNode<'a>, scopes: &'a ScopeArena) -> Self {
Self {
scopes,
@ -1279,7 +1448,7 @@ impl<'a> ChildIterator<'a> {
// }
// }
impl<'a> Iterator for ChildIterator<'a> {
impl<'a> Iterator for RealChildIterator<'a> {
type Item = &'a VNode<'a>;
fn next(&mut self) -> Option<&'a VNode<'a>> {
@ -1402,7 +1571,7 @@ mod tests {
let mut renderer = DebugDom::new();
dom.rebuild(&mut renderer).unwrap();
let starter = dom.base_scope().root();
let ite = ChildIterator::new(starter, &dom.components);
let ite = RealChildIterator::new(starter, &dom.components);
for child in ite {
match child {
VNode::Element(el) => println!("Found: Element {}", el.tag_name),

View file

@ -10,14 +10,62 @@ use crate::{innerlude::ScopeIdx, virtual_dom::RealDomNode};
#[derive(Debug)]
pub struct EventTrigger {
///
pub component_id: ScopeIdx,
///
pub real_node_id: RealDomNode,
///
pub event: VirtualEvent,
///
pub priority: EventPriority,
}
/// Priority of Event Triggers.
///
/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus
/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers
/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
///
/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
#[derive(Debug)]
pub enum EventPriority {
/// "Immediate" work will interrupt whatever work is currently being done and force its way through. This type of work
/// is typically reserved for small changes to single elements.
///
/// The primary user of the "Immediate" priority is the `Signal` API which performs surgical mutations to the DOM.
Immediate,
/// "High Priority" work will not interrupt other high priority work, but will interrupt long medium and low priority work.
///
///
/// This is typically reserved for things like user interaction.
High,
/// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
/// than "High Priority" events and will take presedence over low priority events.
///
/// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
Medium,
/// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be
/// advanced to the front of the work queue until completed.
///
/// The primary user of Low Priority work is the asynchronous work system (suspense).
Low,
}
impl EventTrigger {
pub fn new(event: VirtualEvent, scope: ScopeIdx, mounted_dom_id: RealDomNode) -> Self {
pub fn new(
event: VirtualEvent,
scope: ScopeIdx,
mounted_dom_id: RealDomNode,
priority: EventPriority,
) -> Self {
Self {
priority,
component_id: scope,
real_node_id: mounted_dom_id,
event,
@ -78,7 +126,7 @@ pub mod on {
( $( $eventdata:ident($wrapper:ident): [ $( $name:ident )* ]; )* ) => {
$(
#[derive(Debug)]
pub struct $wrapper(Rc<dyn $eventdata>);
pub struct $wrapper(pub Rc<dyn $eventdata>);
impl Deref for $wrapper {
type Target = Rc<dyn $eventdata>;
fn deref(&self) -> &Self::Target {

View file

@ -9,6 +9,8 @@ use std::{
u128,
};
use bumpalo::Bump;
use crate::{
events::VirtualEvent,
innerlude::{Properties, VComponent, FC},
@ -654,8 +656,9 @@ pub fn text3<'a>(bump: &'a bumpalo::Bump, args: std::fmt::Arguments) -> VNode<'a
// we can just short-circuit to the &'static str version instead of having to allocate in the bump arena.
//
// In the most general case, this prevents the need for any string allocations for simple code IE:
// div {"abc"}
//
// ```
// div {"abc"}
// ```
match args.as_str() {
Some(static_str) => VNode::text(static_str),
None => {
@ -735,13 +738,14 @@ impl<'a, 'b> ChildrenList<'a, 'b> {
}
}
// NodeFactory is used to build VNodes in the component's memory space.
// This struct adds metadata to the final VNode about listeners, attributes, and children
/// This struct provides an ergonomic API to quickly build VNodes.
///
/// NodeFactory is used to build VNodes in the component's memory space.
/// This struct adds metadata to the final VNode about listeners, attributes, and children
#[derive(Clone)]
pub struct NodeFactory<'a> {
pub scope_ref: &'a Scope,
pub listener_id: Cell<usize>,
// pub listener_id: RefCell<usize>,
}
impl<'a> NodeFactory<'a> {
@ -756,7 +760,7 @@ impl<'a> NodeFactory<'a> {
}
/// Create an element builder
pub fn element<'b, Listeners, Attributes, Children>(
pub fn element<'b>(
&'b self,
tag_name: &'static str,
) -> ElementBuilder<
@ -787,6 +791,20 @@ impl<'a> NodeFactory<'a> {
key: NodeKey::new_opt(key),
}))
}
pub fn fragment_from_iter(
&self,
node_iter: impl IntoIterator<Item = impl IntoVNode<'a>>,
) -> VNode<'a> {
let mut nodes = bumpalo::collections::Vec::new_in(self.bump());
for node in node_iter.into_iter() {
nodes.push(node.into_vnode(&self));
}
VNode::Fragment(
self.bump()
.alloc(VFragment::new(None, nodes.into_bump_slice())),
)
}
}
use std::fmt::Debug;

View file

@ -29,7 +29,11 @@ pub enum VNode<'src> {
/// A text node (node type `TEXT_NODE`).
Text(VText<'src>),
/// A fragment is a "virtual position" in the DOM
/// A fragment is a list of elements that might have a dynamic order.
/// Normally, children will have a fixed order. However, Fragments allow a dynamic order and must be diffed differently.
///
/// Fragments don't have a single mount into the dom, so their position is characterized by the head and tail nodes.
///
/// Fragments may have children and keys
Fragment(&'src VFragment<'src>),

View file

@ -487,6 +487,14 @@ impl Scope {
self.caller = broken_caller;
}
pub fn update_children<'creator_node>(
&mut self,
child_nodes: &'creator_node [VNode<'creator_node>],
) {
let child_nodes = unsafe { std::mem::transmute(child_nodes) };
self.child_nodes = child_nodes;
}
/// Create a new context and run the component with references from the Virtual Dom
/// This function downcasts the function pointer based on the stored props_type
///
@ -597,9 +605,9 @@ impl Scope {
///
/// }
///
/// fn example(cx: Context, props: &Props -> VNode {
/// fn example(cx: Context<Props>) -> VNode {
/// html! {
/// <div> "Hello, {cx.cx.name}" </div>
/// <div> "Hello, {cx.name}" </div>
/// }
/// }
/// ```

View file

@ -10,7 +10,7 @@ license = "MIT/Apache-2.0"
[dependencies]
dioxus-core = { path="../core", version="0.1.2" }
js-sys = "0.3"
wasm-bindgen = "0.2.71"
wasm-bindgen = { version="0.2.71", features=["enable-interning"] }
lazy_static = "1.4.0"
wasm-bindgen-futures = "0.4.20"
wasm-logger = "0.2.0"
@ -31,6 +31,7 @@ slotmap = "1.0.3"
version = "0.3.50"
features = [
"Comment",
"Attr",
"Document",
"Element",
"HtmlElement",
@ -47,6 +48,7 @@ features = [
"MouseEvent",
"InputEvent",
"ClipboardEvent",
"NamedNodeMap",
"KeyboardEvent",
"TouchEvent",
"WheelEvent",
@ -70,4 +72,5 @@ crate-type = ["cdylib", "rlib"]
[dev-dependencies]
im-rc = "15.0.0"
rand = { version="0.8.4", features=["small_rng"] }
separator = "0.4.1"
uuid = { version="0.8.2", features=["v4", "wasm-bindgen"] }

View file

@ -3,3 +3,9 @@
Build interactive user experiences directly in the web browser!
Dioxus-web provides a `WebsysRenderer` for the Dioxus Virtual Dom that handles events, progresses components, and updates the actual DOM using web-sys methods.
## Web-specific Optimizations
- Uses string interning of all common node types
- Optimistically interns short strings
- Builds trees completely before mounting them

View file

@ -1,5 +1,6 @@
//! Basic example that renders a simple VNode to the browser.
use dioxus::events::on::MouseEvent;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_web::*;
@ -14,56 +15,42 @@ fn main() {
}
static App: FC<()> = |cx| {
let (state, set_state) = use_state(&cx, || 0);
cx.render(rsx! {
div {
h1 {"hello"}
C1 {}
C2 {}
section { class: "py-12 px-4 text-center"
div { class: "w-full max-w-2xl mx-auto"
span { class: "text-sm font-semibold"
"count: {state}"
}
div {
C1 {
onclick: move |_| set_state(state + 1)
"incr"
}
C1 {
onclick: move |_| set_state(state - 1)
"decr"
}
}
}
}
}
})
};
static C1: FC<()> = |cx| {
#[derive(Props)]
struct IncrementerProps<'a> {
onclick: &'a dyn Fn(MouseEvent),
}
fn C1<'a, 'b>(cx: Context<'a, IncrementerProps<'b>>) -> VNode<'a> {
cx.render(rsx! {
button {
"numba 1"
class: "inline-block py-4 px-8 mr-6 leading-none text-white bg-indigo-600 hover:bg-indigo-900 font-semibold rounded shadow"
onclick: {cx.onclick}
"becr"
{cx.children()}
}
})
};
static C2: FC<()> = |cx| {
cx.render(rsx! {
button {
"numba 2"
}
})
};
static DocExamples: FC<()> = |cx| {
//
let is_ready = false;
let items = (0..10).map(|i| rsx! { li {"{i}"} });
let _ = rsx! {
ul {
{items}
}
};
// rsx! {
// div {}
// h1 {}
// {""}
// "asbasd"
// dioxus::Fragment {
// //
// }
// }
cx.render(rsx! {
div {
{ is_ready.then(|| rsx!{ h1 {"We are ready!"} }) }
}
})
};
}

View file

@ -0,0 +1,181 @@
//! Example: Calculator
//! -------------------------
// use dioxus::events::on::*;
// use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_web::WebsysRenderer;
const STYLE: &str = include_str!("../../../examples/assets/calculator.css");
fn main() {
// Setup logging
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
// Run the app
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App));
}
use dioxus::events::on::*;
use dioxus::prelude::*;
enum Operator {
Add,
Sub,
Mul,
Div,
}
static App: FC<()> = |cx| {
let (cur_val, set_cur_val) = use_state(&cx, || 0.0_f64);
let (operator, set_operator) = use_state(&cx, || None as Option<Operator>);
let (display_value, set_display_value) = use_state(&cx, || "".to_string());
let clear_display = display_value.eq("0");
let clear_text = if clear_display { "C" } else { "AC" };
let input_digit =
move |num: u8| {
log::warn!("Inputting digit {:#?}", num);
let mut new = display_value.clone();
new.push_str(num.to_string().as_str());
set_display_value(new);
};
let input_dot = move || {
let mut new = display_value.clone();
new.push_str(".");
set_display_value(new);
};
let perform_operation = move || {
if let Some(op) = operator.as_ref() {
let rhs = display_value.parse::<f64>().unwrap();
let new_val = match op {
Operator::Add => *cur_val + rhs,
Operator::Sub => *cur_val - rhs,
Operator::Mul => *cur_val * rhs,
Operator::Div => *cur_val / rhs,
};
set_cur_val(new_val);
set_display_value(new_val.to_string());
set_operator(None);
}
};
let toggle_sign = move |_| {
if display_value.starts_with("-") {
set_display_value(display_value.trim_start_matches("-").to_string())
} else {
set_display_value(format!("-{}", *display_value))
}
};
let toggle_percent = move |_| todo!();
let clear_key = move |_| {
set_display_value("0".to_string());
if !clear_display {
set_operator(None);
set_cur_val(0.0);
}
};
let keydownhandler = move |evt: KeyboardEvent| match evt.key_code() {
KeyCode::Backspace => {
//
let mut new = display_value.clone();
if !new.as_str().eq("0") {
new.pop();
}
set_display_value(new);
},
KeyCode::_0 => input_digit(0),
KeyCode::_1 => input_digit(1),
KeyCode::_2 => input_digit(2),
KeyCode::_3 => input_digit(3),
KeyCode::_4 => input_digit(4),
KeyCode::_5 => input_digit(5),
KeyCode::_6 => input_digit(6),
KeyCode::_7 => input_digit(7),
KeyCode::_8 => input_digit(8),
KeyCode::_9 => input_digit(9),
KeyCode::Add => set_operator(Some(Operator::Add)),
KeyCode::Subtract => set_operator(Some(Operator::Sub)),
KeyCode::Divide => set_operator(Some(Operator::Div)),
KeyCode::Multiply => set_operator(Some(Operator::Mul)),
_ => {}
};
cx.render(rsx! {
div { class: "app"
onkeydown: {keydownhandler}
style {
"{STYLE}"
}
div { class: "calculator",
div { class: "input-keys"
CalculatorDisplay { val: *cur_val }
div { class: "function-keys"
CalculatorKey { name: "key-clear", onclick: {clear_key} "{clear_text}" }
CalculatorKey { name: "key-sign", onclick: {toggle_sign}, "±"}
CalculatorKey { name: "key-percent", onclick: {toggle_percent} "%"}
}
div { class: "digit-keys"
CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" }
CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "" }
{(1..9).map(move |k| rsx!{
CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" }
})}
}
div { class: "operator-keys"
CalculatorKey { name: "key-divide", onclick: move |_| set_operator(Some(Operator::Div)) "÷" }
CalculatorKey { name: "key-multiply", onclick: move |_| set_operator(Some(Operator::Mul)) "×" }
CalculatorKey { name: "key-subtract", onclick: move |_| set_operator(Some(Operator::Sub)) "" }
CalculatorKey { name: "key-add", onclick: move |_| set_operator(Some(Operator::Add)) "+" }
CalculatorKey { name: "key-equals", onclick: move |_| perform_operation() "=" }
}
}
}
}
})
};
#[derive(Props)]
struct CalculatorKeyProps<'a> {
/// Name!
name: &'static str,
/// Click!
onclick: &'a dyn Fn(MouseEvent),
}
fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> {
cx.render(rsx! {
button {
class: "calculator-key {cx.name}"
onclick: {cx.onclick}
{cx.children()}
}
})
}
#[derive(Props, PartialEq)]
struct CalculatorDisplayProps {
val: f64,
}
fn CalculatorDisplay(cx: Context<CalculatorDisplayProps>) -> VNode {
use separator::Separatable;
// Todo, add float support to the num-format crate
let formatted = cx.val.separated_string();
// TODO: make it autoscaling with css
cx.render(rsx! {
div { class: "calculator-display"
"{formatted}"
}
})
}

View file

@ -3,15 +3,17 @@ use dioxus_core::prelude::*;
use dioxus_web::WebsysRenderer;
fn main() {
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
// wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
// console_error_panic_hook::set_once();
// log::info!("hello world");
dioxus_web::intern_cache();
log::info!("hello world");
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example));
}
static Example: FC<()> = |cx| {
let nodes = (0..15).map(|f| rsx! (li { key: "{f}", "{f}"}));
let nodes = (0..500).map(|f| rsx! (li {"div"}));
cx.render(rsx! {
div {
span {

View file

@ -77,49 +77,6 @@ impl WebsysRenderer {
.progress_with_event(&mut websys_dom, trigger)?;
}
// let edits = self.internal_dom.rebuild()?;
// log::debug!("Received edits: {:#?}", edits);
// edits.iter().for_each(|edit| {
// log::debug!("patching with {:?}", edit);
// patch_machine.handle_edit(edit);
// });
// patch_machine.reset();
// let root_node = body_element.first_child().unwrap();
// patch_machine.stack.push(root_node.clone());
// log::debug!("patch stack size {:?}", patch_machine.stack);
// Event loop waits for the receiver to finish up
// TODO! Connect the sender to the virtual dom's suspense system
// Suspense is basically an external event that can force renders to specific nodes
// while let Ok(event) = receiver.recv().await {
// log::debug!("Stack before entrance {:#?}", patch_machine.stack.top());
// log::debug!("patch stack size before {:#?}", patch_machine.stack);
// patch_machine.reset();
// patch_machine.stack.push(root_node.clone());
// self.internal_dom
// .progress_with_event(&mut websys_dom, event)?;
// let edits = self.internal_dom.progress_with_event(event)?;
// log::debug!("Received edits: {:#?}", edits);
// for edit in &edits {
// // log::debug!("edit stream {:?}", edit);
// // log::debug!("Stream stack {:#?}", patch_machine.stack.top());
// patch_machine.handle_edit(edit);
// }
// log::debug!("patch stack size after {:#?}", patch_machine.stack);
// patch_machine.reset();
// our root node reference gets invalidated
// not sure why
// for now, just select the first child again.
// eventually, we'll just make our own root element instead of using body
// or just use body directly IDEK
// let root_node = body_element.first_child().unwrap();
// patch_machine.stack.push(root_node.clone());
// }
Ok(()) // should actually never return from this, should be an error, rustc just cant see it
}
}
@ -135,7 +92,7 @@ fn prepare_websys_dom() -> Element {
// Build a dummy div
let container: &Element = body.as_ref();
container.set_inner_html("");
// container.set_inner_html("");
container
.append_child(
document
@ -164,6 +121,299 @@ fn prepare_websys_dom() -> Element {
// };
// }
/// Wasm-bindgen has a performance option to intern commonly used phrases
/// This saves the decoding cost, making the interaction of Rust<->JS more performant.
/// We intern all the HTML tags and attributes, making most operations much faster.
///
/// Interning takes about 1ms at the start of the app, but saves a *ton* of time later on.
pub fn intern_cache() {
let cached_words = [
// All the HTML Tags
"a",
"abbr",
"address",
"area",
"article",
"aside",
"audio",
"b",
"base",
"bdi",
"bdo",
"big",
"blockquote",
"body",
"br",
"button",
"canvas",
"caption",
"cite",
"code",
"col",
"colgroup",
"command",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dialog",
"div",
"dl",
"dt",
"em",
"embed",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"head",
"header",
"hr",
"html",
"i",
"iframe",
"img",
"input",
"ins",
"kbd",
"keygen",
"label",
"legend",
"li",
"link",
"main",
"map",
"mark",
"menu",
"menuitem",
"meta",
"meter",
"nav",
"noscript",
"object",
"ol",
"optgroup",
"option",
"output",
"p",
"param",
"picture",
"pre",
"progress",
"q",
"rp",
"rt",
"ruby",
"s",
"samp",
"script",
"section",
"select",
"small",
"source",
"span",
"strong",
"style",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"textarea",
"tfoot",
"th",
"thead",
"time",
"title",
"tr",
"track",
"u",
"ul",
"var",
"video",
"wbr",
// All the event handlers
"Attribute",
"accept",
"accept-charset",
"accesskey",
"action",
"alt",
"async",
"autocomplete",
"autofocus",
"autoplay",
"charset",
"checked",
"cite",
"class",
"cols",
"colspan",
"content",
"contenteditable",
"controls",
"coords",
"data",
"data-*",
"datetime",
"default",
"defer",
"dir",
"dirname",
"disabled",
"download",
"draggable",
"enctype",
"for",
"form",
"formaction",
"headers",
"height",
"hidden",
"high",
"href",
"hreflang",
"http-equiv",
"id",
"ismap",
"kind",
"label",
"lang",
"list",
"loop",
"low",
"max",
"maxlength",
"media",
"method",
"min",
"multiple",
"muted",
"name",
"novalidate",
"onabort",
"onafterprint",
"onbeforeprint",
"onbeforeunload",
"onblur",
"oncanplay",
"oncanplaythrough",
"onchange",
"onclick",
"oncontextmenu",
"oncopy",
"oncuechange",
"oncut",
"ondblclick",
"ondrag",
"ondragend",
"ondragenter",
"ondragleave",
"ondragover",
"ondragstart",
"ondrop",
"ondurationchange",
"onemptied",
"onended",
"onerror",
"onfocus",
"onhashchange",
"oninput",
"oninvalid",
"onkeydown",
"onkeypress",
"onkeyup",
"onload",
"onloadeddata",
"onloadedmetadata",
"onloadstart",
"onmousedown",
"onmousemove",
"onmouseout",
"onmouseover",
"onmouseup",
"onmousewheel",
"onoffline",
"ononline",
"<body>",
"onpageshow",
"onpaste",
"onpause",
"onplay",
"onplaying",
"<body>",
"onprogress",
"onratechange",
"onreset",
"onresize",
"onscroll",
"onsearch",
"onseeked",
"onseeking",
"onselect",
"onstalled",
"<body>",
"onsubmit",
"onsuspend",
"ontimeupdate",
"ontoggle",
"onunload",
"onvolumechange",
"onwaiting",
"onwheel",
"open",
"optimum",
"pattern",
"placeholder",
"poster",
"preload",
"readonly",
"rel",
"required",
"reversed",
"rows",
"rowspan",
"sandbox",
"scope",
"selected",
"shape",
"size",
"sizes",
"span",
"spellcheck",
"src",
"srcdoc",
"srclang",
"srcset",
"start",
"step",
"style",
"tabindex",
"target",
"title",
"translate",
"type",
"usemap",
"value",
"width",
"wrap",
];
for s in cached_words {
wasm_bindgen::intern(s);
}
}
#[cfg(test)]
mod tests {
use std::env;

View file

@ -171,6 +171,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
tag: &str,
ns: Option<&'static str>,
) -> dioxus_core::virtual_dom::RealDomNode {
let tag = wasm_bindgen::intern(tag);
let el = match ns {
Some(ns) => self
.document
@ -200,6 +201,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
el_id: usize,
real_id: RealDomNode,
) {
let event = wasm_bindgen::intern(event);
log::debug!(
"Called [`new_event_listener`]: {}, {:?}, {}, {:?}",
event,
@ -237,7 +239,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
// Instead, we just build and immediately execute a closure that returns result
match decode_trigger(event) {
Ok(synthetic_event) => trigger.as_ref()(synthetic_event),
Err(_) => log::error!("Error decoding Dioxus event attribute."),
Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
};
}) as Box<dyn FnMut(&Event)>);
@ -407,7 +409,7 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
#[derive(Debug)]
pub struct CustomMouseEvent(web_sys::MouseEvent);
impl dioxus_core::events::on::MouseEvent for CustomMouseEvent {
impl dioxus_core::events::on::MouseEventInner for CustomMouseEvent {
fn alt_key(&self) -> bool {
self.0.alt_key()
}
@ -451,7 +453,7 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
self.0.get_modifier_state(key_code)
}
}
VirtualEvent::MouseEvent(Rc::new(CustomMouseEvent(evt)))
VirtualEvent::MouseEvent(MouseEvent(Rc::new(CustomMouseEvent(evt))))
// MouseEvent(Box::new(RawMouseEvent {
// alt_key: evt.alt_key(),
// button: evt.button() as i32,
@ -547,25 +549,36 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
use anyhow::Context;
// for attr in {
let attrs = target.attributes();
for x in 0..attrs.length() {
let attr = attrs.item(x).unwrap();
log::debug!("attrs include: {:#?}", attr);
}
// }
// for attr in target.attributes() {
// log::debug!("attrs include: {:#?}", attr);
// }
// The error handling here is not very descriptive and needs to be replaced with a zero-cost error system
let val: String = target
.get_attribute(&format!("dioxus-event-{}", typ))
.context("")?;
.context(format!("wrong format - received {:#?}", typ))?;
let mut fields = val.splitn(3, ".");
let gi_id = fields
.next()
.and_then(|f| f.parse::<u64>().ok())
.context("")?;
.context("failed to parse gi id")?;
let el_id = fields
.next()
.and_then(|f| f.parse::<usize>().ok())
.context("")?;
.context("failed to parse el id")?;
let real_id = fields
.next()
.and_then(|f| f.parse::<u64>().ok().map(RealDomNode::new))
.context("")?;
.context("failed to parse real id")?;
// Call the trigger
log::debug!("decoded gi_id: {}, li_idx: {}", gi_id, el_id);
@ -575,6 +588,7 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
virtual_event_from_websys_event(event),
triggered_scope,
real_id,
dioxus_core::events::EventPriority::High,
))
}
@ -584,3 +598,6 @@ impl ListenerMap {
false
}
}
struct JsStringCache {}
impl JsStringCache {}