Merge branch 'master' into jk/form-ma

This commit is contained in:
Jonathan Kelley committed 2022-02-04 18:10:26 -05:00
commit 169f1dac11
31 files changed
+605 -1510

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
FROM rust:1.58-buster
RUN apt update
RUN apt install -y \
libglib2.0-dev \
libgtk-3-dev \
libsoup2.4-dev \
libappindicator3-dev \
libwebkit2gtk-4.0-dev \
firefox-esr
# for kcov and Tarpaulin
#liblzma-dev binutils-dev libcurl4-openssl-dev libdw-dev libelf-dev
RUN cargo install cargo-make --debug
# for test coverage
#RUN cargo install cargo-tarpaulin
# clean up a bit
RUN cargo install cargo-cache && cargo cache -a
CMD ["exit"]
+8
View File
@@ -0,0 +1,8 @@
FROM dioxus-base-test-image
RUN mkdir run_test
COPY tmp /run_test
WORKDIR /run_test
RUN cargo make tests
CMD ["exit"]
+27
View File
@@ -0,0 +1,27 @@
# Why this?
This part is used to test whole package before pushing it
# How to use it?
Just run in the folder:
`bash run_local_tests.sh`. If nothing fails, then you can push your code to the repo.
or run:
`bash run_local_tests.sh --with-full-docker-cleanup`
for cleaning up images as well
# How is it composed of?
1. `Dockerfile_pre_test` will build the base image for the tests to be run into
2. `Dockerfile_test` will run the actual tests based on 1.
3. `run_local_tests.sh` to wrap this up
# Warning
The task requires some amount of CPU work and disk space (5GB per tests). Some clean up is included in the script.
# Requirements
* [docker](https://docs.docker.com/engine/install/)
* bash
* rsync
+37
View File
@@ -0,0 +1,37 @@
set -eux
echo "Test script started"
function run_script {
if [[ -d tmp ]]
then
rm -rf tmp
fi
mkdir tmp
# copy files first
rsync -a --progress ../ tmp --exclude target --exclude docker
# build base image
docker build -f Dockerfile_pre_test -t dioxus-base-test-image .
# run test
docker build -f Dockerfile_test -t dioxus-test-image .
# clean up
rm -rf tmp
if [ $# -ge 1 ]
then
echo "Got some parameter"
if [ $1 = "--with-full-docker-cleanup" ]
then
docker image rm dioxus-base-test-image
docker image rm dioxus-test-image
fi
fi
}
run_script || echo "Error occured.. cleaning a bit." && \
docker system prune -f;
docker system prune -f
echo "Script finished to execute"
+5 -2
View File
@@ -33,10 +33,13 @@ jobs:
- uses: Swatinem/rust-cache@v1
- run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev
- uses: davidB/rust-cargo-make@v1
- uses: browser-actions/setup-firefox@latest
- uses: jetli/wasm-pack-action@v0.3.0
- uses: actions-rs/cargo@v1
with:
command: test
args: --features "desktop, ssr, router"
command: make
args: tests
fmt:
name: Rustfmt
+4
View File
@@ -4,4 +4,8 @@
"desktop",
"router"
],
"editor.formatOnSave": true,
"[toml]": {
"editor.formatOnSave": false
}
}
+51
View File
@@ -0,0 +1,51 @@
[config]
default_to_workspace = false
min_version = "0.32.4"
[env]
CARGO_MAKE_CLIPPY_ARGS = "-- --deny=warnings"
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
[config.modify_core_tasks]
namespace = "core"
private = true
[tasks.tests-setup]
private = true
script = [
"""
test_flags = array --headless --firefox
dioxus_test_features = set wasm_test
dioxus_test_flags = array_join ${test_flags} " "
echo "running tests with flags: ${dioxus_test_flags} and features: ${dioxus_test_features}"
set_env DIOXUS_TEST_FLAGS ${dioxus_test_flags}
set_env DIOXUS_TEST_FEATURES ${dioxus_test_features}
""",
]
script_runner = "@duckscript"
[tasks.tests]
category = "Testing"
dependencies = ["tests-setup"]
description = "Run all tests"
env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"]}
run_task = {name = ["test-flow", "test-with-browser"], fork = true}
[tasks.build]
command = "cargo"
args = ["build"]
[tasks.test-flow]
dependencies = ["test"]
private = true
[tasks.test]
dependencies = ["build"]
command = "cargo"
args = ["test", "--all-targets", "--workspace", "--exclude", "dioxus-router"]
private = true
[tasks.test-with-browser]
env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router"] }
private = true
workspace = true
-2
View File
@@ -26,9 +26,7 @@
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
alt="CI status" />
</a>
</div>
<div align="center">
<!--Awesome -->
<a href="https://github.com/dioxuslabs/awesome-dioxus">
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Page" />
+101
View File
@@ -0,0 +1,101 @@
// Thanks to @japsu and their project https://github.com/japsu/jatsi for the example!
use dioxus::{events::MouseEvent, prelude::*};
fn main() {
dioxus::desktop::launch(app);
}
fn app(cx: Scope) -> Element {
let (val, set_val) = use_state(&cx, || 5);
cx.render(rsx! {
div {
user_select: "none",
webkit_user_select: "none",
margin_left: "10%",
margin_right: "10%",
h1 { "Click die to generate a new value" }
div {
cursor: "pointer",
height: "80%",
width: "80%",
Die {
value: *val,
keep: true,
onclick: move |_| {
use rand::Rng;
let mut rng = rand::thread_rng();
set_val(rng.gen_range(1..6));
}
}
}
}
})
}
#[derive(Props)]
pub struct DieProps<'a> {
pub value: u64,
pub keep: bool,
pub onclick: EventHandler<'a, MouseEvent>,
}
const DOTS: [(i64, i64); 7] = [(-1, -1), (-1, -0), (-1, 1), (1, -1), (1, 0), (1, 1), (0, 0)];
const DOTS_FOR_VALUE: [[bool; 7]; 6] = [
[false, false, false, false, false, false, true],
[false, false, true, true, false, false, false],
[false, false, true, true, false, false, true],
[true, false, true, true, false, true, false],
[true, false, true, true, false, true, true],
[true, true, true, true, true, true, false],
];
const OFFSET: i64 = 600;
const DOT_RADIUS: &str = "200";
const HELD_COLOR: &str = "#aaa";
const UNHELD_COLOR: &str = "#ddd";
// A six-sided die (D6) with dots.
#[allow(non_snake_case)]
pub fn Die<'a>(cx: Scope<'a, DieProps<'a>>) -> Element {
let &DieProps { value, keep, .. } = cx.props;
let active_dots = &DOTS_FOR_VALUE[(value - 1) as usize];
let fill = if keep { HELD_COLOR } else { UNHELD_COLOR };
let dots = DOTS
.iter()
.zip(active_dots.iter())
.filter(|(_, &active)| active)
.map(|((x, y), _)| {
let dcx = x * OFFSET;
let dcy = y * OFFSET;
rsx!(circle {
cx: "{dcx}",
cy: "{dcy}",
r: "{DOT_RADIUS}",
fill: "#333"
})
});
rsx!(cx,
svg {
onclick: move |e| cx.props.onclick.call(e),
prevent_default: "onclick",
"dioxus-prevent-default": "onclick",
class: "die",
view_box: "-1000 -1000 2000 2000",
rect {
x: "-1000",
y: "-1000",
width: "2000",
height: "2000",
rx: "{DOT_RADIUS}",
fill: "{fill}",
}
dots
}
)
}
+6 -3
View File
@@ -120,15 +120,18 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
rsx!(cx, li {
class: "{completed} {editing}",
onclick: move |_| set_is_editing(true),
onfocusout: move |_| set_is_editing(false),
div { class: "view",
input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{todo.checked}",
onchange: move |evt| {
cx.props.set_todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap();
}
}
label { r#for: "cbg-{todo.id}", pointer_events: "none", "{todo.contents}" }
label {
r#for: "cbg-{todo.id}",
onclick: move |_| set_is_editing(true),
onfocusout: move |_| set_is_editing(false),
"{todo.contents}"
}
}
is_editing.then(|| rsx!{
input {
+127 -100
View File
@@ -119,11 +119,15 @@ impl<'b> DiffState<'b> {
pub fn diff_scope(&mut self, scopeid: ScopeId) {
let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid));
self.scope_stack.push(scopeid);
let scope = self.scopes.get_scope(scopeid).unwrap();
self.element_stack.push(scope.container);
self.diff_node(old, new);
self.scope_stack.push(scopeid);
self.element_stack.push(scope.container);
{
self.diff_node(old, new);
}
self.element_stack.pop();
self.scope_stack.pop();
self.mutations.mark_dirty_scope(scopeid);
}
@@ -131,63 +135,26 @@ impl<'b> DiffState<'b> {
pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) {
use VNode::{Component, Element, Fragment, Placeholder, Text};
match (old_node, new_node) {
// Check the most common cases first
// these are *actual* elements, not wrappers around lists
(Text(old), Text(new)) => {
if std::ptr::eq(old, new) {
return;
}
let root = old
.id
.get()
.expect("existing text nodes should have an ElementId");
if old.text != new.text {
self.mutations.set_text(new.text, root.as_u64());
}
self.scopes.update_node(new_node, root);
new.id.set(Some(root));
self.diff_text_nodes(old, new, old_node, new_node);
}
(Placeholder(old), Placeholder(new)) => {
if std::ptr::eq(old, new) {
return;
}
let root = old
.id
.get()
.expect("existing placeholder nodes should have an ElementId");
self.scopes.update_node(new_node, root);
new.id.set(Some(root));
self.diff_placeholder_nodes(old, new, old_node, new_node);
}
(Element(old), Element(new)) => {
if std::ptr::eq(old, new) {
return;
}
self.diff_element_nodes(old, new, old_node, new_node);
}
// These two sets are pointers to nodes but are not actually nodes themselves
(Component(old), Component(new)) => {
if std::ptr::eq(old, new) {
return;
}
self.diff_component_nodes(old_node, new_node, *old, *new);
}
(Fragment(old), Fragment(new)) => {
if std::ptr::eq(old, new) {
return;
}
self.diff_fragment_nodes(old, new);
}
// Anything else is just a basic replace and create
(
Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_),
Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_),
@@ -241,9 +208,7 @@ impl<'b> DiffState<'b> {
{
self.mutations.create_element(tag_name, *namespace, real_id);
let cur_scope_id = self
.current_scope()
.expect("diffing should always have a scope");
let cur_scope_id = self.current_scope();
for listener in listeners.iter() {
listener.mounted_node.set(Some(real_id));
@@ -268,28 +233,25 @@ impl<'b> DiffState<'b> {
}
fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize {
let parent_idx = self.current_scope().unwrap();
let parent_idx = self.current_scope();
// ensure this scope doesn't already exist if we're trying to create it
debug_assert!(
vcomponent
.scope
.get()
.and_then(|f| self.scopes.get_scope(f))
.is_none(),
"component scope already exists"
);
// Insert a new scope into our component list
let props: Box<dyn AnyProps + 'b> = vcomponent.props.borrow_mut().take().unwrap();
let props: Box<dyn AnyProps + 'static> = unsafe { std::mem::transmute(props) };
let new_idx = self.scopes.new_with_key(
vcomponent.user_fc,
props,
Some(parent_idx),
self.element_stack.last().copied().unwrap(),
0,
);
// the component might already exist - if it does, we need to reuse it
// this makes figure out when to drop the component more complicated
let new_idx = if let Some(idx) = vcomponent.scope.get() {
assert!(self.scopes.get_scope(idx).is_some());
idx
} else {
// Insert a new scope into our component list
let props: Box<dyn AnyProps + 'b> = vcomponent.props.borrow_mut().take().unwrap();
let props: Box<dyn AnyProps + 'static> = unsafe { std::mem::transmute(props) };
self.scopes.new_with_key(
vcomponent.user_fc,
props,
Some(parent_idx),
self.element_stack.last().copied().unwrap(),
0,
)
};
// Actually initialize the caller's slot with the right address
vcomponent.scope.set(Some(new_idx));
@@ -325,6 +287,53 @@ impl<'b> DiffState<'b> {
created
}
pub(crate) fn diff_text_nodes(
&mut self,
old: &'b VText<'b>,
new: &'b VText<'b>,
_old_node: &'b VNode<'b>,
new_node: &'b VNode<'b>,
) {
if std::ptr::eq(old, new) {
return;
}
// if the node is comming back not assigned, that means it was borrowed but removed
let root = match old.id.get() {
Some(id) => id,
None => self.scopes.reserve_node(new_node),
};
if old.text != new.text {
self.mutations.set_text(new.text, root.as_u64());
}
self.scopes.update_node(new_node, root);
new.id.set(Some(root));
}
pub(crate) fn diff_placeholder_nodes(
&mut self,
old: &'b VPlaceholder,
new: &'b VPlaceholder,
_old_node: &'b VNode<'b>,
new_node: &'b VNode<'b>,
) {
if std::ptr::eq(old, new) {
return;
}
// if the node is comming back not assigned, that means it was borrowed but removed
let root = match old.id.get() {
Some(id) => id,
None => self.scopes.reserve_node(new_node),
};
self.scopes.update_node(new_node, root);
new.id.set(Some(root));
}
fn diff_element_nodes(
&mut self,
old: &'b VElement<'b>,
@@ -332,10 +341,15 @@ impl<'b> DiffState<'b> {
old_node: &'b VNode<'b>,
new_node: &'b VNode<'b>,
) {
let root = old
.id
.get()
.expect("existing element nodes should have an ElementId");
if std::ptr::eq(old, new) {
return;
}
// if the node is comming back not assigned, that means it was borrowed but removed
let root = match old.id.get() {
Some(id) => id,
None => self.scopes.reserve_node(new_node),
};
// 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
@@ -385,25 +399,25 @@ impl<'b> DiffState<'b> {
// We also need to make sure that all listeners are properly attached to the parent scope (fix_listener)
//
// TODO: take a more efficient path than this
if let Some(cur_scope_id) = self.current_scope() {
if old.listeners.len() == new.listeners.len() {
for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) {
if old_l.event != new_l.event {
self.mutations
.remove_event_listener(old_l.event, root.as_u64());
self.mutations.new_event_listener(new_l, cur_scope_id);
}
new_l.mounted_node.set(old_l.mounted_node.get());
}
} else {
for listener in old.listeners {
let cur_scope_id = self.current_scope();
if old.listeners.len() == new.listeners.len() {
for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) {
if old_l.event != new_l.event {
self.mutations
.remove_event_listener(listener.event, root.as_u64());
}
for listener in new.listeners {
listener.mounted_node.set(Some(root));
self.mutations.new_event_listener(listener, cur_scope_id);
.remove_event_listener(old_l.event, root.as_u64());
self.mutations.new_event_listener(new_l, cur_scope_id);
}
new_l.mounted_node.set(old_l.mounted_node.get());
}
} else {
for listener in old.listeners {
self.mutations
.remove_event_listener(listener.event, root.as_u64());
}
for listener in new.listeners {
listener.mounted_node.set(Some(root));
self.mutations.new_event_listener(listener, cur_scope_id);
}
}
@@ -492,6 +506,10 @@ impl<'b> DiffState<'b> {
}
fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) {
if std::ptr::eq(old, new) {
return;
}
// 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 {
@@ -934,12 +952,18 @@ impl<'b> DiffState<'b> {
log::trace!("Replacing component x2 {:?}", old);
// we can only remove components if they are actively being diffed
if self.scope_stack.contains(&c.originator) {
log::trace!("Removing component {:?}", old);
let scope = self.scopes.get_scope(scope_id).unwrap();
c.scope.set(None);
let props = scope.props.take().unwrap();
c.props.borrow_mut().replace(props);
self.scopes.try_remove(scope_id).unwrap();
self.scopes.try_remove(scope_id).unwrap();
}
// // we can only remove components if they are actively being diffed
// if self.scope_stack.contains(&c.originator) {
// log::trace!("Removing component {:?}", old);
// self.scopes.try_remove(scope_id).unwrap();
// }
}
self.leave_scope();
}
@@ -953,6 +977,7 @@ impl<'b> DiffState<'b> {
// this check exists because our null node will be removed but does not have an ID
if let Some(id) = t.id.get() {
self.scopes.collect_garbage(id);
t.id.set(None);
if gen_muts {
self.mutations.remove(id.as_u64());
@@ -962,6 +987,7 @@ impl<'b> DiffState<'b> {
VNode::Placeholder(a) => {
let id = a.id.get().unwrap();
self.scopes.collect_garbage(id);
a.id.set(None);
if gen_muts {
self.mutations.remove(id.as_u64());
@@ -975,6 +1001,7 @@ impl<'b> DiffState<'b> {
}
self.scopes.collect_garbage(id);
e.id.set(None);
self.remove_nodes(e.children, false);
}
@@ -990,10 +1017,12 @@ impl<'b> DiffState<'b> {
let root = self.scopes.root_node(scope_id);
self.remove_nodes([root], gen_muts);
// we can only remove this node if the originator is actively in our stackß
if self.scope_stack.contains(&c.originator) {
self.scopes.try_remove(scope_id).unwrap();
}
let scope = self.scopes.get_scope(scope_id).unwrap();
c.scope.set(None);
let props = scope.props.take().unwrap();
c.props.borrow_mut().replace(props);
self.scopes.try_remove(scope_id).unwrap();
}
self.leave_scope();
}
@@ -1026,8 +1055,8 @@ impl<'b> DiffState<'b> {
self.mutations.insert_before(first, created as u32);
}
fn current_scope(&self) -> Option<ScopeId> {
self.scope_stack.last().copied()
fn current_scope(&self) -> ScopeId {
self.scope_stack.last().copied().expect("no current scope")
}
fn enter_scope(&mut self, scope: ScopeId) {
@@ -1097,9 +1126,7 @@ impl<'b> DiffState<'b> {
for child in el.children.iter() {
num_on_stack += self.push_all_nodes(child);
}
self.mutations.push_root(el.id.get().unwrap());
num_on_stack + 1
}
}
-34
View File
@@ -1,34 +0,0 @@
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
use std::time::Duration;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
let (count, set_count) = use_state(&cx, || 0);
use_future(&cx, || {
let set_count = set_count.clone();
async move {
loop {
tokio::time::sleep(Duration::from_millis(1000)).await;
set_count.modify(|f| f + 1)
}
}
});
cx.render(rsx! {
div {
h1 { "High-Five counter: {count}" }
button {
onclick: move |_| set_count(0),
"Click me!"
}
}
})
}
+1 -6
View File
@@ -1047,7 +1047,7 @@ impl input {
/// - `text`
/// - `time`
/// - `url`
/// - `week`
/// - `week`
pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("type", val, None, false)
}
@@ -1100,11 +1100,6 @@ impl label {
cx.attr("for", val, None, false)
}
}
impl a {
pub fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("dioxus-prevent-default", val, None, false)
}
}
builder_constructors! {
// SVG components
+16
View File
@@ -49,6 +49,10 @@ macro_rules! aria_trait_methods {
}
pub trait GlobalAttributes {
/// Prevent the default action for this element.
///
/// For more information, see the MDN docs:
/// <https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault>
fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("dioxus-prevent-default", val, None, false)
}
@@ -603,6 +607,11 @@ pub trait GlobalAttributes {
/// Specifies the speed curve of the transition effect.
transition_timing_function: "transition-timing-function",
/// The user-select CSS property controls whether the user can select text.
/// This doesn't have any effect on content loaded as part of a browser's user interface (its chrome), except in textboxes.
user_select: "user-select",
webkit_user_select: "-webkit-user-select",
/// Sets the vertical positioning of an element relative to the current text baseline.
vertical_align: "vertical-align",
@@ -685,6 +694,13 @@ pub trait GlobalAttributes {
}
pub trait SvgAttributes {
/// Prevent the default action for this element.
///
/// For more information, see the MDN docs:
/// <https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault>
fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("dioxus-prevent-default", val, None, false)
}
aria_trait_methods! {
accent_height: "accent-height",
accumulate: "accumulate",
+8
View File
@@ -14,3 +14,11 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wasm-bindgen = { version = "0.2.79", optional = true }
js-sys = { version = "0.3.56", optional = true }
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
[features]
default = []
web = ["wasm-bindgen", "js-sys", "web-sys"]
-16
View File
@@ -1,16 +0,0 @@
use std::process::Command;
fn main() {
println!("cargo:rerun-if-changed=interpreter.ts");
match Command::new("tsc").spawn() {
Ok(_) => println!("Was spawned :)"),
Err(e) => {
if let std::io::ErrorKind::NotFound = e.kind() {
println!("`tsc` was not found! Not going to generate new interpreter")
} else {
println!("Some strange error occurred :(");
}
}
}
}
-519
View File
@@ -1,519 +0,0 @@
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`);
let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
if (event.type == "click") {
event.preventDefault();
if (shouldPreventDefault !== `onclick`) {
if (target.tagName == "A") {
const href = target.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.rpc.call("browser_open", { href });
}
}
}
}
// walk the tree to find the real element
while (realId == null) {
// we've reached the root we don't want to send an event
if (target.parentElement === null) {
return;
}
target = target.parentElement;
realId = target.getAttribute(`data-dioxus-id`);
}
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 (target.tagName == "FORM") {
let formTarget = target;
for (let x = 0; x < formTarget.elements.length; x++) {
let element = formTarget.elements[x];
let name = element.getAttribute("name");
if (name != null) {
if (element.getAttribute("type") == "checkbox") {
// @ts-ignore
contents.values[name] = element.checked ? "true" : "false";
}
else {
// @ts-ignore
contents.values[name] = element.value ?? element.textContent;
}
}
}
}
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;
}
}
}
export 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,
values: {}
};
}
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,
};
@@ -1,18 +1,9 @@
use std::{fs::File, io::Write};
fn main() {
let src = format!(
r###"use js_sys::Function;
use js_sys::Function;
use wasm_bindgen::prelude::*;
use web_sys::{{Element, Node}};
use web_sys::{Element, Node};
/*
This is an autogenerated file from build.rs.
Do not edit this file directly.
*/
#[wasm_bindgen(inline_js = r##"{}"##)]
extern "C" {{
#[wasm_bindgen(module = "/src/interpreter.js")]
extern "C" {
pub type Interpreter;
#[wasm_bindgen(constructor)]
@@ -65,12 +56,4 @@ extern "C" {{
#[wasm_bindgen(method)]
pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str);
}}
"###,
dioxus_interpreter_js::INTERPRTER_JS
);
// write the bindings to a local file
let mut file = File::create("src/bindings.rs").unwrap();
file.write_all(src.as_bytes()).unwrap();
}
@@ -5,25 +5,14 @@ export function main() {
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) {
root;
stack;
listeners;
handlers;
lastNodeWasText;
nodes;
constructor(root) {
this.root = root;
this.stack = [root];
this.listeners = {};
@@ -31,88 +20,70 @@ export class Interpreter {
this.lastNodeWasText = false;
this.nodes = [root];
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
PushRoot(root: number) {
PushRoot(root) {
const node = this.nodes[root];
this.stack.push(node);
}
AppendChildren(many: number) {
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: number, m: number) {
let root = this.nodes[root_id] as Element;
ReplaceWith(root_id, m) {
let root = this.nodes[root_id];
let els = this.stack.splice(this.stack.length - m);
root.replaceWith(...els);
}
InsertAfter(root: number, n: number) {
let old = this.nodes[root] as Element;
InsertAfter(root, n) {
let old = this.nodes[root];
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;
InsertBefore(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.before(...new_nodes);
}
Remove(root: number) {
let node = this.nodes[root] as Element;
Remove(root) {
let node = this.nodes[root];
if (node !== undefined) {
node.remove();
}
}
CreateTextNode(text: string, root: number) {
CreateTextNode(text, root) {
// todo: make it so the types are okay
const node = document.createTextNode(text) as any as Element;
const node = document.createTextNode(text);
this.nodes[root] = node;
this.stack.push(node);
}
CreateElement(tag: string, root: number) {
CreateElement(tag, root) {
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) {
CreateElementNs(tag, root, ns) {
let el = document.createElementNS(ns, tag);
this.stack.push(el);
this.nodes[root] = el;
}
CreatePlaceholder(root: number) {
CreatePlaceholder(root) {
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) {
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;
@@ -121,48 +92,39 @@ export class Interpreter {
this.listeners[event_name]++;
}
}
RemoveEventListener(root: number, event_name: string) {
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: number, text: string) {
SetText(root, text) {
this.nodes[root].textContent = text;
}
SetAttribute(root: number, field: string, value: string, ns: string | undefined) {
SetAttribute(root, field, value, ns) {
const name = field;
const node = this.nodes[root];
if (ns == "style") {
// @ts-ignore
(node as HTMLElement).style[name] = value;
node.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;
if (value != node.value) {
node.value = value;
}
break;
case "checked":
(node as HTMLInputElement).checked = value === "true";
node.checked = value === "true";
break;
case "selected":
(node as HTMLOptionElement).selected = value === "true";
node.selected = value === "true";
break;
case "dangerous_inner_html":
node.innerHTML = value;
@@ -177,33 +139,26 @@ export class Interpreter {
}
}
}
RemoveAttribute(root: number, name: string) {
RemoveAttribute(root, name) {
const node = this.nodes[root];
node.removeAttribute(name);
if (name === "value") {
(node as HTMLInputElement).value = "";
node.value = "";
}
if (name === "checked") {
(node as HTMLInputElement).checked = false;
node.checked = false;
}
if (name === "selected") {
(node as HTMLOptionElement).selected = false;
node.selected = false;
}
}
handleEdits(edits: DomEdit[]) {
handleEdits(edits) {
this.stack.push(this.root);
for (let edit of edits) {
this.handleEdit(edit);
}
}
handleEdit(edit: DomEdit) {
handleEdit(edit) {
switch (edit.type) {
case "PushRoot":
this.PushRoot(edit.root);
@@ -239,29 +194,27 @@ export class Interpreter {
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;
let handler = (event) => {
let target = event.target;
if (target != null) {
let realId = target.getAttribute(`data-dioxus-id`);
let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
let shouldPreventDefault = target.getAttribute(
`dioxus-prevent-default`
);
if (event.type == "click") {
event.preventDefault();
// todo call prevent default if it's the right type of event
if (shouldPreventDefault !== `onclick`) {
if (target.tagName == "A") {
const href = target.getAttribute("href")
const href = target.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.rpc.call("browser_open", { href });
}
}
}
}
// walk the tree to find the real element
while (realId == null) {
// we've reached the root we don't want to send an event
@@ -273,23 +226,20 @@ export class Interpreter {
realId = target.getAttribute(`data-dioxus-id`);
}
shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
shouldPreventDefault = target.getAttribute(
`dioxus-prevent-default`
);
let contents = serialize_event(event);
if (shouldPreventDefault === `on${event.type}`) {
event.preventDefault();
// event.preventDefault();
}
if (event.type == "submit") {
event.preventDefault();
// event.preventDefault();
}
if (target.tagName == "FORM") {
let formTarget = target as HTMLFormElement;
for (let x = 0; x < formTarget.elements.length; x++) {
let element = formTarget.elements[x];
for (let x = 0; x < target.elements.length; x++) {
let element = target.elements[x];
let name = element.getAttribute("name");
if (name != null) {
if (element.getAttribute("type") == "checkbox") {
@@ -297,7 +247,8 @@ export class Interpreter {
contents.values[name] = element.checked ? "true" : "false";
} else {
// @ts-ignore
contents.values[name] = element.value ?? element.textContent;
contents.values[name] =
element.value ?? element.textContent;
}
}
}
@@ -306,9 +257,8 @@ export class Interpreter {
if (realId == null) {
return;
}
window.rpc.call("user_event", {
event: (edit as NewEventListener).event_name,
event: edit.event_name,
mounted_dom_id: parseInt(realId),
contents: contents,
});
@@ -329,25 +279,21 @@ export class Interpreter {
}
}
export function serialize_event(event: Event) {
export function serialize_event(event) {
switch (event.type) {
case "copy":
case "cut":
case "past": {
return {};
}
case "compositionend":
case "compositionstart":
case "compositionupdate": {
let { data } = (event as CompositionEvent);
let { data } = event;
return {
data,
};
}
case "keydown":
case "keypress":
case "keyup": {
@@ -362,8 +308,7 @@ export function serialize_event(event: Event) {
location,
repeat,
which,
} = (event as KeyboardEvent);
} = event;
return {
char_code: charCode,
key: key,
@@ -378,45 +323,36 @@ export function serialize_event(event: Event) {
locale: "locale",
};
}
case "focus":
case "blur": {
return {};
}
case "change": {
let target = event.target as HTMLInputElement;
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 as HTMLFormElement;
let target = event.target;
let value = target.value ?? target.textContent;
if (target.type == "checkbox") {
value = target.checked ? "true" : "false";
}
return {
value: value,
values: {}
values: {},
};
}
case "click":
case "contextmenu":
case "doubleclick":
@@ -448,8 +384,7 @@ export function serialize_event(event: Event) {
screenX,
screenY,
shiftKey,
} = event as MouseEvent;
} = event;
return {
alt_key: altKey,
button: button,
@@ -465,7 +400,6 @@ export function serialize_event(event: Event) {
shift_key: shiftKey,
};
}
case "pointerdown":
case "pointermove":
case "pointerup":
@@ -499,7 +433,7 @@ export function serialize_event(event: Event) {
twist,
pointerType,
isPrimary,
} = event as PointerEvent;
} = event;
return {
alt_key: altKey,
button: button,
@@ -525,21 +459,14 @@ export function serialize_event(event: Event) {
is_primary: isPrimary,
};
}
case "select": {
return {};
}
case "touchcancel":
case "touchend":
case "touchmove":
case "touchstart": {
const {
altKey,
ctrlKey,
metaKey,
shiftKey,
} = event as TouchEvent;
const { altKey, ctrlKey, metaKey, shiftKey } = event;
return {
// changed_touches: event.changedTouches,
// target_touches: event.targetTouches,
@@ -550,18 +477,11 @@ export function serialize_event(event: Event) {
shift_key: shiftKey,
};
}
case "scroll": {
return {};
}
case "wheel": {
const {
deltaX,
deltaY,
deltaZ,
deltaMode,
} = event as WheelEvent;
const { deltaX, deltaY, deltaZ, deltaMode } = event;
return {
delta_x: deltaX,
delta_y: deltaY,
@@ -569,35 +489,24 @@ export function serialize_event(event: Event) {
delta_mode: deltaMode,
};
}
case "animationstart":
case "animationend":
case "animationiteration": {
const {
animationName,
elapsedTime,
pseudoElement,
} = event as AnimationEvent;
const { animationName, elapsedTime, pseudoElement } = event;
return {
animation_name: animationName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "transitionend": {
const {
propertyName,
elapsedTime,
pseudoElement,
} = event as TransitionEvent;
const { propertyName, elapsedTime, pseudoElement } = event;
return {
property_name: propertyName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "abort":
case "canplay":
case "canplaythrough":
@@ -623,17 +532,14 @@ export function serialize_event(event: Event) {
case "waiting": {
return {};
}
case "toggle": {
return {};
}
default: {
return {};
}
}
}
const bool_attrs = {
allowfullscreen: true,
allowpaymentrequest: true,
@@ -662,36 +568,3 @@ const bool_attrs = {
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;
+7 -1
View File
@@ -1 +1,7 @@
pub static INTERPRTER_JS: &str = include_str!("../gen/interpreter.js");
pub static INTERPRTER_JS: &str = include_str!("./interpreter.js");
#[cfg(feature = "web")]
mod bindings;
#[cfg(feature = "web")]
pub use bindings::Interpreter;
-15
View File
@@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ES2015",
"lib": [
"es2015",
"es5",
"es6",
"dom"
],
"rootDir": "src",
"strict": true,
"outDir": "gen",
}
}
+5
View File
@@ -13,3 +13,8 @@ license = "MIT/Apache-2.0"
[dependencies]
dioxus-desktop = { path = "../desktop", version = "^0.1.6" }
[lib]
doctest = false
# tests suspended until package ready
test = false
+7
View File
@@ -0,0 +1,7 @@
[tasks.test]
command = "cargo"
args = [
"test",
"--no-run",
]
+9
View File
@@ -43,9 +43,18 @@ web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"]
desktop = []
mobile = []
derive = []
wasm_test = []
[dev-dependencies]
console_error_panic_hook = "0.1.7"
dioxus-web = { path = "../web" }
log = "0.4.14"
wasm-logger = "0.2.0"
wasm-bindgen-test = "0.3"
gloo-utils = "0.1.2"
[dev-dependencies.web-sys]
version = "0.3"
features = [
"Document",
]
+10
View File
@@ -0,0 +1,10 @@
[tasks.test-with-browser]
extend = "core::wasm-pack-base"
command = "wasm-pack"
args = [
"test",
"@@split(DIOXUS_TEST_FLAGS, )",
"--",
"--features",
"${DIOXUS_TEST_FEATURES}",
]
+63
View File
@@ -0,0 +1,63 @@
#![cfg(target_arch = "wasm32")]
use dioxus_core::prelude::*;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
use dioxus_router::*;
use gloo_utils::document;
use serde::{Deserialize, Serialize};
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn simple_test() {
fn main() {
console_error_panic_hook::set_once();
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
dioxus_web::launch(APP);
}
static APP: Component = |cx| {
cx.render(rsx! {
Router {
onchange: move |route| log::info!("route changed to {}", route),
Route { to: "/", Home {} }
Route { to: "blog"
Route { to: "/", BlogList {} }
Route { to: ":id", BlogPost {} }
}
}
})
};
fn Home(cx: Scope) -> Element {
cx.render(rsx! {
div {
h1 { "Home" }
}
})
}
fn BlogList(cx: Scope) -> Element {
cx.render(rsx! {
div {
}
})
}
fn BlogPost(cx: Scope) -> Element {
let id = use_route(&cx).segment::<usize>("id")?;
cx.render(rsx! {
div {
}
})
}
main();
let element = gloo_utils::document();
}
+11
View File
@@ -0,0 +1,11 @@
{
"moz:firefoxOptions": {
"binary": "/usr/bin/firefox",
"prefs": {
"media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true
},
"args": []
}
}
+11 -13
View File
@@ -13,27 +13,25 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies]
dioxus-core = { path = "../core", version = "^0.1.9" }
dioxus-html = { path = "../html", version = "^0.1.6" }
js-sys = "0.3"
wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] }
js-sys = "0.3.56"
wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] }
lazy_static = "1.4.0"
wasm-bindgen-futures = "0.4.20"
wasm-bindgen-futures = "0.4.29"
log = { version = "0.4.14", features = ["release_max_level_off"] }
fxhash = "0.2.1"
wasm-logger = "0.2.0"
console_error_panic_hook = { version = "0.1.7", optional = true }
wasm-bindgen-test = "0.3.21"
once_cell = "1.8"
wasm-bindgen-test = "0.3.29"
once_cell = "1.9.0"
async-channel = "1.6.1"
anyhow = "1.0"
gloo-timers = { version = "0.2.1", features = ["futures"] }
futures-util = "0.3.15"
anyhow = "1.0.53"
gloo-timers = { version = "0.2.3", features = ["futures"] }
futures-util = "0.3.19"
smallstr = "0.2.0"
[build-dependencies]
dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" }
dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0", features = ["web"] }
[dependencies.web-sys]
version = "0.3.51"
version = "0.3.56"
features = [
"Comment",
"Attr",
@@ -79,5 +77,5 @@ panic_hook = ["console_error_panic_hook"]
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro" }
wasm-bindgen-test = "0.3.28"
wasm-bindgen-test = "0.3.29"
dioxus-ssr = { path = "../ssr" }
-583
View File
@@ -1,583 +0,0 @@
use js_sys::Function;
use wasm_bindgen::prelude::*;
use web_sys::{Element, Node};
/*
This is an autogenerated file from build.rs.
Do not edit this file directly.
*/
#[wasm_bindgen(inline_js = r##"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`);
let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
if (event.type == "click") {
event.preventDefault();
if (shouldPreventDefault !== `onclick`) {
if (target.tagName == "A") {
const href = target.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.rpc.call("browser_open", { href });
}
}
}
}
// walk the tree to find the real element
while (realId == null) {
// we've reached the root we don't want to send an event
if (target.parentElement === null) {
return;
}
target = target.parentElement;
realId = target.getAttribute(`data-dioxus-id`);
}
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 (target.tagName == "FORM") {
let formTarget = target;
for (let x = 0; x < formTarget.elements.length; x++) {
let element = formTarget.elements[x];
let name = element.getAttribute("name");
if (name != null) {
if (element.getAttribute("type") == "checkbox") {
// @ts-ignore
contents.values[name] = element.checked ? "true" : "false";
}
else {
// @ts-ignore
contents.values[name] = element.value ?? element.textContent;
}
}
}
}
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;
}
}
}
export 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,
values: {}
};
}
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,
};
"##)]
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
@@ -7,8 +7,8 @@
//! - tests to ensure dyn_into works for various event types.
//! - Partial delegation?>
use crate::bindings::Interpreter;
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
use dioxus_interpreter_js::Interpreter;
use js_sys::Function;
use std::{any::Any, rc::Rc, sync::Arc};
use wasm_bindgen::{closure::Closure, JsCast};
-1
View File
@@ -61,7 +61,6 @@ 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;