Merge branch 'upstream-master' into fix-hot-reloading
This commit is contained in:
commit
14dd568987
92 files changed
+3469
-2468
No files matched your search
@@ -13,7 +13,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: docs
|
environment: docs
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# NOTE: Comment out when https://github.com/rust-lang/mdBook/pull/1306 is merged and released
|
# NOTE: Comment out when https://github.com/rust-lang/mdBook/pull/1306 is merged and released
|
||||||
# - name: Setup mdBook
|
# - name: Setup mdBook
|
||||||
@@ -25,6 +24,7 @@ jobs:
|
|||||||
- name: Setup mdBook
|
- name: Setup mdBook
|
||||||
run: |
|
run: |
|
||||||
cargo install mdbook --git https://github.com/Ruin0x11/mdBook.git --branch localization --rev e74fdb1
|
cargo install mdbook --git https://github.com/Ruin0x11/mdBook.git --branch localization --rev e74fdb1
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cd docs &&
|
run: cd docs &&
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ jobs:
|
|||||||
name: Test Suite
|
name: Test Suite
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
override: true
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- uses: actions/checkout@v3
|
||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ jobs:
|
|||||||
name: Check
|
name: Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
@@ -41,6 +40,7 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: sudo apt-get update
|
- run: sudo apt-get update
|
||||||
- run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
|
- run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
|
||||||
|
- uses: actions/checkout@v3
|
||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: check
|
command: check
|
||||||
@@ -51,7 +51,6 @@ jobs:
|
|||||||
name: Test Suite
|
name: Test Suite
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
@@ -63,6 +62,7 @@ jobs:
|
|||||||
- uses: davidB/rust-cargo-make@v1
|
- uses: davidB/rust-cargo-make@v1
|
||||||
- uses: browser-actions/setup-firefox@latest
|
- uses: browser-actions/setup-firefox@latest
|
||||||
- uses: jetli/wasm-pack-action@v0.4.0
|
- uses: jetli/wasm-pack-action@v0.4.0
|
||||||
|
- uses: actions/checkout@v3
|
||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: make
|
command: make
|
||||||
@@ -73,7 +73,6 @@ jobs:
|
|||||||
name: Rustfmt
|
name: Rustfmt
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
@@ -81,6 +80,7 @@ jobs:
|
|||||||
override: true
|
override: true
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: rustup component add rustfmt
|
- run: rustup component add rustfmt
|
||||||
|
- uses: actions/checkout@v3
|
||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: fmt
|
command: fmt
|
||||||
@@ -91,7 +91,6 @@ jobs:
|
|||||||
name: Clippy
|
name: Clippy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
@@ -101,6 +100,7 @@ jobs:
|
|||||||
- run: sudo apt-get update
|
- run: sudo apt-get update
|
||||||
- run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
|
- run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
|
||||||
- run: rustup component add clippy
|
- run: rustup component add clippy
|
||||||
|
- uses: actions/checkout@v3
|
||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: clippy
|
command: clippy
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ jobs:
|
|||||||
# which causes failures for some of rustfmt's line-ending sensitive tests
|
# which causes failures for some of rustfmt's line-ending sensitive tests
|
||||||
- name: disable git eol translation
|
- name: disable git eol translation
|
||||||
run: git config --global core.autocrlf false
|
run: git config --global core.autocrlf false
|
||||||
- name: checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Run build
|
# Run build
|
||||||
- name: Install Rustup using win.rustup.rs
|
- name: Install Rustup using win.rustup.rs
|
||||||
@@ -66,6 +64,9 @@ jobs:
|
|||||||
if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly'
|
if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: |
|
run: |
|
||||||
rustc -Vv
|
rustc -Vv
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ reqwest = { version = "0.11.9", features = ["json"] }
|
|||||||
fern = { version = "0.6.0", features = ["colored"] }
|
fern = { version = "0.6.0", features = ["colored"] }
|
||||||
thiserror = "1.0.30"
|
thiserror = "1.0.30"
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.0"
|
||||||
|
simple_logger = "4.0.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
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 nf = NodeFactory::new(cx);
|
||||||
|
|
||||||
// let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump());
|
// 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));
|
// attrs.push(nf.attr("age", format_args!("47"), None, false));
|
||||||
|
|
||||||
// Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None))
|
// Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn component(cx: Scope) -> Element {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
+14
-1
@@ -1,4 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_desktop::EvalResult;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus_desktop::launch(app);
|
dioxus_desktop::launch(app);
|
||||||
@@ -7,6 +8,15 @@ fn main() {
|
|||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let script = use_state(cx, String::new);
|
let script = use_state(cx, String::new);
|
||||||
let eval = dioxus_desktop::use_eval(cx);
|
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! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
@@ -16,7 +26,10 @@ fn app(cx: Scope) -> Element {
|
|||||||
oninput: move |e| script.set(e.value.clone()),
|
oninput: move |e| script.set(e.value.clone()),
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
onclick: move |_| eval(script.to_string()),
|
onclick: move |_| {
|
||||||
|
let fut = eval(script);
|
||||||
|
future.set(Some(fut));
|
||||||
|
},
|
||||||
"Execute"
|
"Execute"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
//! A tour of the rsx! macro
|
//! A tour of the rsx! macro
|
||||||
//! ------------------------
|
//! ------------------------
|
||||||
//!
|
//!
|
||||||
@@ -39,7 +38,7 @@
|
|||||||
//! - Accept a list of vnodes as children for a Fragment component
|
//! - Accept a list of vnodes as children for a Fragment component
|
||||||
//! - Allow keyed fragments in iterators
|
//! - Allow keyed fragments in iterators
|
||||||
//! - Allow top-level fragments
|
//! - Allow top-level fragments
|
||||||
//!
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus_desktop::launch(app);
|
dioxus_desktop::launch(app);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -67,6 +67,12 @@ fn app(cx: Scope) -> Element {
|
|||||||
stroke: "blue",
|
stroke: "blue",
|
||||||
stroke_width: "5",
|
stroke_width: "5",
|
||||||
}
|
}
|
||||||
|
path {
|
||||||
|
d: "M9.00001 9C9 62 103.5 124 103.5 178",
|
||||||
|
stroke: "#3CC4DC",
|
||||||
|
"stroke-linecap": "square",
|
||||||
|
"stroke-width": "square",
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ proc-macro = true
|
|||||||
proc-macro2 = { version = "1.0" }
|
proc-macro2 = { version = "1.0" }
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||||
dioxus-rsx = { path = "../rsx" }
|
dioxus-rsx = { path = "../rsx", version = "0.0.1" }
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -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.
|
/// 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]
|
#[proc_macro]
|
||||||
pub fn rsx(s: TokenStream) -> TokenStream {
|
pub fn rsx(s: TokenStream) -> TokenStream {
|
||||||
match syn::parse::<rsx::CallBody>(s) {
|
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 makes it easy for developers to write jsx-style markup in their components.
|
||||||
///
|
///
|
||||||
/// The render macro automatically renders rsx - making it unhygenic.
|
/// The render macro automatically renders rsx - making it unhygenic.
|
||||||
///
|
|
||||||
/// ## Complete Reference Guide:
|
|
||||||
/// ```ignore
|
|
||||||
#[doc = include_str!("../../../examples/rsx_usage.rs")]
|
|
||||||
/// ```
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn render(s: TokenStream) -> TokenStream {
|
pub fn render(s: TokenStream) -> TokenStream {
|
||||||
match syn::parse::<rsx::CallBody>(s) {
|
match syn::parse::<rsx::CallBody>(s) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
|
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
|
||||||
|
|
||||||
# faster hashmaps
|
# faster hashmaps
|
||||||
fxhash = "0.2"
|
rustc-hash = "1.1.0"
|
||||||
|
|
||||||
# Used in diffing
|
# Used in diffing
|
||||||
longest-increasing-subsequence = "0.1.0"
|
longest-increasing-subsequence = "0.1.0"
|
||||||
@@ -35,6 +35,9 @@ indexmap = "1.7"
|
|||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
anyhow = "1.0.66"
|
anyhow = "1.0.66"
|
||||||
|
|
||||||
|
smallbox = "0.8.1"
|
||||||
|
log = "0.4.17"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "*", features = ["full"] }
|
tokio = { version = "*", features = ["full"] }
|
||||||
dioxus = { path = "../dioxus" }
|
dioxus = { path = "../dioxus" }
|
||||||
|
|||||||
+18
-11
@@ -34,14 +34,14 @@ impl ElementRef {
|
|||||||
|
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
|
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 {
|
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 entry = self.elements.vacant_entry();
|
||||||
let id = entry.key();
|
let id = entry.key();
|
||||||
|
|
||||||
@@ -95,22 +95,27 @@ impl VirtualDom {
|
|||||||
fn drop_scope_inner(&mut self, node: &VNode) {
|
fn drop_scope_inner(&mut self, node: &VNode) {
|
||||||
node.clear_listeners();
|
node.clear_listeners();
|
||||||
node.dynamic_nodes.iter().for_each(|node| match node {
|
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) => {
|
DynamicNode::Fragment(nodes) => {
|
||||||
nodes.iter().for_each(|node| self.drop_scope_inner(node))
|
nodes.iter().for_each(|node| self.drop_scope_inner(node))
|
||||||
}
|
}
|
||||||
DynamicNode::Placeholder(t) => {
|
DynamicNode::Placeholder(t) => {
|
||||||
self.try_reclaim(t.get());
|
self.try_reclaim(t.id.get().unwrap());
|
||||||
}
|
}
|
||||||
DynamicNode::Text(t) => {
|
DynamicNode::Text(t) => {
|
||||||
self.try_reclaim(t.id.get());
|
self.try_reclaim(t.id.get().unwrap());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for root in node.root_ids {
|
for root in node.root_ids {
|
||||||
let id = root.get();
|
if let Some(id) = root.get() {
|
||||||
if id.0 != 0 {
|
if id.0 != 0 {
|
||||||
self.try_reclaim(id);
|
self.try_reclaim(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,8 +136,10 @@ impl VirtualDom {
|
|||||||
node.dynamic_nodes.iter().for_each(|child| match child {
|
node.dynamic_nodes.iter().for_each(|child| match child {
|
||||||
// Only descend if the props are borrowed
|
// Only descend if the props are borrowed
|
||||||
DynamicNode::Component(c) if !c.static_props => {
|
DynamicNode::Component(c) if !c.static_props => {
|
||||||
self.ensure_drop_safety(c.scope.get().unwrap());
|
if let Some(scope) = c.scope.get() {
|
||||||
c.props.set(None);
|
self.ensure_drop_safety(scope);
|
||||||
|
}
|
||||||
|
c.props.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
DynamicNode::Fragment(f) => f
|
DynamicNode::Fragment(f) => f
|
||||||
|
|||||||
+253
-179
@@ -1,13 +1,15 @@
|
|||||||
use std::cell::Cell;
|
use crate::innerlude::{VComponent, VPlaceholder, VText};
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::innerlude::{VComponent, VText};
|
|
||||||
use crate::mutations::Mutation;
|
use crate::mutations::Mutation;
|
||||||
use crate::mutations::Mutation::*;
|
use crate::mutations::Mutation::*;
|
||||||
use crate::nodes::VNode;
|
use crate::nodes::VNode;
|
||||||
use crate::nodes::{DynamicNode, TemplateNode};
|
use crate::nodes::{DynamicNode, TemplateNode};
|
||||||
use crate::virtual_dom::VirtualDom;
|
use crate::virtual_dom::VirtualDom;
|
||||||
use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext};
|
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 {
|
impl<'b> VirtualDom {
|
||||||
/// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
|
/// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
|
||||||
@@ -17,187 +19,232 @@ impl<'b> VirtualDom {
|
|||||||
self.scope_stack.push(scope);
|
self.scope_stack.push(scope);
|
||||||
let out = self.create(template);
|
let out = self.create(template);
|
||||||
self.scope_stack.pop();
|
self.scope_stack.pop();
|
||||||
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create this template and write its mutations
|
/// 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
|
// The best renderers will have templates prehydrated and registered
|
||||||
// Just in case, let's create the template using instructions anyways
|
// Just in case, let's create the template using instructions anyways
|
||||||
if !self.templates.contains_key(&template.template.name) {
|
if !self.templates.contains_key(&node.template.name) {
|
||||||
self.register_template(template);
|
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
|
// Walk the roots, creating nodes and assigning IDs
|
||||||
// todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS)
|
// 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 attrs = node.template.attr_paths.iter().enumerate().peekable();
|
||||||
let mut dynamic_nodes = template.template.node_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;
|
fn write_static_text_root(&mut self, node: &VNode, idx: usize) -> usize {
|
||||||
for (root_idx, root) in template.template.roots.iter().enumerate() {
|
// Simply just load the template root, no modifications needed
|
||||||
// We might need to generate an ID for the root node
|
self.load_template_root(node, idx);
|
||||||
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);
|
|
||||||
|
|
||||||
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
// Text producs just one node on the stack
|
||||||
let unbounded_text = unsafe { std::mem::transmute(*value) };
|
1
|
||||||
self.mutations.push(CreateTextNode {
|
}
|
||||||
value: unbounded_text,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
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) => {
|
fn create_static_text(&mut self, value: &str, id: ElementId) {
|
||||||
let id = self.next_element(template, template.template.node_paths[*id]);
|
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||||
slot.set(id);
|
let unbounded_text: &str = unsafe { std::mem::transmute(value) };
|
||||||
self.mutations.push(CreatePlaceholder { id });
|
self.mutations.push(CreateTextNode {
|
||||||
1
|
value: unbounded_text,
|
||||||
}
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
DynamicNode::Fragment(_) | DynamicNode::Component { .. } => {
|
/// We write all the descndent data for this element
|
||||||
self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id)
|
///
|
||||||
}
|
/// 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 { .. } => {
|
fn write_attribute(&mut self, attribute: &crate::Attribute, id: ElementId) {
|
||||||
let this_id = self.next_root(template, root_idx);
|
// Make sure we set the attribute's associated id
|
||||||
|
attribute.mounted_element.set(id);
|
||||||
|
|
||||||
template.root_ids[root_idx].set(this_id);
|
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||||
self.mutations.push(LoadTemplate {
|
let unbounded_name = unsafe { std::mem::transmute(attribute.name) };
|
||||||
name: template.template.name,
|
|
||||||
index: root_idx,
|
|
||||||
id: this_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// we're on top of a node that has a dynamic attribute for a descendant
|
match &attribute.value {
|
||||||
// Set that attribute now before the stack gets in a weird state
|
AttributeValue::Text(value) => {
|
||||||
while let Some((mut attr_id, path)) =
|
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||||
dynamic_attrs.next_if(|(_, p)| p[0] == root_idx as u8)
|
let unbounded_value = unsafe { std::mem::transmute(*value) };
|
||||||
{
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
self.mutations.push(SetAttribute {
|
||||||
let attribute = template.dynamic_attrs.get(attr_id).unwrap();
|
name: unbounded_name,
|
||||||
attribute.mounted_element.set(id);
|
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
|
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
|
||||||
let unbounded_name = unsafe { std::mem::transmute(attribute.name) };
|
// 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 {
|
self.mutations.push(LoadTemplate {
|
||||||
AttributeValue::Text(value) => {
|
name: template.template.name,
|
||||||
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
index: root_idx,
|
||||||
let unbounded_value = unsafe { std::mem::transmute(*value) };
|
id: this_id,
|
||||||
|
});
|
||||||
|
|
||||||
self.mutations.push(SetAttribute {
|
this_id
|
||||||
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!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only push the dynamic attributes forward if they match the current path (same element)
|
/// We have some dynamic attributes attached to a some node
|
||||||
match dynamic_attrs.next_if(|(_, p)| *p == path) {
|
///
|
||||||
Some((next_attr_id, _)) => attr_id = next_attr_id,
|
/// That node needs to be loaded at runtime, so we need to give it an ID
|
||||||
None => break,
|
///
|
||||||
}
|
/// 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(
|
||||||
// We're on top of a node that has a dynamic child for a descendant
|
&mut self,
|
||||||
// Skip any node that's a root
|
path: &'static [u8],
|
||||||
let mut start = None;
|
this_id: ElementId,
|
||||||
let mut end = None;
|
template: &VNode,
|
||||||
|
attr_id: usize,
|
||||||
// Collect all the dynamic nodes below this root
|
) -> ElementId {
|
||||||
// We assign the start and end of the range of dynamic nodes since they area ordered in terms of tree path
|
if path.len() == 1 {
|
||||||
//
|
return this_id;
|
||||||
// [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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
/// Insert a new template into the VirtualDom's template registry
|
||||||
@@ -207,17 +254,9 @@ impl<'b> VirtualDom {
|
|||||||
.insert(template.template.name, template.template);
|
.insert(template.template.name, template.template);
|
||||||
|
|
||||||
// If it's all dynamic nodes, then we don't need to register it
|
// 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.is_completely_dynamic() {
|
||||||
if template.template.roots.iter().all(|root| {
|
self.mutations.templates.push(template.template);
|
||||||
matches!(
|
|
||||||
root,
|
|
||||||
TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. }
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mutations.templates.push(template.template);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn create_dynamic_node(
|
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]);
|
let new_id = self.next_element(template, template.template.node_paths[idx]);
|
||||||
|
|
||||||
// Make sure the text node is assigned to the correct element
|
// 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
|
// 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) };
|
let value = unsafe { std::mem::transmute(text.value) };
|
||||||
@@ -263,7 +302,7 @@ impl<'b> VirtualDom {
|
|||||||
|
|
||||||
pub(crate) fn create_placeholder(
|
pub(crate) fn create_placeholder(
|
||||||
&mut self,
|
&mut self,
|
||||||
slot: &Cell<ElementId>,
|
placeholder: &VPlaceholder,
|
||||||
template: &'b VNode<'b>,
|
template: &'b VNode<'b>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
@@ -271,7 +310,7 @@ impl<'b> VirtualDom {
|
|||||||
let id = self.next_element(template, template.template.node_paths[idx]);
|
let id = self.next_element(template, template.template.node_paths[idx]);
|
||||||
|
|
||||||
// Make sure the text node is assigned to the correct element
|
// 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
|
// Assign the ID to the existing node in the template
|
||||||
self.mutations.push(AssignId {
|
self.mutations.push(AssignId {
|
||||||
@@ -284,7 +323,7 @@ impl<'b> VirtualDom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
|
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(
|
pub(super) fn create_component_node(
|
||||||
@@ -293,15 +332,17 @@ impl<'b> VirtualDom {
|
|||||||
component: &'b VComponent<'b>,
|
component: &'b VComponent<'b>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let props = component
|
let scope = match component.props.take() {
|
||||||
.props
|
Some(props) => {
|
||||||
.replace(None)
|
let unbounded_props = unsafe { std::mem::transmute(props) };
|
||||||
.expect("Props to always exist when a component is being created");
|
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));
|
component.scope.set(Some(scope));
|
||||||
|
|
||||||
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
||||||
@@ -391,4 +432,37 @@ impl<'b> VirtualDom {
|
|||||||
|
|
||||||
0
|
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
@@ -1,17 +1,15 @@
|
|||||||
use std::cell::Cell;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arena::ElementId,
|
arena::ElementId,
|
||||||
innerlude::{DirtyScope, VComponent, VText},
|
innerlude::{DirtyScope, VComponent, VPlaceholder, VText},
|
||||||
mutations::Mutation,
|
mutations::Mutation,
|
||||||
nodes::RenderReturn,
|
nodes::RenderReturn,
|
||||||
nodes::{DynamicNode, VNode},
|
nodes::{DynamicNode, VNode},
|
||||||
scopes::ScopeId,
|
scopes::ScopeId,
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
AttributeValue, TemplateNode,
|
Attribute, AttributeValue, TemplateNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use fxhash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use DynamicNode::*;
|
use DynamicNode::*;
|
||||||
|
|
||||||
impl<'b> VirtualDom {
|
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_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>) {
|
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);
|
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
|
.dynamic_attrs
|
||||||
.iter()
|
.iter()
|
||||||
.zip(right_template.dynamic_attrs.iter())
|
.zip(right_template.dynamic_attrs.iter())
|
||||||
{
|
.for_each(|(left_attr, right_attr)| {
|
||||||
// Move over the ID from the old to the new
|
// Move over the ID from the old to the new
|
||||||
right_attr
|
right_attr
|
||||||
.mounted_element
|
.mounted_element
|
||||||
.set(left_attr.mounted_element.get());
|
.set(left_attr.mounted_element.get());
|
||||||
|
|
||||||
// We want to make sure anything listener that gets pulled is valid
|
// We want to make sure anything listener that gets pulled is valid
|
||||||
if let AttributeValue::Listener(_) = right_attr.value {
|
if let AttributeValue::Listener(_) = right_attr.value {
|
||||||
self.update_template(left_attr.mounted_element.get(), right_template);
|
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"),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
.dynamic_nodes
|
||||||
.iter()
|
.iter()
|
||||||
.zip(right_template.dynamic_nodes.iter())
|
.zip(right_template.dynamic_nodes.iter())
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
.for_each(|(idx, (left_node, right_node))| {
|
||||||
match (left_node, right_node) {
|
self.diff_dynamic_node(left_node, right_node, right_template, idx);
|
||||||
(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!(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the roots get transferred over
|
// Make sure the roots get transferred over while we're here
|
||||||
for (left, right) in left_template
|
left_template
|
||||||
.root_ids
|
.root_ids
|
||||||
.iter()
|
.iter()
|
||||||
.zip(right_template.root_ids.iter())
|
.zip(right_template.root_ids.iter())
|
||||||
{
|
.for_each(|(left, right)| right.set(left.get()));
|
||||||
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 m = self.create_children(r);
|
||||||
let id = l.get();
|
let id = l.id.get().unwrap();
|
||||||
self.mutations.push(Mutation::ReplaceWith { id, m });
|
self.mutations.push(Mutation::ReplaceWith { id, m });
|
||||||
self.reclaim(id);
|
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
|
// Remove the old nodes, except for one
|
||||||
self.remove_nodes(&l[1..]);
|
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
|
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder
|
||||||
let placeholder = self.next_element(&l[0], &[]);
|
let placeholder = self.next_element(&l[0], &[]);
|
||||||
r.set(placeholder);
|
r.id.set(Some(placeholder));
|
||||||
self.mutations
|
self.mutations
|
||||||
.push(Mutation::CreatePlaceholder { id: placeholder });
|
.push(Mutation::CreatePlaceholder { id: placeholder });
|
||||||
|
|
||||||
@@ -162,6 +174,10 @@ impl<'b> VirtualDom {
|
|||||||
right_template: &'b VNode<'b>,
|
right_template: &'b VNode<'b>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
) {
|
) {
|
||||||
|
if std::ptr::eq(left, right) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Replace components that have different render fns
|
// Replace components that have different render fns
|
||||||
if left.render_fn != right.render_fn {
|
if left.render_fn != right.render_fn {
|
||||||
let created = self.create_component_node(right_template, right, idx);
|
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
|
// 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));
|
right.scope.set(Some(scope_id));
|
||||||
|
|
||||||
// copy out the box for both
|
// copy out the box for both
|
||||||
let old = self.scopes[scope_id.0].props.as_ref();
|
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
|
// 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
|
// 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
|
// Now run the component and diff it
|
||||||
self.run_scope(scope_id);
|
self.run_scope(scope_id);
|
||||||
self.diff_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.
|
/// 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
|
/// 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.
|
/// different.
|
||||||
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
|
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) {
|
||||||
let id = left.id.get();
|
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 {
|
if left.value != right.value {
|
||||||
let value = unsafe { std::mem::transmute(right.value) };
|
let value = unsafe { std::mem::transmute(right.value) };
|
||||||
self.mutations.push(Mutation::SetText { id, value });
|
self.mutations.push(Mutation::SetText { id, value });
|
||||||
@@ -269,9 +303,9 @@ impl<'b> VirtualDom {
|
|||||||
/// All IDs will be garbage collected
|
/// All IDs will be garbage collected
|
||||||
fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId {
|
fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId {
|
||||||
let id = match node.dynamic_root(0) {
|
let id = match node.dynamic_root(0) {
|
||||||
None => node.root_ids[0].get(),
|
None => node.root_ids[0].get().unwrap(),
|
||||||
Some(Text(t)) => t.id.get(),
|
Some(Text(t)) => t.id.get().unwrap(),
|
||||||
Some(Placeholder(e)) => e.get(),
|
Some(Placeholder(e)) => e.id.get().unwrap(),
|
||||||
Some(Fragment(nodes)) => {
|
Some(Fragment(nodes)) => {
|
||||||
let id = self.replace_inner(&nodes[0]);
|
let id = self.replace_inner(&nodes[0]);
|
||||||
self.remove_nodes(&nodes[1..]);
|
self.remove_nodes(&nodes[1..]);
|
||||||
@@ -309,14 +343,23 @@ impl<'b> VirtualDom {
|
|||||||
|
|
||||||
match dyn_node {
|
match dyn_node {
|
||||||
Component(comp) => {
|
Component(comp) => {
|
||||||
let scope = comp.scope.get().unwrap();
|
if let Some(scope) = comp.scope.take() {
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||||
RenderReturn::Sync(Ok(t)) => self.clean_up_node(t),
|
RenderReturn::Sync(Ok(t)) => self.clean_up_node(t),
|
||||||
_ => todo!("cannot handle nonstandard nodes"),
|
_ => 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)),
|
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) {
|
fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) {
|
||||||
match node.dynamic_root(idx) {
|
match node.dynamic_root(idx) {
|
||||||
Some(Text(i)) => {
|
Some(Text(i)) => {
|
||||||
let id = i.id.get();
|
let id = i.id.take().unwrap();
|
||||||
self.mutations.push(Mutation::Remove { id });
|
self.mutations.push(Mutation::Remove { id });
|
||||||
self.reclaim(id);
|
self.reclaim(id);
|
||||||
}
|
}
|
||||||
Some(Placeholder(e)) => {
|
Some(Placeholder(e)) => {
|
||||||
let id = e.get();
|
let id = e.id.take().unwrap();
|
||||||
self.mutations.push(Mutation::Remove { id });
|
self.mutations.push(Mutation::Remove { id });
|
||||||
self.reclaim(id);
|
self.reclaim(id);
|
||||||
}
|
}
|
||||||
Some(Fragment(nodes)) => self.remove_nodes(nodes),
|
Some(Fragment(nodes)) => self.remove_nodes(nodes),
|
||||||
Some(Component(comp)) => {
|
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() } {
|
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||||
RenderReturn::Sync(Ok(t)) => self.remove_node(t),
|
RenderReturn::Sync(Ok(t)) => self.remove_node(t),
|
||||||
_ => todo!("cannot handle nonstandard nodes"),
|
_ => todo!("cannot handle nonstandard nodes"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let id = node.root_ids[idx].get();
|
let id = node.root_ids[idx].get().unwrap();
|
||||||
self.mutations.push(Mutation::Remove { id });
|
self.mutations.push(Mutation::Remove { id });
|
||||||
self.reclaim(id);
|
self.reclaim(id);
|
||||||
}
|
}
|
||||||
@@ -432,7 +475,7 @@ impl<'b> VirtualDom {
|
|||||||
// The stack is empty upon entry.
|
// The stack is empty upon entry.
|
||||||
fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
|
fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
|
||||||
if cfg!(debug_assertions) {
|
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>]| {
|
let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
|
||||||
keys.clear();
|
keys.clear();
|
||||||
for child in children {
|
for child in children {
|
||||||
@@ -748,12 +791,16 @@ impl<'b> VirtualDom {
|
|||||||
fn remove_node(&mut self, node: &'b VNode<'b>) {
|
fn remove_node(&mut self, node: &'b VNode<'b>) {
|
||||||
for (idx, _) in node.template.roots.iter().enumerate() {
|
for (idx, _) in node.template.roots.iter().enumerate() {
|
||||||
let id = match node.dynamic_root(idx) {
|
let id = match node.dynamic_root(idx) {
|
||||||
Some(Text(t)) => t.id.get(),
|
Some(Text(t)) => t.id.take(),
|
||||||
Some(Placeholder(t)) => t.get(),
|
Some(Placeholder(t)) => t.id.take(),
|
||||||
Some(Fragment(t)) => return self.remove_nodes(t),
|
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(),
|
None => node.root_ids[idx].get(),
|
||||||
};
|
}
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
self.mutations.push(Mutation::Remove { id })
|
self.mutations.push(Mutation::Remove { id })
|
||||||
}
|
}
|
||||||
@@ -761,7 +808,7 @@ impl<'b> VirtualDom {
|
|||||||
self.clean_up_node(node);
|
self.clean_up_node(node);
|
||||||
|
|
||||||
for root in node.root_ids {
|
for root in node.root_ids {
|
||||||
let id = root.get();
|
let id = root.get().unwrap();
|
||||||
if id.0 != 0 {
|
if id.0 != 0 {
|
||||||
self.reclaim(id);
|
self.reclaim(id);
|
||||||
}
|
}
|
||||||
@@ -784,41 +831,45 @@ impl<'b> VirtualDom {
|
|||||||
|
|
||||||
/// Push all the real nodes on the stack
|
/// Push all the real nodes on the stack
|
||||||
fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
|
fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
|
||||||
let mut onstack = 0;
|
node.template
|
||||||
|
.roots
|
||||||
for (idx, _) in node.template.roots.iter().enumerate() {
|
.iter()
|
||||||
match node.dynamic_root(idx) {
|
.enumerate()
|
||||||
Some(Text(t)) => {
|
.map(|(idx, _)| {
|
||||||
self.mutations.push(Mutation::PushRoot { id: t.id.get() });
|
match node.dynamic_root(idx) {
|
||||||
onstack += 1;
|
Some(Text(t)) => {
|
||||||
}
|
self.mutations.push(Mutation::PushRoot {
|
||||||
Some(Placeholder(t)) => {
|
id: t.id.get().unwrap(),
|
||||||
self.mutations.push(Mutation::PushRoot { id: t.get() });
|
});
|
||||||
onstack += 1;
|
1
|
||||||
}
|
|
||||||
Some(Fragment(nodes)) => {
|
|
||||||
for node in *nodes {
|
|
||||||
onstack += self.push_all_real_nodes(node);
|
|
||||||
}
|
}
|
||||||
}
|
Some(Placeholder(t)) => {
|
||||||
Some(Component(comp)) => {
|
self.mutations.push(Mutation::PushRoot {
|
||||||
let scope = comp.scope.get().unwrap();
|
id: t.id.get().unwrap(),
|
||||||
onstack +=
|
});
|
||||||
|
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() } {
|
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||||
RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node),
|
RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node),
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.mutations.push(Mutation::PushRoot {
|
self.mutations.push(Mutation::PushRoot {
|
||||||
id: node.root_ids[idx].get(),
|
id: node.root_ids[idx].get().unwrap(),
|
||||||
});
|
});
|
||||||
onstack += 1;
|
1
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
})
|
||||||
|
.count()
|
||||||
onstack
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize {
|
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 {
|
fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
|
||||||
match node.dynamic_root(0) {
|
match node.dynamic_root(0) {
|
||||||
None => node.root_ids[0].get(),
|
None => node.root_ids[0].get().unwrap(),
|
||||||
Some(Text(t)) => t.id.get(),
|
Some(Text(t)) => t.id.get().unwrap(),
|
||||||
Some(Fragment(t)) => self.find_first_element(&t[0]),
|
Some(Fragment(t)) => self.find_first_element(&t[0]),
|
||||||
Some(Placeholder(t)) => t.get(),
|
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||||
Some(Component(comp)) => {
|
Some(Component(comp)) => {
|
||||||
let scope = comp.scope.get().unwrap();
|
let scope = comp.scope.get().unwrap();
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
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 {
|
fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
|
||||||
match node.dynamic_root(node.template.roots.len() - 1) {
|
match node.dynamic_root(node.template.roots.len() - 1) {
|
||||||
None => node.root_ids.last().unwrap().get(),
|
None => node.root_ids.last().unwrap().get().unwrap(),
|
||||||
Some(Text(t)) => t.id.get(),
|
Some(Text(t)) => t.id.get().unwrap(),
|
||||||
Some(Fragment(t)) => self.find_last_element(t.last().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)) => {
|
Some(Component(comp)) => {
|
||||||
let scope = comp.scope.get().unwrap();
|
let scope = comp.scope.get().unwrap();
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
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>(
|
fn matching_components<'a>(
|
||||||
left: &'a VNode<'a>,
|
left: &'a VNode<'a>,
|
||||||
right: &'a VNode<'a>,
|
right: &'a VNode<'a>,
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
use crate::ScopeId;
|
use crate::ScopeId;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialOrd, Ord)]
|
||||||
pub struct DirtyScope {
|
pub struct DirtyScope {
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub id: ScopeId,
|
pub id: ScopeId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for DirtyScope {
|
impl PartialEq for DirtyScope {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
Some(self.height.cmp(&other.height))
|
self.id == other.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ord for DirtyScope {
|
impl Hash for DirtyScope {
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
self.height.cmp(&other.height)
|
self.id.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
//! 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.
|
//! 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 crate::{innerlude::VNode, ScopeState};
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
/// A concrete type provider for closures that build [`VNode`] structures.
|
/// A concrete type provider for closures that build [`VNode`] structures.
|
||||||
///
|
///
|
||||||
@@ -24,14 +25,7 @@ use std::mem;
|
|||||||
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
|
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
|
||||||
/// ```
|
/// ```
|
||||||
pub struct LazyNodes<'a, 'b> {
|
pub struct LazyNodes<'a, 'b> {
|
||||||
inner: StackNodeStorage<'a, 'b>,
|
inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
|
||||||
}
|
|
||||||
|
|
||||||
type StackHeapSize = [usize; 16];
|
|
||||||
|
|
||||||
enum StackNodeStorage<'a, 'b> {
|
|
||||||
Stack(LazyStack),
|
|
||||||
Heap(Box<dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> LazyNodes<'a, 'b> {
|
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
|
// 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 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 {
|
Self {
|
||||||
inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| {
|
inner: smallbox!(move |f| {
|
||||||
fac.map(
|
let val = slot.take().expect("cannot call LazyNodes twice");
|
||||||
slot.take()
|
val(f)
|
||||||
.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,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,88 +54,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||||||
/// let node = f.call(cac);
|
/// let node = f.call(cac);
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn call(self, f: &'a ScopeState) -> VNode<'a> {
|
pub fn call(mut self, f: &'a ScopeState) -> VNode<'a> {
|
||||||
match self.inner {
|
(self.inner)(f)
|
||||||
StackNodeStorage::Heap(mut lazy) => {
|
|
||||||
lazy(Some(f)).expect("Closure should not be called twice")
|
|
||||||
}
|
|
||||||
StackNodeStorage::Stack(mut stack) => stack.call(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>()
|
|
||||||
}
|
|
||||||
@@ -81,9 +81,9 @@ pub use crate::innerlude::{
|
|||||||
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::innerlude::{
|
pub use crate::innerlude::{
|
||||||
fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope,
|
fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties,
|
||||||
ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode,
|
Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode,
|
||||||
VirtualDom,
|
VNode, VirtualDom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use fxhash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
use crate::{arena::ElementId, ScopeId, Template};
|
use crate::{arena::ElementId, ScopeId, Template};
|
||||||
|
|
||||||
@@ -230,9 +230,6 @@ pub enum Mutation<'a> {
|
|||||||
/// The name of the event to listen for.
|
/// The name of the event to listen for.
|
||||||
name: &'a str,
|
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.
|
/// The ID of the node to attach the listener to.
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 IDs for the roots of this template - to be used when moving the template around and removing it from
|
||||||
/// the actual Dom
|
/// the actual Dom
|
||||||
pub root_ids: &'a [Cell<ElementId>],
|
pub root_ids: &'a [Cell<Option<ElementId>>],
|
||||||
|
|
||||||
/// The dynamic parts of the template
|
/// The dynamic parts of the template
|
||||||
pub dynamic_nodes: &'a [DynamicNode<'a>],
|
pub dynamic_nodes: &'a [DynamicNode<'a>],
|
||||||
@@ -145,6 +145,18 @@ where
|
|||||||
Ok(&*Box::leak(deserialized))
|
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.
|
/// A statically known node in a layout.
|
||||||
///
|
///
|
||||||
/// This can be created at compile time, saving the VirtualDom time when diffing the tree
|
/// 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
|
/// 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
|
/// 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.
|
/// 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
|
/// 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 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> {
|
impl<'a> std::fmt::Debug for VComponent<'a> {
|
||||||
@@ -279,7 +291,14 @@ pub struct VText<'a> {
|
|||||||
pub value: &'a str,
|
pub value: &'a str,
|
||||||
|
|
||||||
/// The ID of this node in the real DOM
|
/// 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
|
/// 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
|
// An element that's an error is currently lost into the ether
|
||||||
impl<'a> IntoDynNode<'a> for Element<'a> {
|
impl<'a> IntoDynNode<'a> for Element<'a> {
|
||||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
scopes::{ScopeId, ScopeState},
|
scopes::{ScopeId, ScopeState},
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
};
|
};
|
||||||
|
use bumpalo::Bump;
|
||||||
use futures_util::FutureExt;
|
use futures_util::FutureExt;
|
||||||
use std::{
|
use std::{
|
||||||
mem,
|
mem,
|
||||||
@@ -34,8 +35,8 @@ impl VirtualDom {
|
|||||||
props: Some(props),
|
props: Some(props),
|
||||||
name,
|
name,
|
||||||
placeholder: Default::default(),
|
placeholder: Default::default(),
|
||||||
node_arena_1: BumpFrame::new(50),
|
node_arena_1: BumpFrame::new(0),
|
||||||
node_arena_2: BumpFrame::new(50),
|
node_arena_2: BumpFrame::new(0),
|
||||||
spawned_tasks: Default::default(),
|
spawned_tasks: Default::default(),
|
||||||
render_cnt: Default::default(),
|
render_cnt: Default::default(),
|
||||||
hook_arena: Default::default(),
|
hook_arena: Default::default(),
|
||||||
@@ -62,7 +63,13 @@ impl VirtualDom {
|
|||||||
let mut new_nodes = unsafe {
|
let mut new_nodes = unsafe {
|
||||||
let scope = self.scopes[scope_id.0].as_mut();
|
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
|
// Make sure to reset the hook counter so we give out hooks in the right order
|
||||||
scope.hook_idx.set(0);
|
scope.hook_idx.set(0);
|
||||||
@@ -128,8 +135,8 @@ impl VirtualDom {
|
|||||||
let frame = scope.previous_frame();
|
let frame = scope.previous_frame();
|
||||||
|
|
||||||
// set the new head of the bump frame
|
// set the new head of the bump frame
|
||||||
let alloced = &*frame.bump.alloc(new_nodes);
|
let allocated = &*frame.bump.alloc(new_nodes);
|
||||||
frame.node.set(alloced);
|
frame.node.set(allocated);
|
||||||
|
|
||||||
// And move the render generation forward by one
|
// And move the render generation forward by one
|
||||||
scope.render_cnt.set(scope.render_cnt.get() + 1);
|
scope.render_cnt.set(scope.render_cnt.get() + 1);
|
||||||
@@ -141,6 +148,6 @@ impl VirtualDom {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// rebind the lifetime now that its stored internally
|
// rebind the lifetime now that its stored internally
|
||||||
unsafe { mem::transmute(alloced) }
|
unsafe { mem::transmute(allocated) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,10 @@ use crate::{
|
|||||||
Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||||
};
|
};
|
||||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||||
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
fmt::Arguments,
|
fmt::Arguments,
|
||||||
future::Future,
|
future::Future,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
@@ -82,10 +82,10 @@ pub struct ScopeState {
|
|||||||
pub(crate) hook_list: RefCell<Vec<*mut dyn Any>>,
|
pub(crate) hook_list: RefCell<Vec<*mut dyn Any>>,
|
||||||
pub(crate) hook_idx: Cell<usize>,
|
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) 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) props: Option<Box<dyn AnyProps<'static>>>,
|
||||||
pub(crate) placeholder: Cell<Option<ElementId>>,
|
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
|
/// 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> {
|
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||||
let chan = self.tasks.sender.clone();
|
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.
|
/// Mark this scope as dirty, and schedule a render for it.
|
||||||
@@ -448,7 +450,7 @@ impl<'src> ScopeState {
|
|||||||
name: fn_name,
|
name: fn_name,
|
||||||
render_fn: component as *const (),
|
render_fn: component as *const (),
|
||||||
static_props: P::IS_STATIC,
|
static_props: P::IS_STATIC,
|
||||||
props: Cell::new(Some(extended)),
|
props: RefCell::new(Some(extended)),
|
||||||
scope: Cell::new(None),
|
scope: Cell::new(None),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,9 @@ use crate::{
|
|||||||
AttributeValue, Element, Event, Scope, SuspenseContext,
|
AttributeValue, Element, Event, Scope, SuspenseContext,
|
||||||
};
|
};
|
||||||
use futures_util::{pin_mut, StreamExt};
|
use futures_util::{pin_mut, StreamExt};
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use std::{
|
use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
|
||||||
any::Any,
|
|
||||||
borrow::BorrowMut,
|
|
||||||
cell::Cell,
|
|
||||||
collections::{BTreeSet, HashMap},
|
|
||||||
future::Future,
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A virtual node system that progresses user events and diffs UI trees.
|
/// A virtual node system that progresses user events and diffs UI trees.
|
||||||
///
|
///
|
||||||
@@ -148,7 +142,7 @@ use std::{
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct VirtualDom {
|
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) scopes: Slab<Box<ScopeState>>,
|
||||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||||
pub(crate) scheduler: Rc<Scheduler>,
|
pub(crate) scheduler: Rc<Scheduler>,
|
||||||
@@ -542,6 +536,8 @@ impl VirtualDom {
|
|||||||
pub async fn render_with_deadline(&mut self, deadline: impl Future<Output = ()>) -> Mutations {
|
pub async fn render_with_deadline(&mut self, deadline: impl Future<Output = ()>) -> Mutations {
|
||||||
pin_mut!(deadline);
|
pin_mut!(deadline);
|
||||||
|
|
||||||
|
self.process_events();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// first, unload any complete suspense trees
|
// first, unload any complete suspense trees
|
||||||
for finished_fiber in self.finished_fibers.drain(..) {
|
for finished_fiber in self.finished_fibers.drain(..) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fn dual_stream() {
|
|||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None },
|
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) },
|
HydrateText { path: &[0, 0], value: "123", id: ElementId(2) },
|
||||||
AppendChildren { id: ElementId(0), m: 1 }
|
AppendChildren { id: ElementId(0), m: 1 }
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ fn contexts_drop() {
|
|||||||
_ = dom.render_immediate();
|
_ = dom.render_immediate();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn tasks_drop() {
|
async fn tasks_drop() {
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
cx.spawn(async {
|
cx.spawn(async {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ fn test_memory_leak() {
|
|||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let val = cx.generation();
|
let val = cx.generation();
|
||||||
|
|
||||||
cx.spawn(async {
|
cx.spawn(async {});
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
|
|
||||||
});
|
|
||||||
|
|
||||||
if val == 2 || val == 4 {
|
if val == 2 || val == 4 {
|
||||||
return cx.render(rsx!(()));
|
return cx.render(rsx!(()));
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ serde = "1.0.136"
|
|||||||
serde_json = "1.0.79"
|
serde_json = "1.0.79"
|
||||||
thiserror = "1.0.30"
|
thiserror = "1.0.30"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
wry = { version = "0.22.0" }
|
wry = { version = "0.23.4" }
|
||||||
futures-channel = "0.3.21"
|
futures-channel = "0.3.21"
|
||||||
tokio = { version = "1.16.1", features = [
|
tokio = { version = "1.16.1", features = [
|
||||||
"sync",
|
"sync",
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
||||||
use crate::events::{decode_event, EventMessage};
|
use crate::events::IpcMessage;
|
||||||
use dioxus_core::*;
|
use dioxus_core::*;
|
||||||
|
use dioxus_html::{Html, HtmlEvent};
|
||||||
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
|
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
use objc::runtime::Object;
|
use objc::runtime::Object;
|
||||||
|
use serde_json::Value;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@@ -19,11 +21,12 @@ use wry::{
|
|||||||
|
|
||||||
pub(super) struct DesktopController {
|
pub(super) struct DesktopController {
|
||||||
pub(super) webviews: HashMap<WindowId, WebView>,
|
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) pending_edits: Arc<Mutex<Vec<String>>>,
|
||||||
pub(super) quit_app_on_close: bool,
|
pub(super) quit_app_on_close: bool,
|
||||||
pub(super) is_ready: Arc<AtomicBool>,
|
pub(super) is_ready: Arc<AtomicBool>,
|
||||||
pub(super) proxy: EventLoopProxy<UserWindowEvent>,
|
pub(super) proxy: EventLoopProxy<UserWindowEvent>,
|
||||||
pub(super) event_tx: UnboundedSender<serde_json::Value>,
|
pub(super) event_tx: UnboundedSender<Html>,
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub(super) templates_tx: UnboundedSender<Template<'static>>,
|
pub(super) templates_tx: UnboundedSender<Template<'static>>,
|
||||||
|
|
||||||
@@ -40,12 +43,13 @@ impl DesktopController {
|
|||||||
proxy: EventLoopProxy<UserWindowEvent>,
|
proxy: EventLoopProxy<UserWindowEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let edit_queue = Arc::new(Mutex::new(Vec::new()));
|
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 (templates_tx, mut templates_rx) = unbounded();
|
||||||
let proxy2 = proxy.clone();
|
let proxy2 = proxy.clone();
|
||||||
|
|
||||||
let pending_edits = edit_queue.clone();
|
let pending_edits = edit_queue.clone();
|
||||||
let desktop_context_proxy = proxy.clone();
|
let desktop_context_proxy = proxy.clone();
|
||||||
|
let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::<Value>();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
// We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
|
// 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 {
|
runtime.block_on(async move {
|
||||||
let mut dom = VirtualDom::new_with_props(root, props)
|
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 edits = dom.rebuild();
|
||||||
let mut queue = edit_queue.lock().unwrap();
|
let mut queue = edit_queue.lock().unwrap();
|
||||||
@@ -80,14 +84,8 @@ impl DesktopController {
|
|||||||
dom.replace_template(template);
|
dom.replace_template(template);
|
||||||
}
|
}
|
||||||
_ = dom.wait_for_work() => {}
|
_ = dom.wait_for_work() => {}
|
||||||
Some(json_value) = event_rx.next() => {
|
Some(value) = event_rx.next() => {
|
||||||
if let Ok(value) = serde_json::from_value::<EventMessage>(json_value) {
|
dom.handle_event(&value.name, value.data.into_any(), value.element, dioxus_html::events::event_bubbles(&value.name));
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +101,7 @@ impl DesktopController {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
pending_edits,
|
pending_edits,
|
||||||
|
eval_sender,
|
||||||
webviews: HashMap::new(),
|
webviews: HashMap::new(),
|
||||||
is_ready: Arc::new(AtomicBool::new(false)),
|
is_ready: Arc::new(AtomicBool::new(false)),
|
||||||
quit_app_on_close: true,
|
quit_app_on_close: true,
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use crate::controller::DesktopController;
|
use crate::controller::DesktopController;
|
||||||
use dioxus_core::ScopeState;
|
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::ControlFlow;
|
||||||
use wry::application::event_loop::EventLoopProxy;
|
use wry::application::event_loop::EventLoopProxy;
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
@@ -19,16 +24,6 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
|||||||
.unwrap()
|
.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.
|
/// An imperative interface to the current window.
|
||||||
///
|
///
|
||||||
/// To get a handle to the current window, use the [`use_window`] hook.
|
/// 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 {
|
pub struct DesktopContext {
|
||||||
/// The wry/tao proxy to the current window
|
/// The wry/tao proxy to the current window
|
||||||
pub proxy: ProxyType,
|
pub proxy: ProxyType,
|
||||||
|
pub(super) eval_reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Value>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DesktopContext {
|
impl DesktopContext {
|
||||||
pub(crate) fn new(proxy: ProxyType) -> Self {
|
pub(crate) fn new(
|
||||||
Self { proxy }
|
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
|
/// trigger the drag-window event
|
||||||
@@ -236,6 +238,18 @@ impl DesktopController {
|
|||||||
Resizable(state) => window.set_resizable(state),
|
Resizable(state) => window.set_resizable(state),
|
||||||
AlwaysOnTop(state) => window.set_always_on_top(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),
|
CursorVisible(state) => window.set_cursor_visible(state),
|
||||||
CursorGrab(state) => {
|
CursorGrab(state) => {
|
||||||
let _ = window.set_cursor_grab(state);
|
let _ = window.set_cursor_grab(state);
|
||||||
@@ -259,13 +273,6 @@ impl DesktopController {
|
|||||||
log::warn!("Devtools are disabled in release builds");
|
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")]
|
#[cfg(target_os = "ios")]
|
||||||
PushView(view) => unsafe {
|
PushView(view) => unsafe {
|
||||||
use objc::runtime::Object;
|
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")]
|
#[cfg(target_os = "ios")]
|
||||||
fn is_main_thread() -> bool {
|
fn is_main_thread() -> bool {
|
||||||
use objc::runtime::{Class, BOOL, NO};
|
use objc::runtime::{Class, BOOL, NO};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use serde_json::from_value;
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
pub(crate) struct IpcMessage {
|
pub(crate) struct IpcMessage {
|
||||||
method: String,
|
method: String,
|
||||||
params: serde_json::Value,
|
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
@@ -17,7 +17,8 @@ use std::sync::atomic::AtomicBool;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use desktop_context::UserWindowEvent;
|
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;
|
use futures_channel::mpsc::UnboundedSender;
|
||||||
pub use wry;
|
pub use wry;
|
||||||
pub use wry::application as tao;
|
pub use wry::application as tao;
|
||||||
@@ -145,6 +146,7 @@ impl DesktopController {
|
|||||||
event_loop,
|
event_loop,
|
||||||
self.is_ready.clone(),
|
self.is_ready.clone(),
|
||||||
self.proxy.clone(),
|
self.proxy.clone(),
|
||||||
|
self.eval_sender.clone(),
|
||||||
self.event_tx.clone(),
|
self.event_tx.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -157,7 +159,8 @@ fn build_webview(
|
|||||||
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
||||||
is_ready: Arc<AtomicBool>,
|
is_ready: Arc<AtomicBool>,
|
||||||
proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
|
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 {
|
) -> wry::webview::WebView {
|
||||||
let builder = cfg.window.clone();
|
let builder = cfg.window.clone();
|
||||||
let window = builder.build(event_loop).unwrap();
|
let window = builder.build(event_loop).unwrap();
|
||||||
@@ -186,8 +189,14 @@ fn build_webview(
|
|||||||
.with_ipc_handler(move |_window: &Window, payload: String| {
|
.with_ipc_handler(move |_window: &Window, payload: String| {
|
||||||
parse_ipc_message(&payload)
|
parse_ipc_message(&payload)
|
||||||
.map(|message| match message.method() {
|
.map(|message| match message.method() {
|
||||||
|
"eval_result" => {
|
||||||
|
let result = message.params();
|
||||||
|
eval_sender.send(result).unwrap();
|
||||||
|
}
|
||||||
"user_event" => {
|
"user_event" => {
|
||||||
_ = event_tx.unbounded_send(message.params());
|
if let Ok(evt) = serde_json::from_value(message.params()) {
|
||||||
|
_ = event_tx.unbounded_send(evt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"initialize" => {
|
"initialize" => {
|
||||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
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
|
// in release mode, we don't want to show the dev tool or reload menus
|
||||||
webview = webview.with_initialization_script(
|
webview = webview.with_initialization_script(
|
||||||
r#"
|
r#"
|
||||||
if (document.addEventListener) {
|
if (document.addEventListener) {
|
||||||
document.addEventListener('contextmenu', function(e) {
|
document.addEventListener('contextmenu', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}, false);
|
}, false);
|
||||||
} else {
|
} else {
|
||||||
document.attachEvent('oncontextmenu', function() {
|
document.attachEvent('oncontextmenu', function() {
|
||||||
window.event.returnValue = false;
|
window.event.returnValue = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// in debug, we are okay with the reload menu showing and dev tool
|
// in debug, we are okay with the reload menu showing and dev tool
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,14 @@ pub(super) fn desktop_handler(
|
|||||||
} else if trimmed == "index.js" {
|
} else if trimmed == "index.js" {
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.header("Content-Type", "text/javascript")
|
.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)
|
.map_err(From::from)
|
||||||
} else {
|
} else {
|
||||||
let asset_root = asset_root
|
let asset_root = asset_root
|
||||||
@@ -104,10 +111,10 @@ fn get_asset_root() -> Option<PathBuf> {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
let bundle = core_foundation::bundle::CFBundle::main_bundle();
|
let bundle = core_foundation::bundle::CFBundle::main_bundle();
|
||||||
let bundle_path = dbg!(bundle.path()?);
|
let bundle_path = bundle.path()?;
|
||||||
let resources_path = dbg!(bundle.resources_path()?);
|
let resources_path = bundle.resources_path()?;
|
||||||
let absolute_resources_root = dbg!(bundle_path.join(resources_path));
|
let absolute_resources_root = bundle_path.join(resources_path);
|
||||||
let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?);
|
let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
|
||||||
|
|
||||||
return Some(canonical_resources_root);
|
return Some(canonical_resources_root);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#![doc = include_str!("../../../notes/README.md")]
|
|
||||||
|
|
||||||
pub use dioxus_core as core;
|
pub use dioxus_core as core;
|
||||||
|
|
||||||
#[cfg(feature = "hooks")]
|
#[cfg(feature = "hooks")]
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ features = [
|
|||||||
"ClipboardEvent",
|
"ClipboardEvent",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json = "*"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["serialize"]
|
||||||
serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde"]
|
serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"]
|
||||||
wasm-bind = ["web-sys", "wasm-bindgen"]
|
wasm-bind = ["web-sys", "wasm-bindgen"]
|
||||||
@@ -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
|
/// 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
|
/// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an
|
||||||
/// application-specific way.
|
/// application-specific way.
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct DragData {
|
pub struct DragData {
|
||||||
/// Inherit mouse data
|
/// Inherit mouse data
|
||||||
pub mouse: MouseData,
|
pub mouse: MouseData,
|
||||||
|
|
||||||
/// And then add the rest of the drag data
|
|
||||||
pub data: Box<dyn Any>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_event! {
|
impl_event! {
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ pub struct FormData {
|
|||||||
pub value: String,
|
pub value: String,
|
||||||
|
|
||||||
pub values: HashMap<String, String>,
|
pub values: HashMap<String, String>,
|
||||||
|
// #[cfg_attr(feature = "serialize", serde(skip))]
|
||||||
#[cfg_attr(feature = "serialize", serde(skip))]
|
// pub files: Option<Arc<dyn FileEngine>>,
|
||||||
pub files: Option<Arc<dyn FileEngine>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for FormData {
|
impl Debug for FormData {
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ mod render_template;
|
|||||||
#[cfg(feature = "wasm-bind")]
|
#[cfg(feature = "wasm-bind")]
|
||||||
mod web_sys_bind;
|
mod web_sys_bind;
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
mod transit;
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
pub use transit::*;
|
||||||
|
|
||||||
pub use elements::*;
|
pub use elements::*;
|
||||||
pub use events::*;
|
pub use events::*;
|
||||||
pub use global_attributes::*;
|
pub use global_attributes::*;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
// }
|
||||||
@@ -17,8 +17,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
wasm-bindgen = { version = "0.2.79", optional = true }
|
wasm-bindgen = { version = "0.2.79", optional = true }
|
||||||
js-sys = { version = "0.3.56", optional = true }
|
js-sys = { version = "0.3.56", optional = true }
|
||||||
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
|
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]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
web = ["wasm-bindgen", "js-sys", "web-sys"]
|
web = ["wasm-bindgen", "js-sys", "web-sys"]
|
||||||
|
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
|
||||||
@@ -17,9 +17,6 @@ extern "C" {
|
|||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn MountToRoot(this: &Interpreter);
|
pub fn MountToRoot(this: &Interpreter);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
|
||||||
pub fn AppendChildren(this: &Interpreter, m: u32, id: u32);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
|
pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
|
||||||
|
|
||||||
@@ -76,4 +73,7 @@ extern "C" {
|
|||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn PushRoot(this: &Interpreter, id: u32);
|
pub fn PushRoot(this: &Interpreter, id: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn AppendChildren(this: &Interpreter, id: u32, m: u32);
|
||||||
}
|
}
|
||||||
@@ -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 {
|
class ListenerMap {
|
||||||
constructor(root) {
|
constructor(root) {
|
||||||
// bubbling events can listen at the root element
|
// bubbling events can listen at the root element
|
||||||
@@ -60,7 +52,7 @@ class ListenerMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Interpreter {
|
class Interpreter {
|
||||||
constructor(root) {
|
constructor(root) {
|
||||||
this.root = root;
|
this.root = root;
|
||||||
this.listeners = new ListenerMap(root);
|
this.listeners = new ListenerMap(root);
|
||||||
@@ -90,7 +82,8 @@ export class Interpreter {
|
|||||||
this.stack.pop();
|
this.stack.pop();
|
||||||
}
|
}
|
||||||
AppendChildren(many) {
|
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);
|
let to_add = this.stack.splice(this.stack.length - many);
|
||||||
for (let i = 0; i < many; i++) {
|
for (let i = 0; i < many; i++) {
|
||||||
root.appendChild(to_add[i]);
|
root.appendChild(to_add[i]);
|
||||||
@@ -352,7 +345,10 @@ export class Interpreter {
|
|||||||
break;
|
break;
|
||||||
case "NewEventListener":
|
case "NewEventListener":
|
||||||
// this handler is only provided on desktop implementations since this
|
// 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 handler = (event) => {
|
||||||
let target = event.target;
|
let target = event.target;
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
@@ -434,20 +430,21 @@ export class Interpreter {
|
|||||||
}
|
}
|
||||||
window.ipc.postMessage(
|
window.ipc.postMessage(
|
||||||
serializeIpcMessage("user_event", {
|
serializeIpcMessage("user_event", {
|
||||||
event: edit.name,
|
name: edit.name,
|
||||||
mounted_dom_id: parseInt(realId),
|
element: parseInt(realId),
|
||||||
contents: contents,
|
data: contents,
|
||||||
|
bubbles: bubbles,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler);
|
this.NewEventListener(edit.name, edit.id, bubbles, handler);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serialize_event(event) {
|
function serialize_event(event) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "copy":
|
case "copy":
|
||||||
case "cut":
|
case "cut":
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
|
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")]
|
#[cfg(feature = "web")]
|
||||||
mod bindings;
|
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);}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ description = "Build server-side apps with Dioxus"
|
|||||||
license = "MIT/Apache-2.0"
|
license = "MIT/Apache-2.0"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -28,7 +27,7 @@ tokio-util = { version = "0.7.0", features = ["full"] }
|
|||||||
|
|
||||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
|
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
|
||||||
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" }
|
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" }
|
||||||
|
dioxus-interpreter-js = { path = "../interpreter" }
|
||||||
|
|
||||||
# warp
|
# warp
|
||||||
warp = { version = "0.3", optional = true }
|
warp = { version = "0.3", optional = true }
|
||||||
@@ -39,6 +38,9 @@ tower = { version = "0.4.12", optional = true }
|
|||||||
|
|
||||||
# salvo
|
# salvo
|
||||||
salvo = { version = "0.32.0", optional = true, features = ["ws"] }
|
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]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
@@ -49,4 +51,16 @@ salvo = { version = "0.32.0", features = ["affix", "ws"] }
|
|||||||
tower = "0.4.12"
|
tower = "0.4.12"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["salvo"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "axum"
|
||||||
|
required-features = ["axum"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "salvo"
|
||||||
|
required-features = ["salvo"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "warp"
|
||||||
|
required-features = ["warp"]
|
||||||
@@ -1,32 +1,53 @@
|
|||||||
#[cfg(not(feature = "axum"))]
|
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
|
||||||
fn main() {}
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
|
|
||||||
use dioxus_core::{Element, LazyNodes, Scope};
|
|
||||||
pretty_env_logger::init();
|
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 addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
|
||||||
|
|
||||||
let view = dioxus_liveview::new(addr);
|
let view = dioxus_liveview::LiveViewPool::new();
|
||||||
let body = view.body("<title>Dioxus Liveview</title>");
|
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(move || async { Html(body) }))
|
|
||||||
.route(
|
.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 {
|
get(move |ws: WebSocketUpgrade| async move {
|
||||||
ws.on_upgrade(move |socket| 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())
|
axum::Server::bind(&addr.to_string().parse().unwrap())
|
||||||
.serve(app.into_make_service())
|
.serve(app.into_make_service())
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,55 +1,71 @@
|
|||||||
#[cfg(not(feature = "salvo"))]
|
use dioxus::prelude::*;
|
||||||
fn main() {}
|
|
||||||
|
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]
|
#[tokio::main]
|
||||||
async fn 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();
|
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()
|
let router = Router::new()
|
||||||
.hoop(affix::inject(Arc::new(view)))
|
.hoop(affix::inject(Arc::new(view)))
|
||||||
.get(index)
|
.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;
|
Server::new(TcpListener::bind(addr)).serve(router).await;
|
||||||
|
}
|
||||||
|
|
||||||
#[handler]
|
#[handler]
|
||||||
fn index(depot: &mut Depot, res: &mut Response) {
|
fn index(_depot: &mut Depot, res: &mut Response) {
|
||||||
let view = depot.obtain::<Arc<Liveview>>().unwrap();
|
let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
|
||||||
let body = view.body("<title>Dioxus LiveView</title>");
|
res.render(Text::Html(format!(
|
||||||
res.render(Text::Html(body));
|
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]
|
#[handler]
|
||||||
async fn connect(
|
async fn connect(
|
||||||
req: &mut Request,
|
req: &mut Request,
|
||||||
depot: &mut Depot,
|
depot: &mut Depot,
|
||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
) -> Result<(), StatusError> {
|
) -> Result<(), StatusError> {
|
||||||
let view = depot.obtain::<Arc<Liveview>>().unwrap().clone();
|
let view = depot.obtain::<Arc<LiveViewPool>>().unwrap().clone();
|
||||||
let fut = WsHandler::new().handle(req, res)?;
|
let fut = WsHandler::new().handle(req, res)?;
|
||||||
let fut = async move {
|
|
||||||
if let Some(ws) = fut.await {
|
tokio::spawn(async move {
|
||||||
view.upgrade_salvo(ws, app).await;
|
if let Some(ws) = fut.await {
|
||||||
}
|
_ = view.launch(dioxus_liveview::salvo_socket(ws), app).await;
|
||||||
};
|
}
|
||||||
tokio::task::spawn(fut);
|
});
|
||||||
Ok(())
|
|
||||||
}
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,56 @@
|
|||||||
#[cfg(not(feature = "warp"))]
|
use dioxus::prelude::*;
|
||||||
fn main() {}
|
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]
|
#[tokio::main]
|
||||||
async fn 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();
|
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 index = warp::path::end().map(move || {
|
||||||
let view = liveview::new(addr);
|
warp::reply::html(format!(
|
||||||
let body = view.body("<title>Dioxus LiveView</title>");
|
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()
|
let pool = LiveViewPool::new();
|
||||||
.map(move || warp::reply::html(body.clone()))
|
|
||||||
.or(warp::path("app")
|
let ws = warp::path("ws")
|
||||||
.and(warp::ws())
|
.and(warp::ws())
|
||||||
.and(warp::any().map(move || view.clone()))
|
.and(warp::any().map(move || pool.clone()))
|
||||||
.map(|ws: Ws, view: liveview::Liveview| {
|
.map(move |ws: Ws, pool: LiveViewPool| {
|
||||||
ws.on_upgrade(|socket| async move {
|
ws.on_upgrade(|ws| async move {
|
||||||
view.upgrade_warp(socket, app).await;
|
let _ = pool.launch(warp_socket(ws), app).await;
|
||||||
})
|
})
|
||||||
}));
|
});
|
||||||
warp::serve(routes).run(addr).await;
|
|
||||||
|
println!("Listening on http://{}", addr);
|
||||||
|
|
||||||
|
warp::serve(index.or(ws)).run(addr).await;
|
||||||
}
|
}
|
||||||
@@ -1,94 +1,23 @@
|
|||||||
use crate::events;
|
use crate::{LiveViewError, LiveViewSocket};
|
||||||
use axum::extract::ws::{Message, WebSocket};
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
use dioxus_core::prelude::*;
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use futures_util::{
|
|
||||||
future::{select, Either},
|
|
||||||
pin_mut, SinkExt, StreamExt,
|
|
||||||
};
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
|
||||||
use tokio_util::task::LocalPoolHandle;
|
|
||||||
|
|
||||||
impl crate::Liveview {
|
/// Convert a warp websocket into a LiveViewSocket
|
||||||
pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) {
|
///
|
||||||
connect(ws, self.pool.clone(), app, ()).await;
|
/// 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)
|
||||||
pub async fn upgrade_axum_with_props<T>(
|
.with(transform_tx)
|
||||||
&self,
|
.sink_map_err(|_| LiveViewError::SendingFailed)
|
||||||
ws: WebSocket,
|
|
||||||
app: fn(Scope<T>) -> Element,
|
|
||||||
props: T,
|
|
||||||
) where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
connect(ws, self.pool.clone(), app, props).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect<T>(
|
fn transform_rx(message: Result<Message, axum::Error>) -> Result<String, LiveViewError> {
|
||||||
socket: WebSocket,
|
message
|
||||||
pool: LocalPoolHandle,
|
.map_err(|_| LiveViewError::SendingFailed)?
|
||||||
app: fn(Scope<T>) -> Element,
|
.into_text()
|
||||||
props: T,
|
.map_err(|_| LiveViewError::SendingFailed)
|
||||||
) where
|
}
|
||||||
T: Send + Sync + 'static,
|
|
||||||
{
|
async fn transform_tx(message: String) -> Result<Message, axum::Error> {
|
||||||
let (mut user_ws_tx, mut user_ws_rx) = socket.split();
|
Ok(Message::Text(message))
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
@@ -1,110 +1,25 @@
|
|||||||
use crate::events;
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use dioxus_core::prelude::*;
|
|
||||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
|
||||||
use salvo::extra::ws::{Message, WebSocket};
|
use salvo::extra::ws::{Message, WebSocket};
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
|
||||||
use tokio_util::task::LocalPoolHandle;
|
|
||||||
|
|
||||||
impl crate::Liveview {
|
use crate::{LiveViewError, LiveViewSocket};
|
||||||
pub async fn upgrade_salvo(&self, ws: salvo::extra::ws::WebSocket, app: fn(Scope) -> Element) {
|
|
||||||
connect(ws, self.pool.clone(), app, ()).await;
|
/// Convert a salvo websocket into a LiveViewSocket
|
||||||
}
|
///
|
||||||
pub async fn upgrade_salvo_with_props<T>(
|
/// This is required to launch a LiveView app using the warp web framework
|
||||||
&self,
|
pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket {
|
||||||
ws: salvo::extra::ws::WebSocket,
|
ws.map(transform_rx)
|
||||||
app: fn(Scope<T>) -> Element,
|
.with(transform_tx)
|
||||||
props: T,
|
.sink_map_err(|_| LiveViewError::SendingFailed)
|
||||||
) where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
connect(ws, self.pool.clone(), app, props).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect<T>(
|
fn transform_rx(message: Result<Message, salvo::Error>) -> Result<String, LiveViewError> {
|
||||||
ws: WebSocket,
|
let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?;
|
||||||
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.
|
|
||||||
|
|
||||||
// Split the socket into a sender and receive of messages.
|
let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?;
|
||||||
let (mut user_ws_tx, mut user_ws_rx) = ws.split();
|
|
||||||
|
|
||||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
Ok(msg)
|
||||||
let (edits_tx, edits_rx) = mpsc::unbounded_channel();
|
}
|
||||||
|
|
||||||
let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
|
async fn transform_tx(message: String) -> Result<Message, salvo::Error> {
|
||||||
let mut event_rx = UnboundedReceiverStream::new(event_rx);
|
Ok(Message::text(message))
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
@@ -1,110 +1,28 @@
|
|||||||
use crate::events;
|
use crate::{LiveViewError, LiveViewSocket};
|
||||||
use dioxus_core::prelude::*;
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
|
||||||
use tokio_util::task::LocalPoolHandle;
|
|
||||||
use warp::ws::{Message, WebSocket};
|
use warp::ws::{Message, WebSocket};
|
||||||
|
|
||||||
impl crate::Liveview {
|
/// Convert a warp websocket into a LiveViewSocket
|
||||||
pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) {
|
///
|
||||||
connect(ws, self.pool.clone(), app, ()).await;
|
/// This is required to launch a LiveView app using the warp web framework
|
||||||
}
|
pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket {
|
||||||
pub async fn upgrade_warp_with_props<T>(
|
ws.map(transform_rx)
|
||||||
&self,
|
.with(transform_tx)
|
||||||
ws: warp::ws::WebSocket,
|
.sink_map_err(|_| LiveViewError::SendingFailed)
|
||||||
app: fn(Scope<T>) -> Element,
|
|
||||||
props: T,
|
|
||||||
) where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
connect(ws, self.pool.clone(), app, props).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect<T>(
|
fn transform_rx(message: Result<Message, warp::Error>) -> Result<String, LiveViewError> {
|
||||||
ws: WebSocket,
|
// destructure the message into the buffer we got from warp
|
||||||
pool: LocalPoolHandle,
|
let msg = message
|
||||||
app: fn(Scope<T>) -> Element,
|
.map_err(|_| LiveViewError::SendingFailed)?
|
||||||
props: T,
|
.into_bytes();
|
||||||
) where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
// Use a counter to assign a new unique ID for this user.
|
|
||||||
|
|
||||||
// Split the socket into a sender and receive of messages.
|
// transform it back into a string, saving us the allocation
|
||||||
let (mut user_ws_tx, mut user_ws_rx) = ws.split();
|
let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?;
|
||||||
|
|
||||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
Ok(msg)
|
||||||
let (edits_tx, edits_rx) = mpsc::unbounded_channel();
|
}
|
||||||
|
|
||||||
let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
|
async fn transform_tx(message: String) -> Result<Message, warp::Error> {
|
||||||
let mut event_rx = UnboundedReceiverStream::new(event_rx);
|
Ok(Message::text(message))
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +1,55 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
pub(crate) mod events;
|
|
||||||
pub mod adapters {
|
pub mod adapters {
|
||||||
#[cfg(feature = "warp")]
|
#[cfg(feature = "warp")]
|
||||||
pub mod warp_adapter;
|
pub mod warp_adapter;
|
||||||
|
#[cfg(feature = "warp")]
|
||||||
|
pub use warp_adapter::*;
|
||||||
|
|
||||||
#[cfg(feature = "axum")]
|
#[cfg(feature = "axum")]
|
||||||
pub mod axum_adapter;
|
pub mod axum_adapter;
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
pub use axum_adapter::*;
|
||||||
|
|
||||||
#[cfg(feature = "salvo")]
|
#[cfg(feature = "salvo")]
|
||||||
pub mod salvo_adapter;
|
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 trait WebsocketTx: SinkExt<String, Error = LiveViewError> {}
|
||||||
pub struct Liveview {
|
impl<T> WebsocketTx for T where T: SinkExt<String, Error = LiveViewError> {}
|
||||||
pool: LocalPoolHandle,
|
|
||||||
addr: String,
|
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 {
|
use dioxus_interpreter_js::INTERPRETER_JS;
|
||||||
pub fn body(&self, header: &str) -> String {
|
static MAIN_JS: &str = include_str!("./main.js");
|
||||||
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")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(addr: impl Into<SocketAddr>) -> Liveview {
|
/// This script that gets injected into your app connects this page to the websocket endpoint
|
||||||
let addr: SocketAddr = addr.into();
|
///
|
||||||
|
/// Once the endpoint is connected, it will send the initial state of the app, and then start
|
||||||
Liveview {
|
/// processing user events and returning edits to the liveview instance
|
||||||
pool: LocalPoolHandle::new(16),
|
pub fn interpreter_glue(url: &str) -> String {
|
||||||
addr: addr.to_string(),
|
format!(
|
||||||
}
|
r#"
|
||||||
|
<script>
|
||||||
|
var WS_ADDR = "{url}";
|
||||||
|
{INTERPRETER_JS}
|
||||||
|
{MAIN_JS}
|
||||||
|
main();
|
||||||
|
</script>
|
||||||
|
"#
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(¶ms.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?;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -306,8 +306,7 @@ impl Member {
|
|||||||
+ field.ty.to_token_stream().to_string().as_str())
|
+ field.ty.to_token_stream().to_string().as_str())
|
||||||
.as_str(),
|
.as_str(),
|
||||||
Span::call_site(),
|
Span::call_site(),
|
||||||
)
|
),
|
||||||
.into(),
|
|
||||||
ident: field.ident.as_ref()?.clone(),
|
ident: field.ident.as_ref()?.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,11 +102,7 @@ impl<S: State> RealDom<S> {
|
|||||||
self.tree.add_child(node_id, child_id);
|
self.tree.add_child(node_id, child_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_template_node(
|
fn create_template_node(&mut self, node: &TemplateNode) -> RealNodeId {
|
||||||
&mut self,
|
|
||||||
node: &TemplateNode,
|
|
||||||
mutations_vec: &mut FxHashMap<RealNodeId, NodeMask>,
|
|
||||||
) -> RealNodeId {
|
|
||||||
match node {
|
match node {
|
||||||
TemplateNode::Element {
|
TemplateNode::Element {
|
||||||
tag,
|
tag,
|
||||||
@@ -139,27 +135,18 @@ impl<S: State> RealDom<S> {
|
|||||||
});
|
});
|
||||||
let node_id = self.create_node(node);
|
let node_id = self.create_node(node);
|
||||||
for child in *children {
|
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);
|
self.add_child(node_id, child_id);
|
||||||
}
|
}
|
||||||
node_id
|
node_id
|
||||||
}
|
}
|
||||||
TemplateNode::Text { text } => {
|
TemplateNode::Text { text } => self.create_node(Node::new(NodeType::Text {
|
||||||
let node_id = self.create_node(Node::new(NodeType::Text {
|
text: text.to_string(),
|
||||||
text: text.to_string(),
|
})),
|
||||||
}));
|
TemplateNode::Dynamic { .. } => self.create_node(Node::new(NodeType::Placeholder)),
|
||||||
node_id
|
TemplateNode::DynamicText { .. } => self.create_node(Node::new(NodeType::Text {
|
||||||
}
|
text: String::new(),
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +159,7 @@ impl<S: State> RealDom<S> {
|
|||||||
for template in mutations.templates {
|
for template in mutations.templates {
|
||||||
let mut template_root_ids = Vec::new();
|
let mut template_root_ids = Vec::new();
|
||||||
for root in template.roots {
|
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);
|
template_root_ids.push(id);
|
||||||
}
|
}
|
||||||
self.templates
|
self.templates
|
||||||
@@ -319,7 +306,7 @@ impl<S: State> RealDom<S> {
|
|||||||
}
|
}
|
||||||
mark_dirty(node_id, NodeMask::new().with_text(), &mut nodes_updated);
|
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_id = self.element_to_node_id(id);
|
||||||
let node = self.tree.get_mut(node_id).unwrap();
|
let node = self.tree.get_mut(node_id).unwrap();
|
||||||
if let NodeType::Element { listeners, .. } = &mut node.node_data.node_type {
|
if let NodeType::Element { listeners, .. } = &mut node.node_data.node_type {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mod persistant_iterator;
|
||||||
|
pub use persistant_iterator::*;
|
||||||
|
pub mod cursor;
|
||||||
File renamed without changes.
@@ -36,6 +36,7 @@ thiserror = "1.0.30"
|
|||||||
futures-util = "0.3.21"
|
futures-util = "0.3.21"
|
||||||
serde = { version = "1", optional = true }
|
serde = { version = "1", optional = true }
|
||||||
serde_urlencoded = { version = "0.7.1", optional = true }
|
serde_urlencoded = { version = "0.7.1", optional = true }
|
||||||
|
simple_logger = "4.0.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["query"]
|
default = ["query"]
|
||||||
@@ -50,6 +51,9 @@ wasm-logger = "0.2.0"
|
|||||||
wasm-bindgen-test = "0.3"
|
wasm-bindgen-test = "0.3"
|
||||||
gloo-utils = "0.1.2"
|
gloo-utils = "0.1.2"
|
||||||
dioxus-web = { path = "../web" }
|
dioxus-web = { path = "../web" }
|
||||||
|
# dioxus-desktop = { path = "../desktop", optional = true }
|
||||||
|
|
||||||
|
# not wasm
|
||||||
|
|
||||||
[target.wasm32-unknown-unknown.dev-dependencies]
|
[target.wasm32-unknown-unknown.dev-dependencies]
|
||||||
dioxus-router = { path = ".", features = ["web"] }
|
dioxus-router = { path = ".", features = ["web"] }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
Link { to: "/blog", li { "blog" } }
|
Link { to: "/blog", li { "blog" } }
|
||||||
Link { to: "/blog/tim", li { "tims' blog" } }
|
Link { to: "/blog/tim", li { "tims' blog" } }
|
||||||
Link { to: "/blog/bill", li { "bills' blog" } }
|
Link { to: "/blog/bill", li { "bills' blog" } }
|
||||||
|
Link { to: "/blog/james", li { "james amazing' blog" } }
|
||||||
Link { to: "/apples", li { "go to apples" } }
|
Link { to: "/apples", li { "go to apples" } }
|
||||||
}
|
}
|
||||||
Route { to: "/", Home {} }
|
Route { to: "/", Home {} }
|
||||||
@@ -42,5 +43,10 @@ fn BlogPost(cx: Scope) -> Element {
|
|||||||
|
|
||||||
log::trace!("rendering blog post {}", id);
|
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" }
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -120,9 +120,17 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
|||||||
prevent_default: "{prevent_default}",
|
prevent_default: "{prevent_default}",
|
||||||
target: format_args!("{}", if * new_tab { "_blank" } else { "" }),
|
target: format_args!("{}", if * new_tab { "_blank" } else { "" }),
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
|
log::trace!("Clicked link to {}", to);
|
||||||
|
|
||||||
if !outerlink {
|
if !outerlink {
|
||||||
if let Some(service) = svc {
|
if let Some(service) = svc {
|
||||||
|
log::trace!("Pushing route to {}", to);
|
||||||
service.push_route(to, cx.props.title.map(|f| f.to_string()), None);
|
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 {
|
} else {
|
||||||
log::error!(
|
log::error!(
|
||||||
"Attempted to create a Link to {} outside of a Router context", cx.props
|
"Attempted to create a Link to {} outside of a Router context", cx.props
|
||||||
|
|||||||
@@ -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());
|
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()) {
|
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))
|
cx.render(rsx!(&cx.props.children))
|
||||||
} else {
|
} else {
|
||||||
log::trace!("Route should *not* render: {:?}", cx.scope_id());
|
log::debug!("Route should *not* render: {:?}", cx.scope_id());
|
||||||
cx.render(rsx!(()))
|
cx.render(rsx!(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,6 +180,7 @@ impl RouterService {
|
|||||||
(self.regen_any_route)(self.router_id);
|
(self.regen_any_route)(self.router_id);
|
||||||
|
|
||||||
for listener in self.onchange_listeners.borrow().iter() {
|
for listener in self.onchange_listeners.borrow().iter() {
|
||||||
|
log::trace!("Regenerating scope {:?}", listener);
|
||||||
(self.regen_any_route)(*listener);
|
(self.regen_any_route)(*listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dioxus-rsx"
|
name = "dioxus-rsx"
|
||||||
version = "0.0.0"
|
version = "0.0.1"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MIT/Apache-2.0"
|
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
|
# 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"] }
|
proc-macro2 = { version = "1.0", features = ["span-locations"] }
|
||||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||||
quote = { version = "1.0" }
|
quote = { version = "1.0" }
|
||||||
dioxus-core = { path = "../core", features = ["serialize"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
internment = "0.7.0"
|
internment = "0.7.0"
|
||||||
@@ -45,7 +45,7 @@ impl Parse for Element {
|
|||||||
|
|
||||||
content.parse::<Token![:]>()?;
|
content.parse::<Token![:]>()?;
|
||||||
|
|
||||||
if content.peek(LitStr) && content.peek2(Token![,]) {
|
if content.peek(LitStr) {
|
||||||
let value = content.parse()?;
|
let value = content.parse()?;
|
||||||
attributes.push(ElementAttrNamed {
|
attributes.push(ElementAttrNamed {
|
||||||
el_name: el_name.clone(),
|
el_name: el_name.clone(),
|
||||||
@@ -53,7 +53,6 @@ impl Parse for Element {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let value = content.parse::<Expr>()?;
|
let value = content.parse::<Expr>()?;
|
||||||
|
|
||||||
attributes.push(ElementAttrNamed {
|
attributes.push(ElementAttrNamed {
|
||||||
el_name: el_name.clone(),
|
el_name: el_name.clone(),
|
||||||
attr: ElementAttr::CustomAttrExpression { name, value },
|
attr: ElementAttr::CustomAttrExpression { name, value },
|
||||||
|
|||||||
@@ -217,7 +217,8 @@ impl<'a, Ctx: HotReloadingContext> ToTokens for TemplateRenderer<'a, Ctx> {
|
|||||||
parent: None,
|
parent: None,
|
||||||
key: #key_tokens,
|
key: #key_tokens,
|
||||||
template: TEMPLATE,
|
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_nodes: __cx.bump().alloc([ #( #node_printer ),* ]),
|
||||||
dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_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 el_name = &el.name;
|
||||||
let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
|
let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
|
||||||
ElementAttr::AttrText { name, value } if value.is_static() => {
|
ElementAttr::AttrText { name, value } if value.is_static() => {
|
||||||
let value = value.source.as_ref().unwrap();
|
let value = value.to_static().unwrap();
|
||||||
quote! {
|
quote! {
|
||||||
::dioxus::core::TemplateAttribute::Static {
|
::dioxus::core::TemplateAttribute::Static {
|
||||||
name: dioxus_elements::#el_name::#name.0,
|
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() => {
|
ElementAttr::CustomAttrText { name, value } if value.is_static() => {
|
||||||
let value = value.source.as_ref().unwrap();
|
let value = value.to_static().unwrap();
|
||||||
quote! {
|
quote! {
|
||||||
::dioxus::core::TemplateAttribute::Static {
|
::dioxus::core::TemplateAttribute::Static {
|
||||||
name: #name,
|
name: #name,
|
||||||
@@ -503,7 +504,7 @@ impl<'a, Ctx: HotReloadingContext> DynamicContext<'a, Ctx> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BodyNode::Text(text) if text.is_static() => {
|
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 } }
|
quote! { ::dioxus::core::TemplateNode::Text{ text: #text } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
dioxus = { path = "../dioxus", version = "^0.2.1" }
|
||||||
dioxus-core = { path = "../core", version = "^0.2.1" }
|
dioxus-core = { path = "../core", version = "^0.2.1" }
|
||||||
dioxus-html = { path = "../html", version = "^0.2.1" }
|
dioxus-html = { path = "../html", version = "^0.2.1" }
|
||||||
dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
|
dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ fn app(cx: Scope) -> Element {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
background_color: "hsl({hue}, 70%, {brightness}%)",
|
background_color: "hsl({hue}, 70%, {brightness}%)",
|
||||||
onmousemove: move |evt| {
|
onmousemove: move |evt| {
|
||||||
if let RenderReturn::Sync(Ok(node))=cx.root_node(){
|
if let RenderReturn::Sync(Ok(node)) = cx.root_node() {
|
||||||
let node = tui_query.get(node.root_ids[0].get());
|
if let Some(id) = node.root_ids[0].get() {
|
||||||
let Size{width, height} = node.size().unwrap();
|
let node = tui_query.get(id);
|
||||||
let pos = evt.inner().element_coordinates();
|
let Size{width, height} = node.size().unwrap();
|
||||||
hue.set((pos.x as f32/width as f32)*255.0);
|
let pos = evt.inner().element_coordinates();
|
||||||
brightness.set((pos.y as f32/height as f32)*100.0);
|
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}%)",
|
"hsl({hue}, 70%, {brightness}%)",
|
||||||
|
|||||||
@@ -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%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -287,7 +287,10 @@ impl InnerInputState {
|
|||||||
|
|
||||||
fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData {
|
fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData {
|
||||||
let Point { x, y } = layout.location;
|
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)
|
let new_client_coordinates = (mouse_data.client_coordinates() - node_origin)
|
||||||
.to_point()
|
.to_point()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use dioxus_native_core::state::ChildDepState;
|
|||||||
use dioxus_native_core_macro::sorted_str_slice;
|
use dioxus_native_core_macro::sorted_str_slice;
|
||||||
use taffy::prelude::*;
|
use taffy::prelude::*;
|
||||||
|
|
||||||
use crate::screen_to_layout_space;
|
use crate::{screen_to_layout_space, unit_to_layout_space};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub(crate) enum PossiblyUninitalized<T> {
|
pub(crate) enum PossiblyUninitalized<T> {
|
||||||
@@ -105,6 +105,59 @@ impl ChildDepState for TaffyLayout {
|
|||||||
child_layout.push(l.node.unwrap());
|
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 let PossiblyUninitalized::Initialized(n) = self.node {
|
||||||
if self.style != style {
|
if self.style != style {
|
||||||
taffy.set_style(n, style).unwrap();
|
taffy.set_style(n, style).unwrap();
|
||||||
|
|||||||
+18
-3
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
|
cursor::{MoveTo, RestorePosition, SavePosition, Show},
|
||||||
event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
|
event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
@@ -28,11 +29,13 @@ mod focus;
|
|||||||
mod hooks;
|
mod hooks;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod node;
|
mod node;
|
||||||
|
pub mod prelude;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
mod render;
|
mod render;
|
||||||
mod style;
|
mod style;
|
||||||
mod style_attributes;
|
mod style_attributes;
|
||||||
mod widget;
|
mod widget;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use hooks::*;
|
pub use hooks::*;
|
||||||
@@ -43,6 +46,10 @@ pub(crate) fn screen_to_layout_space(screen: u16) -> f32 {
|
|||||||
screen as f32 * 10.0
|
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 {
|
pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
|
||||||
layout / 10.0
|
layout / 10.0
|
||||||
}
|
}
|
||||||
@@ -136,7 +143,13 @@ fn render_vdom(
|
|||||||
let mut terminal = (!cfg.headless).then(|| {
|
let mut terminal = (!cfg.headless).then(|| {
|
||||||
enable_raw_mode().unwrap();
|
enable_raw_mode().unwrap();
|
||||||
let mut stdout = std::io::stdout();
|
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());
|
let backend = CrosstermBackend::new(io::stdout());
|
||||||
Terminal::new(backend).unwrap()
|
Terminal::new(backend).unwrap()
|
||||||
});
|
});
|
||||||
@@ -181,14 +194,16 @@ fn render_vdom(
|
|||||||
taffy.compute_layout(root_node, size).unwrap();
|
taffy.compute_layout(root_node, size).unwrap();
|
||||||
}
|
}
|
||||||
if let Some(terminal) = &mut terminal {
|
if let Some(terminal) = &mut terminal {
|
||||||
|
execute!(terminal.backend_mut(), SavePosition).unwrap();
|
||||||
terminal.draw(|frame| {
|
terminal.draw(|frame| {
|
||||||
let rdom = rdom.borrow();
|
let rdom = rdom.borrow();
|
||||||
let mut taffy = taffy.lock().expect("taffy lock poisoned");
|
let mut taffy = taffy.lock().expect("taffy lock poisoned");
|
||||||
// size is guaranteed to not change when rendering
|
// 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)];
|
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 {
|
} else {
|
||||||
let rdom = rdom.borrow();
|
let rdom = rdom.borrow();
|
||||||
resize(
|
resize(
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pub use crate::widgets::*;
|
||||||
@@ -89,7 +89,7 @@ impl<'a> ElementRef<'a> {
|
|||||||
.ok();
|
.ok();
|
||||||
layout.map(|layout| Layout {
|
layout.map(|layout| Layout {
|
||||||
order: layout.order,
|
order: layout.order,
|
||||||
size: layout.size.map(|v| layout_to_screen_space(v)),
|
size: layout.size.map(layout_to_screen_space),
|
||||||
location: Point {
|
location: Point {
|
||||||
x: layout_to_screen_space(layout.location.x),
|
x: layout_to_screen_space(layout.location.x),
|
||||||
y: layout_to_screen_space(layout.location.y),
|
y: layout_to_screen_space(layout.location.y),
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub(crate) fn render_vnode(
|
|||||||
let x = layout_to_screen_space(fx).round() as u16;
|
let x = layout_to_screen_space(fx).round() as u16;
|
||||||
let y = layout_to_screen_space(fy).round() as u16;
|
let y = layout_to_screen_space(fy).round() as u16;
|
||||||
let Size { width, height } = *size;
|
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;
|
let height = layout_to_screen_space(fy + height).round() as u16 - y;
|
||||||
|
|
||||||
match &node.node_data.node_type {
|
match &node.node_data.node_type {
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
|
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
|
||||||
dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
|
dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
|
||||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
|
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
|
||||||
"web"
|
"sledgehammer"
|
||||||
] }
|
] }
|
||||||
|
|
||||||
js-sys = "0.3.56"
|
js-sys = "0.3.56"
|
||||||
@@ -30,6 +30,7 @@ futures-util = "0.3.19"
|
|||||||
smallstr = "0.2.0"
|
smallstr = "0.2.0"
|
||||||
futures-channel = "0.3.21"
|
futures-channel = "0.3.21"
|
||||||
serde_json = { version = "1.0" }
|
serde_json = { version = "1.0" }
|
||||||
|
serde = { version = "1.0" }
|
||||||
serde-wasm-bindgen = "0.4.5"
|
serde-wasm-bindgen = "0.4.5"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
|
|||||||
+166
-46
@@ -7,24 +7,34 @@
|
|||||||
//! - tests to ensure dyn_into works for various event types.
|
//! - tests to ensure dyn_into works for various event types.
|
||||||
//! - Partial delegation?>
|
//! - Partial delegation?>
|
||||||
|
|
||||||
use dioxus_core::{Mutation, Template};
|
use dioxus_core::{ElementId, Mutation, Template, TemplateAttribute, TemplateNode};
|
||||||
use dioxus_html::{event_bubbles, CompositionData, FormData};
|
use dioxus_html::{event_bubbles, CompositionData, FormData};
|
||||||
use dioxus_interpreter_js::Interpreter;
|
use dioxus_interpreter_js::{save_template, Channel};
|
||||||
use futures_channel::mpsc;
|
use futures_channel::mpsc;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
use std::{any::Any, rc::Rc};
|
use std::{any::Any, rc::Rc};
|
||||||
use wasm_bindgen::{closure::Closure, JsCast};
|
use wasm_bindgen::{closure::Closure, JsCast};
|
||||||
use web_sys::{Document, Element, Event};
|
use web_sys::{Document, Element, Event, HtmlElement};
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
|
||||||
pub struct WebsysDom {
|
pub struct WebsysDom {
|
||||||
interpreter: Interpreter,
|
document: Document,
|
||||||
handler: Closure<dyn FnMut(&Event)>,
|
templates: FxHashMap<String, u32>,
|
||||||
_root: Element,
|
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 {
|
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
|
// 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
|
// a match here in order to avoid some error during runtime browser test
|
||||||
let document = load_document();
|
let document = load_document();
|
||||||
@@ -32,67 +42,158 @@ impl WebsysDom {
|
|||||||
Some(root) => root,
|
Some(root) => root,
|
||||||
None => document.create_element("body").ok().unwrap(),
|
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 {
|
Self {
|
||||||
interpreter: Interpreter::new(root.clone()),
|
document,
|
||||||
_root: root,
|
interpreter,
|
||||||
handler: Closure::wrap(Box::new(move |event: &web_sys::Event| {
|
templates: FxHashMap::default(),
|
||||||
let _ = event_channel.unbounded_send(event.clone());
|
max_template_id: 0,
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mount(&mut self) {
|
pub fn mount(&mut self) {
|
||||||
self.interpreter.MountToRoot();
|
self.interpreter.mount_to_root();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_templates(&mut self, templates: &[Template]) {
|
pub fn load_templates(&mut self, templates: &[Template]) {
|
||||||
log::debug!("Loading templates {:?}", templates);
|
|
||||||
|
|
||||||
for template in templates {
|
for template in templates {
|
||||||
self.interpreter
|
let mut roots = vec![];
|
||||||
.SaveTemplate(serde_wasm_bindgen::to_value(&template).unwrap());
|
|
||||||
|
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>) {
|
pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
|
||||||
use Mutation::*;
|
use Mutation::*;
|
||||||
let i = &self.interpreter;
|
let i = &mut self.interpreter;
|
||||||
for edit in edits.drain(..) {
|
for edit in &edits {
|
||||||
match edit {
|
match edit {
|
||||||
AppendChildren { id, m } => i.AppendChildren(m as u32, id.0 as u32),
|
AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
|
||||||
AssignId { path, id } => i.AssignId(path, id.0 as u32),
|
AssignId { path, id } => {
|
||||||
CreatePlaceholder { id } => i.CreatePlaceholder(id.0 as u32),
|
i.assign_id(path.as_ptr() as u32, path.len() as u8, 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),
|
CreatePlaceholder { id } => i.create_placeholder(id.0 as u32),
|
||||||
LoadTemplate { name, index, id } => i.LoadTemplate(name, index as u32, id.0 as u32),
|
CreateTextNode { value, id } => i.create_text_node(value, id.0 as u32),
|
||||||
ReplaceWith { id, m } => i.ReplaceWith(id.0 as u32, m as u32),
|
HydrateText { path, value, id } => {
|
||||||
ReplacePlaceholder { path, m } => i.ReplacePlaceholder(path, m as u32),
|
i.hydrate_text(path.as_ptr() as u32, path.len() as u8, value, id.0 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),
|
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 {
|
SetAttribute {
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
id,
|
id,
|
||||||
ns,
|
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 } => {
|
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, .. } => {
|
NewEventListener { name, id, .. } => {
|
||||||
self.interpreter.NewEventListener(
|
i.new_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8);
|
||||||
name,
|
|
||||||
id.0 as u32,
|
|
||||||
event_bubbles(&name[2..]),
|
|
||||||
self.handler.as_ref().unchecked_ref(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
RemoveEventListener { name, id } => i.RemoveEventListener(name, id.0 as u32),
|
RemoveEventListener { name, id } => {
|
||||||
Remove { id } => i.Remove(id.0 as u32),
|
i.remove_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8)
|
||||||
PushRoot { id } => i.PushRoot(id.0 as u32),
|
}
|
||||||
|
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 {
|
Rc::new(FormData { value, values })
|
||||||
value,
|
}
|
||||||
values,
|
|
||||||
files: None,
|
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
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
pub use crate::cfg::Config;
|
pub use crate::cfg::Config;
|
||||||
use crate::dom::virtual_event_from_websys_event;
|
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 dioxus_core::{Element, ElementId, Scope, VirtualDom};
|
||||||
use futures_util::{pin_mut, FutureExt, StreamExt};
|
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
|
// the mutations come back with nothing - we need to actually mount them
|
||||||
websys_dom.mount();
|
websys_dom.mount();
|
||||||
|
|
||||||
let mut work_loop = ric_raf::RafLoop::new();
|
let _work_loop = ric_raf::RafLoop::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
log::debug!("waiting for work");
|
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
|
// Dequeue all of the events from the channel in send order
|
||||||
// todo: we should re-order these if possible
|
// todo: we should re-order these if possible
|
||||||
while let Some(evt) = res {
|
while let Some(evt) = res {
|
||||||
let name = evt.type_();
|
dom.handle_event(evt.name.as_str(), evt.data, evt.element, evt.bubbles);
|
||||||
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);
|
|
||||||
}
|
|
||||||
res = rx.try_next().transpose().unwrap().ok();
|
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
|
// Jank free rendering
|
||||||
//
|
//
|
||||||
// 1. wait for the browser to give us "idle" time
|
// 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
|
// 4. Wait for the animation frame to patch the dom
|
||||||
|
|
||||||
// wait for the mainthread to schedule us in
|
// 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
|
// 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
|
// 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.load_templates(&edits.templates);
|
||||||
websys_dom.apply_edits(edits.edits);
|
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 {
|
// if should_hydrate {
|
||||||
// // todo: we need to split rebuild and initialize into two phases
|
// // todo: we need to split rebuild and initialize into two phases
|
||||||
// // it's a waste to produce edits just to get the vdom loaded
|
// // it's a waste to produce edits just to get the vdom loaded
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
//! Utilities specific to websys
|
//! Utilities specific to websys
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
future::{IntoFuture, Ready},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
use dioxus_core::*;
|
use dioxus_core::*;
|
||||||
|
use serde::de::Error;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
/// Get a closure that executes any JavaScript in the webpage.
|
/// 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
|
/// The closure will panic if the provided script is not valid JavaScript code
|
||||||
/// or if it returns an uncaught error.
|
/// 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(|| {
|
cx.use_hook(|| {
|
||||||
|script: S| {
|
|script: S| {
|
||||||
js_sys::Function::new_no_args(&script.to_string())
|
let body = script.to_string();
|
||||||
.call0(&wasm_bindgen::JsValue::NULL)
|
EvalResult {
|
||||||
.expect("failed to eval script");
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user