Merge branch 'upstream-master' into fix-hot-reloading

This commit is contained in:
= committed 2022-12-16 10:07:11 -06:00
commit 14dd568987
92 files changed
+3469 -2468

No files matched your search

+1 -1
View File
@@ -13,7 +13,6 @@ jobs:
runs-on: ubuntu-latest
environment: docs
steps:
- uses: actions/checkout@v3
# NOTE: Comment out when https://github.com/rust-lang/mdBook/pull/1306 is merged and released
# - name: Setup mdBook
@@ -25,6 +24,7 @@ jobs:
- name: Setup mdBook
run: |
cargo install mdbook --git https://github.com/Ruin0x11/mdBook.git --branch localization --rev e74fdb1
- uses: actions/checkout@v3
- name: Build
run: cd docs &&
+1 -1
View File
@@ -30,13 +30,13 @@ jobs:
name: Test Suite
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- uses: actions-rs/cargo@v1
with:
command: test
+4 -4
View File
@@ -32,7 +32,6 @@ jobs:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
@@ -41,6 +40,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
- uses: actions/checkout@v3
- uses: actions-rs/cargo@v1
with:
command: check
@@ -51,7 +51,6 @@ jobs:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
@@ -63,6 +62,7 @@ jobs:
- uses: davidB/rust-cargo-make@v1
- uses: browser-actions/setup-firefox@latest
- uses: jetli/wasm-pack-action@v0.4.0
- uses: actions/checkout@v3
- uses: actions-rs/cargo@v1
with:
command: make
@@ -73,7 +73,6 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
@@ -81,6 +80,7 @@ jobs:
override: true
- uses: Swatinem/rust-cache@v2
- run: rustup component add rustfmt
- uses: actions/checkout@v3
- uses: actions-rs/cargo@v1
with:
command: fmt
@@ -91,7 +91,6 @@ jobs:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
@@ -101,6 +100,7 @@ jobs:
- run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
- run: rustup component add clippy
- uses: actions/checkout@v3
- uses: actions-rs/cargo@v1
with:
command: clippy
+3 -2
View File
@@ -47,8 +47,6 @@ jobs:
# which causes failures for some of rustfmt's line-ending sensitive tests
- name: disable git eol translation
run: git config --global core.autocrlf false
- name: checkout
uses: actions/checkout@v3
# Run build
- name: Install Rustup using win.rustup.rs
@@ -66,6 +64,9 @@ jobs:
if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly'
shell: bash
- name: checkout
uses: actions/checkout@v3
- name: test
run: |
rustc -Vv
+1
View File
@@ -57,6 +57,7 @@ reqwest = { version = "0.11.9", features = ["json"] }
fern = { version = "0.6.0", features = ["colored"] }
thiserror = "1.0.30"
env_logger = "0.9.0"
simple_logger = "4.0.0"
[profile.release]
opt-level = 3
+8
View File
@@ -16,6 +16,12 @@ fn main() {
}
fn app(cx: Scope) -> Element {
let g = cx.component(component, (), "component");
let c = cx.make_node(g);
cx.render(rsx! {
div { c }
})
// let nf = NodeFactory::new(cx);
// let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump());
@@ -27,6 +33,8 @@ fn app(cx: Scope) -> Element {
// attrs.push(nf.attr("age", format_args!("47"), None, false));
// Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None))
}
fn component(cx: Scope) -> Element {
todo!()
}
+14 -1
View File
@@ -1,4 +1,5 @@
use dioxus::prelude::*;
use dioxus_desktop::EvalResult;
fn main() {
dioxus_desktop::launch(app);
@@ -7,6 +8,15 @@ fn main() {
fn app(cx: Scope) -> Element {
let script = use_state(cx, String::new);
let eval = dioxus_desktop::use_eval(cx);
let future: &UseRef<Option<EvalResult>> = use_ref(cx, || None);
if future.read().is_some() {
let future_clone = future.clone();
cx.spawn(async move {
if let Some(fut) = future_clone.with_mut(|o| o.take()) {
println!("{:?}", fut.await)
}
});
}
cx.render(rsx! {
div {
@@ -16,7 +26,10 @@ fn app(cx: Scope) -> Element {
oninput: move |e| script.set(e.value.clone()),
}
button {
onclick: move |_| eval(script.to_string()),
onclick: move |_| {
let fut = eval(script);
future.set(Some(fut));
},
"Execute"
}
}
+1 -2
View File
@@ -1,5 +1,4 @@
#![allow(non_snake_case)]
//! A tour of the rsx! macro
//! ------------------------
//!
@@ -39,7 +38,7 @@
//! - Accept a list of vnodes as children for a Fragment component
//! - Allow keyed fragments in iterators
//! - Allow top-level fragments
//!
fn main() {
dioxus_desktop::launch(app);
}
+60
View File
@@ -0,0 +1,60 @@
use dioxus::prelude::*;
use dioxus_router::*;
fn main() {
simple_logger::SimpleLogger::new()
.with_level(log::LevelFilter::Debug)
.with_module_level("dioxus_router", log::LevelFilter::Trace)
.with_module_level("dioxus", log::LevelFilter::Trace)
.init()
.unwrap();
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
Router {
h1 { "Your app here" }
ul {
Link { to: "/", li { "home" } }
Link { to: "/blog", li { "blog" } }
Link { to: "/blog/tim", li { "tims' blog" } }
Link { to: "/blog/bill", li { "bills' blog" } }
Link { to: "/blog/james",
li { "james amazing' blog" }
}
Link { to: "/apples", li { "go to apples" } }
}
Route { to: "/", Home {} }
Route { to: "/blog/", BlogList {} }
Route { to: "/blog/:id/", BlogPost {} }
Route { to: "/oranges", "Oranges are not apples!" }
Redirect { from: "/apples", to: "/oranges" }
}
})
}
fn Home(cx: Scope) -> Element {
log::debug!("rendering home {:?}", cx.scope_id());
cx.render(rsx! { h1 { "Home" } })
}
fn BlogList(cx: Scope) -> Element {
log::debug!("rendering blog list {:?}", cx.scope_id());
cx.render(rsx! { div { "Blog List" } })
}
fn BlogPost(cx: Scope) -> Element {
let Some(id) = use_route(cx).segment("id") else {
return cx.render(rsx! { div { "No blog post id" } })
};
log::debug!("rendering blog post {}", id);
cx.render(rsx! {
div {
h3 { "blog post: {id:?}" }
Link { to: "/blog/", "back to blog list" }
}
})
}
+6
View File
@@ -67,6 +67,12 @@ fn app(cx: Scope) -> Element {
stroke: "blue",
stroke_width: "5",
}
path {
d: "M9.00001 9C9 62 103.5 124 103.5 178",
stroke: "#3CC4DC",
"stroke-linecap": "square",
"stroke-width": "square",
}
}))
}
+1 -1
View File
@@ -18,7 +18,7 @@ proc-macro = true
proc-macro2 = { version = "1.0" }
quote = "1.0"
syn = { version = "1.0", features = ["full", "extra-traits"] }
dioxus-rsx = { path = "../rsx" }
dioxus-rsx = { path = "../rsx", version = "0.0.1" }
# testing
[dev-dependencies]
-10
View File
@@ -26,11 +26,6 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
}
/// The rsx! macro makes it easy for developers to write jsx-style markup in their components.
///
/// ## Complete Reference Guide:
/// ```ignore
#[doc = include_str!("../../../examples/rsx_usage.rs")]
/// ```
#[proc_macro]
pub fn rsx(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) {
@@ -42,11 +37,6 @@ pub fn rsx(s: TokenStream) -> TokenStream {
/// The render! macro makes it easy for developers to write jsx-style markup in their components.
///
/// The render macro automatically renders rsx - making it unhygenic.
///
/// ## Complete Reference Guide:
/// ```ignore
#[doc = include_str!("../../../examples/rsx_usage.rs")]
/// ```
#[proc_macro]
pub fn render(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) {
+4 -1
View File
@@ -18,7 +18,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
# faster hashmaps
fxhash = "0.2"
rustc-hash = "1.1.0"
# Used in diffing
longest-increasing-subsequence = "0.1.0"
@@ -35,6 +35,9 @@ indexmap = "1.7"
serde = { version = "1", features = ["derive"], optional = true }
anyhow = "1.0.66"
smallbox = "0.8.1"
log = "0.4.17"
[dev-dependencies]
tokio = { version = "*", features = ["full"] }
dioxus = { path = "../dioxus" }
+18 -11
View File
@@ -34,14 +34,14 @@ impl ElementRef {
impl VirtualDom {
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
self.next(template, ElementPath::Deep(path))
self.next_reference(template, ElementPath::Deep(path))
}
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
self.next(template, ElementPath::Root(path))
self.next_reference(template, ElementPath::Root(path))
}
fn next(&mut self, template: &VNode, path: ElementPath) -> ElementId {
fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId {
let entry = self.elements.vacant_entry();
let id = entry.key();
@@ -95,22 +95,27 @@ impl VirtualDom {
fn drop_scope_inner(&mut self, node: &VNode) {
node.clear_listeners();
node.dynamic_nodes.iter().for_each(|node| match node {
DynamicNode::Component(c) => self.drop_scope(c.scope.get().unwrap()),
DynamicNode::Component(c) => {
if let Some(f) = c.scope.get() {
self.drop_scope(f)
}
}
DynamicNode::Fragment(nodes) => {
nodes.iter().for_each(|node| self.drop_scope_inner(node))
}
DynamicNode::Placeholder(t) => {
self.try_reclaim(t.get());
self.try_reclaim(t.id.get().unwrap());
}
DynamicNode::Text(t) => {
self.try_reclaim(t.id.get());
self.try_reclaim(t.id.get().unwrap());
}
});
for root in node.root_ids {
let id = root.get();
if id.0 != 0 {
self.try_reclaim(id);
if let Some(id) = root.get() {
if id.0 != 0 {
self.try_reclaim(id);
}
}
}
}
@@ -131,8 +136,10 @@ impl VirtualDom {
node.dynamic_nodes.iter().for_each(|child| match child {
// Only descend if the props are borrowed
DynamicNode::Component(c) if !c.static_props => {
self.ensure_drop_safety(c.scope.get().unwrap());
c.props.set(None);
if let Some(scope) = c.scope.get() {
self.ensure_drop_safety(scope);
}
c.props.take();
}
DynamicNode::Fragment(f) => f
+253 -179
View File
@@ -1,13 +1,15 @@
use std::cell::Cell;
use std::rc::Rc;
use crate::innerlude::{VComponent, VText};
use crate::innerlude::{VComponent, VPlaceholder, VText};
use crate::mutations::Mutation;
use crate::mutations::Mutation::*;
use crate::nodes::VNode;
use crate::nodes::{DynamicNode, TemplateNode};
use crate::virtual_dom::VirtualDom;
use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext};
use std::cell::Cell;
use std::iter::{Enumerate, Peekable};
use std::rc::Rc;
use std::slice;
use TemplateNode::*;
impl<'b> VirtualDom {
/// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
@@ -17,187 +19,232 @@ impl<'b> VirtualDom {
self.scope_stack.push(scope);
let out = self.create(template);
self.scope_stack.pop();
out
}
/// Create this template and write its mutations
pub(crate) fn create(&mut self, template: &'b VNode<'b>) -> usize {
pub(crate) fn create(&mut self, node: &'b VNode<'b>) -> usize {
// The best renderers will have templates prehydrated and registered
// Just in case, let's create the template using instructions anyways
if !self.templates.contains_key(&template.template.name) {
self.register_template(template);
if !self.templates.contains_key(&node.template.name) {
self.register_template(node);
}
// we know that this will generate at least one mutation per node
self.mutations.edits.reserve(node.template.roots.len());
// Walk the roots, creating nodes and assigning IDs
// todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS)
let mut dynamic_attrs = template.template.attr_paths.iter().enumerate().peekable();
let mut dynamic_nodes = template.template.node_paths.iter().enumerate().peekable();
let mut attrs = node.template.attr_paths.iter().enumerate().peekable();
let mut nodes = node.template.node_paths.iter().enumerate().peekable();
let cur_scope = self.scope_stack.last().copied().unwrap();
node.template
.roots
.iter()
.enumerate()
.map(|(idx, root)| match root {
DynamicText { id } | Dynamic { id } => self.write_dynamic_root(node, *id),
Element { .. } => self.write_element_root(node, idx, &mut attrs, &mut nodes),
Text { .. } => self.write_static_text_root(node, idx),
})
.sum()
}
let mut on_stack = 0;
for (root_idx, root) in template.template.roots.iter().enumerate() {
// We might need to generate an ID for the root node
on_stack += match root {
TemplateNode::DynamicText { id } | TemplateNode::Dynamic { id } => {
match &template.dynamic_nodes[*id] {
// a dynamic text node doesn't replace a template node, instead we create it on the fly
DynamicNode::Text(VText { id: slot, value }) => {
let id = self.next_element(template, template.template.node_paths[*id]);
slot.set(id);
fn write_static_text_root(&mut self, node: &VNode, idx: usize) -> usize {
// Simply just load the template root, no modifications needed
self.load_template_root(node, idx);
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_text = unsafe { std::mem::transmute(*value) };
self.mutations.push(CreateTextNode {
value: unbounded_text,
id,
});
// Text producs just one node on the stack
1
}
1
}
fn write_dynamic_root(&mut self, template: &'b VNode<'b>, idx: usize) -> usize {
use DynamicNode::*;
match &template.dynamic_nodes[idx] {
node @ Fragment(_) => self.create_dynamic_node(template, node, idx),
node @ Component { .. } => self.create_dynamic_node(template, node, idx),
Placeholder(VPlaceholder { id }) => {
let id = self.set_slot(template, id, idx);
self.mutations.push(CreatePlaceholder { id });
1
}
Text(VText { id, value }) => {
let id = self.set_slot(template, id, idx);
self.create_static_text(value, id);
1
}
}
}
DynamicNode::Placeholder(slot) => {
let id = self.next_element(template, template.template.node_paths[*id]);
slot.set(id);
self.mutations.push(CreatePlaceholder { id });
1
}
fn create_static_text(&mut self, value: &str, id: ElementId) {
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_text: &str = unsafe { std::mem::transmute(value) };
self.mutations.push(CreateTextNode {
value: unbounded_text,
id,
});
}
DynamicNode::Fragment(_) | DynamicNode::Component { .. } => {
self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id)
}
}
/// We write all the descndent data for this element
///
/// Elements can contain other nodes - and those nodes can be dynamic or static
///
/// We want to make sure we write these nodes while on top of the root
fn write_element_root(
&mut self,
template: &'b VNode<'b>,
root_idx: usize,
dynamic_attrs: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
) -> usize {
// Load the template root and get the ID for the node on the stack
let root_on_stack = self.load_template_root(template, root_idx);
// Write all the attributes below this root
self.write_attrs_on_root(dynamic_attrs, root_idx, root_on_stack, template);
// Load in all of the placeholder or dynamic content under this root too
self.load_placeholders(dynamic_nodes, root_idx, template);
1
}
/// Load all of the placeholder nodes for descendents of this root node
///
/// ```rust, ignore
/// rsx! {
/// div {
/// // This is a placeholder
/// some_value,
///
/// // Load this too
/// "{some_text}"
/// }
/// }
/// ```
fn load_placeholders(
&mut self,
dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
root_idx: usize,
template: &'b VNode<'b>,
) {
let (start, end) = match collect_dyn_node_range(dynamic_nodes, root_idx) {
Some((a, b)) => (a, b),
None => return,
};
for idx in (start..=end).rev() {
let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx);
if m > 0 {
// The path is one shorter because the top node is the root
let path = &template.template.node_paths[idx][1..];
self.mutations.push(ReplacePlaceholder { m, path });
}
}
}
fn write_attrs_on_root(
&mut self,
attrs: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
root_idx: usize,
root: ElementId,
node: &VNode,
) {
while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p[0] == root_idx as u8) {
let id = self.assign_static_node_as_dynamic(path, root, node, attr_id);
loop {
self.write_attribute(&node.dynamic_attrs[attr_id], id);
// Only push the dynamic attributes forward if they match the current path (same element)
match attrs.next_if(|(_, p)| *p == path) {
Some((next_attr_id, _)) => attr_id = next_attr_id,
None => break,
}
}
}
}
TemplateNode::Element { .. } | TemplateNode::Text { .. } => {
let this_id = self.next_root(template, root_idx);
fn write_attribute(&mut self, attribute: &crate::Attribute, id: ElementId) {
// Make sure we set the attribute's associated id
attribute.mounted_element.set(id);
template.root_ids[root_idx].set(this_id);
self.mutations.push(LoadTemplate {
name: template.template.name,
index: root_idx,
id: this_id,
});
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_name = unsafe { std::mem::transmute(attribute.name) };
// we're on top of a node that has a dynamic attribute for a descendant
// Set that attribute now before the stack gets in a weird state
while let Some((mut attr_id, path)) =
dynamic_attrs.next_if(|(_, p)| p[0] == root_idx as u8)
{
// if attribute is on a root node, then we've already created the element
// Else, it's deep in the template and we should create a new id for it
let id = match path.len() {
1 => this_id,
_ => {
let id = self
.next_element(template, template.template.attr_paths[attr_id]);
self.mutations.push(Mutation::AssignId {
path: &path[1..],
id,
});
id
}
};
match &attribute.value {
AttributeValue::Text(value) => {
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_value = unsafe { std::mem::transmute(*value) };
loop {
let attribute = template.dynamic_attrs.get(attr_id).unwrap();
attribute.mounted_element.set(id);
self.mutations.push(SetAttribute {
name: unbounded_name,
value: unbounded_value,
ns: attribute.namespace,
id,
})
}
AttributeValue::Bool(value) => self.mutations.push(SetBoolAttribute {
name: unbounded_name,
value: *value,
id,
}),
AttributeValue::Listener(_) => {
self.mutations.push(NewEventListener {
// all listeners start with "on"
name: &unbounded_name[2..],
id,
})
}
AttributeValue::Float(_) => todo!(),
AttributeValue::Int(_) => todo!(),
AttributeValue::Any(_) => todo!(),
AttributeValue::None => todo!(),
}
}
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_name = unsafe { std::mem::transmute(attribute.name) };
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
// Get an ID for this root since it's a real root
let this_id = self.next_root(template, root_idx);
template.root_ids[root_idx].set(Some(this_id));
match &attribute.value {
AttributeValue::Text(value) => {
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_value = unsafe { std::mem::transmute(*value) };
self.mutations.push(LoadTemplate {
name: template.template.name,
index: root_idx,
id: this_id,
});
self.mutations.push(SetAttribute {
name: unbounded_name,
value: unbounded_value,
ns: attribute.namespace,
id,
})
}
AttributeValue::Bool(value) => {
self.mutations.push(SetBoolAttribute {
name: unbounded_name,
value: *value,
id,
})
}
AttributeValue::Listener(_) => {
self.mutations.push(NewEventListener {
// all listeners start with "on"
name: &unbounded_name[2..],
scope: cur_scope,
id,
})
}
AttributeValue::Float(_) => todo!(),
AttributeValue::Int(_) => todo!(),
AttributeValue::Any(_) => todo!(),
AttributeValue::None => todo!(),
}
this_id
}
// Only push the dynamic attributes forward if they match the current path (same element)
match dynamic_attrs.next_if(|(_, p)| *p == path) {
Some((next_attr_id, _)) => attr_id = next_attr_id,
None => break,
}
}
}
// We're on top of a node that has a dynamic child for a descendant
// Skip any node that's a root
let mut start = None;
let mut end = None;
// Collect all the dynamic nodes below this root
// We assign the start and end of the range of dynamic nodes since they area ordered in terms of tree path
//
// [0]
// [1, 1] <---|
// [1, 1, 1] <---| these are the range of dynamic nodes below root 1
// [1, 1, 2] <---|
// [2]
//
// We collect each range and then create them and replace the placeholder in the template
while let Some((idx, p)) =
dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8)
{
if p.len() == 1 {
continue;
}
if start.is_none() {
start = Some(idx);
}
end = Some(idx);
}
//
if let (Some(start), Some(end)) = (start, end) {
for idx in start..=end {
let node = &template.dynamic_nodes[idx];
let m = self.create_dynamic_node(template, node, idx);
if m > 0 {
self.mutations.push(ReplacePlaceholder {
m,
path: &template.template.node_paths[idx][1..],
});
}
}
}
// elements create only one node :-)
1
}
};
/// We have some dynamic attributes attached to a some node
///
/// That node needs to be loaded at runtime, so we need to give it an ID
///
/// If the node in question is on the stack, we just return that ID
///
/// If the node is not on the stack, we create a new ID for it and assign it
fn assign_static_node_as_dynamic(
&mut self,
path: &'static [u8],
this_id: ElementId,
template: &VNode,
attr_id: usize,
) -> ElementId {
if path.len() == 1 {
return this_id;
}
on_stack
// if attribute is on a root node, then we've already created the element
// Else, it's deep in the template and we should create a new id for it
let id = self.next_element(template, template.template.attr_paths[attr_id]);
self.mutations.push(Mutation::AssignId {
path: &path[1..],
id,
});
id
}
/// Insert a new template into the VirtualDom's template registry
@@ -207,17 +254,9 @@ impl<'b> VirtualDom {
.insert(template.template.name, template.template);
// If it's all dynamic nodes, then we don't need to register it
// Quickly run through and see if it's all just dynamic nodes
if template.template.roots.iter().all(|root| {
matches!(
root,
TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. }
)
}) {
return;
if !template.template.is_completely_dynamic() {
self.mutations.templates.push(template.template);
}
self.mutations.templates.push(template.template);
}
pub(crate) fn create_dynamic_node(
@@ -245,7 +284,7 @@ impl<'b> VirtualDom {
let new_id = self.next_element(template, template.template.node_paths[idx]);
// Make sure the text node is assigned to the correct element
text.id.set(new_id);
text.id.set(Some(new_id));
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let value = unsafe { std::mem::transmute(text.value) };
@@ -263,7 +302,7 @@ impl<'b> VirtualDom {
pub(crate) fn create_placeholder(
&mut self,
slot: &Cell<ElementId>,
placeholder: &VPlaceholder,
template: &'b VNode<'b>,
idx: usize,
) -> usize {
@@ -271,7 +310,7 @@ impl<'b> VirtualDom {
let id = self.next_element(template, template.template.node_paths[idx]);
// Make sure the text node is assigned to the correct element
slot.set(id);
placeholder.id.set(Some(id));
// Assign the ID to the existing node in the template
self.mutations.push(AssignId {
@@ -284,7 +323,7 @@ impl<'b> VirtualDom {
}
pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
nodes.iter().fold(0, |acc, child| acc + self.create(child))
nodes.iter().map(|child| self.create(child)).sum()
}
pub(super) fn create_component_node(
@@ -293,15 +332,17 @@ impl<'b> VirtualDom {
component: &'b VComponent<'b>,
idx: usize,
) -> usize {
let props = component
.props
.replace(None)
.expect("Props to always exist when a component is being created");
let scope = match component.props.take() {
Some(props) => {
let unbounded_props = unsafe { std::mem::transmute(props) };
let scope = self.new_scope(unbounded_props, component.name);
scope.id
}
let unbounded_props = unsafe { std::mem::transmute(props) };
// Component is coming back, it probably still exists, right?
None => component.scope.get().unwrap(),
};
let scope = self.new_scope(unbounded_props, component.name);
let scope = scope.id;
component.scope.set(Some(scope));
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
@@ -391,4 +432,37 @@ impl<'b> VirtualDom {
0
}
fn set_slot(
&mut self,
template: &'b VNode<'b>,
slot: &'b Cell<Option<ElementId>>,
id: usize,
) -> ElementId {
let id = self.next_element(template, template.template.node_paths[id]);
slot.set(Some(id));
id
}
}
fn collect_dyn_node_range(
dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&[u8]>>>,
root_idx: usize,
) -> Option<(usize, usize)> {
let start = match dynamic_nodes.peek() {
Some((idx, p)) if p[0] == root_idx as u8 => *idx,
_ => return None,
};
let mut end = start;
while let Some((idx, p)) = dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) {
if p.len() == 1 {
continue;
}
end = idx;
}
Some((start, end))
}
+189 -123
View File
@@ -1,17 +1,15 @@
use std::cell::Cell;
use crate::{
arena::ElementId,
innerlude::{DirtyScope, VComponent, VText},
innerlude::{DirtyScope, VComponent, VPlaceholder, VText},
mutations::Mutation,
nodes::RenderReturn,
nodes::{DynamicNode, VNode},
scopes::ScopeId,
virtual_dom::VirtualDom,
AttributeValue, TemplateNode,
Attribute, AttributeValue, TemplateNode,
};
use fxhash::{FxHashMap, FxHashSet};
use rustc_hash::{FxHashMap, FxHashSet};
use DynamicNode::*;
impl<'b> VirtualDom {
@@ -56,87 +54,101 @@ impl<'b> VirtualDom {
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {}
fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
if left_template.template.name != right_template.template.name {
// If the templates are the same, we don't need to do anything, nor do we want to
if templates_are_the_same(left_template, right_template) {
return;
}
// If the templates are different by name, we need to replace the entire template
if templates_are_different(left_template, right_template) {
return self.light_diff_templates(left_template, right_template);
}
for (left_attr, right_attr) in left_template
// If the templates are the same, we can diff the attributes and children
// Start with the attributes
left_template
.dynamic_attrs
.iter()
.zip(right_template.dynamic_attrs.iter())
{
// Move over the ID from the old to the new
right_attr
.mounted_element
.set(left_attr.mounted_element.get());
.for_each(|(left_attr, right_attr)| {
// Move over the ID from the old to the new
right_attr
.mounted_element
.set(left_attr.mounted_element.get());
// We want to make sure anything listener that gets pulled is valid
if let AttributeValue::Listener(_) = right_attr.value {
self.update_template(left_attr.mounted_element.get(), right_template);
}
if left_attr.value != right_attr.value || left_attr.volatile {
// todo: add more types of attribute values
match right_attr.value {
AttributeValue::Text(text) => {
let name = unsafe { std::mem::transmute(left_attr.name) };
let value = unsafe { std::mem::transmute(text) };
self.mutations.push(Mutation::SetAttribute {
id: left_attr.mounted_element.get(),
ns: right_attr.namespace,
name,
value,
});
}
// todo: more types of attribute values
_ => todo!("other attribute types"),
// We want to make sure anything listener that gets pulled is valid
if let AttributeValue::Listener(_) = right_attr.value {
self.update_template(left_attr.mounted_element.get(), right_template);
}
}
}
for (idx, (left_node, right_node)) in left_template
// If the attributes are different (or volatile), we need to update them
if left_attr.value != right_attr.value || left_attr.volatile {
self.update_attribute(right_attr, left_attr);
}
});
// Now diff the dynamic nodes
left_template
.dynamic_nodes
.iter()
.zip(right_template.dynamic_nodes.iter())
.enumerate()
{
match (left_node, right_node) {
(Text(left), Text(right)) => self.diff_vtext(left, right),
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
(Placeholder(left), Placeholder(right)) => {
right.set(left.get());
}
(Component(left), Component(right)) => {
self.diff_vcomponent(left, right, right_template, idx)
}
(Placeholder(left), Fragment(right)) => {
self.replace_placeholder_with_nodes(left, right)
}
(Fragment(left), Placeholder(right)) => {
self.replace_nodes_with_placeholder(left, right)
}
_ => todo!(),
};
}
.for_each(|(idx, (left_node, right_node))| {
self.diff_dynamic_node(left_node, right_node, right_template, idx);
});
// Make sure the roots get transferred over
for (left, right) in left_template
// Make sure the roots get transferred over while we're here
left_template
.root_ids
.iter()
.zip(right_template.root_ids.iter())
{
right.set(left.get());
.for_each(|(left, right)| right.set(left.get()));
}
fn diff_dynamic_node(
&mut self,
left_node: &'b DynamicNode<'b>,
right_node: &'b DynamicNode<'b>,
node: &'b VNode<'b>,
idx: usize,
) {
match (left_node, right_node) {
(Text(left), Text(right)) => self.diff_vtext(left, right, node),
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
(Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()),
(Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx),
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, right),
(Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right),
_ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."),
};
}
fn update_attribute(&mut self, right_attr: &Attribute, left_attr: &Attribute) {
// todo: add more types of attribute values
match right_attr.value {
AttributeValue::Text(text) => {
let name = unsafe { std::mem::transmute(left_attr.name) };
let value = unsafe { std::mem::transmute(text) };
self.mutations.push(Mutation::SetAttribute {
id: left_attr.mounted_element.get(),
ns: right_attr.namespace,
name,
value,
});
}
// todo: more types of attribute values
_ => todo!("other attribute types"),
}
}
fn replace_placeholder_with_nodes(&mut self, l: &'b Cell<ElementId>, r: &'b [VNode<'b>]) {
fn replace_placeholder(&mut self, l: &'b VPlaceholder, r: &'b [VNode<'b>]) {
let m = self.create_children(r);
let id = l.get();
let id = l.id.get().unwrap();
self.mutations.push(Mutation::ReplaceWith { id, m });
self.reclaim(id);
}
fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell<ElementId>) {
fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) {
// Remove the old nodes, except for one
self.remove_nodes(&l[1..]);
@@ -145,7 +157,7 @@ impl<'b> VirtualDom {
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder
let placeholder = self.next_element(&l[0], &[]);
r.set(placeholder);
r.id.set(Some(placeholder));
self.mutations
.push(Mutation::CreatePlaceholder { id: placeholder });
@@ -162,6 +174,10 @@ impl<'b> VirtualDom {
right_template: &'b VNode<'b>,
idx: usize,
) {
if std::ptr::eq(left, right) {
return;
}
// Replace components that have different render fns
if left.render_fn != right.render_fn {
let created = self.create_component_node(right_template, right, idx);
@@ -181,12 +197,22 @@ impl<'b> VirtualDom {
}
// Make sure the new vcomponent has the right scopeid associated to it
let scope_id = left.scope.get().unwrap();
let Some(scope_id) = left.scope.get() else {
return;
};
// let scope_id = left.scope.get().unwrap_or_else(|| {
// panic!(
// "A component should always have a scope associated to it. {:?}\n {:#?}",
// right.name,
// std::backtrace::Backtrace::force_capture()
// )
// });
right.scope.set(Some(scope_id));
// copy out the box for both
let old = self.scopes[scope_id.0].props.as_ref();
let new = right.props.replace(None).unwrap();
let new = right.props.take().unwrap();
// If the props are static, then we try to memoize by setting the new with the old
// The target scopestate still has the reference to the old props, so there's no need to update anything
@@ -201,6 +227,11 @@ impl<'b> VirtualDom {
// Now run the component and diff it
self.run_scope(scope_id);
self.diff_scope(scope_id);
self.dirty_scopes.remove(&DirtyScope {
height: self.scopes[scope_id.0].height,
id: scope_id,
});
}
/// Lightly diff the two templates, checking only their roots.
@@ -254,10 +285,13 @@ impl<'b> VirtualDom {
///
/// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's
/// different.
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
let id = left.id.get();
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) {
let id = left
.id
.get()
.unwrap_or_else(|| self.next_element(node, &[0]));
right.id.set(id);
right.id.set(Some(id));
if left.value != right.value {
let value = unsafe { std::mem::transmute(right.value) };
self.mutations.push(Mutation::SetText { id, value });
@@ -269,9 +303,9 @@ impl<'b> VirtualDom {
/// All IDs will be garbage collected
fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId {
let id = match node.dynamic_root(0) {
None => node.root_ids[0].get(),
Some(Text(t)) => t.id.get(),
Some(Placeholder(e)) => e.get(),
None => node.root_ids[0].get().unwrap(),
Some(Text(t)) => t.id.get().unwrap(),
Some(Placeholder(e)) => e.id.get().unwrap(),
Some(Fragment(nodes)) => {
let id = self.replace_inner(&nodes[0]);
self.remove_nodes(&nodes[1..]);
@@ -309,14 +343,23 @@ impl<'b> VirtualDom {
match dyn_node {
Component(comp) => {
let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
RenderReturn::Sync(Ok(t)) => self.clean_up_node(t),
_ => todo!("cannot handle nonstandard nodes"),
};
if let Some(scope) = comp.scope.take() {
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
RenderReturn::Sync(Ok(t)) => self.clean_up_node(t),
_ => todo!("cannot handle nonstandard nodes"),
};
}
}
Text(t) => {
if let Some(id) = t.id.take() {
self.reclaim(id)
}
}
Placeholder(t) => {
if let Some(id) = t.id.take() {
self.reclaim(id)
}
}
Text(t) => self.reclaim(t.id.get()),
Placeholder(t) => self.reclaim(t.get()),
Fragment(nodes) => nodes.iter().for_each(|node| self.clean_up_node(node)),
};
}
@@ -344,25 +387,25 @@ impl<'b> VirtualDom {
fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) {
match node.dynamic_root(idx) {
Some(Text(i)) => {
let id = i.id.get();
let id = i.id.take().unwrap();
self.mutations.push(Mutation::Remove { id });
self.reclaim(id);
}
Some(Placeholder(e)) => {
let id = e.get();
let id = e.id.take().unwrap();
self.mutations.push(Mutation::Remove { id });
self.reclaim(id);
}
Some(Fragment(nodes)) => self.remove_nodes(nodes),
Some(Component(comp)) => {
let scope = comp.scope.get().unwrap();
let scope = comp.scope.take().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
RenderReturn::Sync(Ok(t)) => self.remove_node(t),
_ => todo!("cannot handle nonstandard nodes"),
};
}
None => {
let id = node.root_ids[idx].get();
let id = node.root_ids[idx].get().unwrap();
self.mutations.push(Mutation::Remove { id });
self.reclaim(id);
}
@@ -432,7 +475,7 @@ impl<'b> VirtualDom {
// The stack is empty upon entry.
fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
if cfg!(debug_assertions) {
let mut keys = fxhash::FxHashSet::default();
let mut keys = rustc_hash::FxHashSet::default();
let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
keys.clear();
for child in children {
@@ -748,12 +791,16 @@ impl<'b> VirtualDom {
fn remove_node(&mut self, node: &'b VNode<'b>) {
for (idx, _) in node.template.roots.iter().enumerate() {
let id = match node.dynamic_root(idx) {
Some(Text(t)) => t.id.get(),
Some(Placeholder(t)) => t.get(),
Some(Text(t)) => t.id.take(),
Some(Placeholder(t)) => t.id.take(),
Some(Fragment(t)) => return self.remove_nodes(t),
Some(Component(comp)) => return self.remove_component(comp.scope.get().unwrap()),
Some(Component(comp)) => {
comp.scope.set(None);
return self.remove_component(comp.scope.get().unwrap());
}
None => node.root_ids[idx].get(),
};
}
.unwrap();
self.mutations.push(Mutation::Remove { id })
}
@@ -761,7 +808,7 @@ impl<'b> VirtualDom {
self.clean_up_node(node);
for root in node.root_ids {
let id = root.get();
let id = root.get().unwrap();
if id.0 != 0 {
self.reclaim(id);
}
@@ -784,41 +831,45 @@ impl<'b> VirtualDom {
/// Push all the real nodes on the stack
fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
let mut onstack = 0;
for (idx, _) in node.template.roots.iter().enumerate() {
match node.dynamic_root(idx) {
Some(Text(t)) => {
self.mutations.push(Mutation::PushRoot { id: t.id.get() });
onstack += 1;
}
Some(Placeholder(t)) => {
self.mutations.push(Mutation::PushRoot { id: t.get() });
onstack += 1;
}
Some(Fragment(nodes)) => {
for node in *nodes {
onstack += self.push_all_real_nodes(node);
node.template
.roots
.iter()
.enumerate()
.map(|(idx, _)| {
match node.dynamic_root(idx) {
Some(Text(t)) => {
self.mutations.push(Mutation::PushRoot {
id: t.id.get().unwrap(),
});
1
}
}
Some(Component(comp)) => {
let scope = comp.scope.get().unwrap();
onstack +=
Some(Placeholder(t)) => {
self.mutations.push(Mutation::PushRoot {
id: t.id.get().unwrap(),
});
1
}
Some(Fragment(nodes)) => nodes
.iter()
.map(|node| self.push_all_real_nodes(node))
.count(),
Some(Component(comp)) => {
let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node),
_ => todo!(),
}
}
None => {
self.mutations.push(Mutation::PushRoot {
id: node.root_ids[idx].get(),
});
onstack += 1;
}
};
}
onstack
}
None => {
self.mutations.push(Mutation::PushRoot {
id: node.root_ids[idx].get().unwrap(),
});
1
}
};
})
.count()
}
fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize {
@@ -839,10 +890,10 @@ impl<'b> VirtualDom {
fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
match node.dynamic_root(0) {
None => node.root_ids[0].get(),
Some(Text(t)) => t.id.get(),
None => node.root_ids[0].get().unwrap(),
Some(Text(t)) => t.id.get().unwrap(),
Some(Fragment(t)) => self.find_first_element(&t[0]),
Some(Placeholder(t)) => t.get(),
Some(Placeholder(t)) => t.id.get().unwrap(),
Some(Component(comp)) => {
let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
@@ -855,10 +906,10 @@ impl<'b> VirtualDom {
fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
match node.dynamic_root(node.template.roots.len() - 1) {
None => node.root_ids.last().unwrap().get(),
Some(Text(t)) => t.id.get(),
None => node.root_ids.last().unwrap().get().unwrap(),
Some(Text(t)) => t.id.get().unwrap(),
Some(Fragment(t)) => self.find_last_element(t.last().unwrap()),
Some(Placeholder(t)) => t.get(),
Some(Placeholder(t)) => t.id.get().unwrap(),
Some(Component(comp)) => {
let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
@@ -892,6 +943,21 @@ impl<'b> VirtualDom {
}
}
/// Are the templates the same?
///
/// We need to check for the obvious case, and the non-obvious case where the template as cloned
///
/// We use the pointer of the dynamic_node list in this case
fn templates_are_the_same<'b>(left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) -> bool {
std::ptr::eq(left_template, right_template)
|| std::ptr::eq(left_template.dynamic_nodes, right_template.dynamic_nodes)
}
fn templates_are_different(left_template: &VNode, right_template: &VNode) -> bool {
!std::ptr::eq(left_template.template.name, right_template.template.name)
&& left_template.template.name != right_template.template.name
}
fn matching_components<'a>(
left: &'a VNode<'a>,
right: &'a VNode<'a>,
+9 -7
View File
@@ -1,19 +1,21 @@
use std::hash::Hash;
use crate::ScopeId;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Eq, PartialOrd, Ord)]
pub struct DirtyScope {
pub height: u32,
pub id: ScopeId,
}
impl PartialOrd for DirtyScope {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.height.cmp(&other.height))
impl PartialEq for DirtyScope {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Ord for DirtyScope {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.height.cmp(&other.height)
impl Hash for DirtyScope {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
+9 -199
View File
@@ -11,8 +11,9 @@
//! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
//! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
use smallbox::{smallbox, space::S16, SmallBox};
use crate::{innerlude::VNode, ScopeState};
use std::mem;
/// A concrete type provider for closures that build [`VNode`] structures.
///
@@ -24,14 +25,7 @@ use std::mem;
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
/// ```
pub struct LazyNodes<'a, 'b> {
inner: StackNodeStorage<'a, 'b>,
}
type StackHeapSize = [usize; 16];
enum StackNodeStorage<'a, 'b> {
Stack(LazyStack),
Heap(Box<dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b>),
inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
}
impl<'a, 'b> LazyNodes<'a, 'b> {
@@ -44,114 +38,11 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
let mut slot = Some(val);
let val = move |fac: Option<&'a ScopeState>| {
fac.map(
slot.take()
.expect("LazyNodes closure to be called only once"),
)
};
// miri does not know how to work with mucking directly into bytes
// just use a heap allocated type when miri is running
if cfg!(miri) {
Self {
inner: StackNodeStorage::Heap(Box::new(val)),
}
} else {
unsafe { LazyNodes::new_inner(val) }
}
}
/// Create a new [`LazyNodes`] closure, but force it onto the heap.
pub fn new_boxed<F>(inner: F) -> Self
where
F: FnOnce(&'a ScopeState) -> VNode<'a> + 'b,
{
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
let mut slot = Some(inner);
Self {
inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| {
fac.map(
slot.take()
.expect("LazyNodes closure to be called only once"),
)
})),
}
}
unsafe fn new_inner<F>(val: F) -> Self
where
F: FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b,
{
let mut ptr: *const _ = &val as &dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>>;
assert_eq!(
ptr as *const u8, &val as *const _ as *const u8,
"MISUSE: Closure returned different pointer"
);
assert_eq!(
std::mem::size_of_val(&*ptr),
std::mem::size_of::<F>(),
"MISUSE: Closure returned a subset pointer"
);
let words = ptr_as_slice(&mut ptr);
assert!(
words[0] == &val as *const _ as usize,
"BUG: Pointer layout is not (data_ptr, info...)"
);
// - Ensure that Self is aligned same as data requires
assert!(
std::mem::align_of::<F>() <= std::mem::align_of::<Self>(),
"TODO: Enforce alignment >{} (requires {})",
std::mem::align_of::<Self>(),
std::mem::align_of::<F>()
);
let info = &words[1..];
let data = words[0] as *mut ();
let size = mem::size_of::<F>();
let stored_size = info.len() * mem::size_of::<usize>() + size;
let max_size = mem::size_of::<StackHeapSize>();
if stored_size > max_size {
Self {
inner: StackNodeStorage::Heap(Box::new(val)),
}
} else {
let mut buf: StackHeapSize = StackHeapSize::default();
assert!(info.len() + round_to_words(size) <= buf.as_ref().len());
// Place pointer information at the end of the region
// - Allows the data to be at the start for alignment purposes
{
let info_ofs = buf.as_ref().len() - info.len();
let info_dst = &mut buf.as_mut()[info_ofs..];
for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) {
*d = *v;
}
}
let src_ptr = data as *const u8;
let dataptr = buf.as_mut_ptr().cast::<u8>();
for i in 0..size {
*dataptr.add(i) = *src_ptr.add(i);
}
std::mem::forget(val);
Self {
inner: StackNodeStorage::Stack(LazyStack {
_align: [],
buf,
dropped: false,
}),
}
inner: smallbox!(move |f| {
let val = slot.take().expect("cannot call LazyNodes twice");
val(f)
}),
}
}
@@ -163,88 +54,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
/// let node = f.call(cac);
/// ```
#[must_use]
pub fn call(self, f: &'a ScopeState) -> VNode<'a> {
match self.inner {
StackNodeStorage::Heap(mut lazy) => {
lazy(Some(f)).expect("Closure should not be called twice")
}
StackNodeStorage::Stack(mut stack) => stack.call(f),
}
pub fn call(mut self, f: &'a ScopeState) -> VNode<'a> {
(self.inner)(f)
}
}
struct LazyStack {
_align: [u64; 0],
buf: StackHeapSize,
dropped: bool,
}
impl LazyStack {
fn call<'a>(&mut self, f: &'a ScopeState) -> VNode<'a> {
let LazyStack { buf, .. } = self;
let data = buf.as_ref();
let info_size =
mem::size_of::<*mut dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>>>()
/ mem::size_of::<usize>()
- 1;
let info_ofs = data.len() - info_size;
let g: *mut dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> =
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
self.dropped = true;
let clos = unsafe { &mut *g };
clos(Some(f)).unwrap()
}
}
impl Drop for LazyStack {
fn drop(&mut self) {
if !self.dropped {
let LazyStack { buf, .. } = self;
let data = buf.as_ref();
let info_size =
mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>>>()
/ mem::size_of::<usize>()
- 1;
let info_ofs = data.len() - info_size;
let g: *mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>> =
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
self.dropped = true;
let clos = unsafe { &mut *g };
clos(None);
}
}
}
/// Obtain mutable access to a pointer's words
fn ptr_as_slice<T>(ptr: &mut T) -> &mut [usize] {
assert!(mem::size_of::<T>() % mem::size_of::<usize>() == 0);
let words = mem::size_of::<T>() / mem::size_of::<usize>();
// SAFE: Points to valid memory (a raw pointer)
unsafe { core::slice::from_raw_parts_mut(ptr as *mut _ as *mut usize, words) }
}
/// Re-construct a fat pointer
unsafe fn make_fat_ptr<T: ?Sized>(data_ptr: usize, meta_vals: &[usize]) -> *mut T {
let mut rv = mem::MaybeUninit::<*mut T>::uninit();
{
let s = ptr_as_slice(&mut rv);
s[0] = data_ptr;
s[1..].copy_from_slice(meta_vals);
}
let rv = rv.assume_init();
assert_eq!(rv as *const (), data_ptr as *const ());
rv
}
fn round_to_words(len: usize) -> usize {
(len + mem::size_of::<usize>() - 1) / mem::size_of::<usize>()
}
+3 -3
View File
@@ -81,9 +81,9 @@ pub use crate::innerlude::{
/// This includes types like [`Scope`], [`Element`], and [`Component`].
pub mod prelude {
pub use crate::innerlude::{
fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope,
ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode,
VirtualDom,
fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties,
Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode,
VNode, VirtualDom,
};
}
+1 -4
View File
@@ -1,4 +1,4 @@
use fxhash::FxHashSet;
use rustc_hash::FxHashSet;
use crate::{arena::ElementId, ScopeId, Template};
@@ -230,9 +230,6 @@ pub enum Mutation<'a> {
/// The name of the event to listen for.
name: &'a str,
/// The ID of the node to attach the listener to.
scope: ScopeId,
/// The ID of the node to attach the listener to.
id: ElementId,
},
+29 -4
View File
@@ -46,7 +46,7 @@ pub struct VNode<'a> {
/// The IDs for the roots of this template - to be used when moving the template around and removing it from
/// the actual Dom
pub root_ids: &'a [Cell<ElementId>],
pub root_ids: &'a [Cell<Option<ElementId>>],
/// The dynamic parts of the template
pub dynamic_nodes: &'a [DynamicNode<'a>],
@@ -145,6 +145,18 @@ where
Ok(&*Box::leak(deserialized))
}
impl<'a> Template<'a> {
/// Is this template worth caching at all, since it's completely runtime?
///
/// There's no point in saving templates that are completely dynamic, since they'll be recreated every time anyway.
pub fn is_completely_dynamic(&self) -> bool {
use TemplateNode::*;
self.roots
.iter()
.all(|root| matches!(root, Dynamic { .. } | DynamicText { .. }))
}
}
/// A statically known node in a layout.
///
/// This can be created at compile time, saving the VirtualDom time when diffing the tree
@@ -224,7 +236,7 @@ pub enum DynamicNode<'a> {
/// Used by suspense when a node isn't ready and by fragments that don't render anything
///
/// In code, this is just an ElementId whose initial value is set to 0 upon creation
Placeholder(Cell<ElementId>),
Placeholder(VPlaceholder),
/// A list of VNodes.
///
@@ -259,7 +271,7 @@ pub struct VComponent<'a> {
/// It is possible that components get folded at comppile time, so these shouldn't be really used as a key
pub render_fn: *const (),
pub(crate) props: Cell<Option<Box<dyn AnyProps<'a> + 'a>>>,
pub(crate) props: RefCell<Option<Box<dyn AnyProps<'a> + 'a>>>,
}
impl<'a> std::fmt::Debug for VComponent<'a> {
@@ -279,7 +291,14 @@ pub struct VText<'a> {
pub value: &'a str,
/// The ID of this node in the real DOM
pub id: Cell<ElementId>,
pub id: Cell<Option<ElementId>>,
}
/// A placeholder node, used by suspense and fragments
#[derive(Debug, Default)]
pub struct VPlaceholder {
/// The ID of this node in the real DOM
pub id: Cell<Option<ElementId>>,
}
/// An attribute of the TemplateNode, created at compile time
@@ -464,6 +483,12 @@ impl<'a> IntoDynNode<'a> for VNode<'a> {
}
}
impl<'a> IntoDynNode<'a> for DynamicNode<'a> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
self
}
}
// An element that's an error is currently lost into the ether
impl<'a> IntoDynNode<'a> for Element<'a> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
+13 -6
View File
@@ -8,6 +8,7 @@ use crate::{
scopes::{ScopeId, ScopeState},
virtual_dom::VirtualDom,
};
use bumpalo::Bump;
use futures_util::FutureExt;
use std::{
mem,
@@ -34,8 +35,8 @@ impl VirtualDom {
props: Some(props),
name,
placeholder: Default::default(),
node_arena_1: BumpFrame::new(50),
node_arena_2: BumpFrame::new(50),
node_arena_1: BumpFrame::new(0),
node_arena_2: BumpFrame::new(0),
spawned_tasks: Default::default(),
render_cnt: Default::default(),
hook_arena: Default::default(),
@@ -62,7 +63,13 @@ impl VirtualDom {
let mut new_nodes = unsafe {
let scope = self.scopes[scope_id.0].as_mut();
scope.previous_frame_mut().bump.reset();
// if this frame hasn't been intialized yet, we can guess the size of the next frame to be more efficient
if scope.previous_frame().bump.allocated_bytes() == 0 {
scope.previous_frame_mut().bump =
Bump::with_capacity(scope.current_frame().bump.allocated_bytes());
} else {
scope.previous_frame_mut().bump.reset();
}
// Make sure to reset the hook counter so we give out hooks in the right order
scope.hook_idx.set(0);
@@ -128,8 +135,8 @@ impl VirtualDom {
let frame = scope.previous_frame();
// set the new head of the bump frame
let alloced = &*frame.bump.alloc(new_nodes);
frame.node.set(alloced);
let allocated = &*frame.bump.alloc(new_nodes);
frame.node.set(allocated);
// And move the render generation forward by one
scope.render_cnt.set(scope.render_cnt.get() + 1);
@@ -141,6 +148,6 @@ impl VirtualDom {
});
// rebind the lifetime now that its stored internally
unsafe { mem::transmute(alloced) }
unsafe { mem::transmute(allocated) }
}
}
+7 -5
View File
@@ -10,10 +10,10 @@ use crate::{
Attribute, AttributeValue, Element, Event, Properties, TaskId,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use rustc_hash::{FxHashMap, FxHashSet};
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
collections::{HashMap, HashSet},
fmt::Arguments,
future::Future,
rc::Rc,
@@ -82,10 +82,10 @@ pub struct ScopeState {
pub(crate) hook_list: RefCell<Vec<*mut dyn Any>>,
pub(crate) hook_idx: Cell<usize>,
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Box<dyn Any>>>,
pub(crate) shared_contexts: RefCell<FxHashMap<TypeId, Box<dyn Any>>>,
pub(crate) tasks: Rc<Scheduler>,
pub(crate) spawned_tasks: HashSet<TaskId>,
pub(crate) spawned_tasks: FxHashSet<TaskId>,
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
pub(crate) placeholder: Cell<Option<ElementId>>,
@@ -237,7 +237,9 @@ impl<'src> ScopeState {
/// This method should be used when you want to schedule an update for a component
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
let chan = self.tasks.sender.clone();
Arc::new(move |id| drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
Arc::new(move |id| {
chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
})
}
/// Mark this scope as dirty, and schedule a render for it.
@@ -448,7 +450,7 @@ impl<'src> ScopeState {
name: fn_name,
render_fn: component as *const (),
static_props: P::IS_STATIC,
props: Cell::new(Some(extended)),
props: RefCell::new(Some(extended)),
scope: Cell::new(None),
})
}
+5 -9
View File
@@ -14,15 +14,9 @@ use crate::{
AttributeValue, Element, Event, Scope, SuspenseContext,
};
use futures_util::{pin_mut, StreamExt};
use rustc_hash::FxHashMap;
use slab::Slab;
use std::{
any::Any,
borrow::BorrowMut,
cell::Cell,
collections::{BTreeSet, HashMap},
future::Future,
rc::Rc,
};
use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
/// A virtual node system that progresses user events and diffs UI trees.
///
@@ -148,7 +142,7 @@ use std::{
/// }
/// ```
pub struct VirtualDom {
pub(crate) templates: HashMap<TemplateId, Template<'static>>,
pub(crate) templates: FxHashMap<TemplateId, Template<'static>>,
pub(crate) scopes: Slab<Box<ScopeState>>,
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
pub(crate) scheduler: Rc<Scheduler>,
@@ -542,6 +536,8 @@ impl VirtualDom {
pub async fn render_with_deadline(&mut self, deadline: impl Future<Output = ()>) -> Mutations {
pin_mut!(deadline);
self.process_events();
loop {
// first, unload any complete suspense trees
for finished_fiber in self.finished_fibers.drain(..) {
+1 -1
View File
@@ -34,7 +34,7 @@ fn dual_stream() {
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None },
NewEventListener { name: "click", scope: ScopeId(0), id: ElementId(1) },
NewEventListener { name: "click", id: ElementId(1) },
HydrateText { path: &[0, 0], value: "123", id: ElementId(2) },
AppendChildren { id: ElementId(0), m: 1 }
],
+2 -2
View File
@@ -62,8 +62,8 @@ fn contexts_drop() {
_ = dom.render_immediate();
}
#[test]
fn tasks_drop() {
#[tokio::test]
async fn tasks_drop() {
fn app(cx: Scope) -> Element {
cx.spawn(async {
tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
+1 -3
View File
@@ -12,9 +12,7 @@ fn test_memory_leak() {
fn app(cx: Scope) -> Element {
let val = cx.generation();
cx.spawn(async {
tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
});
cx.spawn(async {});
if val == 2 || val == 4 {
return cx.render(rsx!(()));
+1 -1
View File
@@ -20,7 +20,7 @@ serde = "1.0.136"
serde_json = "1.0.79"
thiserror = "1.0.30"
log = "0.4.14"
wry = { version = "0.22.0" }
wry = { version = "0.23.4" }
futures-channel = "0.3.21"
tokio = { version = "1.16.1", features = [
"sync",
+11 -12
View File
@@ -1,10 +1,12 @@
use crate::desktop_context::{DesktopContext, UserWindowEvent};
use crate::events::{decode_event, EventMessage};
use crate::events::IpcMessage;
use dioxus_core::*;
use dioxus_html::{Html, HtmlEvent};
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use futures_util::StreamExt;
#[cfg(target_os = "ios")]
use objc::runtime::Object;
use serde_json::Value;
use std::{
collections::HashMap,
sync::Arc,
@@ -19,11 +21,12 @@ use wry::{
pub(super) struct DesktopController {
pub(super) webviews: HashMap<WindowId, WebView>,
pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender<Value>,
pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
pub(super) quit_app_on_close: bool,
pub(super) is_ready: Arc<AtomicBool>,
pub(super) proxy: EventLoopProxy<UserWindowEvent>,
pub(super) event_tx: UnboundedSender<serde_json::Value>,
pub(super) event_tx: UnboundedSender<Html>,
#[cfg(debug_assertions)]
pub(super) templates_tx: UnboundedSender<Template<'static>>,
@@ -40,12 +43,13 @@ impl DesktopController {
proxy: EventLoopProxy<UserWindowEvent>,
) -> Self {
let edit_queue = Arc::new(Mutex::new(Vec::new()));
let (event_tx, mut event_rx) = unbounded();
let (event_tx, mut event_rx) = unbounded::<HtmlEvent>();
let (templates_tx, mut templates_rx) = unbounded();
let proxy2 = proxy.clone();
let pending_edits = edit_queue.clone();
let desktop_context_proxy = proxy.clone();
let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::<Value>();
std::thread::spawn(move || {
// We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
@@ -57,7 +61,7 @@ impl DesktopController {
runtime.block_on(async move {
let mut dom = VirtualDom::new_with_props(root, props)
.with_root_context(DesktopContext::new(desktop_context_proxy));
.with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever));
{
let edits = dom.rebuild();
let mut queue = edit_queue.lock().unwrap();
@@ -80,14 +84,8 @@ impl DesktopController {
dom.replace_template(template);
}
_ = dom.wait_for_work() => {}
Some(json_value) = event_rx.next() => {
if let Ok(value) = serde_json::from_value::<EventMessage>(json_value) {
let name = value.event.clone();
let el_id = ElementId(value.mounted_dom_id);
if let Some(evt) = decode_event(value) {
dom.handle_event(&name, evt, el_id, dioxus_html::events::event_bubbles(&name));
}
}
Some(value) = event_rx.next() => {
dom.handle_event(&value.name, value.data.into_any(), value.element, dioxus_html::events::event_bubbles(&value.name));
}
}
@@ -103,6 +101,7 @@ impl DesktopController {
Self {
pending_edits,
eval_sender,
webviews: HashMap::new(),
is_ready: Arc::new(AtomicBool::new(false)),
quit_app_on_close: true,
+59 -19
View File
@@ -2,6 +2,11 @@ use std::rc::Rc;
use crate::controller::DesktopController;
use dioxus_core::ScopeState;
use serde::de::Error;
use serde_json::Value;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
use wry::application::event_loop::ControlFlow;
use wry::application::event_loop::EventLoopProxy;
#[cfg(target_os = "ios")]
@@ -19,16 +24,6 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
.unwrap()
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String)> {
let desktop = use_window(cx);
&*cx.use_hook(|| {
let desktop = desktop.clone();
Rc::new(move |script| desktop.eval(script))
} as Rc<dyn Fn(String)>)
}
/// An imperative interface to the current window.
///
/// To get a handle to the current window, use the [`use_window`] hook.
@@ -45,11 +40,18 @@ pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String)> {
pub struct DesktopContext {
/// The wry/tao proxy to the current window
pub proxy: ProxyType,
pub(super) eval_reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Value>>>,
}
impl DesktopContext {
pub(crate) fn new(proxy: ProxyType) -> Self {
Self { proxy }
pub(crate) fn new(
proxy: ProxyType,
eval_reciever: tokio::sync::mpsc::UnboundedReceiver<Value>,
) -> Self {
Self {
proxy,
eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)),
}
}
/// trigger the drag-window event
@@ -236,6 +238,18 @@ impl DesktopController {
Resizable(state) => window.set_resizable(state),
AlwaysOnTop(state) => window.set_always_on_top(state),
Eval(code) => {
let script = format!(
r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{
{}
}})()}}));"#,
code
);
if let Err(e) = webview.evaluate_script(&script) {
// we can't panic this error.
log::warn!("Eval script error: {e}");
}
}
CursorVisible(state) => window.set_cursor_visible(state),
CursorGrab(state) => {
let _ = window.set_cursor_grab(state);
@@ -259,13 +273,6 @@ impl DesktopController {
log::warn!("Devtools are disabled in release builds");
}
Eval(code) => {
if let Err(e) = webview.evaluate_script(code.as_str()) {
// we can't panic this error.
log::warn!("Eval script error: {e}");
}
}
#[cfg(target_os = "ios")]
PushView(view) => unsafe {
use objc::runtime::Object;
@@ -295,6 +302,39 @@ impl DesktopController {
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult {
let desktop = use_window(cx).clone();
cx.use_hook(|| {
move |script| {
desktop.eval(script);
let recv = desktop.eval_reciever.clone();
EvalResult { reciever: recv }
}
})
}
/// A future that resolves to the result of a JavaScript evaluation.
pub struct EvalResult {
reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>>,
}
impl IntoFuture for EvalResult {
type Output = Result<serde_json::Value, serde_json::Error>;
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut reciever = self.reciever.lock().await;
match reciever.recv().await {
Some(result) => Ok(result),
None => Err(serde_json::Error::custom("No result returned")),
}
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
}
}
#[cfg(target_os = "ios")]
fn is_main_thread() -> bool {
use objc::runtime::{Class, BOOL, NO};
+1 -59
View File
@@ -6,7 +6,7 @@ use serde_json::from_value;
use std::any::Any;
use std::rc::Rc;
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, Debug)]
pub(crate) struct IpcMessage {
method: String,
params: serde_json::Value,
@@ -31,61 +31,3 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
}
}
}
macro_rules! match_data {
(
$m:ident;
$name:ident;
$(
$tip:ty => $($mname:literal)|* ;
)*
) => {
match $name {
$( $($mname)|* => {
let val: $tip = from_value::<$tip>($m).ok()?;
Rc::new(val) as Rc<dyn Any>
})*
_ => return None,
}
};
}
#[derive(Deserialize)]
pub struct EventMessage {
pub contents: serde_json::Value,
pub event: String,
pub mounted_dom_id: usize,
}
pub fn decode_event(value: EventMessage) -> Option<Rc<dyn Any>> {
let val = value.contents;
let name = value.event.as_str();
type DragData = MouseData;
let evt = match_data! { val; name;
MouseData => "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup";
ClipboardData => "copy" | "cut" | "paste";
CompositionData => "compositionend" | "compositionstart" | "compositionupdate";
KeyboardData => "keydown" | "keypress" | "keyup";
FocusData => "blur" | "focus" | "focusin" | "focusout";
FormData => "change" | "input" | "invalid" | "reset" | "submit";
DragData => "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop";
PointerData => "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture";
SelectionData => "selectstart" | "selectionchange" | "select";
TouchData => "touchcancel" | "touchend" | "touchmove" | "touchstart";
ScrollData => "scroll";
WheelData => "wheel";
MediaData => "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied"
| "encrypted" | "ended" | "interruptbegin" | "interruptend" | "loadeddata"
| "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress"
| "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate"
| "volumechange" | "waiting" | "error" | "load" | "loadend" | "timeout";
AnimationData => "animationstart" | "animationend" | "animationiteration";
TransitionData => "transitionend";
ToggleData => "toggle";
// ImageData => "load" | "error";
// OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
};
Some(evt)
}
+22 -13
View File
@@ -17,7 +17,8 @@ use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use desktop_context::UserWindowEvent;
pub use desktop_context::{use_eval, use_window, DesktopContext};
pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult};
use dioxus_html::HtmlEvent;
use futures_channel::mpsc::UnboundedSender;
pub use wry;
pub use wry::application as tao;
@@ -145,6 +146,7 @@ impl DesktopController {
event_loop,
self.is_ready.clone(),
self.proxy.clone(),
self.eval_sender.clone(),
self.event_tx.clone(),
);
@@ -157,7 +159,8 @@ fn build_webview(
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
is_ready: Arc<AtomicBool>,
proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
event_tx: UnboundedSender<serde_json::Value>,
eval_sender: tokio::sync::mpsc::UnboundedSender<serde_json::Value>,
event_tx: UnboundedSender<HtmlEvent>,
) -> wry::webview::WebView {
let builder = cfg.window.clone();
let window = builder.build(event_loop).unwrap();
@@ -186,8 +189,14 @@ fn build_webview(
.with_ipc_handler(move |_window: &Window, payload: String| {
parse_ipc_message(&payload)
.map(|message| match message.method() {
"eval_result" => {
let result = message.params();
eval_sender.send(result).unwrap();
}
"user_event" => {
_ = event_tx.unbounded_send(message.params());
if let Ok(evt) = serde_json::from_value(message.params()) {
_ = event_tx.unbounded_send(evt);
}
}
"initialize" => {
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
@@ -234,16 +243,16 @@ fn build_webview(
// in release mode, we don't want to show the dev tool or reload menus
webview = webview.with_initialization_script(
r#"
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
window.event.returnValue = false;
});
}
"#,
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
window.event.returnValue = false;
});
}
"#,
)
} else {
// in debug, we are okay with the reload menu showing and dev tool
+7
View File
@@ -0,0 +1,7 @@
export function main() {
let root = window.document.getElementById("main");
if (root != null) {
window.interpreter = new Interpreter(root);
window.ipc.postMessage(serializeIpcMessage("initialize"));
}
}
+12 -5
View File
@@ -52,7 +52,14 @@ pub(super) fn desktop_handler(
} else if trimmed == "index.js" {
Response::builder()
.header("Content-Type", "text/javascript")
.body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec())
.body(
format!(
"{} {}",
dioxus_interpreter_js::INTERPRETER_JS,
include_str!("./main.js")
)
.into_bytes(),
)
.map_err(From::from)
} else {
let asset_root = asset_root
@@ -104,10 +111,10 @@ fn get_asset_root() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
let bundle = core_foundation::bundle::CFBundle::main_bundle();
let bundle_path = dbg!(bundle.path()?);
let resources_path = dbg!(bundle.resources_path()?);
let absolute_resources_root = dbg!(bundle_path.join(resources_path));
let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?);
let bundle_path = bundle.path()?;
let resources_path = bundle.resources_path()?;
let absolute_resources_root = bundle_path.join(resources_path);
let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
return Some(canonical_resources_root);
}
-2
View File
@@ -1,5 +1,3 @@
#![doc = include_str!("../../../notes/README.md")]
pub use dioxus_core as core;
#[cfg(feature = "hooks")]
+5 -2
View File
@@ -40,7 +40,10 @@ features = [
"ClipboardEvent",
]
[dev-dependencies]
serde_json = "*"
[features]
default = []
serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde"]
default = ["serialize"]
serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"]
wasm-bind = ["web-sys", "wasm-bindgen"]
+2 -3
View File
@@ -10,12 +10,11 @@ pub type DragEvent = Event<DragData>;
/// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location
/// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an
/// application-specific way.
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct DragData {
/// Inherit mouse data
pub mouse: MouseData,
/// And then add the rest of the drag data
pub data: Box<dyn Any>,
}
impl_event! {
+2 -3
View File
@@ -11,9 +11,8 @@ pub struct FormData {
pub value: String,
pub values: HashMap<String, String>,
#[cfg_attr(feature = "serialize", serde(skip))]
pub files: Option<Arc<dyn FileEngine>>,
// #[cfg_attr(feature = "serialize", serde(skip))]
// pub files: Option<Arc<dyn FileEngine>>,
}
impl Debug for FormData {
+6
View File
@@ -22,6 +22,12 @@ mod render_template;
#[cfg(feature = "wasm-bind")]
mod web_sys_bind;
#[cfg(feature = "serialize")]
mod transit;
#[cfg(feature = "serialize")]
pub use transit::*;
pub use elements::*;
pub use events::*;
pub use global_attributes::*;
+152
View File
@@ -0,0 +1,152 @@
use std::{any::Any, rc::Rc};
use crate::events::*;
use dioxus_core::ElementId;
use serde::{Deserialize, Serialize};
// macro_rules! match_data {
// (
// $m:ident;
// $name:ident;
// $(
// $tip:ty => $($mname:literal)|* ;
// )*
// ) => {
// match $name {
// $( $($mname)|* => {
// let val: $tip = from_value::<$tip>($m).ok()?;
// Rc::new(val) as Rc<dyn Any>
// })*
// _ => return None,
// }
// };
// }
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct HtmlEvent {
pub element: ElementId,
pub name: String,
pub data: EventData,
pub bubbles: bool,
}
impl HtmlEvent {
pub fn bubbles(&self) -> bool {
event_bubbles(&self.name)
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum EventData {
Mouse(MouseData),
Clipboard(ClipboardData),
Composition(CompositionData),
Keyboard(KeyboardData),
Focus(FocusData),
Form(FormData),
Drag(DragData),
Pointer(PointerData),
Selection(SelectionData),
Touch(TouchData),
Scroll(ScrollData),
Wheel(WheelData),
Media(MediaData),
Animation(AnimationData),
Transition(TransitionData),
Toggle(ToggleData),
}
impl EventData {
pub fn into_any(self) -> Rc<dyn Any> {
match self {
EventData::Mouse(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Clipboard(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Composition(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Keyboard(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Focus(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Form(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Drag(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Pointer(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Selection(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Touch(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Scroll(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Wheel(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Media(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Transition(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
}
}
}
#[test]
fn test_back_and_forth() {
let data = HtmlEvent {
element: ElementId(0),
data: EventData::Mouse(MouseData::default()),
name: "click".to_string(),
bubbles: true,
};
println!("{}", serde_json::to_string_pretty(&data).unwrap());
let o = r#"
{
"element": 0,
"name": "click",
"bubbles": true,
"data": {
"alt_key": false,
"button": 0,
"buttons": 0,
"client_x": 0,
"client_y": 0,
"ctrl_key": false,
"meta_key": false,
"offset_x": 0,
"offset_y": 0,
"page_x": 0,
"page_y": 0,
"screen_x": 0,
"screen_y": 0,
"shift_key": false
}
}
"#;
let p: HtmlEvent = serde_json::from_str(o).unwrap();
}
// pub fn decode_event(value: ) -> Option<Rc<dyn Any>> {
// let val = value.data;
// let name = value.event.as_str();
// type DragData = MouseData;
// let evt = match_data! { val; name;
// MouseData => "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup";
// ClipboardData => "copy" | "cut" | "paste";
// CompositionData => "compositionend" | "compositionstart" | "compositionupdate";
// KeyboardData => "keydown" | "keypress" | "keyup";
// FocusData => "blur" | "focus" | "focusin" | "focusout";
// FormData => "change" | "input" | "invalid" | "reset" | "submit";
// DragData => "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop";
// PointerData => "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture";
// SelectionData => "selectstart" | "selectionchange" | "select";
// TouchData => "touchcancel" | "touchend" | "touchmove" | "touchstart";
// ScrollData => "scroll";
// WheelData => "wheel";
// MediaData => "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied"
// | "encrypted" | "ended" | "interruptbegin" | "interruptend" | "loadeddata"
// | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress"
// | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate"
// | "volumechange" | "waiting" | "error" | "load" | "loadend" | "timeout";
// AnimationData => "animationstart" | "animationend" | "animationiteration";
// TransitionData => "transitionend";
// ToggleData => "toggle";
// // ImageData => "load" | "error";
// // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
// };
// Some(evt)
// }
+3 -1
View File
@@ -17,8 +17,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
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"] }
sledgehammer_bindgen = { version = "0.1.3", optional = true }
sledgehammer_utils = { version = "0.1.0", optional = true }
[features]
default = []
web = ["wasm-bindgen", "js-sys", "web-sys"]
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
+3 -3
View File
@@ -17,9 +17,6 @@ extern "C" {
#[wasm_bindgen(method)]
pub fn MountToRoot(this: &Interpreter);
#[wasm_bindgen(method)]
pub fn AppendChildren(this: &Interpreter, m: u32, id: u32);
#[wasm_bindgen(method)]
pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
@@ -76,4 +73,7 @@ extern "C" {
#[wasm_bindgen(method)]
pub fn PushRoot(this: &Interpreter, id: u32);
#[wasm_bindgen(method)]
pub fn AppendChildren(this: &Interpreter, id: u32, m: u32);
}
+13 -16
View File
@@ -1,11 +1,3 @@
export function main() {
let root = window.document.getElementById("main");
if (root != null) {
window.interpreter = new Interpreter(root);
window.ipc.postMessage(serializeIpcMessage("initialize"));
}
}
class ListenerMap {
constructor(root) {
// bubbling events can listen at the root element
@@ -60,7 +52,7 @@ class ListenerMap {
}
}
export class Interpreter {
class Interpreter {
constructor(root) {
this.root = root;
this.listeners = new ListenerMap(root);
@@ -90,7 +82,8 @@ export class Interpreter {
this.stack.pop();
}
AppendChildren(many) {
let root = this.stack[this.stack.length - (1 + many)];
// let root = this.nodes[id];
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]);
@@ -352,7 +345,10 @@ export class Interpreter {
break;
case "NewEventListener":
// this handler is only provided on desktop implementations since this
// method is not used by the web implementation
// method is not used by the web implementationa
let bubbles = event_bubbles(edit.name);
let handler = (event) => {
let target = event.target;
if (target != null) {
@@ -434,20 +430,21 @@ export class Interpreter {
}
window.ipc.postMessage(
serializeIpcMessage("user_event", {
event: edit.name,
mounted_dom_id: parseInt(realId),
contents: contents,
name: edit.name,
element: parseInt(realId),
data: contents,
bubbles: bubbles,
})
);
}
};
this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler);
this.NewEventListener(edit.name, edit.id, bubbles, handler);
break;
}
}
}
export function serialize_event(event) {
function serialize_event(event) {
switch (event.type) {
case "copy":
case "cut":
+5
View File
@@ -1,5 +1,10 @@
pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
#[cfg(feature = "sledgehammer")]
mod sledgehammer_bindings;
#[cfg(feature = "sledgehammer")]
pub use sledgehammer_bindings::*;
#[cfg(feature = "web")]
mod bindings;
@@ -0,0 +1,231 @@
use js_sys::Function;
use sledgehammer_bindgen::bindgen;
use web_sys::Node;
#[bindgen]
mod js {
const JS: &str = r#"
class ListenerMap {
constructor(root) {
// bubbling events can listen at the root element
this.global = {};
// non bubbling events listen at the element the listener was created at
this.local = {};
this.root = null;
this.handler = null;
}
create(event_name, element, bubbles) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = {};
this.global[event_name].active = 1;
this.root.addEventListener(event_name, this.handler);
} else {
this.global[event_name].active++;
}
}
else {
const id = element.getAttribute("data-dioxus-id");
if (!this.local[id]) {
this.local[id] = {};
}
element.addEventListener(event_name, this.handler);
}
}
remove(element, event_name, bubbles) {
if (bubbles) {
this.global[event_name].active--;
if (this.global[event_name].active === 0) {
this.root.removeEventListener(event_name, this.global[event_name].callback);
delete this.global[event_name];
}
}
else {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (this.local[id].length === 0) {
delete this.local[id];
}
element.removeEventListener(event_name, this.handler);
}
}
removeAllNonBubbling(element) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id];
}
}
function SetAttributeInner(node, field, value, ns) {
const name = field;
if (ns === "style") {
// ????? why do we need to do this
if (node.style === undefined) {
node.style = {};
}
node.style[name] = value;
} else if (ns !== null && ns !== undefined && ns !== "") {
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);
}
}
}
}
function LoadChild(ptr, len) {
// iterate through each number and get that child
node = stack[stack.length - 1];
ptr_end = ptr + len;
for (; ptr < ptr_end; ptr++) {
end = m.getUint8(ptr);
for (node = node.firstChild; end > 0; end--) {
node = node.nextSibling;
}
}
return node;
}
const listeners = new ListenerMap();
let nodes = [];
let stack = [];
const templates = {};
let node, els, end, ptr_end, k;
export function save_template(nodes, tmpl_id) {
templates[tmpl_id] = nodes;
}
export function set_node(id, node) {
nodes[id] = node;
}
export function initilize(root, handler) {
listeners.handler = handler;
nodes = [root];
stack = [root];
listeners.root = root;
}
function AppendChildren(id, many){
root = nodes[id];
els = stack.splice(stack.length-many);
for (k = 0; k < many; k++) {
root.appendChild(els[k]);
}
}
"#;
extern "C" {
#[wasm_bindgen]
pub fn save_template(nodes: Vec<Node>, tmpl_id: u32);
#[wasm_bindgen]
pub fn set_node(id: u32, node: Node);
#[wasm_bindgen]
pub fn initilize(root: Node, handler: &Function);
}
fn mount_to_root() {
"{AppendChildren(root, stack.length-1);}"
}
fn push_root(root: u32) {
"{stack.push(nodes[$root$]);}"
}
fn append_children(id: u32, many: u32) {
"{AppendChildren($id$, $many$);}"
}
fn pop_root() {
"{stack.pop();}"
}
fn replace_with(id: u32, n: u32) {
"{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
}
fn insert_after(id: u32, n: u32) {
"{nodes[$id$].after(...stack.splice(stack.length-$n$));}"
}
fn insert_before(id: u32, n: u32) {
"{nodes[$id$].before(...stack.splice(stack.length-$n$));}"
}
fn remove(id: u32) {
"{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}"
}
fn create_raw_text(text: &str) {
"{stack.push(document.createTextNode($text$));}"
}
fn create_text_node(text: &str, id: u32) {
"{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}"
}
fn create_placeholder(id: u32) {
"{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}"
}
fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
r#"node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); listeners.create($event_name$, node, $bubbles$);"#
}
fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
"{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}"
}
fn set_text(id: u32, text: &str) {
"{nodes[$id$].textContent = $text$;}"
}
fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
"{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}"
}
fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
r#"{name = $field$;
node = this.nodes[$id$];
if (ns == "style") {
node.style.removeProperty(name);
} else if (ns !== null && ns !== undefined && ns !== "") {
node.removeAttributeNS(ns, name);
} else if (name === "value") {
node.value = "";
} else if (name === "checked") {
node.checked = false;
} else if (name === "selected") {
node.selected = false;
} else if (name === "dangerous_inner_html") {
node.innerHTML = "";
} else {
node.removeAttribute(name);
}}"#
}
fn assign_id(ptr: u32, len: u8, id: u32) {
"{nodes[$id$] = LoadChild($ptr$, $len$);}"
}
fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) {
r#"{
node = LoadChild($ptr$, $len$);
if (node.nodeType == Node.TEXT_NODE) {
node.textContent = value;
} else {
let text = document.createTextNode(value);
node.replaceWith(text);
node = text;
}
nodes[$id$] = node;
}"#
}
fn replace_placeholder(ptr: u32, len: u8, n: u32) {
"{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}"
}
fn load_template(tmpl_id: u32, index: u32, id: u32) {
"{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}"
}
}
+17 -3
View File
@@ -10,7 +10,6 @@ description = "Build server-side apps with Dioxus"
license = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
@@ -28,7 +27,7 @@ tokio-util = { version = "0.7.0", features = ["full"] }
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" }
dioxus-interpreter-js = { path = "../interpreter" }
# warp
warp = { version = "0.3", optional = true }
@@ -39,6 +38,9 @@ tower = { version = "0.4.12", optional = true }
# salvo
salvo = { version = "0.32.0", optional = true, features = ["ws"] }
thiserror = "1.0.37"
uuid = { version = "1.2.2", features = ["v4"] }
anyhow = "1.0.66"
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
@@ -49,4 +51,16 @@ salvo = { version = "0.32.0", features = ["affix", "ws"] }
tower = "0.4.12"
[features]
default = []
default = ["salvo"]
[[example]]
name = "axum"
required-features = ["axum"]
[[example]]
name = "salvo"
required-features = ["salvo"]
[[example]]
name = "warp"
required-features = ["warp"]
+35 -14
View File
@@ -1,32 +1,53 @@
#[cfg(not(feature = "axum"))]
fn main() {}
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
use dioxus::prelude::*;
fn app(cx: Scope) -> Element {
let mut num = use_state(cx, || 0);
cx.render(rsx! {
div {
"hello axum! {num}"
button { onclick: move |_| num += 1, "Increment" }
}
})
}
#[cfg(feature = "axum")]
#[tokio::main]
async fn main() {
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
use dioxus_core::{Element, LazyNodes, Scope};
pretty_env_logger::init();
fn app(cx: Scope) -> Element {
cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
}
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
let view = dioxus_liveview::new(addr);
let body = view.body("<title>Dioxus Liveview</title>");
let view = dioxus_liveview::LiveViewPool::new();
let app = Router::new()
.route("/", get(move || async { Html(body) }))
.route(
"/app",
"/",
get(move || async move {
Html(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with Warp</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
))
}),
)
.route(
"/ws",
get(move |ws: WebSocketUpgrade| async move {
ws.on_upgrade(move |socket| async move {
view.upgrade_axum(socket, app).await;
_ = view.launch(dioxus_liveview::axum_socket(socket), app).await;
})
}),
);
println!("Listening on http://{}", addr);
axum::Server::bind(&addr.to_string().parse().unwrap())
.serve(app.into_make_service())
.await
+58 -42
View File
@@ -1,55 +1,71 @@
#[cfg(not(feature = "salvo"))]
fn main() {}
use dioxus::prelude::*;
use dioxus_liveview::LiveViewPool;
use salvo::extra::affix;
use salvo::extra::ws::WsHandler;
use salvo::prelude::*;
use std::net::SocketAddr;
use std::sync::Arc;
fn app(cx: Scope) -> Element {
let mut num = use_state(cx, || 0);
cx.render(rsx! {
div {
"hello salvo! {num}"
button { onclick: move |_| num += 1, "Increment" }
}
})
}
#[cfg(feature = "salvo")]
#[tokio::main]
async fn main() {
use std::sync::Arc;
use dioxus_core::{Element, LazyNodes, Scope};
use dioxus_liveview as liveview;
use dioxus_liveview::Liveview;
use salvo::extra::affix;
use salvo::extra::ws::WsHandler;
use salvo::prelude::*;
fn app(cx: Scope) -> Element {
cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
}
pretty_env_logger::init();
let addr = ([127, 0, 0, 1], 3030);
let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
let view = LiveViewPool::new();
// todo: compactify this routing under one liveview::app method
let view = liveview::new(addr);
let router = Router::new()
.hoop(affix::inject(Arc::new(view)))
.get(index)
.push(Router::with_path("app").get(connect));
.push(Router::with_path("ws").get(connect));
println!("Listening on http://{}", addr);
Server::new(TcpListener::bind(addr)).serve(router).await;
}
#[handler]
fn index(depot: &mut Depot, res: &mut Response) {
let view = depot.obtain::<Arc<Liveview>>().unwrap();
let body = view.body("<title>Dioxus LiveView</title>");
res.render(Text::Html(body));
}
#[handler]
fn index(_depot: &mut Depot, res: &mut Response) {
let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
res.render(Text::Html(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with Warp</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
)));
}
#[handler]
async fn connect(
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
) -> Result<(), StatusError> {
let view = depot.obtain::<Arc<Liveview>>().unwrap().clone();
let fut = WsHandler::new().handle(req, res)?;
let fut = async move {
if let Some(ws) = fut.await {
view.upgrade_salvo(ws, app).await;
}
};
tokio::task::spawn(fut);
Ok(())
}
#[handler]
async fn connect(
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
) -> Result<(), StatusError> {
let view = depot.obtain::<Arc<LiveViewPool>>().unwrap().clone();
let fut = WsHandler::new().handle(req, res)?;
tokio::spawn(async move {
if let Some(ws) = fut.await {
_ = view.launch(dioxus_liveview::salvo_socket(ws), app).await;
}
});
Ok(())
}
+48 -27
View File
@@ -1,35 +1,56 @@
#[cfg(not(feature = "warp"))]
fn main() {}
use dioxus::prelude::*;
use dioxus_liveview::adapters::warp_adapter::warp_socket;
use dioxus_liveview::LiveViewPool;
use std::net::SocketAddr;
use warp::ws::Ws;
use warp::Filter;
fn app(cx: Scope) -> Element {
let mut num = use_state(cx, || 0);
cx.render(rsx! {
div {
"hello warp! {num}"
button {
onclick: move |_| num += 1,
"Increment"
}
}
})
}
#[cfg(feature = "warp")]
#[tokio::main]
async fn main() {
use dioxus_core::{Element, LazyNodes, Scope};
use dioxus_liveview as liveview;
use warp::ws::Ws;
use warp::Filter;
fn app(cx: Scope) -> Element {
cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
}
pretty_env_logger::init();
let addr = ([127, 0, 0, 1], 3030);
let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
// todo: compactify this routing under one liveview::app method
let view = liveview::new(addr);
let body = view.body("<title>Dioxus LiveView</title>");
let index = warp::path::end().map(move || {
warp::reply::html(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with Warp</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws/"))
))
});
let routes = warp::path::end()
.map(move || warp::reply::html(body.clone()))
.or(warp::path("app")
.and(warp::ws())
.and(warp::any().map(move || view.clone()))
.map(|ws: Ws, view: liveview::Liveview| {
ws.on_upgrade(|socket| async move {
view.upgrade_warp(socket, app).await;
})
}));
warp::serve(routes).run(addr).await;
let pool = LiveViewPool::new();
let ws = warp::path("ws")
.and(warp::ws())
.and(warp::any().map(move || pool.clone()))
.map(move |ws: Ws, pool: LiveViewPool| {
ws.on_upgrade(|ws| async move {
let _ = pool.launch(warp_socket(ws), app).await;
})
});
println!("Listening on http://{}", addr);
warp::serve(index.or(ws)).run(addr).await;
}
+18 -89
View File
@@ -1,94 +1,23 @@
use crate::events;
use crate::{LiveViewError, LiveViewSocket};
use axum::extract::ws::{Message, WebSocket};
use dioxus_core::prelude::*;
use futures_util::{
future::{select, Either},
pin_mut, SinkExt, StreamExt,
};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use tokio_util::task::LocalPoolHandle;
use futures_util::{SinkExt, StreamExt};
impl crate::Liveview {
pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) {
connect(ws, self.pool.clone(), app, ()).await;
}
pub async fn upgrade_axum_with_props<T>(
&self,
ws: WebSocket,
app: fn(Scope<T>) -> Element,
props: T,
) where
T: Send + Sync + 'static,
{
connect(ws, self.pool.clone(), app, props).await;
}
/// Convert a warp websocket into a LiveViewSocket
///
/// This is required to launch a LiveView app using the warp web framework
pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket {
ws.map(transform_rx)
.with(transform_tx)
.sink_map_err(|_| LiveViewError::SendingFailed)
}
pub async fn connect<T>(
socket: WebSocket,
pool: LocalPoolHandle,
app: fn(Scope<T>) -> Element,
props: T,
) where
T: Send + Sync + 'static,
{
let (mut user_ws_tx, mut user_ws_rx) = socket.split();
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (edits_tx, edits_rx) = mpsc::unbounded_channel();
let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
let mut event_rx = UnboundedReceiverStream::new(event_rx);
let vdom_fut = pool.clone().spawn_pinned(move || async move {
let mut vdom = VirtualDom::new_with_props(app, props);
let edits = vdom.rebuild();
let serialized = serde_json::to_string(&edits.edits).unwrap();
edits_tx.send(serialized).unwrap();
loop {
let new_event = {
let vdom_fut = vdom.wait_for_work();
pin_mut!(vdom_fut);
match select(event_rx.next(), vdom_fut).await {
Either::Left((l, _)) => l,
Either::Right((_, _)) => None,
}
};
if let Some(new_event) = new_event {
vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event));
} else {
let mutations = vdom.work_with_deadline(|| false);
for mutation in mutations {
let edits = serde_json::to_string(&mutation.edits).unwrap();
edits_tx.send(edits).unwrap();
}
}
}
});
loop {
match select(user_ws_rx.next(), edits_rx.next()).await {
Either::Left((l, _)) => {
if let Some(Ok(msg)) = l {
if let Ok(Some(msg)) = msg.to_text().map(events::parse_ipc_message) {
let user_event = events::trigger_from_serialized(msg.params);
event_tx.send(user_event).unwrap();
} else {
break;
}
} else {
break;
}
}
Either::Right((edits, _)) => {
if let Some(edits) = edits {
// send the edits to the client
if user_ws_tx.send(Message::Text(edits)).await.is_err() {
break;
}
} else {
break;
}
}
}
}
vdom_fut.abort();
fn transform_rx(message: Result<Message, axum::Error>) -> Result<String, LiveViewError> {
message
.map_err(|_| LiveViewError::SendingFailed)?
.into_text()
.map_err(|_| LiveViewError::SendingFailed)
}
async fn transform_tx(message: String) -> Result<Message, axum::Error> {
Ok(Message::Text(message))
}
+17 -102
View File
@@ -1,110 +1,25 @@
use crate::events;
use dioxus_core::prelude::*;
use futures_util::{pin_mut, SinkExt, StreamExt};
use futures_util::{SinkExt, StreamExt};
use salvo::extra::ws::{Message, WebSocket};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use tokio_util::task::LocalPoolHandle;
impl crate::Liveview {
pub async fn upgrade_salvo(&self, ws: salvo::extra::ws::WebSocket, app: fn(Scope) -> Element) {
connect(ws, self.pool.clone(), app, ()).await;
}
pub async fn upgrade_salvo_with_props<T>(
&self,
ws: salvo::extra::ws::WebSocket,
app: fn(Scope<T>) -> Element,
props: T,
) where
T: Send + Sync + 'static,
{
connect(ws, self.pool.clone(), app, props).await;
}
use crate::{LiveViewError, LiveViewSocket};
/// Convert a salvo websocket into a LiveViewSocket
///
/// This is required to launch a LiveView app using the warp web framework
pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket {
ws.map(transform_rx)
.with(transform_tx)
.sink_map_err(|_| LiveViewError::SendingFailed)
}
pub async fn connect<T>(
ws: WebSocket,
pool: LocalPoolHandle,
app: fn(Scope<T>) -> Element,
props: T,
) where
T: Send + Sync + 'static,
{
// Use a counter to assign a new unique ID for this user.
fn transform_rx(message: Result<Message, salvo::Error>) -> Result<String, LiveViewError> {
let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?;
// Split the socket into a sender and receive of messages.
let (mut user_ws_tx, mut user_ws_rx) = ws.split();
let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (edits_tx, edits_rx) = mpsc::unbounded_channel();
Ok(msg)
}
let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
let mut event_rx = UnboundedReceiverStream::new(event_rx);
let vdom_fut = pool.spawn_pinned(move || async move {
let mut vdom = VirtualDom::new_with_props(app, props);
let edits = vdom.rebuild();
let serialized = serde_json::to_string(&edits.edits).unwrap();
edits_tx.send(serialized).unwrap();
loop {
use futures_util::future::{select, Either};
let new_event = {
let vdom_fut = vdom.wait_for_work();
pin_mut!(vdom_fut);
match select(event_rx.next(), vdom_fut).await {
Either::Left((l, _)) => l,
Either::Right((_, _)) => None,
}
};
if let Some(new_event) = new_event {
vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event));
} else {
let mutations = vdom.work_with_deadline(|| false);
for mutation in mutations {
let edits = serde_json::to_string(&mutation.edits).unwrap();
edits_tx.send(edits).unwrap();
}
}
}
});
loop {
use futures_util::future::{select, Either};
match select(user_ws_rx.next(), edits_rx.next()).await {
Either::Left((l, _)) => {
if let Some(Ok(msg)) = l {
if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) {
if msg.method == "user_event" {
let user_event = events::trigger_from_serialized(msg.params);
event_tx.send(user_event).unwrap();
}
} else {
break;
}
} else {
break;
}
}
Either::Right((edits, _)) => {
if let Some(edits) = edits {
// send the edits to the client
if user_ws_tx.send(Message::text(edits)).await.is_err() {
break;
}
} else {
break;
}
}
}
}
vdom_fut.abort();
async fn transform_tx(message: String) -> Result<Message, salvo::Error> {
Ok(Message::text(message))
}
+20 -102
View File
@@ -1,110 +1,28 @@
use crate::events;
use dioxus_core::prelude::*;
use futures_util::{pin_mut, SinkExt, StreamExt};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use tokio_util::task::LocalPoolHandle;
use crate::{LiveViewError, LiveViewSocket};
use futures_util::{SinkExt, StreamExt};
use warp::ws::{Message, WebSocket};
impl crate::Liveview {
pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) {
connect(ws, self.pool.clone(), app, ()).await;
}
pub async fn upgrade_warp_with_props<T>(
&self,
ws: warp::ws::WebSocket,
app: fn(Scope<T>) -> Element,
props: T,
) where
T: Send + Sync + 'static,
{
connect(ws, self.pool.clone(), app, props).await;
}
/// Convert a warp websocket into a LiveViewSocket
///
/// This is required to launch a LiveView app using the warp web framework
pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket {
ws.map(transform_rx)
.with(transform_tx)
.sink_map_err(|_| LiveViewError::SendingFailed)
}
pub async fn connect<T>(
ws: WebSocket,
pool: LocalPoolHandle,
app: fn(Scope<T>) -> Element,
props: T,
) where
T: Send + Sync + 'static,
{
// Use a counter to assign a new unique ID for this user.
fn transform_rx(message: Result<Message, warp::Error>) -> Result<String, LiveViewError> {
// destructure the message into the buffer we got from warp
let msg = message
.map_err(|_| LiveViewError::SendingFailed)?
.into_bytes();
// Split the socket into a sender and receive of messages.
let (mut user_ws_tx, mut user_ws_rx) = ws.split();
// transform it back into a string, saving us the allocation
let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (edits_tx, edits_rx) = mpsc::unbounded_channel();
Ok(msg)
}
let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
let mut event_rx = UnboundedReceiverStream::new(event_rx);
let vdom_fut = pool.spawn_pinned(move || async move {
let mut vdom = VirtualDom::new_with_props(app, props);
let edits = vdom.rebuild();
let serialized = serde_json::to_string(&edits.edits).unwrap();
edits_tx.send(serialized).unwrap();
loop {
use futures_util::future::{select, Either};
let new_event = {
let vdom_fut = vdom.wait_for_work();
pin_mut!(vdom_fut);
match select(event_rx.next(), vdom_fut).await {
Either::Left((l, _)) => l,
Either::Right((_, _)) => None,
}
};
if let Some(new_event) = new_event {
vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event));
} else {
let mutations = vdom.work_with_deadline(|| false);
for mutation in mutations {
let edits = serde_json::to_string(&mutation.edits).unwrap();
edits_tx.send(edits).unwrap();
}
}
}
});
loop {
use futures_util::future::{select, Either};
match select(user_ws_rx.next(), edits_rx.next()).await {
Either::Left((l, _)) => {
if let Some(Ok(msg)) = l {
if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) {
if msg.method == "user_event" {
let user_event = events::trigger_from_serialized(msg.params);
event_tx.send(user_event).unwrap();
}
} else {
break;
}
} else {
break;
}
}
Either::Right((edits, _)) => {
if let Some(edits) = edits {
// send the edits to the client
if user_ws_tx.send(Message::text(edits)).await.is_err() {
break;
}
} else {
break;
}
}
}
}
vdom_fut.abort();
async fn transform_tx(message: String) -> Result<Message, warp::Error> {
Ok(Message::text(message))
}
-207
View File
@@ -1,207 +0,0 @@
#![allow(dead_code)]
//! Convert a serialized event to an event trigger
use std::any::Any;
use std::sync::Arc;
use dioxus_core::ElementId;
// use dioxus_html::event_bubbles;
use dioxus_html::events::*;
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct IpcMessage {
pub method: String,
pub params: serde_json::Value,
}
pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
match serde_json::from_str(payload) {
Ok(message) => Some(message),
Err(_) => None,
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct ImEvent {
event: String,
mounted_dom_id: ElementId,
contents: serde_json::Value,
}
pub fn trigger_from_serialized(_val: serde_json::Value) {
todo!()
// let ImEvent {
// event,
// mounted_dom_id,
// contents,
// } = serde_json::from_value(val).unwrap();
// let mounted_dom_id = Some(mounted_dom_id);
// let name = event_name_from_type(&event);
// let event = make_synthetic_event(&event, contents);
// UserEvent {
// name,
// scope_id: None,
// element: mounted_dom_id,
// data: event,
// bubbles: event_bubbles(name),
// }
}
fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any> {
match name {
"copy" | "cut" | "paste" => {
//
Arc::new(ClipboardData {})
}
"compositionend" | "compositionstart" | "compositionupdate" => {
Arc::new(serde_json::from_value::<CompositionData>(val).unwrap())
}
"keydown" | "keypress" | "keyup" => {
let evt = serde_json::from_value::<KeyboardData>(val).unwrap();
Arc::new(evt)
}
"focus" | "blur" | "focusout" | "focusin" => {
//
Arc::new(FocusData {})
}
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
// don't have a good solution with the serialized event problem
"change" | "input" | "invalid" | "reset" | "submit" => {
Arc::new(serde_json::from_value::<FormData>(val).unwrap())
}
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
Arc::new(serde_json::from_value::<MouseData>(val).unwrap())
}
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
Arc::new(serde_json::from_value::<PointerData>(val).unwrap())
}
"select" => {
//
Arc::new(serde_json::from_value::<SelectionData>(val).unwrap())
}
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
Arc::new(serde_json::from_value::<TouchData>(val).unwrap())
}
"scroll" => Arc::new(()),
"wheel" => Arc::new(serde_json::from_value::<WheelData>(val).unwrap()),
"animationstart" | "animationend" | "animationiteration" => {
Arc::new(serde_json::from_value::<AnimationData>(val).unwrap())
}
"transitionend" => Arc::new(serde_json::from_value::<TransitionData>(val).unwrap()),
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => {
//
Arc::new(MediaData {})
}
"toggle" => Arc::new(ToggleData {}),
_ => Arc::new(()),
}
}
fn event_name_from_type(typ: &str) -> &'static str {
match typ {
"copy" => "copy",
"cut" => "cut",
"paste" => "paste",
"compositionend" => "compositionend",
"compositionstart" => "compositionstart",
"compositionupdate" => "compositionupdate",
"keydown" => "keydown",
"keypress" => "keypress",
"keyup" => "keyup",
"focus" => "focus",
"focusout" => "focusout",
"focusin" => "focusin",
"blur" => "blur",
"change" => "change",
"input" => "input",
"invalid" => "invalid",
"reset" => "reset",
"submit" => "submit",
"click" => "click",
"contextmenu" => "contextmenu",
"doubleclick" => "doubleclick",
"drag" => "drag",
"dragend" => "dragend",
"dragenter" => "dragenter",
"dragexit" => "dragexit",
"dragleave" => "dragleave",
"dragover" => "dragover",
"dragstart" => "dragstart",
"drop" => "drop",
"mousedown" => "mousedown",
"mouseenter" => "mouseenter",
"mouseleave" => "mouseleave",
"mousemove" => "mousemove",
"mouseout" => "mouseout",
"mouseover" => "mouseover",
"mouseup" => "mouseup",
"pointerdown" => "pointerdown",
"pointermove" => "pointermove",
"pointerup" => "pointerup",
"pointercancel" => "pointercancel",
"gotpointercapture" => "gotpointercapture",
"lostpointercapture" => "lostpointercapture",
"pointerenter" => "pointerenter",
"pointerleave" => "pointerleave",
"pointerover" => "pointerover",
"pointerout" => "pointerout",
"select" => "select",
"touchcancel" => "touchcancel",
"touchend" => "touchend",
"touchmove" => "touchmove",
"touchstart" => "touchstart",
"scroll" => "scroll",
"wheel" => "wheel",
"animationstart" => "animationstart",
"animationend" => "animationend",
"animationiteration" => "animationiteration",
"transitionend" => "transitionend",
"abort" => "abort",
"canplay" => "canplay",
"canplaythrough" => "canplaythrough",
"durationchange" => "durationchange",
"emptied" => "emptied",
"encrypted" => "encrypted",
"ended" => "ended",
"error" => "error",
"loadeddata" => "loadeddata",
"loadedmetadata" => "loadedmetadata",
"loadstart" => "loadstart",
"pause" => "pause",
"play" => "play",
"playing" => "playing",
"progress" => "progress",
"ratechange" => "ratechange",
"seeked" => "seeked",
"seeking" => "seeking",
"stalled" => "stalled",
"suspend" => "suspend",
"timeupdate" => "timeupdate",
"volumechange" => "volumechange",
"waiting" => "waiting",
"toggle" => "toggle",
_ => {
panic!("unsupported event type")
}
}
}
-15
View File
@@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Dioxus app</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="main"></div>
<script>
import("./index.js").then(function (module) {
module.main();
});
</script>
</body>
</html>
-973
View File
@@ -1,973 +0,0 @@
function main() {
let root = window.document.getElementById("main");
if (root != null) {
// create a new ipc
window.ipc = new IPC(root);
window.ipc.send(serializeIpcMessage("initialize"));
}
}
class IPC {
constructor(root) {
// connect to the websocket
window.interpreter = new Interpreter(root);
this.ws = new WebSocket(WS_ADDR);
this.ws.onopen = () => {
console.log("Connected to the websocket");
};
this.ws.onerror = (err) => {
console.error("Error: ", err);
};
this.ws.onmessage = (event) => {
let edits = JSON.parse(event.data);
window.interpreter.handleEdits(edits);
};
}
send(msg) {
this.ws.send(msg);
}
}
class ListenerMap {
constructor(root) {
// bubbling events can listen at the root element
this.global = {};
// non bubbling events listen at the element the listener was created at
this.local = {};
this.root = root;
}
create(event_name, element, handler, bubbles) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = {};
this.global[event_name].active = 1;
this.global[event_name].callback = handler;
this.root.addEventListener(event_name, handler);
} else {
this.global[event_name].active++;
}
}
else {
const id = element.getAttribute("data-dioxus-id");
if (!this.local[id]) {
this.local[id] = {};
}
this.local[id][event_name] = handler;
element.addEventListener(event_name, handler);
}
}
remove(element, event_name, bubbles) {
if (bubbles) {
this.global[event_name].active--;
if (this.global[event_name].active === 0) {
this.root.removeEventListener(event_name, this.global[event_name].callback);
delete this.global[event_name];
}
}
else {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (this.local[id].length === 0) {
delete this.local[id];
}
element.removeEventListener(event_name, handler);
}
}
}
class Interpreter {
constructor(root) {
this.root = root;
this.lastNode = root;
this.listeners = new ListenerMap(root);
this.handlers = {};
this.nodes = [root];
this.parents = [];
}
checkAppendParent() {
if (this.parents.length > 0) {
const lastParent = this.parents[this.parents.length - 1];
lastParent[1]--;
if (lastParent[1] === 0) {
this.parents.pop();
}
lastParent[0].appendChild(this.lastNode);
}
}
AppendChildren(root, children) {
let node;
if (root == null) {
node = this.lastNode;
} else {
node = this.nodes[root];
}
for (let i = 0; i < children.length; i++) {
node.appendChild(this.nodes[children[i]]);
}
}
ReplaceWith(root, nodes) {
let node;
if (root == null) {
node = this.lastNode;
} else {
node = this.nodes[root];
}
let els = [];
for (let i = 0; i < nodes.length; i++) {
els.push(this.nodes[nodes[i]])
}
node.replaceWith(...els);
}
InsertAfter(root, nodes) {
let node;
if (root == null) {
node = this.lastNode;
} else {
node = this.nodes[root];
}
let els = [];
for (let i = 0; i < nodes.length; i++) {
els.push(this.nodes[nodes[i]])
}
node.after(...els);
}
InsertBefore(root, nodes) {
let node;
if (root == null) {
node = this.lastNode;
} else {
node = this.nodes[root];
}
let els = [];
for (let i = 0; i < nodes.length; i++) {
els.push(this.nodes[nodes[i]])
}
node.before(...els);
}
Remove(root) {
let node;
if (root == null) {
node = this.lastNode;
} else {
node = this.nodes[root];
}
if (node !== undefined) {
node.remove();
}
}
CreateTextNode(text, root) {
this.lastNode = document.createTextNode(text);
this.checkAppendParent();
if (root != null) {
this.nodes[root] = this.lastNode;
}
}
CreateElement(tag, root, children) {
this.lastNode = document.createElement(tag);
this.checkAppendParent();
if (root != null) {
this.nodes[root] = this.lastNode;
}
if (children > 0) {
this.parents.push([this.lastNode, children]);
}
}
CreateElementNs(tag, root, ns, children) {
this.lastNode = document.createElementNS(ns, tag);
this.checkAppendParent();
if (root != null) {
this.nodes[root] = this.lastNode;
}
if (children > 0) {
this.parents.push([this.lastNode, children]);
}
}
CreatePlaceholder(root) {
this.lastNode = document.createElement("pre");
this.lastNode.hidden = true;
this.checkAppendParent();
if (root != null) {
this.nodes[root] = this.lastNode;
}
}
NewEventListener(event_name, root, handler, bubbles) {
let node;
if (root == null) {
node = this.lastNode;
} else {
node = this.nodes[root];
}
node.setAttribute("data-dioxus-id", `${root}`);
this.listeners.create(event_name, node, handler, bubbles);
}
RemoveEventListener(root, event_name, bubbles) {
let node;
if (root == null) {
node = this.lastNode;
} else {
node = this.nodes[root];
}
node.removeAttribute(`data-dioxus-id`);
this.listeners.remove(node, event_name, bubbles);
}
SetText(root, text) {
let node;
if (root == null) {
node = this.lastNode;
} else {
node = this.nodes[root];
}
node.data = text;
}
SetAttribute(root, field, value, ns) {
const name = field;
let node;
if (root == null) {
node = this.lastNode;
} else {
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, field, ns) {
const name = field;
let node;
if (root == null) {
node = this.lastNode;
} else {
node = this.nodes[root];
}
if (ns == "style") {
node.style.removeProperty(name);
} else if (ns !== null || ns !== undefined) {
node.removeAttributeNS(ns, name);
} else if (name === "value") {
node.value = "";
} else if (name === "checked") {
node.checked = false;
} else if (name === "selected") {
node.selected = false;
} else if (name === "dangerous_inner_html") {
node.innerHTML = "";
} else {
node.removeAttribute(name);
}
}
CloneNode(old, new_id) {
let node;
if (old === null) {
node = this.lastNode;
} else {
node = this.nodes[old];
}
this.nodes[new_id] = node.cloneNode(true);
}
CloneNodeChildren(old, new_ids) {
let node;
if (old === null) {
node = this.lastNode;
} else {
node = this.nodes[old];
}
const old_node = node.cloneNode(true);
let i = 0;
for (let node = old_node.firstChild; i < new_ids.length; node = node.nextSibling) {
this.nodes[new_ids[i++]] = node;
}
}
FirstChild() {
this.lastNode = this.lastNode.firstChild;
}
NextSibling() {
this.lastNode = this.lastNode.nextSibling;
}
ParentNode() {
this.lastNode = this.lastNode.parentNode;
}
StoreWithId(id) {
this.nodes[id] = this.lastNode;
}
SetLastNode(root) {
this.lastNode = this.nodes[root];
}
handleEdits(edits) {
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.root, edit.children);
break;
case "ReplaceWith":
this.ReplaceWith(edit.root, edit.nodes);
break;
case "InsertAfter":
this.InsertAfter(edit.root, edit.nodes);
break;
case "InsertBefore":
this.InsertBefore(edit.root, edit.nodes);
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, edit.children);
break;
case "CreateElementNs":
this.CreateElementNs(edit.tag, edit.root, edit.ns, edit.children);
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") {
// todo call prevent default if it's the right type of event
if (shouldPreventDefault !== `onclick`) {
if (target.tagName === "A") {
event.preventDefault();
const href = target.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.ipc.postMessage(
serializeIpcMessage("browser_open", { href })
);
}
}
}
// also prevent buttons from submitting
if (target.tagName === "BUTTON" && event.type == "submit") {
event.preventDefault();
}
}
// 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" &&
(event.type === "submit" || event.type === "input")
) {
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") {
// @ts-ignore
contents.values[name] = element.checked ? "true" : "false";
} else if (element.getAttribute("type") === "radio") {
if (element.checked) {
contents.values[name] = element.value;
}
} else {
// @ts-ignore
contents.values[name] =
element.value ?? element.textContent;
}
}
}
}
if (realId === null) {
return;
}
realId = parseInt(realId);
window.ipc.send(
serializeIpcMessage("user_event", {
event: edit.event_name,
mounted_dom_id: realId,
contents: contents,
})
);
}
};
this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name));
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, edit.ns);
break;
case "CloneNode":
this.CloneNode(edit.id, edit.new_id);
break;
case "CloneNodeChildren":
this.CloneNodeChildren(edit.id, edit.new_ids);
break;
case "FirstChild":
this.FirstChild();
break;
case "NextSibling":
this.NextSibling();
break;
case "ParentNode":
this.ParentNode();
break;
case "StoreWithId":
this.StoreWithId(BigInt(edit.id));
break;
case "SetLastNode":
this.SetLastNode(BigInt(edit.id));
break;
}
}
}
function serialize_event(event) {
switch (event.type) {
case "copy":
case "cut":
case "past": {
return {};
}
case "compositionend":
case "compositionstart":
case "compositionupdate": {
let { data } = event;
return {
data,
};
}
case "keydown":
case "keypress":
case "keyup": {
let {
charCode,
key,
altKey,
ctrlKey,
metaKey,
keyCode,
shiftKey,
location,
repeat,
which,
code,
} = 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,
code,
};
}
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,
values: {},
};
}
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 "dblclick":
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,
offsetX,
offsetY,
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,
offset_x: offsetX,
offset_y: offsetY,
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 {};
}
}
}
function serializeIpcMessage(method, params = {}) {
return JSON.stringify({ method, params });
}
const bool_attrs = {
allowfullscreen: true,
allowpaymentrequest: true,
async: true,
autofocus: true,
autoplay: true,
checked: true,
controls: true,
default: true,
defer: true,
disabled: true,
formnovalidate: true,
hidden: true,
ismap: true,
itemscope: true,
loop: true,
multiple: true,
muted: true,
nomodule: true,
novalidate: true,
open: true,
playsinline: true,
readonly: true,
required: true,
reversed: true,
selected: true,
truespeed: true,
};
function is_element_node(node) {
return node.nodeType == 1;
}
function event_bubbles(event) {
switch (event) {
case "copy":
return true;
case "cut":
return true;
case "paste":
return true;
case "compositionend":
return true;
case "compositionstart":
return true;
case "compositionupdate":
return true;
case "keydown":
return true;
case "keypress":
return true;
case "keyup":
return true;
case "focus":
return false;
case "focusout":
return true;
case "focusin":
return true;
case "blur":
return false;
case "change":
return true;
case "input":
return true;
case "invalid":
return true;
case "reset":
return true;
case "submit":
return true;
case "click":
return true;
case "contextmenu":
return true;
case "doubleclick":
return true;
case "dblclick":
return true;
case "drag":
return true;
case "dragend":
return true;
case "dragenter":
return false;
case "dragexit":
return false;
case "dragleave":
return true;
case "dragover":
return true;
case "dragstart":
return true;
case "drop":
return true;
case "mousedown":
return true;
case "mouseenter":
return false;
case "mouseleave":
return false;
case "mousemove":
return true;
case "mouseout":
return true;
case "scroll":
return false;
case "mouseover":
return true;
case "mouseup":
return true;
case "pointerdown":
return true;
case "pointermove":
return true;
case "pointerup":
return true;
case "pointercancel":
return true;
case "gotpointercapture":
return true;
case "lostpointercapture":
return true;
case "pointerenter":
return false;
case "pointerleave":
return false;
case "pointerover":
return true;
case "pointerout":
return true;
case "select":
return true;
case "touchcancel":
return true;
case "touchend":
return true;
case "touchmove":
return true;
case "touchstart":
return true;
case "wheel":
return true;
case "abort":
return false;
case "canplay":
return false;
case "canplaythrough":
return false;
case "durationchange":
return false;
case "emptied":
return false;
case "encrypted":
return true;
case "ended":
return false;
case "error":
return false;
case "loadeddata":
return false;
case "loadedmetadata":
return false;
case "loadstart":
return false;
case "pause":
return false;
case "play":
return false;
case "playing":
return false;
case "progress":
return false;
case "ratechange":
return false;
case "seeked":
return false;
case "seeking":
return false;
case "stalled":
return false;
case "suspend":
return false;
case "timeupdate":
return false;
case "volumechange":
return false;
case "waiting":
return false;
case "animationstart":
return true;
case "animationend":
return true;
case "animationiteration":
return true;
case "transitionend":
return true;
case "toggle":
return true;
}
}
+38 -39
View File
@@ -1,56 +1,55 @@
#![allow(dead_code)]
pub(crate) mod events;
pub mod adapters {
#[cfg(feature = "warp")]
pub mod warp_adapter;
#[cfg(feature = "warp")]
pub use warp_adapter::*;
#[cfg(feature = "axum")]
pub mod axum_adapter;
#[cfg(feature = "axum")]
pub use axum_adapter::*;
#[cfg(feature = "salvo")]
pub mod salvo_adapter;
#[cfg(feature = "salvo")]
pub use salvo_adapter::*;
}
use std::net::SocketAddr;
pub use adapters::*;
use tokio_util::task::LocalPoolHandle;
pub mod pool;
use futures_util::{SinkExt, StreamExt};
pub use pool::*;
#[derive(Clone)]
pub struct Liveview {
pool: LocalPoolHandle,
addr: String,
pub trait WebsocketTx: SinkExt<String, Error = LiveViewError> {}
impl<T> WebsocketTx for T where T: SinkExt<String, Error = LiveViewError> {}
pub trait WebsocketRx: StreamExt<Item = Result<String, LiveViewError>> {}
impl<T> WebsocketRx for T where T: StreamExt<Item = Result<String, LiveViewError>> {}
#[derive(Debug, thiserror::Error)]
pub enum LiveViewError {
#[error("warp error")]
SendingFailed,
}
impl Liveview {
pub fn body(&self, header: &str) -> String {
format!(
r#"
<!DOCTYPE html>
<html>
<head>
{header}
</head>
<body>
<div id="main"></div>
<script>
var WS_ADDR = "ws://{addr}/app";
{interpreter}
main();
</script>
</body>
</html>"#,
addr = self.addr,
interpreter = include_str!("../src/interpreter.js")
)
}
}
use dioxus_interpreter_js::INTERPRETER_JS;
static MAIN_JS: &str = include_str!("./main.js");
pub fn new(addr: impl Into<SocketAddr>) -> Liveview {
let addr: SocketAddr = addr.into();
Liveview {
pool: LocalPoolHandle::new(16),
addr: addr.to_string(),
}
/// This script that gets injected into your app connects this page to the websocket endpoint
///
/// Once the endpoint is connected, it will send the initial state of the app, and then start
/// processing user events and returning edits to the liveview instance
pub fn interpreter_glue(url: &str) -> String {
format!(
r#"
<script>
var WS_ADDR = "{url}";
{INTERPRETER_JS}
{MAIN_JS}
main();
</script>
"#
)
}
+36
View File
@@ -0,0 +1,36 @@
function main() {
let root = window.document.getElementById("main");
if (root != null) {
// create a new ipc
window.ipc = new IPC(root);
window.ipc.postMessage(serializeIpcMessage("initialize"));
}
}
class IPC {
constructor(root) {
// connect to the websocket
window.interpreter = new Interpreter(root);
this.ws = new WebSocket(WS_ADDR);
this.ws.onopen = () => {
console.log("Connected to the websocket");
};
this.ws.onerror = (err) => {
console.error("Error: ", err);
};
this.ws.onmessage = (event) => {
console.log("Received message: ", event.data);
let edits = JSON.parse(event.data);
window.interpreter.handleEdits(edits);
};
}
postMessage(msg) {
this.ws.send(msg);
}
}
+123
View File
@@ -0,0 +1,123 @@
use crate::LiveViewError;
use dioxus_core::prelude::*;
use dioxus_html::HtmlEvent;
use futures_util::{pin_mut, SinkExt, StreamExt};
use std::time::Duration;
use tokio_util::task::LocalPoolHandle;
#[derive(Clone)]
pub struct LiveViewPool {
pub(crate) pool: LocalPoolHandle,
}
impl Default for LiveViewPool {
fn default() -> Self {
Self::new()
}
}
impl LiveViewPool {
pub fn new() -> Self {
LiveViewPool {
pool: LocalPoolHandle::new(16),
}
}
pub async fn launch(
&self,
ws: impl LiveViewSocket,
app: fn(Scope<()>) -> Element,
) -> Result<(), LiveViewError> {
self.launch_with_props(ws, app, ()).await
}
pub async fn launch_with_props<T: Send + 'static>(
&self,
ws: impl LiveViewSocket,
app: fn(Scope<T>) -> Element,
props: T,
) -> Result<(), LiveViewError> {
match self.pool.spawn_pinned(move || run(app, props, ws)).await {
Ok(Ok(_)) => Ok(()),
Ok(Err(e)) => Err(e),
Err(_) => Err(LiveViewError::SendingFailed),
}
}
}
/// A LiveViewSocket is a Sink and Stream of Strings that Dioxus uses to communicate with the client
pub trait LiveViewSocket:
SinkExt<String, Error = LiveViewError>
+ StreamExt<Item = Result<String, LiveViewError>>
+ Send
+ 'static
{
}
impl<S> LiveViewSocket for S where
S: SinkExt<String, Error = LiveViewError>
+ StreamExt<Item = Result<String, LiveViewError>>
+ Send
+ 'static
{
}
/// The primary event loop for the VirtualDom waiting for user input
///
/// This function makes it easy to integrate Dioxus LiveView with any socket-based framework.
///
/// As long as your framework can provide a Sink and Stream of Strings, you can use this function.
///
/// You might need to transform the error types of the web backend into the LiveView error type.
pub async fn run<T>(
app: Component<T>,
props: T,
ws: impl LiveViewSocket,
) -> Result<(), LiveViewError>
where
T: Send + 'static,
{
let mut vdom = VirtualDom::new_with_props(app, props);
// todo: use an efficient binary packed format for this
let edits = serde_json::to_string(&vdom.rebuild()).unwrap();
// pin the futures so we can use select!
pin_mut!(ws);
// send the initial render to the client
ws.send(edits).await?;
// desktop uses this wrapper struct thing around the actual event itself
// this is sorta driven by tao/wry
#[derive(serde::Deserialize)]
struct IpcMessage {
params: HtmlEvent,
}
loop {
tokio::select! {
// poll any futures or suspense
_ = vdom.wait_for_work() => {}
evt = ws.next() => {
match evt {
Some(Ok(evt)) => {
if let Ok(IpcMessage { params }) = serde_json::from_str::<IpcMessage>(&evt) {
vdom.handle_event(&params.name, params.data.into_any(), params.element, params.bubbles);
}
}
// log this I guess? when would we get an error here?
Some(Err(_e)) => {},
None => return Ok(()),
}
}
}
let edits = vdom
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
.await;
ws.send(serde_json::to_string(&edits).unwrap()).await?;
}
}
+1 -2
View File
@@ -306,8 +306,7 @@ impl Member {
+ field.ty.to_token_stream().to_string().as_str())
.as_str(),
Span::call_site(),
)
.into(),
),
ident: field.ident.as_ref()?.clone(),
})
}
+11 -24
View File
@@ -102,11 +102,7 @@ impl<S: State> RealDom<S> {
self.tree.add_child(node_id, child_id);
}
fn create_template_node(
&mut self,
node: &TemplateNode,
mutations_vec: &mut FxHashMap<RealNodeId, NodeMask>,
) -> RealNodeId {
fn create_template_node(&mut self, node: &TemplateNode) -> RealNodeId {
match node {
TemplateNode::Element {
tag,
@@ -139,27 +135,18 @@ impl<S: State> RealDom<S> {
});
let node_id = self.create_node(node);
for child in *children {
let child_id = self.create_template_node(child, mutations_vec);
let child_id = self.create_template_node(child);
self.add_child(node_id, child_id);
}
node_id
}
TemplateNode::Text { text } => {
let node_id = self.create_node(Node::new(NodeType::Text {
text: text.to_string(),
}));
node_id
}
TemplateNode::Dynamic { .. } => {
let node_id = self.create_node(Node::new(NodeType::Placeholder));
node_id
}
TemplateNode::DynamicText { .. } => {
let node_id = self.create_node(Node::new(NodeType::Text {
text: String::new(),
}));
node_id
}
TemplateNode::Text { text } => self.create_node(Node::new(NodeType::Text {
text: text.to_string(),
})),
TemplateNode::Dynamic { .. } => self.create_node(Node::new(NodeType::Placeholder)),
TemplateNode::DynamicText { .. } => self.create_node(Node::new(NodeType::Text {
text: String::new(),
})),
}
}
@@ -172,7 +159,7 @@ impl<S: State> RealDom<S> {
for template in mutations.templates {
let mut template_root_ids = Vec::new();
for root in template.roots {
let id = self.create_template_node(root, &mut nodes_updated);
let id = self.create_template_node(root);
template_root_ids.push(id);
}
self.templates
@@ -319,7 +306,7 @@ impl<S: State> RealDom<S> {
}
mark_dirty(node_id, NodeMask::new().with_text(), &mut nodes_updated);
}
NewEventListener { name, scope: _, id } => {
NewEventListener { name, id } => {
let node_id = self.element_to_node_id(id);
let node = self.tree.get_mut(node_id).unwrap();
if let NodeType::Element { listeners, .. } = &mut node.node_data.node_type {
+484
View File
@@ -0,0 +1,484 @@
use std::cmp::Ordering;
use dioxus_html::input_data::keyboard_types::{Code, Key, Modifiers};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pos {
pub col: usize,
pub row: usize,
}
impl Pos {
pub fn new(col: usize, row: usize) -> Self {
Self { row, col }
}
pub fn up(&mut self, rope: &str) {
self.move_row(-1, rope);
}
pub fn down(&mut self, rope: &str) {
self.move_row(1, rope);
}
pub fn right(&mut self, rope: &str) {
self.move_col(1, rope);
}
pub fn left(&mut self, rope: &str) {
self.move_col(-1, rope);
}
pub fn move_row(&mut self, change: i32, rope: &str) {
let new = self.row as i32 + change;
if new >= 0 && new < rope.lines().count() as i32 {
self.row = new as usize;
}
}
pub fn move_col(&mut self, change: i32, rope: &str) {
self.realize_col(rope);
let idx = self.idx(rope) as i32;
if idx + change >= 0 && idx + change <= rope.len() as i32 {
let len_line = self.len_line(rope) as i32;
let new_col = self.col as i32 + change;
let diff = new_col - len_line;
if diff > 0 {
self.down(rope);
self.col = 0;
self.move_col(diff - 1, rope);
} else if new_col < 0 {
self.up(rope);
self.col = self.len_line(rope);
self.move_col(new_col + 1, rope);
} else {
self.col = new_col as usize;
}
}
}
pub fn col(&self, rope: &str) -> usize {
self.col.min(self.len_line(rope))
}
pub fn row(&self) -> usize {
self.row
}
fn len_line(&self, rope: &str) -> usize {
let line = rope.lines().nth(self.row).unwrap_or_default();
let len = line.len();
if len > 0 && line.chars().nth(len - 1) == Some('\n') {
len - 1
} else {
len
}
}
pub fn idx(&self, rope: &str) -> usize {
rope.lines().take(self.row).map(|l| l.len()).sum::<usize>() + self.col(rope)
}
// the column can be more than the line length, cap it
pub fn realize_col(&mut self, rope: &str) {
self.col = self.col(rope);
}
}
impl Ord for Pos {
fn cmp(&self, other: &Self) -> Ordering {
self.row.cmp(&other.row).then(self.col.cmp(&other.col))
}
}
impl PartialOrd for Pos {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cursor {
pub start: Pos,
pub end: Option<Pos>,
}
impl Cursor {
pub fn from_start(pos: Pos) -> Self {
Self {
start: pos,
end: None,
}
}
pub fn new(start: Pos, end: Pos) -> Self {
Self {
start,
end: Some(end),
}
}
fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) {
if shift {
self.with_end(f);
} else {
f(&mut self.start);
self.end = None;
}
}
fn delete_selection(&mut self, text: &mut String) -> [i32; 2] {
let first = self.first();
let last = self.last();
let dr = first.row as i32 - last.row as i32;
let dc = if dr != 0 {
-(last.col as i32)
} else {
first.col as i32 - last.col as i32
};
text.replace_range(first.idx(text)..last.idx(text), "");
if let Some(end) = self.end.take() {
if self.start > end {
self.start = end;
}
}
[dc, dr]
}
pub fn handle_input(
&mut self,
data: &dioxus_html::KeyboardData,
text: &mut String,
max_width: usize,
) {
use Code::*;
match data.code() {
ArrowUp => {
self.move_cursor(|c| c.up(text), data.modifiers().contains(Modifiers::SHIFT));
}
ArrowDown => {
self.move_cursor(
|c| c.down(text),
data.modifiers().contains(Modifiers::SHIFT),
);
}
ArrowRight => {
if data.modifiers().contains(Modifiers::CONTROL) {
self.move_cursor(
|c| {
let mut change = 1;
let idx = c.idx(text);
let length = text.len();
while idx + change < length {
let chr = text.chars().nth(idx + change).unwrap();
if chr.is_whitespace() {
break;
}
change += 1;
}
c.move_col(change as i32, text);
},
data.modifiers().contains(Modifiers::SHIFT),
);
} else {
self.move_cursor(
|c| c.right(text),
data.modifiers().contains(Modifiers::SHIFT),
);
}
}
ArrowLeft => {
if data.modifiers().contains(Modifiers::CONTROL) {
self.move_cursor(
|c| {
let mut change = -1;
let idx = c.idx(text) as i32;
while idx + change > 0 {
let chr = text.chars().nth((idx + change) as usize).unwrap();
if chr == ' ' {
break;
}
change -= 1;
}
c.move_col(change as i32, text);
},
data.modifiers().contains(Modifiers::SHIFT),
);
} else {
self.move_cursor(
|c| c.left(text),
data.modifiers().contains(Modifiers::SHIFT),
);
}
}
End => {
self.move_cursor(
|c| c.col = c.len_line(text),
data.modifiers().contains(Modifiers::SHIFT),
);
}
Home => {
self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT));
}
Backspace => {
self.start.realize_col(text);
let mut start_idx = self.start.idx(text);
if self.end.is_some() {
self.delete_selection(text);
} else if start_idx > 0 {
self.start.left(text);
text.replace_range(start_idx - 1..start_idx, "");
if data.modifiers().contains(Modifiers::CONTROL) {
start_idx = self.start.idx(text);
while start_idx > 0
&& text
.chars()
.nth(start_idx - 1)
.filter(|c| *c != ' ')
.is_some()
{
self.start.left(text);
text.replace_range(start_idx - 1..start_idx, "");
start_idx = self.start.idx(text);
}
}
}
}
Enter => {
if text.len() + 1 - self.selection_len(text) <= max_width {
text.insert(self.start.idx(text), '\n');
self.start.col = 0;
self.start.down(text);
}
}
Tab => {
if text.len() + 1 - self.selection_len(text) <= max_width {
self.start.realize_col(text);
self.delete_selection(text);
text.insert(self.start.idx(text), '\t');
self.start.right(text);
}
}
_ => {
self.start.realize_col(text);
if let Key::Character(character) = data.key() {
if text.len() + 1 - self.selection_len(text) <= max_width {
self.delete_selection(text);
let character = character.chars().next().unwrap();
text.insert(self.start.idx(text), character);
self.start.right(text);
}
}
}
}
}
pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) {
let mut new = self.end.take().unwrap_or_else(|| self.start.clone());
f(&mut new);
self.end.replace(new);
}
pub fn first(&self) -> &Pos {
if let Some(e) = &self.end {
e.min(&self.start)
} else {
&self.start
}
}
pub fn last(&self) -> &Pos {
if let Some(e) = &self.end {
e.max(&self.start)
} else {
&self.start
}
}
pub fn selection_len(&self, text: &str) -> usize {
self.last().idx(text) - self.first().idx(text)
}
}
impl Default for Cursor {
fn default() -> Self {
Self {
start: Pos::new(0, 0),
end: None,
}
}
}
#[test]
fn pos_direction_movement() {
let mut pos = Pos::new(100, 0);
let text = "hello world\nhi";
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
pos.down(text);
assert_eq!(pos.col(text), text.lines().nth(1).unwrap_or_default().len());
pos.up(text);
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
pos.left(text);
assert_eq!(
pos.col(text),
text.lines().next().unwrap_or_default().len() - 1
);
pos.right(text);
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
}
#[test]
fn pos_col_movement() {
let mut pos = Pos::new(100, 0);
let text = "hello world\nhi";
// move inside a row
pos.move_col(-5, text);
assert_eq!(
pos.col(text),
text.lines().next().unwrap_or_default().len() - 5
);
pos.move_col(5, text);
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
// move between rows
pos.move_col(3, text);
assert_eq!(pos.col(text), 2);
pos.move_col(-3, text);
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
// don't panic if moving out of range
pos.move_col(-100, text);
pos.move_col(1000, text);
}
#[test]
fn cursor_row_movement() {
let mut pos = Pos::new(100, 0);
let text = "hello world\nhi";
pos.move_row(1, text);
assert_eq!(pos.row(), 1);
pos.move_row(-1, text);
assert_eq!(pos.row(), 0);
// don't panic if moving out of range
pos.move_row(-100, text);
pos.move_row(1000, text);
}
#[test]
fn cursor_input() {
let mut cursor = Cursor::from_start(Pos::new(0, 0));
let mut text = "hello world\nhi".to_string();
for _ in 0..5 {
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::ArrowRight,
dioxus_html::input_data::keyboard_types::Code::ArrowRight,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&mut text,
10,
);
}
for _ in 0..5 {
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Backspace,
dioxus_html::input_data::keyboard_types::Code::Backspace,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&mut text,
10,
);
}
assert_eq!(text, " world\nhi");
let goal_text = "hello world\nhi";
let max_width = goal_text.len();
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("h".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyH,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&mut text,
max_width,
);
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("e".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyE,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&mut text,
max_width,
);
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyL,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&mut text,
max_width,
);
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyL,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&mut text,
max_width,
);
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyO,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&mut text,
max_width,
);
// these should be ignored
for _ in 0..10 {
cursor.handle_input(
&dioxus_html::KeyboardData::new(
dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()),
dioxus_html::input_data::keyboard_types::Code::KeyO,
dioxus_html::input_data::keyboard_types::Location::Standard,
false,
Modifiers::empty(),
),
&mut text,
max_width,
);
}
assert_eq!(text.to_string(), goal_text);
}
+3
View File
@@ -0,0 +1,3 @@
mod persistant_iterator;
pub use persistant_iterator::*;
pub mod cursor;
+4
View File
@@ -36,6 +36,7 @@ thiserror = "1.0.30"
futures-util = "0.3.21"
serde = { version = "1", optional = true }
serde_urlencoded = { version = "0.7.1", optional = true }
simple_logger = "4.0.0"
[features]
default = ["query"]
@@ -50,6 +51,9 @@ wasm-logger = "0.2.0"
wasm-bindgen-test = "0.3"
gloo-utils = "0.1.2"
dioxus-web = { path = "../web" }
# dioxus-desktop = { path = "../desktop", optional = true }
# not wasm
[target.wasm32-unknown-unknown.dev-dependencies]
dioxus-router = { path = ".", features = ["web"] }
+7 -1
View File
@@ -16,6 +16,7 @@ fn app(cx: Scope) -> Element {
Link { to: "/blog", li { "blog" } }
Link { to: "/blog/tim", li { "tims' blog" } }
Link { to: "/blog/bill", li { "bills' blog" } }
Link { to: "/blog/james", li { "james amazing' blog" } }
Link { to: "/apples", li { "go to apples" } }
}
Route { to: "/", Home {} }
@@ -42,5 +43,10 @@ fn BlogPost(cx: Scope) -> Element {
log::trace!("rendering blog post {}", id);
cx.render(rsx! { div { "{id:?}" } })
cx.render(rsx! {
div {
h3 { "blog post: {id:?}" }
Link { to: "/blog/", "back to blog list" }
}
})
}
+8
View File
@@ -120,9 +120,17 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
prevent_default: "{prevent_default}",
target: format_args!("{}", if * new_tab { "_blank" } else { "" }),
onclick: move |_| {
log::trace!("Clicked link to {}", to);
if !outerlink {
if let Some(service) = svc {
log::trace!("Pushing route to {}", to);
service.push_route(to, cx.props.title.map(|f| f.to_string()), None);
#[cfg(feature = "web")]
{
web_sys::window().unwrap().scroll_to_with_x_and_y(0.0, 0.0);
}
} else {
log::error!(
"Attempted to create a Link to {} outside of a Router context", cx.props
+3 -3
View File
@@ -45,13 +45,13 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
router_root.register_total_route(route_context.total_route, cx.scope_id());
});
log::trace!("Checking Route: {:?}", cx.props.to);
log::debug!("Checking Route: {:?}", cx.props.to);
if router_root.should_render(cx.scope_id()) {
log::trace!("Route should render: {:?}", cx.scope_id());
log::debug!("Route should render: {:?}", cx.scope_id());
cx.render(rsx!(&cx.props.children))
} else {
log::trace!("Route should *not* render: {:?}", cx.scope_id());
log::debug!("Route should *not* render: {:?}", cx.scope_id());
cx.render(rsx!(()))
}
}
+1
View File
@@ -180,6 +180,7 @@ impl RouterService {
(self.regen_any_route)(self.router_id);
for listener in self.onchange_listeners.borrow().iter() {
log::trace!("Regenerating scope {:?}", listener);
(self.regen_any_route)(*listener);
}
+6 -2
View File
@@ -1,8 +1,13 @@
[package]
name = "dioxus-rsx"
version = "0.0.0"
version = "0.0.1"
edition = "2018"
license = "MIT/Apache-2.0"
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://docs.rs/dioxus-rsx"
keywords = ["dom", "ui", "gui", "react", "wasm"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -10,6 +15,5 @@ license = "MIT/Apache-2.0"
proc-macro2 = { version = "1.0", features = ["span-locations"] }
syn = { version = "1.0", features = ["full", "extra-traits"] }
quote = { version = "1.0" }
dioxus-core = { path = "../core", features = ["serialize"] }
serde = { version = "1.0", features = ["derive"] }
internment = "0.7.0"
+1 -2
View File
@@ -45,7 +45,7 @@ impl Parse for Element {
content.parse::<Token![:]>()?;
if content.peek(LitStr) && content.peek2(Token![,]) {
if content.peek(LitStr) {
let value = content.parse()?;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
@@ -53,7 +53,6 @@ impl Parse for Element {
});
} else {
let value = content.parse::<Expr>()?;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::CustomAttrExpression { name, value },
+5 -4
View File
@@ -217,7 +217,8 @@ impl<'a, Ctx: HotReloadingContext> ToTokens for TemplateRenderer<'a, Ctx> {
parent: None,
key: #key_tokens,
template: TEMPLATE,
root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([::dioxus::core::ElementId(0); #num_roots]) as &mut [::dioxus::core::ElementId]).as_slice_of_cells(),
root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([None; #num_roots]) as &mut _).as_slice_of_cells(),
// root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([None; #num_roots]) as &mut [::dioxus::core::ElementId]).as_slice_of_cells(),
dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]),
dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]),
}
@@ -441,7 +442,7 @@ impl<'a, Ctx: HotReloadingContext> DynamicContext<'a, Ctx> {
let el_name = &el.name;
let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
ElementAttr::AttrText { name, value } if value.is_static() => {
let value = value.source.as_ref().unwrap();
let value = value.to_static().unwrap();
quote! {
::dioxus::core::TemplateAttribute::Static {
name: dioxus_elements::#el_name::#name.0,
@@ -455,7 +456,7 @@ impl<'a, Ctx: HotReloadingContext> DynamicContext<'a, Ctx> {
}
ElementAttr::CustomAttrText { name, value } if value.is_static() => {
let value = value.source.as_ref().unwrap();
let value = value.to_static().unwrap();
quote! {
::dioxus::core::TemplateAttribute::Static {
name: #name,
@@ -503,7 +504,7 @@ impl<'a, Ctx: HotReloadingContext> DynamicContext<'a, Ctx> {
}
BodyNode::Text(text) if text.is_static() => {
let text = text.source.as_ref().unwrap();
let text = text.to_static().unwrap();
quote! { ::dioxus::core::TemplateNode::Text{ text: #text } }
}
+1
View File
@@ -13,6 +13,7 @@ license = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { path = "../dioxus", version = "^0.2.1" }
dioxus-core = { path = "../core", version = "^0.2.1" }
dioxus-html = { path = "../html", version = "^0.2.1" }
dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
+8 -6
View File
@@ -17,12 +17,14 @@ fn app(cx: Scope) -> Element {
width: "100%",
background_color: "hsl({hue}, 70%, {brightness}%)",
onmousemove: move |evt| {
if let RenderReturn::Sync(Ok(node))=cx.root_node(){
let node = tui_query.get(node.root_ids[0].get());
let Size{width, height} = node.size().unwrap();
let pos = evt.inner().element_coordinates();
hue.set((pos.x as f32/width as f32)*255.0);
brightness.set((pos.y as f32/height as f32)*100.0);
if let RenderReturn::Sync(Ok(node)) = cx.root_node() {
if let Some(id) = node.root_ids[0].get() {
let node = tui_query.get(id);
let Size{width, height} = node.size().unwrap();
let pos = evt.inner().element_coordinates();
hue.set((pos.x as f32/width as f32)*255.0);
brightness.set((pos.y as f32/height as f32)*100.0);
}
}
},
"hsl({hue}, 70%, {brightness}%)",
+93
View File
@@ -0,0 +1,93 @@
use dioxus::prelude::*;
use dioxus_html::FormData;
use dioxus_tui::prelude::*;
use dioxus_tui::Config;
fn main() {
dioxus_tui::launch_cfg(app, Config::new());
}
fn app(cx: Scope) -> Element {
let bg_green = use_state(cx, || false);
let color = if *bg_green.get() { "green" } else { "red" };
cx.render(rsx! {
div{
width: "100%",
background_color: "{color}",
flex_direction: "column",
align_items: "center",
justify_content: "center",
Input{
oninput: |data: FormData| if &data.value == "good"{
bg_green.set(true);
} else{
bg_green.set(false);
},
r#type: "checkbox",
value: "good",
width: "50%",
height: "10%",
checked: "true",
}
Input{
oninput: |data: FormData| if &data.value == "hello world"{
bg_green.set(true);
} else{
bg_green.set(false);
},
width: "50%",
height: "10%",
maxlength: "11",
}
Input{
oninput: |data: FormData| {
if (data.value.parse::<f32>().unwrap() - 40.0).abs() < 5.0 {
bg_green.set(true);
} else{
bg_green.set(false);
}
},
r#type: "range",
width: "50%",
height: "10%",
min: "20",
max: "80",
}
Input{
oninput: |data: FormData| {
if data.value == "10"{
bg_green.set(true);
} else{
bg_green.set(false);
}
},
r#type: "number",
width: "50%",
height: "10%",
maxlength: "4",
}
Input{
oninput: |data: FormData| {
if data.value == "hello world"{
bg_green.set(true);
} else{
bg_green.set(false);
}
},
r#type: "password",
width: "50%",
height: "10%",
maxlength: "11",
}
Input{
onclick: |_: FormData| bg_green.set(true),
r#type: "button",
value: "green",
width: "50%",
height: "10%",
}
}
})
}
+4 -1
View File
@@ -287,7 +287,10 @@ impl InnerInputState {
fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData {
let Point { x, y } = layout.location;
let node_origin = ClientPoint::new(x.into(), y.into());
let node_origin = ClientPoint::new(
layout_to_screen_space(x).into(),
layout_to_screen_space(y).into(),
);
let new_client_coordinates = (mouse_data.client_coordinates() - node_origin)
.to_point()
+54 -1
View File
@@ -7,7 +7,7 @@ use dioxus_native_core::state::ChildDepState;
use dioxus_native_core_macro::sorted_str_slice;
use taffy::prelude::*;
use crate::screen_to_layout_space;
use crate::{screen_to_layout_space, unit_to_layout_space};
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum PossiblyUninitalized<T> {
@@ -105,6 +105,59 @@ impl ChildDepState for TaffyLayout {
child_layout.push(l.node.unwrap());
}
fn scale_dimention(d: Dimension) -> Dimension {
match d {
Dimension::Points(p) => Dimension::Points(unit_to_layout_space(p)),
Dimension::Percent(p) => Dimension::Percent(p),
Dimension::Auto => Dimension::Auto,
Dimension::Undefined => Dimension::Undefined,
}
}
let style = Style {
position: Rect {
left: scale_dimention(style.position.left),
right: scale_dimention(style.position.right),
top: scale_dimention(style.position.top),
bottom: scale_dimention(style.position.bottom),
},
margin: Rect {
left: scale_dimention(style.margin.left),
right: scale_dimention(style.margin.right),
top: scale_dimention(style.margin.top),
bottom: scale_dimention(style.margin.bottom),
},
padding: Rect {
left: scale_dimention(style.padding.left),
right: scale_dimention(style.padding.right),
top: scale_dimention(style.padding.top),
bottom: scale_dimention(style.padding.bottom),
},
border: Rect {
left: scale_dimention(style.border.left),
right: scale_dimention(style.border.right),
top: scale_dimention(style.border.top),
bottom: scale_dimention(style.border.bottom),
},
gap: Size {
width: scale_dimention(style.gap.width),
height: scale_dimention(style.gap.height),
},
flex_basis: scale_dimention(style.flex_basis),
size: Size {
width: scale_dimention(style.size.width),
height: scale_dimention(style.size.height),
},
min_size: Size {
width: scale_dimention(style.min_size.width),
height: scale_dimention(style.min_size.height),
},
max_size: Size {
width: scale_dimention(style.max_size.width),
height: scale_dimention(style.max_size.height),
},
..style
};
if let PossiblyUninitalized::Initialized(n) = self.node {
if self.style != style {
taffy.set_style(n, style).unwrap();
+18 -3
View File
@@ -1,5 +1,6 @@
use anyhow::Result;
use crossterm::{
cursor::{MoveTo, RestorePosition, SavePosition, Show},
event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
@@ -28,11 +29,13 @@ mod focus;
mod hooks;
mod layout;
mod node;
pub mod prelude;
pub mod query;
mod render;
mod style;
mod style_attributes;
mod widget;
mod widgets;
pub use config::*;
pub use hooks::*;
@@ -43,6 +46,10 @@ pub(crate) fn screen_to_layout_space(screen: u16) -> f32 {
screen as f32 * 10.0
}
pub(crate) fn unit_to_layout_space(screen: f32) -> f32 {
screen * 10.0
}
pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
layout / 10.0
}
@@ -136,7 +143,13 @@ fn render_vdom(
let mut terminal = (!cfg.headless).then(|| {
enable_raw_mode().unwrap();
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap();
execute!(
stdout,
EnterAlternateScreen,
EnableMouseCapture,
MoveTo(0, 1000)
)
.unwrap();
let backend = CrosstermBackend::new(io::stdout());
Terminal::new(backend).unwrap()
});
@@ -181,14 +194,16 @@ fn render_vdom(
taffy.compute_layout(root_node, size).unwrap();
}
if let Some(terminal) = &mut terminal {
execute!(terminal.backend_mut(), SavePosition).unwrap();
terminal.draw(|frame| {
let rdom = rdom.borrow();
let mut taffy = taffy.lock().expect("taffy lock poisoned");
// size is guaranteed to not change when rendering
resize(frame.size(), &mut *taffy, &rdom);
resize(frame.size(), &mut taffy, &rdom);
let root = &rdom[NodeId(0)];
render::render_vnode(frame, &*taffy, &rdom, root, cfg, Point::ZERO);
render::render_vnode(frame, &taffy, &rdom, root, cfg, Point::ZERO);
})?;
execute!(terminal.backend_mut(), RestorePosition, Show).unwrap();
} else {
let rdom = rdom.borrow();
resize(
+1
View File
@@ -0,0 +1 @@
pub use crate::widgets::*;
+1 -1
View File
@@ -89,7 +89,7 @@ impl<'a> ElementRef<'a> {
.ok();
layout.map(|layout| Layout {
order: layout.order,
size: layout.size.map(|v| layout_to_screen_space(v)),
size: layout.size.map(layout_to_screen_space),
location: Point {
x: layout_to_screen_space(layout.location.x),
y: layout_to_screen_space(layout.location.y),
+1 -1
View File
@@ -41,7 +41,7 @@ pub(crate) fn render_vnode(
let x = layout_to_screen_space(fx).round() as u16;
let y = layout_to_screen_space(fy).round() as u16;
let Size { width, height } = *size;
let width = layout_to_screen_space(fx + width).round() as u16 + x;
let width = layout_to_screen_space(fx + width).round() as u16 - x;
let height = layout_to_screen_space(fy + height).round() as u16 - y;
match &node.node_data.node_type {
+58
View File
@@ -0,0 +1,58 @@
use std::collections::HashMap;
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
#[derive(Props)]
pub(crate) struct ButtonProps<'a> {
#[props(!optional)]
raw_onclick: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> {
let state = use_state(cx, || false);
let width = cx.props.width.unwrap_or("1px");
let height = cx.props.height.unwrap_or("1px");
let single_char = width == "1px" || height == "1px";
let text = if let Some(v) = cx.props.value { v } else { "" };
let border_style = if single_char { "none" } else { "solid" };
let update = || {
let new_state = !state.get();
if let Some(callback) = cx.props.raw_onclick {
callback.call(FormData {
value: text.to_string(),
values: HashMap::new(),
});
}
state.set(new_state);
};
cx.render(rsx! {
div{
width: "{width}",
height: "{height}",
border_style: "{border_style}",
flex_direction: "row",
align_items: "center",
justify_content: "center",
onclick: move |_| {
update();
},
onkeydown: move |evt|{
if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } {
update();
}
},
"{text}"
}
})
}
+81
View File
@@ -0,0 +1,81 @@
use std::collections::HashMap;
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
#[derive(Props)]
pub(crate) struct CheckBoxProps<'a> {
#[props(!optional)]
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
#[props(!optional)]
checked: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> {
let state = use_state(cx, || cx.props.checked.filter(|&c| c == "true").is_some());
let width = cx.props.width.unwrap_or("1px");
let height = cx.props.height.unwrap_or("1px");
let single_char = width == "1px" && height == "1px";
let text = if single_char {
if *state.get() {
"☑"
} else {
"☐"
}
} else if *state.get() {
"✓"
} else {
" "
};
let border_style = if width == "1px" || height == "1px" {
"none"
} else {
"solid"
};
let update = move || {
let new_state = !state.get();
if let Some(callback) = cx.props.raw_oninput {
callback.call(FormData {
value: if let Some(value) = &cx.props.value {
if new_state {
value.to_string()
} else {
String::new()
}
} else {
"on".to_string()
},
values: HashMap::new(),
});
}
state.set(new_state);
};
cx.render(rsx! {
div {
width: "{width}",
height: "{height}",
border_style: "{border_style}",
align_items: "center",
justify_content: "center",
onclick: move |_| {
update();
},
onkeydown: move |evt| {
if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } {
update();
}
},
"{text}"
}
})
}
+102
View File
@@ -0,0 +1,102 @@
use dioxus::prelude::*;
use dioxus_core::prelude::fc_to_builder;
use dioxus_html::FormData;
use crate::widgets::button::Button;
use crate::widgets::checkbox::CheckBox;
use crate::widgets::number::NumbericInput;
use crate::widgets::password::Password;
use crate::widgets::slider::Slider;
use crate::widgets::textbox::TextBox;
#[derive(Props)]
pub struct InputProps<'a> {
r#type: Option<&'static str>,
oninput: Option<EventHandler<'a, FormData>>,
onclick: Option<EventHandler<'a, FormData>>,
value: Option<&'a str>,
size: Option<&'a str>,
maxlength: Option<&'a str>,
width: Option<&'a str>,
height: Option<&'a str>,
min: Option<&'a str>,
max: Option<&'a str>,
step: Option<&'a str>,
checked: Option<&'a str>,
}
#[allow(non_snake_case)]
pub fn Input<'a>(cx: Scope<'a, InputProps<'a>>) -> Element<'a> {
cx.render(match cx.props.r#type {
Some("checkbox") => {
rsx! {
CheckBox{
raw_oninput: cx.props.oninput.as_ref(),
value: cx.props.value,
width: cx.props.width,
height: cx.props.height,
checked: cx.props.checked,
}
}
}
Some("range") => {
rsx! {
Slider{
raw_oninput: cx.props.oninput.as_ref(),
value: cx.props.value,
width: cx.props.width,
height: cx.props.height,
max: cx.props.max,
min: cx.props.min,
step: cx.props.step,
}
}
}
Some("button") => {
rsx! {
Button{
raw_onclick: cx.props.onclick.as_ref(),
value: cx.props.value,
width: cx.props.width,
height: cx.props.height,
}
}
}
Some("number") => {
rsx! {
NumbericInput{
raw_oninput: cx.props.oninput.as_ref(),
value: cx.props.value,
size: cx.props.size,
max_length: cx.props.maxlength,
width: cx.props.width,
height: cx.props.height,
}
}
}
Some("password") => {
rsx! {
Password{
raw_oninput: cx.props.oninput.as_ref(),
value: cx.props.value,
size: cx.props.size,
max_length: cx.props.maxlength,
width: cx.props.width,
height: cx.props.height,
}
}
}
_ => {
rsx! {
TextBox{
raw_oninput: cx.props.oninput.as_ref(),
value: cx.props.value,
size: cx.props.size,
max_length: cx.props.maxlength,
width: cx.props.width,
height: cx.props.height,
}
}
}
})
}
+18
View File
@@ -0,0 +1,18 @@
mod button;
mod checkbox;
mod input;
mod number;
mod password;
mod slider;
mod textbox;
use dioxus_core::{ElementId, RenderReturn, Scope};
pub use input::*;
pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<ElementId> {
if let RenderReturn::Sync(Ok(sync)) = cx.root_node() {
sync.root_ids.get(0).and_then(|id| id.get())
} else {
None
}
}
+208
View File
@@ -0,0 +1,208 @@
use crate::widgets::get_root_id;
use crate::Query;
use crossterm::{cursor::MoveTo, execute};
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use dioxus_native_core::utils::cursor::{Cursor, Pos};
use std::{collections::HashMap, io::stdout};
use taffy::geometry::Point;
#[derive(Props)]
pub(crate) struct NumbericInputProps<'a> {
#[props(!optional)]
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
size: Option<&'a str>,
#[props(!optional)]
max_length: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a> {
let tui_query: Query = cx.consume_context().unwrap();
let tui_query_clone = tui_query.clone();
let text_ref = use_ref(cx, || {
if let Some(intial_text) = cx.props.value {
intial_text.to_string()
} else {
String::new()
}
});
let cursor = use_ref(cx, Cursor::default);
let dragging = use_state(cx, || false);
let text = text_ref.read().clone();
let start_highlight = cursor.read().first().idx(&text);
let end_highlight = cursor.read().last().idx(&text);
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
let (text_highlighted, text_after_second_cursor) =
text_after_first_cursor.split_at(end_highlight - start_highlight);
let max_len = cx
.props
.max_length
.as_ref()
.and_then(|s| s.parse().ok())
.unwrap_or(usize::MAX);
let width = cx
.props
.width
.map(|s| s.to_string())
// px is the same as em in tui
.or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
.unwrap_or_else(|| "10px".to_string());
let height = cx.props.height.unwrap_or("3px");
// don't draw a border unless there is enough space
let border = if width
.strip_suffix("px")
.and_then(|w| w.parse::<i32>().ok())
.filter(|w| *w < 3)
.is_some()
|| height
.strip_suffix("px")
.and_then(|h| h.parse::<i32>().ok())
.filter(|h| *h < 3)
.is_some()
{
"none"
} else {
"solid"
};
let update = |text: String| {
if let Some(input_handler) = &cx.props.raw_oninput {
input_handler.call(FormData {
value: text,
values: HashMap::new(),
});
}
};
let increase = move || {
let mut text = text_ref.write();
*text = (text.parse::<f64>().unwrap_or(0.0) + 1.0).to_string();
update(text.clone());
};
let decrease = move || {
let mut text = text_ref.write();
*text = (text.parse::<f64>().unwrap_or(0.0) - 1.0).to_string();
update(text.clone());
};
render! {
div{
width: "{width}",
height: "{height}",
border_style: "{border}",
onkeydown: move |k| {
let is_text = match k.key(){
Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true,
Key::Character(c) if c=="." || c== "-" || c.chars().all(|c|c.is_numeric())=> true,
_ => false,
};
if is_text{
let mut text = text_ref.write();
cursor.write().handle_input(&k, &mut text, max_len);
update(text.clone());
let node = tui_query.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
match k.key() {
Key::ArrowUp =>{
increase();
}
Key::ArrowDown =>{
decrease();
}
_ => ()
}
}
},
onmousemove: move |evt| {
if *dragging.get() {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
// textboxs are only one line tall
new.row = 0;
if new != cursor.read().start {
cursor.write().end = Some(new);
}
}
},
onmousedown: move |evt| {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
new.row = 0;
new.realize_col(&text_ref.read());
cursor.set(Cursor::from_start(new));
dragging.set(true);
let node = tui_query_clone.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
execute!(stdout(), MoveTo(x, y)).unwrap();
}
},
onmouseup: move |_| {
dragging.set(false);
},
onmouseleave: move |_| {
dragging.set(false);
},
onmouseenter: move |_| {
dragging.set(false);
},
onfocusout: |_| {
execute!(stdout(), MoveTo(0, 1000)).unwrap();
},
"{text_before_first_cursor}"
span{
background_color: "rgba(255, 255, 255, 50%)",
"{text_highlighted}"
}
"{text_after_second_cursor}"
}
}
}
+185
View File
@@ -0,0 +1,185 @@
use crate::widgets::get_root_id;
use crate::Query;
use crossterm::{cursor::*, execute};
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use dioxus_native_core::utils::cursor::{Cursor, Pos};
use std::{collections::HashMap, io::stdout};
use taffy::geometry::Point;
#[derive(Props)]
pub(crate) struct PasswordProps<'a> {
#[props(!optional)]
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
size: Option<&'a str>,
#[props(!optional)]
max_length: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
let tui_query: Query = cx.consume_context().unwrap();
let tui_query_clone = tui_query.clone();
let text_ref = use_ref(cx, || {
if let Some(intial_text) = cx.props.value {
intial_text.to_string()
} else {
String::new()
}
});
let cursor = use_ref(cx, Cursor::default);
let dragging = use_state(cx, || false);
let text = text_ref.read().clone();
let start_highlight = cursor.read().first().idx(&text);
let end_highlight = cursor.read().last().idx(&text);
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
let (text_highlighted, text_after_second_cursor) =
text_after_first_cursor.split_at(end_highlight - start_highlight);
let text_before_first_cursor = ".".repeat(text_before_first_cursor.len());
let text_highlighted = ".".repeat(text_highlighted.len());
let text_after_second_cursor = ".".repeat(text_after_second_cursor.len());
let max_len = cx
.props
.max_length
.as_ref()
.and_then(|s| s.parse().ok())
.unwrap_or(usize::MAX);
let width = cx
.props
.width
.map(|s| s.to_string())
// px is the same as em in tui
.or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
.unwrap_or_else(|| "10px".to_string());
let height = cx.props.height.unwrap_or("3px");
// don't draw a border unless there is enough space
let border = if width
.strip_suffix("px")
.and_then(|w| w.parse::<i32>().ok())
.filter(|w| *w < 3)
.is_some()
|| height
.strip_suffix("px")
.and_then(|h| h.parse::<i32>().ok())
.filter(|h| *h < 3)
.is_some()
{
"none"
} else {
"solid"
};
render! {
div {
width: "{width}",
height: "{height}",
border_style: "{border}",
onkeydown: move |k| {
if k.key()== Key::Enter {
return;
}
let mut text = text_ref.write();
cursor.write().handle_input(&k, &mut text, max_len);
if let Some(input_handler) = &cx.props.raw_oninput{
input_handler.call(FormData{
value: text.clone(),
values: HashMap::new(),
});
}
let node = tui_query.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
execute!(stdout(), MoveTo(x, y)).unwrap();
}
},
onmousemove: move |evt| {
if *dragging.get() {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
// textboxs are only one line tall
new.row = 0;
if new != cursor.read().start {
cursor.write().end = Some(new);
}
}
},
onmousedown: move |evt| {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
// textboxs are only one line tall
new.row = 0;
new.realize_col(&text_ref.read());
cursor.set(Cursor::from_start(new));
dragging.set(true);
let node = tui_query_clone.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
execute!(stdout(), MoveTo(x, y)).unwrap();
}
},
onmouseup: move |_| {
dragging.set(false);
},
onmouseleave: move |_| {
dragging.set(false);
},
onmouseenter: move |_| {
dragging.set(false);
},
onfocusout: |_| {
execute!(stdout(), MoveTo(0, 1000)).unwrap();
},
"{text_before_first_cursor}"
span{
background_color: "rgba(255, 255, 255, 50%)",
"{text_highlighted}"
}
"{text_after_second_cursor}"
}
}
}
+107
View File
@@ -0,0 +1,107 @@
use std::collections::HashMap;
use crate::widgets::get_root_id;
use crate::Query;
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
#[derive(Props)]
pub(crate) struct SliderProps<'a> {
#[props(!optional)]
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
#[props(!optional)]
min: Option<&'a str>,
#[props(!optional)]
max: Option<&'a str>,
#[props(!optional)]
step: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> {
let tui_query: Query = cx.consume_context().unwrap();
let value_state = use_state(cx, || 0.0);
let value: Option<f32> = cx.props.value.and_then(|v| v.parse().ok());
let width = cx.props.width.unwrap_or("20px");
let height = cx.props.height.unwrap_or("1px");
let min = cx.props.min.and_then(|v| v.parse().ok()).unwrap_or(0.0);
let max = cx.props.max.and_then(|v| v.parse().ok()).unwrap_or(100.0);
let size = max - min;
let step = cx
.props
.step
.and_then(|v| v.parse().ok())
.unwrap_or(size / 10.0);
let current_value = if let Some(value) = value {
value
} else {
*value_state.get()
}
.max(min)
.min(max);
let fst_width = 100.0 * (current_value - min) / size;
let snd_width = 100.0 * (max - current_value) / size;
assert!(fst_width + snd_width > 99.0 && fst_width + snd_width < 101.0);
let update = |value: String| {
if let Some(oninput) = cx.props.raw_oninput {
oninput.call(FormData {
value,
values: HashMap::new(),
});
}
};
render! {
div{
width: "{width}",
height: "{height}",
display: "flex",
flex_direction: "row",
onkeydown: move |event| {
match event.key() {
Key::ArrowLeft => {
value_state.set((current_value - step).max(min).min(max));
update(value_state.current().to_string());
}
Key::ArrowRight => {
value_state.set((current_value + step).max(min).min(max));
update(value_state.current().to_string());
}
_ => ()
}
},
onmousemove: move |evt| {
let mouse = evt.data;
if !mouse.held_buttons().is_empty(){
let node = tui_query.get(get_root_id(cx).unwrap());
let width = node.size().unwrap().width;
let offset = mouse.element_coordinates();
value_state.set(min + size*(offset.x as f32) / width as f32);
update(value_state.current().to_string());
}
},
div{
width: "{fst_width}%",
background_color: "rgba(10,10,10,0.5)",
}
div{
"|"
}
div{
width: "{snd_width}%",
background_color: "rgba(10,10,10,0.5)",
}
}
}
}
+181
View File
@@ -0,0 +1,181 @@
use crate::widgets::get_root_id;
use crate::Query;
use crossterm::{cursor::*, execute};
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use dioxus_native_core::utils::cursor::{Cursor, Pos};
use std::{collections::HashMap, io::stdout};
use taffy::geometry::Point;
#[derive(Props)]
pub(crate) struct TextBoxProps<'a> {
#[props(!optional)]
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
size: Option<&'a str>,
#[props(!optional)]
max_length: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
let tui_query: Query = cx.consume_context().unwrap();
let tui_query_clone = tui_query.clone();
let text_ref = use_ref(cx, || {
if let Some(intial_text) = cx.props.value {
intial_text.to_string()
} else {
String::new()
}
});
let cursor = use_ref(cx, Cursor::default);
let dragging = use_state(cx, || false);
let text = text_ref.read().clone();
let start_highlight = cursor.read().first().idx(&text);
let end_highlight = cursor.read().last().idx(&text);
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
let (text_highlighted, text_after_second_cursor) =
text_after_first_cursor.split_at(end_highlight - start_highlight);
let max_len = cx
.props
.max_length
.as_ref()
.and_then(|s| s.parse().ok())
.unwrap_or(usize::MAX);
let width = cx
.props
.width
.map(|s| s.to_string())
// px is the same as em in tui
.or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
.unwrap_or_else(|| "10px".to_string());
let height = cx.props.height.unwrap_or("3px");
// don't draw a border unless there is enough space
let border = if width
.strip_suffix("px")
.and_then(|w| w.parse::<i32>().ok())
.filter(|w| *w < 3)
.is_some()
|| height
.strip_suffix("px")
.and_then(|h| h.parse::<i32>().ok())
.filter(|h| *h < 3)
.is_some()
{
"none"
} else {
"solid"
};
render! {
div{
width: "{width}",
height: "{height}",
border_style: "{border}",
onkeydown: move |k| {
if k.key() == Key::Enter {
return;
}
let mut text = text_ref.write();
cursor.write().handle_input(&k, &mut text, max_len);
if let Some(input_handler) = &cx.props.raw_oninput{
input_handler.call(FormData{
value: text.clone(),
values: HashMap::new(),
});
}
let node = tui_query.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
execute!(stdout(), MoveTo(x, y)).unwrap();
}
},
onmousemove: move |evt| {
if *dragging.get() {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
// textboxs are only one line tall
new.row = 0;
if new != cursor.read().start {
cursor.write().end = Some(new);
}
}
},
onmousedown: move |evt| {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
// textboxs are only one line tall
new.row = 0;
new.realize_col(&text_ref.read());
cursor.set(Cursor::from_start(new));
dragging.set(true);
let node = tui_query_clone.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
execute!(stdout(), MoveTo(x, y)).unwrap();
}
},
onmouseup: move |_| {
dragging.set(false);
},
onmouseleave: move |_| {
dragging.set(false);
},
onmouseenter: move |_| {
dragging.set(false);
},
onfocusout: |_| {
execute!(stdout(), MoveTo(0, 1000)).unwrap();
},
"{text_before_first_cursor}"
span{
background_color: "rgba(255, 255, 255, 50%)",
"{text_highlighted}"
}
"{text_after_second_cursor}"
}
}
}
+2 -1
View File
@@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
"web"
"sledgehammer"
] }
js-sys = "0.3.56"
@@ -30,6 +30,7 @@ futures-util = "0.3.19"
smallstr = "0.2.0"
futures-channel = "0.3.21"
serde_json = { version = "1.0" }
serde = { version = "1.0" }
serde-wasm-bindgen = "0.4.5"
[dependencies.web-sys]
+166 -46
View File
@@ -7,24 +7,34 @@
//! - tests to ensure dyn_into works for various event types.
//! - Partial delegation?>
use dioxus_core::{Mutation, Template};
use dioxus_core::{ElementId, Mutation, Template, TemplateAttribute, TemplateNode};
use dioxus_html::{event_bubbles, CompositionData, FormData};
use dioxus_interpreter_js::Interpreter;
use dioxus_interpreter_js::{save_template, Channel};
use futures_channel::mpsc;
use rustc_hash::FxHashMap;
use std::{any::Any, rc::Rc};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{Document, Element, Event};
use web_sys::{Document, Element, Event, HtmlElement};
use crate::Config;
pub struct WebsysDom {
interpreter: Interpreter,
handler: Closure<dyn FnMut(&Event)>,
_root: Element,
document: Document,
templates: FxHashMap<String, u32>,
max_template_id: u32,
interpreter: Channel,
}
pub struct UiEvent {
pub name: String,
pub bubbles: bool,
pub element: ElementId,
pub data: Rc<dyn Any>,
pub event: Event,
}
impl WebsysDom {
pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender<Event>) -> Self {
pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender<UiEvent>) -> Self {
// eventually, we just want to let the interpreter do all the work of decoding events into our event type
// a match here in order to avoid some error during runtime browser test
let document = load_document();
@@ -32,67 +42,158 @@ impl WebsysDom {
Some(root) => root,
None => document.create_element("body").ok().unwrap(),
};
let interpreter = Channel::default();
let handler: Closure<dyn FnMut(&Event)> =
Closure::wrap(Box::new(move |event: &web_sys::Event| {
let name = event.type_();
let element = walk_event_for_id(event);
let bubbles = dioxus_html::event_bubbles(name.as_str());
if let Some((element, target)) = element {
if target
.get_attribute("dioxus-prevent-default")
.as_deref()
.map(|f| f.trim_start_matches("on"))
== Some(&name)
{
event.prevent_default();
}
let data = virtual_event_from_websys_event(event.clone(), target);
let _ = event_channel.unbounded_send(UiEvent {
name,
bubbles,
element,
data,
event: event.clone(),
});
}
}));
dioxus_interpreter_js::initilize(root.unchecked_into(), handler.as_ref().unchecked_ref());
handler.forget();
Self {
interpreter: Interpreter::new(root.clone()),
_root: root,
handler: Closure::wrap(Box::new(move |event: &web_sys::Event| {
let _ = event_channel.unbounded_send(event.clone());
})),
document,
interpreter,
templates: FxHashMap::default(),
max_template_id: 0,
}
}
pub fn mount(&mut self) {
self.interpreter.MountToRoot();
self.interpreter.mount_to_root();
}
pub fn load_templates(&mut self, templates: &[Template]) {
log::debug!("Loading templates {:?}", templates);
for template in templates {
self.interpreter
.SaveTemplate(serde_wasm_bindgen::to_value(&template).unwrap());
let mut roots = vec![];
for root in template.roots {
roots.push(self.create_template_node(root))
}
self.templates
.insert(template.name.to_owned(), self.max_template_id);
save_template(roots, self.max_template_id);
self.max_template_id += 1
}
}
fn create_template_node(&self, v: &TemplateNode) -> web_sys::Node {
use TemplateNode::*;
match v {
Element {
tag,
namespace,
attrs,
children,
..
} => {
let el = match namespace {
Some(ns) => self.document.create_element_ns(Some(ns), tag).unwrap(),
None => self.document.create_element(tag).unwrap(),
};
for attr in *attrs {
if let TemplateAttribute::Static {
name,
value,
namespace,
} = attr
{
match namespace {
Some(ns) if *ns == "style" => el
.dyn_ref::<HtmlElement>()
.unwrap()
.style()
.set_property(name, value)
.unwrap(),
Some(ns) => el.set_attribute_ns(Some(ns), name, value).unwrap(),
None => el.set_attribute(name, value).unwrap(),
}
}
}
for child in *children {
let _ = el.append_child(&self.create_template_node(child));
}
el.dyn_into().unwrap()
}
Text { text } => self.document.create_text_node(text).dyn_into().unwrap(),
DynamicText { .. } => self.document.create_text_node("p").dyn_into().unwrap(),
Dynamic { .. } => {
let el = self.document.create_element("pre").unwrap();
let _ = el.toggle_attribute("hidden");
el.dyn_into().unwrap()
}
}
}
pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
use Mutation::*;
let i = &self.interpreter;
for edit in edits.drain(..) {
let i = &mut self.interpreter;
for edit in &edits {
match edit {
AppendChildren { id, m } => i.AppendChildren(m as u32, id.0 as u32),
AssignId { path, id } => i.AssignId(path, id.0 as u32),
CreatePlaceholder { id } => i.CreatePlaceholder(id.0 as u32),
CreateTextNode { value, id } => i.CreateTextNode(value.into(), id.0 as u32),
HydrateText { path, value, id } => i.HydrateText(path, value, id.0 as u32),
LoadTemplate { name, index, id } => i.LoadTemplate(name, index as u32, id.0 as u32),
ReplaceWith { id, m } => i.ReplaceWith(id.0 as u32, m as u32),
ReplacePlaceholder { path, m } => i.ReplacePlaceholder(path, m as u32),
InsertAfter { id, m } => i.InsertAfter(id.0 as u32, m as u32),
InsertBefore { id, m } => i.InsertBefore(id.0 as u32, m as u32),
AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
AssignId { path, id } => {
i.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32)
}
CreatePlaceholder { id } => i.create_placeholder(id.0 as u32),
CreateTextNode { value, id } => i.create_text_node(value, id.0 as u32),
HydrateText { path, value, id } => {
i.hydrate_text(path.as_ptr() as u32, path.len() as u8, value, id.0 as u32)
}
LoadTemplate { name, index, id } => {
if let Some(tmpl_id) = self.templates.get(*name) {
i.load_template(*tmpl_id, *index as u32, id.0 as u32)
}
}
ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u32),
ReplacePlaceholder { path, m } => {
i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u32)
}
InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u32),
InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u32),
SetAttribute {
name,
value,
id,
ns,
} => i.SetAttribute(id.0 as u32, name, value.into(), ns),
} => i.set_attribute(id.0 as u32, name, value, ns.unwrap_or_default()),
SetBoolAttribute { name, value, id } => {
i.SetBoolAttribute(id.0 as u32, name, value)
i.set_attribute(id.0 as u32, name, if *value { "true" } else { "false" }, "")
}
SetText { value, id } => i.SetText(id.0 as u32, value.into()),
SetText { value, id } => i.set_text(id.0 as u32, value),
NewEventListener { name, id, .. } => {
self.interpreter.NewEventListener(
name,
id.0 as u32,
event_bubbles(&name[2..]),
self.handler.as_ref().unchecked_ref(),
);
i.new_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8);
}
RemoveEventListener { name, id } => i.RemoveEventListener(name, id.0 as u32),
Remove { id } => i.Remove(id.0 as u32),
PushRoot { id } => i.PushRoot(id.0 as u32),
RemoveEventListener { name, id } => {
i.remove_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8)
}
Remove { id } => i.remove(id.0 as u32),
PushRoot { id } => i.push_root(id.0 as u32),
}
}
edits.clear();
i.flush();
}
}
@@ -230,9 +331,28 @@ fn read_input_to_data(target: Element) -> Rc<FormData> {
}
}
Rc::new(FormData {
value,
values,
files: None,
})
Rc::new(FormData { value, values })
}
fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> {
use wasm_bindgen::JsCast;
let mut target = event
.target()
.expect("missing target")
.dyn_into::<web_sys::Element>()
.expect("not a valid element");
loop {
match target.get_attribute("data-dioxus-id").map(|f| f.parse()) {
Some(Ok(id)) => return Some((ElementId(id), target)),
Some(Err(_)) => return None,
// walk the tree upwards until we actually find an event target
None => match target.parent_element() {
Some(parent) => target = parent,
None => return None,
},
}
}
}
+7 -35
View File
@@ -55,7 +55,7 @@
pub use crate::cfg::Config;
use crate::dom::virtual_event_from_websys_event;
pub use crate::util::use_eval;
pub use crate::util::{use_eval, EvalResult};
use dioxus_core::{Element, ElementId, Scope, VirtualDom};
use futures_util::{pin_mut, FutureExt, StreamExt};
@@ -195,7 +195,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
// the mutations come back with nothing - we need to actually mount them
websys_dom.mount();
let mut work_loop = ric_raf::RafLoop::new();
let _work_loop = ric_raf::RafLoop::new();
loop {
log::debug!("waiting for work");
@@ -222,16 +222,11 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
// Dequeue all of the events from the channel in send order
// todo: we should re-order these if possible
while let Some(evt) = res {
let name = evt.type_();
let element = walk_event_for_id(&evt);
let bubbles = dioxus_html::event_bubbles(name.as_str());
if let Some((element, target)) = element {
let data = virtual_event_from_websys_event(evt, target);
dom.handle_event(name.as_str(), data, element, bubbles);
}
dom.handle_event(evt.name.as_str(), evt.data, evt.element, evt.bubbles);
res = rx.try_next().transpose().unwrap().ok();
}
// Todo: This is currently disabled because it has a negative impact on responce times for events but it could be re-enabled for tasks
// Jank free rendering
//
// 1. wait for the browser to give us "idle" time
@@ -240,42 +235,19 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
// 4. Wait for the animation frame to patch the dom
// wait for the mainthread to schedule us in
let deadline = work_loop.wait_for_idle_time().await;
// let deadline = work_loop.wait_for_idle_time().await;
// run the virtualdom work phase until the frame deadline is reached
let edits = dom.render_with_deadline(deadline).await;
let edits = dom.render_immediate();
// wait for the animation frame to fire so we can apply our changes
work_loop.wait_for_raf().await;
// work_loop.wait_for_raf().await;
websys_dom.load_templates(&edits.templates);
websys_dom.apply_edits(edits.edits);
}
}
fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> {
use wasm_bindgen::JsCast;
let mut target = event
.target()
.expect("missing target")
.dyn_into::<web_sys::Element>()
.expect("not a valid element");
loop {
match target.get_attribute("data-dioxus-id").map(|f| f.parse()) {
Some(Ok(id)) => return Some((ElementId(id), target)),
Some(Err(_)) => return None,
// walk the tree upwards until we actually find an event target
None => match target.parent_element() {
Some(parent) => target = parent,
None => return None,
},
}
}
}
// if should_hydrate {
// // todo: we need to split rebuild and initialize into two phases
// // it's a waste to produce edits just to get the vdom loaded
+50 -4
View File
@@ -1,6 +1,13 @@
//! Utilities specific to websys
use std::{
future::{IntoFuture, Ready},
str::FromStr,
};
use dioxus_core::*;
use serde::de::Error;
use serde_json::Value;
/// Get a closure that executes any JavaScript in the webpage.
///
@@ -15,12 +22,51 @@ use dioxus_core::*;
///
/// The closure will panic if the provided script is not valid JavaScript code
/// or if it returns an uncaught error.
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) {
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult {
cx.use_hook(|| {
|script: S| {
js_sys::Function::new_no_args(&script.to_string())
.call0(&wasm_bindgen::JsValue::NULL)
.expect("failed to eval script");
let body = script.to_string();
EvalResult {
value: if let Ok(value) =
js_sys::Function::new_no_args(&body).call0(&wasm_bindgen::JsValue::NULL)
{
if let Ok(stringified) = js_sys::JSON::stringify(&value) {
if !stringified.is_undefined() && stringified.is_valid_utf16() {
let string: String = stringified.into();
Value::from_str(&string)
} else {
Err(serde_json::Error::custom("Failed to stringify result"))
}
} else {
Err(serde_json::Error::custom("Failed to stringify result"))
}
} else {
Err(serde_json::Error::custom("Failed to execute script"))
},
}
}
})
}
/// A wrapper around the result of a JavaScript evaluation.
/// This implements IntoFuture to be compatible with the desktop renderer's EvalResult.
pub struct EvalResult {
value: Result<Value, serde_json::Error>,
}
impl EvalResult {
/// Get the result of the Javascript execution.
pub fn get(self) -> Result<Value, serde_json::Error> {
self.value
}
}
impl IntoFuture for EvalResult {
type Output = Result<Value, serde_json::Error>;
type IntoFuture = Ready<Result<Value, serde_json::Error>>;
fn into_future(self) -> Self::IntoFuture {
std::future::ready(self.value)
}
}