mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 14:44:12 +00:00
merge upstream changes
This commit is contained in:
commit
231e32d76e
343 changed files with 20249 additions and 14080 deletions
39
.github/workflows/docs stable.yml
vendored
Normal file
39
.github/workflows/docs stable.yml
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
name: docs stable
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: docs
|
||||||
|
steps:
|
||||||
|
|
||||||
|
# NOTE: Comment out when https://github.com/rust-lang/mdBook/pull/1306 is merged and released
|
||||||
|
# - name: Setup mdBook
|
||||||
|
# uses: peaceiris/actions-mdbook@v1
|
||||||
|
# with:
|
||||||
|
# mdbook-version: "0.4.10"
|
||||||
|
|
||||||
|
# NOTE: Delete when the previous one is enabled
|
||||||
|
- name: Setup mdBook
|
||||||
|
run: |
|
||||||
|
cargo install mdbook --git https://github.com/Demonthos/mdBook.git --branch master
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cd docs &&
|
||||||
|
cd guide && mdbook build -d ../nightly/guide && cd .. &&
|
||||||
|
cd router && mdbook build -d ../nightly/router && cd ..
|
||||||
|
# cd reference && mdbook build -d ../nightly/reference && cd .. &&
|
||||||
|
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
||||||
|
|
||||||
|
- name: Deploy 🚀
|
||||||
|
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||||
|
with:
|
||||||
|
branch: gh-pages # The branch the action should deploy to.
|
||||||
|
folder: docs/nightly # The folder the action should deploy.
|
||||||
|
target-folder: docs
|
||||||
|
repository-name: dioxuslabs/docsite
|
||||||
|
clean: false
|
||||||
|
token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now
|
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
|
@ -23,13 +23,15 @@ jobs:
|
||||||
# NOTE: Delete when the previous one is enabled
|
# NOTE: Delete when the previous one is enabled
|
||||||
- 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/Demonthos/mdBook.git --branch master
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cd docs &&
|
run: cd docs &&
|
||||||
cd guide && mdbook build -d ../nightly/guide && cd .. &&
|
cd guide && mdbook build -d ../nightly/guide && cd .. &&
|
||||||
cd router && mdbook build -d ../nightly/router && cd ..
|
cd router && mdbook build -d ../nightly/router && cd ..
|
||||||
|
# cd reference && mdbook build -d ../nightly/reference && cd .. &&
|
||||||
|
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||||
|
|
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
|
@ -104,7 +104,7 @@ jobs:
|
||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: clippy
|
command: clippy
|
||||||
args: --workspace -- -D warnings
|
args: --workspace --examples --tests -- -D warnings
|
||||||
|
|
||||||
# Coverage is disabled until we can fix it
|
# Coverage is disabled until we can fix it
|
||||||
# coverage:
|
# coverage:
|
||||||
|
|
124
.github/workflows/miri.yml
vendored
Normal file
124
.github/workflows/miri.yml
vendored
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
name: Miri Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Run in PRs and for bors, but not on master.
|
||||||
|
branches:
|
||||||
|
- 'auto'
|
||||||
|
- 'try'
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
schedule:
|
||||||
|
- cron: '6 6 * * *' # At 6:06 UTC every day.
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_UNSTABLE_SPARSE_REGISTRY: 'true'
|
||||||
|
RUSTFLAGS: -Dwarnings
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
# Change to specific Rust release to pin
|
||||||
|
rust_stable: stable
|
||||||
|
rust_nightly: nightly-2022-11-03
|
||||||
|
rust_clippy: 1.65.0
|
||||||
|
# When updating this, also update:
|
||||||
|
# - README.md
|
||||||
|
# - tokio/README.md
|
||||||
|
# - CONTRIBUTING.md
|
||||||
|
# - tokio/Cargo.toml
|
||||||
|
# - tokio-util/Cargo.toml
|
||||||
|
# - tokio-test/Cargo.toml
|
||||||
|
# - tokio-stream/Cargo.toml
|
||||||
|
# rust_min: 1.49.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
HOST_TARGET: ${{ matrix.host_target }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
host_target: x86_64-unknown-linux-gnu
|
||||||
|
# - os: macos-latest
|
||||||
|
# host_target: x86_64-apple-darwin
|
||||||
|
# - os: windows-latest
|
||||||
|
# host_target: i686-pc-windows-msvc
|
||||||
|
# - os: windows-latest
|
||||||
|
# host_target: i686-pc-windows-msvc
|
||||||
|
# - os: windows-latest
|
||||||
|
# host_target: i686-pc-windows-msvc
|
||||||
|
# - os: windows-latest
|
||||||
|
# host_target: i686-pc-windows-msvc
|
||||||
|
steps:
|
||||||
|
- name: Set the tag GC interval to 1 on linux
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: echo "MIRIFLAGS=-Zmiri-tag-gc=1" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Install Rust ${{ env.rust_nightly }}
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.rust_nightly }}
|
||||||
|
components: miri
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- name: miri
|
||||||
|
# Many of tests in tokio/tests and doctests use #[tokio::test] or
|
||||||
|
# #[tokio::main] that calls epoll_create1 that Miri does not support.
|
||||||
|
# run: cargo miri test --features full --lib --no-fail-fast
|
||||||
|
run: |
|
||||||
|
cargo miri test --package dioxus-core --test miri_stress -- --exact --nocapture
|
||||||
|
cargo miri test --package dioxus-native-core --test miri_native -- --exact --nocapture
|
||||||
|
|
||||||
|
# working-directory: tokio
|
||||||
|
env:
|
||||||
|
# todo: disable memory leaks ignore
|
||||||
|
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks
|
||||||
|
PROPTEST_CASES: 10
|
||||||
|
|
||||||
|
# Cache the global cargo directory, but NOT the local `target` directory which
|
||||||
|
# we cannot reuse anyway when the nightly changes (and it grows quite large
|
||||||
|
# over time).
|
||||||
|
# - name: Add cache for cargo
|
||||||
|
# id: cache
|
||||||
|
# uses: actions/cache@v3
|
||||||
|
# with:
|
||||||
|
# path: |
|
||||||
|
# # Taken from <https://doc.rust-lang.org/nightly/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci>.
|
||||||
|
# ~/.cargo/bin
|
||||||
|
# ~/.cargo/registry/index
|
||||||
|
# ~/.cargo/registry/cache
|
||||||
|
# ~/.cargo/git/db
|
||||||
|
# # contains package information of crates installed via `cargo install`.
|
||||||
|
# ~/.cargo/.crates.toml
|
||||||
|
# ~/.cargo/.crates2.json
|
||||||
|
# key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
# restore-keys: ${{ runner.os }}-cargo
|
||||||
|
|
||||||
|
# - name: Install rustup-toolchain-install-master
|
||||||
|
# if: ${{ steps.cache.outputs.cache-hit != 'true' }}
|
||||||
|
# shell: bash
|
||||||
|
# run: |
|
||||||
|
# cargo install -f rustup-toolchain-install-master
|
||||||
|
# - name: Install "master" toolchain
|
||||||
|
# shell: bash
|
||||||
|
# run: |
|
||||||
|
# if [[ ${{ github.event_name }} == 'schedule' ]]; then
|
||||||
|
# echo "Building against latest rustc git version"
|
||||||
|
# git ls-remote https://github.com/rust-lang/rust/ HEAD | cut -f 1 > rust-version
|
||||||
|
# fi
|
||||||
|
# toolchain --host ${{ matrix.host_target }}
|
||||||
|
# - name: Show Rust version
|
||||||
|
# run: |
|
||||||
|
# rustup show
|
||||||
|
# rustc -Vv
|
||||||
|
# cargo -V
|
||||||
|
# - name: Test
|
||||||
|
# run: |
|
||||||
|
# MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-ignore-leaks" cargo +nightly miri test --package dioxus-core --test miri_stress -- --exact --nocapture
|
||||||
|
# MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-ignore-leaks" cargo +nightly miri test --package dioxus-native-core --test miri_native -- --exact --nocapture
|
19
.github/workflows/windows.yml
vendored
19
.github/workflows/windows.yml
vendored
|
@ -36,8 +36,8 @@ jobs:
|
||||||
# There's a limit of 60 concurrent jobs across all repos in the rust-lang organization.
|
# There's a limit of 60 concurrent jobs across all repos in the rust-lang organization.
|
||||||
# In order to prevent overusing too much of that 60 limit, we throttle the
|
# In order to prevent overusing too much of that 60 limit, we throttle the
|
||||||
# number of rustfmt jobs that will run concurrently.
|
# number of rustfmt jobs that will run concurrently.
|
||||||
max-parallel: 2
|
# max-parallel:
|
||||||
fail-fast: false
|
# fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
target: [x86_64-pc-windows-gnu, x86_64-pc-windows-msvc]
|
target: [x86_64-pc-windows-gnu, x86_64-pc-windows-msvc]
|
||||||
cfg_release_channel: [stable]
|
cfg_release_channel: [stable]
|
||||||
|
@ -64,10 +64,21 @@ 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
|
# - name: checkout
|
||||||
uses: actions/checkout@v3
|
# uses: actions/checkout@v3
|
||||||
|
# with:
|
||||||
|
# path: C:/dioxus.git
|
||||||
|
# fetch-depth: 1
|
||||||
|
|
||||||
|
# we need to use the C drive as the working directory
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
run: |
|
||||||
|
mkdir C:/dioxus.git
|
||||||
|
git clone https://github.com/dioxuslabs/dioxus.git C:/dioxus.git --depth 1
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
|
working-directory: C:/dioxus.git
|
||||||
run: |
|
run: |
|
||||||
rustc -Vv
|
rustc -Vv
|
||||||
cargo -V
|
cargo -V
|
||||||
|
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -2,5 +2,6 @@
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"[toml]": {
|
"[toml]": {
|
||||||
"editor.formatOnSave": false
|
"editor.formatOnSave": false
|
||||||
}
|
},
|
||||||
|
"rust-analyzer.checkOnSave.allTargets": false,
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,13 @@ members = [
|
||||||
"packages/liveview",
|
"packages/liveview",
|
||||||
"packages/autofmt",
|
"packages/autofmt",
|
||||||
"packages/rsx",
|
"packages/rsx",
|
||||||
"packages/tui",
|
"packages/dioxus-tui",
|
||||||
|
"packages/rink",
|
||||||
"packages/native-core",
|
"packages/native-core",
|
||||||
"packages/native-core-macro",
|
"packages/native-core-macro",
|
||||||
|
"packages/rsx-rosetta",
|
||||||
|
"packages/signals",
|
||||||
|
"packages/hot-reload",
|
||||||
"docs/guide",
|
"docs/guide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -40,9 +44,10 @@ publish = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus = { path = "./packages/dioxus" }
|
dioxus = { path = "./packages/dioxus" }
|
||||||
dioxus-desktop = { path = "./packages/desktop" }
|
dioxus-desktop = { path = "./packages/desktop", features = ["transparent"] }
|
||||||
dioxus-ssr = { path = "./packages/ssr" }
|
dioxus-ssr = { path = "./packages/ssr" }
|
||||||
dioxus-router = { path = "./packages/router" }
|
dioxus-router = { path = "./packages/router" }
|
||||||
|
dioxus-signals = { path = "./packages/signals" }
|
||||||
fermi = { path = "./packages/fermi" }
|
fermi = { path = "./packages/fermi" }
|
||||||
futures-util = "0.3.21"
|
futures-util = "0.3.21"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/DioxusLabs/example-projects"> Examples </a>
|
<a href="https://github.com/DioxusLabs/example-projects"> Examples </a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://dioxuslabs.com/guide"> Guide </a>
|
<a href="https://dioxuslabs.com/docs/0.3/guide/en/"> Guide </a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/DioxusLabs/dioxus/blob/master/notes/README/ZH_CN.md"> 中文 </a>
|
<a href="https://github.com/DioxusLabs/dioxus/blob/master/notes/README/ZH_CN.md"> 中文 </a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
|
|
|
@ -4,18 +4,24 @@ version = "0.0.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Dioxus guide, including testable examples"
|
description = "Dioxus guide, including testable examples"
|
||||||
license = "MIT/Apache-2.0"
|
license = "MIT/Apache-2.0"
|
||||||
|
publish = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus = { path = "../../packages/dioxus" }
|
dioxus = { path = "../../packages/dioxus" }
|
||||||
dioxus-desktop = { path = "../../packages/desktop" }
|
dioxus-desktop = { path = "../../packages/desktop" }
|
||||||
dioxus-web = { path = "../../packages/web" }
|
dioxus-web = { path = "../../packages/web" }
|
||||||
dioxus-ssr = { path = "../../packages/ssr" }
|
dioxus-ssr = { path = "../../packages/ssr" }
|
||||||
|
dioxus-native-core = { path = "../../packages/native-core" }
|
||||||
|
dioxus-native-core-macro = { path = "../../packages/native-core-macro" }
|
||||||
dioxus-router = { path = "../../packages/router" }
|
dioxus-router = { path = "../../packages/router" }
|
||||||
dioxus-tui = { path = "../../packages/tui" }
|
dioxus-liveview = { path = "../../packages/liveview", features = ["axum"] }
|
||||||
|
dioxus-tui = { path = "../../packages/dioxus-tui" }
|
||||||
fermi = { path = "../../packages/fermi" }
|
fermi = { path = "../../packages/fermi" }
|
||||||
|
shipyard = "0.6.2"
|
||||||
|
|
||||||
|
|
||||||
# dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] }
|
# dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] }
|
||||||
serde = { version = "1.0.138", features=["derive"] }
|
serde = { version = "1.0.138", features=["derive"] }
|
||||||
reqwest = { version = "0.11.11", features = ["json"] }
|
reqwest = { version = "0.11.11", features = ["json"] }
|
||||||
tokio = { version = "1.19.2" , features=[]}
|
tokio = { version = "1.19.2" , features=[]}
|
||||||
|
axum = { version = "0.6.1", features = ["ws"] }
|
||||||
|
|
|
@ -24,6 +24,8 @@ edit-url-template = "https://github.com/DioxusLabs/dioxus/edit/master/docs/guide
|
||||||
[output.html.playground]
|
[output.html.playground]
|
||||||
editable = true
|
editable = true
|
||||||
line-numbers = true
|
line-numbers = true
|
||||||
|
# running examples will not work because dioxus is not a included in the playground
|
||||||
|
runnable = false
|
||||||
|
|
||||||
[output.html.search]
|
[output.html.search]
|
||||||
limit-results = 20
|
limit-results = 20
|
||||||
|
|
|
@ -5,6 +5,7 @@ fn main() {
|
||||||
dioxus_desktop::launch(App);
|
dioxus_desktop::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
// ANCHOR: boolean_attribute
|
// ANCHOR: boolean_attribute
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
|
|
@ -5,6 +5,7 @@ fn main() {
|
||||||
dioxus_desktop::launch(App);
|
dioxus_desktop::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
// ANCHOR: OptionalProps_usage
|
// ANCHOR: OptionalProps_usage
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
#![allow(unused)]
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ pub fn App(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline_props]
|
#[inline_props]
|
||||||
|
#[rustfmt::skip]
|
||||||
fn LogIn<'a>(
|
fn LogIn<'a>(
|
||||||
cx: Scope<'a>,
|
cx: Scope<'a>,
|
||||||
is_logged_in: bool,
|
is_logged_in: bool,
|
||||||
|
@ -25,13 +27,11 @@ fn LogIn<'a>(
|
||||||
// ANCHOR: if_else
|
// ANCHOR: if_else
|
||||||
if *is_logged_in {
|
if *is_logged_in {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
"Welcome!"
|
||||||
"Welcome!",
|
|
||||||
button {
|
button {
|
||||||
onclick: move |_| on_log_out.call(()),
|
onclick: move |_| on_log_out.call(()),
|
||||||
"Log Out",
|
"Log Out",
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
@ -45,10 +45,47 @@ fn LogIn<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline_props]
|
#[inline_props]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn LogInImproved<'a>(
|
||||||
|
cx: Scope<'a>,
|
||||||
|
is_logged_in: bool,
|
||||||
|
on_log_in: EventHandler<'a>,
|
||||||
|
on_log_out: EventHandler<'a>,
|
||||||
|
) -> Element<'a> {
|
||||||
|
// ANCHOR: if_else_improved
|
||||||
|
cx.render(rsx! {
|
||||||
|
// We only render the welcome message if we are logged in
|
||||||
|
// You can use if statements in the middle of a render block to conditionally render elements
|
||||||
|
if *is_logged_in {
|
||||||
|
// Notice the body of this if statment is rsx code, not an expression
|
||||||
|
"Welcome!"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
// depending on the value of `is_logged_in`, we will call a different event handler
|
||||||
|
onclick: move |_| if *is_logged_in {
|
||||||
|
on_log_in.call(())
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
on_log_out.call(())
|
||||||
|
},
|
||||||
|
if *is_logged_in {
|
||||||
|
// if we are logged in, the button should say "Log Out"
|
||||||
|
"Log Out"
|
||||||
|
} else {
|
||||||
|
// if we are not logged in, the button should say "Log In"
|
||||||
|
"Log In"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// ANCHOR_END: if_else_improved
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline_props]
|
||||||
|
#[rustfmt::skip]
|
||||||
fn LogInWarning(cx: Scope, is_logged_in: bool) -> Element {
|
fn LogInWarning(cx: Scope, is_logged_in: bool) -> Element {
|
||||||
// ANCHOR: conditional_none
|
// ANCHOR: conditional_none
|
||||||
if *is_logged_in {
|
if *is_logged_in {
|
||||||
return cx.render(rsx!(()));
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
|
311
docs/guide/examples/custom_renderer.rs
Normal file
311
docs/guide/examples/custom_renderer.rs
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
use dioxus::html::input_data::keyboard_types::{Code, Key, Modifiers};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_native_core::exports::shipyard::Component;
|
||||||
|
use dioxus_native_core::node_ref::*;
|
||||||
|
use dioxus_native_core::prelude::*;
|
||||||
|
use dioxus_native_core::utils::cursor::{Cursor, Pos};
|
||||||
|
use dioxus_native_core_macro::partial_derive_state;
|
||||||
|
|
||||||
|
// ANCHOR: state_impl
|
||||||
|
struct FontSize(f64);
|
||||||
|
|
||||||
|
// All states need to derive Component
|
||||||
|
#[derive(Default, Debug, Copy, Clone, Component)]
|
||||||
|
struct Size(f64, f64);
|
||||||
|
|
||||||
|
/// Derive some of the boilerplate for the State implementation
|
||||||
|
#[partial_derive_state]
|
||||||
|
impl State for Size {
|
||||||
|
type ParentDependencies = ();
|
||||||
|
|
||||||
|
// The size of the current node depends on the size of its children
|
||||||
|
type ChildDependencies = (Self,);
|
||||||
|
|
||||||
|
type NodeDependencies = ();
|
||||||
|
|
||||||
|
// Size only cares about the width, height, and text parts of the current node
|
||||||
|
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
|
||||||
|
// Get access to the width and height attributes
|
||||||
|
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
|
||||||
|
// Get access to the text of the node
|
||||||
|
.with_text();
|
||||||
|
|
||||||
|
fn update<'a>(
|
||||||
|
&mut self,
|
||||||
|
node_view: NodeView<()>,
|
||||||
|
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||||
|
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||||
|
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||||
|
context: &SendAnyMap,
|
||||||
|
) -> bool {
|
||||||
|
let font_size = context.get::<FontSize>().unwrap().0;
|
||||||
|
let mut width;
|
||||||
|
let mut height;
|
||||||
|
if let Some(text) = node_view.text() {
|
||||||
|
// if the node has text, use the text to size our object
|
||||||
|
width = text.len() as f64 * font_size;
|
||||||
|
height = font_size;
|
||||||
|
} else {
|
||||||
|
// otherwise, the size is the maximum size of the children
|
||||||
|
width = children
|
||||||
|
.iter()
|
||||||
|
.map(|(item,)| item.0)
|
||||||
|
.reduce(|accum, item| if accum >= item { accum } else { item })
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
height = children
|
||||||
|
.iter()
|
||||||
|
.map(|(item,)| item.1)
|
||||||
|
.reduce(|accum, item| if accum >= item { accum } else { item })
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
}
|
||||||
|
// if the node contains a width or height attribute it overrides the other size
|
||||||
|
for a in node_view.attributes().into_iter().flatten() {
|
||||||
|
match &*a.attribute.name {
|
||||||
|
"width" => width = a.value.as_float().unwrap(),
|
||||||
|
"height" => height = a.value.as_float().unwrap(),
|
||||||
|
// because Size only depends on the width and height, no other attributes will be passed to the member
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
|
||||||
|
let changed = (width != self.0) || (height != self.1);
|
||||||
|
*self = Self(width, height);
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
|
||||||
|
struct TextColor {
|
||||||
|
r: u8,
|
||||||
|
g: u8,
|
||||||
|
b: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[partial_derive_state]
|
||||||
|
impl State for TextColor {
|
||||||
|
// TextColor depends on the TextColor part of the parent
|
||||||
|
type ParentDependencies = (Self,);
|
||||||
|
|
||||||
|
type ChildDependencies = ();
|
||||||
|
|
||||||
|
type NodeDependencies = ();
|
||||||
|
|
||||||
|
// TextColor only cares about the color attribute of the current node
|
||||||
|
const NODE_MASK: NodeMaskBuilder<'static> =
|
||||||
|
// Get access to the color attribute
|
||||||
|
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
|
||||||
|
|
||||||
|
fn update<'a>(
|
||||||
|
&mut self,
|
||||||
|
node_view: NodeView<()>,
|
||||||
|
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||||
|
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||||
|
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||||
|
_context: &SendAnyMap,
|
||||||
|
) -> bool {
|
||||||
|
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
|
||||||
|
let new = match node_view
|
||||||
|
.attributes()
|
||||||
|
.and_then(|mut attrs| attrs.next())
|
||||||
|
.and_then(|attr| attr.value.as_text())
|
||||||
|
{
|
||||||
|
// if there is a color tag, translate it
|
||||||
|
Some("red") => TextColor { r: 255, g: 0, b: 0 },
|
||||||
|
Some("green") => TextColor { r: 0, g: 255, b: 0 },
|
||||||
|
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
|
||||||
|
Some(color) => panic!("unknown color {color}"),
|
||||||
|
// otherwise check if the node has a parent and inherit that color
|
||||||
|
None => match parent {
|
||||||
|
Some((parent,)) => *parent,
|
||||||
|
None => Self::default(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// check if the member has changed
|
||||||
|
let changed = new != *self;
|
||||||
|
*self = new;
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
|
||||||
|
struct Border(bool);
|
||||||
|
|
||||||
|
#[partial_derive_state]
|
||||||
|
impl State for Border {
|
||||||
|
// TextColor depends on the TextColor part of the parent
|
||||||
|
type ParentDependencies = (Self,);
|
||||||
|
|
||||||
|
type ChildDependencies = ();
|
||||||
|
|
||||||
|
type NodeDependencies = ();
|
||||||
|
|
||||||
|
// Border does not depended on any other member in the current node
|
||||||
|
const NODE_MASK: NodeMaskBuilder<'static> =
|
||||||
|
// Get access to the border attribute
|
||||||
|
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
|
||||||
|
|
||||||
|
fn update<'a>(
|
||||||
|
&mut self,
|
||||||
|
node_view: NodeView<()>,
|
||||||
|
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||||
|
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||||
|
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||||
|
_context: &SendAnyMap,
|
||||||
|
) -> bool {
|
||||||
|
// check if the node contians a border attribute
|
||||||
|
let new = Self(
|
||||||
|
node_view
|
||||||
|
.attributes()
|
||||||
|
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
|
||||||
|
.is_some(),
|
||||||
|
);
|
||||||
|
// check if the member has changed
|
||||||
|
let changed = new != *self;
|
||||||
|
*self = new;
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ANCHOR_END: state_impl
|
||||||
|
|
||||||
|
// ANCHOR: rendering
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let count = use_state(cx, || 0);
|
||||||
|
|
||||||
|
use_future(cx, (count,), |(count,)| async move {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
count.set(*count + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div{
|
||||||
|
color: "red",
|
||||||
|
"{count}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the vdom, the real_dom, and the binding layer between them
|
||||||
|
let mut vdom = VirtualDom::new(app);
|
||||||
|
let mut rdom: RealDom = RealDom::new([
|
||||||
|
Border::to_type_erased(),
|
||||||
|
TextColor::to_type_erased(),
|
||||||
|
Size::to_type_erased(),
|
||||||
|
]);
|
||||||
|
let mut dioxus_intigration_state = DioxusState::create(&mut rdom);
|
||||||
|
|
||||||
|
let mutations = vdom.rebuild();
|
||||||
|
// update the structure of the real_dom tree
|
||||||
|
dioxus_intigration_state.apply_mutations(&mut rdom, mutations);
|
||||||
|
let mut ctx = SendAnyMap::new();
|
||||||
|
// set the font size to 3.3
|
||||||
|
ctx.insert(FontSize(3.3));
|
||||||
|
// update the State for nodes in the real_dom tree
|
||||||
|
let _to_rerender = rdom.update_state(ctx);
|
||||||
|
|
||||||
|
// we need to run the vdom in a async runtime
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?
|
||||||
|
.block_on(async {
|
||||||
|
loop {
|
||||||
|
// wait for the vdom to update
|
||||||
|
vdom.wait_for_work().await;
|
||||||
|
|
||||||
|
// get the mutations from the vdom
|
||||||
|
let mutations = vdom.render_immediate();
|
||||||
|
|
||||||
|
// update the structure of the real_dom tree
|
||||||
|
dioxus_intigration_state.apply_mutations(&mut rdom, mutations);
|
||||||
|
|
||||||
|
// update the state of the real_dom tree
|
||||||
|
let mut ctx = SendAnyMap::new();
|
||||||
|
// set the font size to 3.3
|
||||||
|
ctx.insert(FontSize(3.3));
|
||||||
|
let _to_rerender = rdom.update_state(ctx);
|
||||||
|
|
||||||
|
// render...
|
||||||
|
rdom.traverse_depth_first(|node| {
|
||||||
|
let indent = " ".repeat(node.height() as usize);
|
||||||
|
let color = *node.get::<TextColor>().unwrap();
|
||||||
|
let size = *node.get::<Size>().unwrap();
|
||||||
|
let border = *node.get::<Border>().unwrap();
|
||||||
|
let id = node.id();
|
||||||
|
let node = node.node_type();
|
||||||
|
let node_type = &*node;
|
||||||
|
println!("{indent}{id:?} {color:?} {size:?} {border:?} {node_type:?}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// ANCHOR_END: rendering
|
||||||
|
|
||||||
|
// ANCHOR: derive_state
|
||||||
|
// All states must derive Component (https://docs.rs/shipyard/latest/shipyard/derive.Component.html)
|
||||||
|
// They also must implement Default or provide a custom implementation of create in the State trait
|
||||||
|
#[derive(Default, Component)]
|
||||||
|
struct MyState;
|
||||||
|
|
||||||
|
/// Derive some of the boilerplate for the State implementation
|
||||||
|
#[partial_derive_state]
|
||||||
|
impl State for MyState {
|
||||||
|
// The states of the parent nodes this state depends on
|
||||||
|
type ParentDependencies = ();
|
||||||
|
|
||||||
|
// The states of the child nodes this state depends on
|
||||||
|
type ChildDependencies = (Self,);
|
||||||
|
|
||||||
|
// The states of the current node this state depends on
|
||||||
|
type NodeDependencies = ();
|
||||||
|
|
||||||
|
// The parts of the current text, element, or placeholder node in the tree that this state depends on
|
||||||
|
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
|
||||||
|
|
||||||
|
// How to update the state of the current node based on the state of the parent nodes, child nodes, and the current node
|
||||||
|
// Returns true if the node was updated and false if the node was not updated
|
||||||
|
fn update<'a>(
|
||||||
|
&mut self,
|
||||||
|
// The view of the current node limited to the parts this state depends on
|
||||||
|
_node_view: NodeView<()>,
|
||||||
|
// The state of the current node that this state depends on
|
||||||
|
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||||
|
// The state of the parent nodes that this state depends on
|
||||||
|
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||||
|
// The state of the child nodes that this state depends on
|
||||||
|
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||||
|
// The context of the current node used to pass global state into the tree
|
||||||
|
_context: &SendAnyMap,
|
||||||
|
) -> bool {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
// partial_derive_state will generate a default implementation of all the other methods
|
||||||
|
}
|
||||||
|
// ANCHOR_END: derive_state
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
// ANCHOR: cursor
|
||||||
|
fn text_editing() {
|
||||||
|
let mut cursor = Cursor::default();
|
||||||
|
let mut text = String::new();
|
||||||
|
|
||||||
|
// handle keyboard input with a max text length of 10
|
||||||
|
cursor.handle_input(
|
||||||
|
&Code::ArrowRight,
|
||||||
|
&Key::ArrowRight,
|
||||||
|
&Modifiers::empty(),
|
||||||
|
&mut text,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
// mannually select text between characters 0-5 on the first line (this could be from dragging with a mouse)
|
||||||
|
cursor.start = Pos::new(0, 0);
|
||||||
|
cursor.end = Some(Pos::new(5, 0));
|
||||||
|
|
||||||
|
// delete the selected text and move the cursor to the start of the selection
|
||||||
|
cursor.delete_selection(&mut text);
|
||||||
|
}
|
||||||
|
// ANCHOR_END: cursor
|
|
@ -5,6 +5,7 @@ fn main() {
|
||||||
dioxus_desktop::launch(App);
|
dioxus_desktop::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
// ANCHOR: dangerous_inner_html
|
// ANCHOR: dangerous_inner_html
|
||||||
// this should come from a trusted source
|
// this should come from a trusted source
|
||||||
|
|
|
@ -5,6 +5,7 @@ fn main() {
|
||||||
dioxus_desktop::launch(App);
|
dioxus_desktop::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
// ANCHOR: rsx
|
// ANCHOR: rsx
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
|
|
@ -5,6 +5,7 @@ fn main() {
|
||||||
dioxus_desktop::launch(App);
|
dioxus_desktop::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
// ANCHOR: rsx
|
// ANCHOR: rsx
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
@ -14,7 +15,7 @@ fn App(cx: Scope) -> Element {
|
||||||
button {
|
button {
|
||||||
onclick: move |event| {
|
onclick: move |event| {
|
||||||
// now, outer won't be triggered
|
// now, outer won't be triggered
|
||||||
event.stop_propogation();
|
event.stop_propagation();
|
||||||
},
|
},
|
||||||
"inner"
|
"inner"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ fn main() {
|
||||||
dioxus_desktop::launch(App);
|
dioxus_desktop::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
// ANCHOR: prevent_default
|
// ANCHOR: prevent_default
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
// ANCHOR: all
|
// ANCHOR: all
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// launch the dioxus app in a webview
|
||||||
dioxus_desktop::launch(App);
|
dioxus_desktop::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ANCHOR: component
|
// ANCHOR: component
|
||||||
|
// define a component that renders a div with the text "Hello, world!"
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
|
|
60
docs/guide/examples/hello_world_liveview.rs
Normal file
60
docs/guide/examples/hello_world_liveview.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
// ANCHOR: all
|
||||||
|
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
// ANCHOR: glue
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
|
||||||
|
|
||||||
|
let view = dioxus_liveview::LiveViewPool::new();
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
// The root route contains the glue code to connect to the WebSocket
|
||||||
|
.route(
|
||||||
|
"/",
|
||||||
|
get(move || async move {
|
||||||
|
Html(format!(
|
||||||
|
r#"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head> <title>Dioxus LiveView with Axum</title> </head>
|
||||||
|
<body> <div id="main"></div> </body>
|
||||||
|
{glue}
|
||||||
|
</html>
|
||||||
|
"#,
|
||||||
|
// Create the glue code to connect to the WebSocket on the "/ws" route
|
||||||
|
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// The WebSocket route is what Dioxus uses to communicate with the browser
|
||||||
|
.route(
|
||||||
|
"/ws",
|
||||||
|
get(move |ws: WebSocketUpgrade| async move {
|
||||||
|
ws.on_upgrade(move |socket| async move {
|
||||||
|
// When the WebSocket is upgraded, launch the LiveView with the app component
|
||||||
|
_ = view.launch(dioxus_liveview::axum_socket(socket), app).await;
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("Listening on http://{addr}");
|
||||||
|
|
||||||
|
axum::Server::bind(&addr.to_string().parse().unwrap())
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
// ANCHOR_END: glue
|
||||||
|
|
||||||
|
// ANCHOR: app
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
"Hello, world!"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// ANCHOR_END: app
|
||||||
|
// ANCHOR_END: all
|
|
@ -1,20 +1,17 @@
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// launch the app in the terminal
|
||||||
dioxus_tui::launch(App);
|
dioxus_tui::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a component that renders a div with the text "Hello, world!"
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
width: "100%",
|
"Hello, world!"
|
||||||
height: "10px",
|
|
||||||
background_color: "red",
|
|
||||||
justify_content: "center",
|
|
||||||
align_items: "center",
|
|
||||||
|
|
||||||
"Hello world!"
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// launch the web app
|
||||||
dioxus_web::launch(App);
|
dioxus_web::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a component that renders a div with the text "Hello, world!"
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
|
|
|
@ -7,6 +7,7 @@ fn main() {
|
||||||
dioxus_desktop::launch(App);
|
dioxus_desktop::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
let you_are_happy = true;
|
let you_are_happy = true;
|
||||||
let you_know_it = false;
|
let you_know_it = false;
|
||||||
|
@ -31,7 +32,7 @@ fn App(cx: Scope) -> Element {
|
||||||
|
|
||||||
// ANCHOR: closure
|
// ANCHOR: closure
|
||||||
// ❌ don't call hooks inside closures!
|
// ❌ don't call hooks inside closures!
|
||||||
// We can't guarantee that the closure, if used, will be called at the same time every time
|
// We can't guarantee that the closure, if used, will be called in the same order every time
|
||||||
let _a = || {
|
let _a = || {
|
||||||
let b = use_state(cx, || 0);
|
let b = use_state(cx, || 0);
|
||||||
b.get()
|
b.get()
|
||||||
|
|
|
@ -7,7 +7,7 @@ fn main() {}
|
||||||
struct AppSettings {}
|
struct AppSettings {}
|
||||||
|
|
||||||
// ANCHOR: wrap_context
|
// ANCHOR: wrap_context
|
||||||
fn use_settings(cx: &ScopeState) -> UseSharedState<AppSettings> {
|
fn use_settings(cx: &ScopeState) -> &UseSharedState<AppSettings> {
|
||||||
use_shared_state::<AppSettings>(cx).expect("App settings not provided")
|
use_shared_state::<AppSettings>(cx).expect("App settings not provided")
|
||||||
}
|
}
|
||||||
// ANCHOR_END: wrap_context
|
// ANCHOR_END: wrap_context
|
||||||
|
|
|
@ -7,12 +7,25 @@ fn main() {
|
||||||
|
|
||||||
// ANCHOR: component
|
// ANCHOR: component
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
|
// count will be initialized to 0 the first time the component is rendered
|
||||||
let mut count = use_state(cx, || 0);
|
let mut count = use_state(cx, || 0);
|
||||||
|
|
||||||
cx.render(rsx!(
|
cx.render(rsx!(
|
||||||
h1 { "High-Five counter: {count}" }
|
h1 { "High-Five counter: {count}" }
|
||||||
button { onclick: move |_| count += 1, "Up high!" }
|
button {
|
||||||
button { onclick: move |_| count -= 1, "Down low!" }
|
onclick: move |_| {
|
||||||
|
// changing the count will cause the component to re-render
|
||||||
|
count += 1
|
||||||
|
},
|
||||||
|
"Up high!"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
// changing the count will cause the component to re-render
|
||||||
|
count -= 1
|
||||||
|
},
|
||||||
|
"Down low!"
|
||||||
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
// ANCHOR_END: component
|
// ANCHOR_END: component
|
||||||
|
|
|
@ -8,13 +8,12 @@ fn main() {
|
||||||
// ANCHOR: component
|
// ANCHOR: component
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
let list = use_ref(cx, Vec::new);
|
let list = use_ref(cx, Vec::new);
|
||||||
let list_formatted = format!("{:?}", *list.read());
|
|
||||||
|
|
||||||
cx.render(rsx!(
|
cx.render(rsx!(
|
||||||
p { "Current list: {list_formatted}" }
|
p { "Current list: {list.read():?}" }
|
||||||
button {
|
button {
|
||||||
onclick: move |event| {
|
onclick: move |event| {
|
||||||
list.write().push(event)
|
list.with_mut(|list| list.push(event));
|
||||||
},
|
},
|
||||||
"Click me!"
|
"Click me!"
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ fn main() {
|
||||||
struct DarkMode(bool);
|
struct DarkMode(bool);
|
||||||
// ANCHOR_END: DarkMode_struct
|
// ANCHOR_END: DarkMode_struct
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn App(cx: Scope) -> Element {
|
pub fn App(cx: Scope) -> Element {
|
||||||
// ANCHOR: context_provider
|
// ANCHOR: context_provider
|
||||||
use_shared_state_provider(cx, || DarkMode(false));
|
use_shared_state_provider(cx, || DarkMode(false));
|
||||||
|
@ -34,6 +35,7 @@ pub fn App(cx: Scope) -> Element {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn use_is_dark_mode(cx: &ScopeState) -> bool {
|
pub fn use_is_dark_mode(cx: &ScopeState) -> bool {
|
||||||
// ANCHOR: use_context
|
// ANCHOR: use_context
|
||||||
let dark_mode_context = use_shared_state::<DarkMode>(cx);
|
let dark_mode_context = use_shared_state::<DarkMode>(cx);
|
||||||
|
|
|
@ -12,6 +12,7 @@ struct Comment {
|
||||||
id: usize,
|
id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn App(cx: Scope) -> Element {
|
pub fn App(cx: Scope) -> Element {
|
||||||
// ANCHOR: render_list
|
// ANCHOR: render_list
|
||||||
let comment_field = use_state(cx, String::new);
|
let comment_field = use_state(cx, String::new);
|
||||||
|
@ -20,10 +21,10 @@ pub fn App(cx: Scope) -> Element {
|
||||||
|
|
||||||
let comments_lock = comments.read();
|
let comments_lock = comments.read();
|
||||||
let comments_rendered = comments_lock.iter().map(|comment| {
|
let comments_rendered = comments_lock.iter().map(|comment| {
|
||||||
cx.render(rsx!(CommentComponent {
|
rsx!(CommentComponent {
|
||||||
key: "{comment.id}",
|
key: "{comment.id}",
|
||||||
comment: comment.clone(),
|
comment: comment.clone(),
|
||||||
}))
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.render(rsx!(
|
cx.render(rsx!(
|
||||||
|
@ -50,6 +51,43 @@ pub fn App(cx: Scope) -> Element {
|
||||||
// ANCHOR_END: render_list
|
// ANCHOR_END: render_list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn AppForLoop(cx: Scope) -> Element {
|
||||||
|
// ANCHOR: render_list_for_loop
|
||||||
|
let comment_field = use_state(cx, String::new);
|
||||||
|
let mut next_id = use_state(cx, || 0);
|
||||||
|
let comments = use_ref(cx, Vec::<Comment>::new);
|
||||||
|
|
||||||
|
cx.render(rsx!(
|
||||||
|
form {
|
||||||
|
onsubmit: move |_| {
|
||||||
|
comments.write().push(Comment {
|
||||||
|
content: comment_field.get().clone(),
|
||||||
|
id: *next_id.get(),
|
||||||
|
});
|
||||||
|
next_id += 1;
|
||||||
|
|
||||||
|
comment_field.set(String::new());
|
||||||
|
},
|
||||||
|
input {
|
||||||
|
value: "{comment_field}",
|
||||||
|
oninput: |event| comment_field.set(event.value.clone()),
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
r#type: "submit",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
for comment in &*comments.read() {
|
||||||
|
// Notice the body of this for loop is rsx code, not an expression
|
||||||
|
CommentComponent {
|
||||||
|
key: "{comment.id}",
|
||||||
|
comment: comment.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
// ANCHOR_END: render_list_for_loop
|
||||||
|
}
|
||||||
|
|
||||||
#[inline_props]
|
#[inline_props]
|
||||||
fn CommentComponent(cx: Scope, comment: Comment) -> Element {
|
fn CommentComponent(cx: Scope, comment: Comment) -> Element {
|
||||||
cx.render(rsx!(div {
|
cx.render(rsx!(div {
|
||||||
|
|
|
@ -19,12 +19,17 @@ pub fn App(cx: Scope) -> Element {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn Empty(cx: Scope) -> Element {
|
pub fn Empty(cx: Scope) -> Element {
|
||||||
// ANCHOR: empty
|
// ANCHOR: empty
|
||||||
cx.render(rsx!(div {}))
|
cx.render(rsx!(div {
|
||||||
|
// attributes / listeners
|
||||||
|
// children
|
||||||
|
}))
|
||||||
// ANCHOR_END: empty
|
// ANCHOR_END: empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn Children(cx: Scope) -> Element {
|
pub fn Children(cx: Scope) -> Element {
|
||||||
// ANCHOR: children
|
// ANCHOR: children
|
||||||
cx.render(rsx!(ol {
|
cx.render(rsx!(ol {
|
||||||
|
@ -35,6 +40,7 @@ pub fn Children(cx: Scope) -> Element {
|
||||||
// ANCHOR_END: children
|
// ANCHOR_END: children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn Fragments(cx: Scope) -> Element {
|
pub fn Fragments(cx: Scope) -> Element {
|
||||||
// ANCHOR: fragments
|
// ANCHOR: fragments
|
||||||
cx.render(rsx!(
|
cx.render(rsx!(
|
||||||
|
@ -49,16 +55,28 @@ pub fn Fragments(cx: Scope) -> Element {
|
||||||
// ANCHOR_END: fragments
|
// ANCHOR_END: fragments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn ManyRoots(cx: Scope) -> Element {
|
||||||
|
// ANCHOR: manyroots
|
||||||
|
cx.render(rsx!(
|
||||||
|
p {"First Item"},
|
||||||
|
p {"Second Item"},
|
||||||
|
))
|
||||||
|
// ANCHOR_END: manyroots
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn Attributes(cx: Scope) -> Element {
|
pub fn Attributes(cx: Scope) -> Element {
|
||||||
// ANCHOR: attributes
|
// ANCHOR: attributes
|
||||||
cx.render(rsx!(a {
|
cx.render(rsx!(a {
|
||||||
href: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
href: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
class: "primary_button",
|
class: "primary_button",
|
||||||
"Log In"
|
color: "red",
|
||||||
}))
|
}))
|
||||||
// ANCHOR_END: attributes
|
// ANCHOR_END: attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn VariableAttributes(cx: Scope) -> Element {
|
pub fn VariableAttributes(cx: Scope) -> Element {
|
||||||
// ANCHOR: variable_attributes
|
// ANCHOR: variable_attributes
|
||||||
let written_in_rust = true;
|
let written_in_rust = true;
|
||||||
|
@ -71,22 +89,23 @@ pub fn VariableAttributes(cx: Scope) -> Element {
|
||||||
// ANCHOR_END: variable_attributes
|
// ANCHOR_END: variable_attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn CustomAttributes(cx: Scope) -> Element {
|
pub fn CustomAttributes(cx: Scope) -> Element {
|
||||||
// ANCHOR: custom_attributes
|
// ANCHOR: custom_attributes
|
||||||
cx.render(rsx!(b {
|
cx.render(rsx!(b {
|
||||||
"customAttribute": "value",
|
"customAttribute": "value",
|
||||||
"Rust is Cool"
|
|
||||||
}))
|
}))
|
||||||
// ANCHOR_END: custom_attributes
|
// ANCHOR_END: custom_attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn Formatting(cx: Scope) -> Element {
|
pub fn Formatting(cx: Scope) -> Element {
|
||||||
// ANCHOR: formatting
|
// ANCHOR: formatting
|
||||||
let coordinates = (42, 0);
|
let coordinates = (42, 0);
|
||||||
let country = "es";
|
let country = "es";
|
||||||
cx.render(rsx!(div {
|
cx.render(rsx!(div {
|
||||||
class: "country-{country}",
|
class: "country-{country}",
|
||||||
"Coordinates: {coordinates:?}",
|
"position": "{coordinates:?}",
|
||||||
// arbitrary expressions are allowed,
|
// arbitrary expressions are allowed,
|
||||||
// as long as they don't contain `{}`
|
// as long as they don't contain `{}`
|
||||||
div {
|
div {
|
||||||
|
@ -95,15 +114,56 @@ pub fn Formatting(cx: Scope) -> Element {
|
||||||
div {
|
div {
|
||||||
"{7*6}"
|
"{7*6}"
|
||||||
},
|
},
|
||||||
|
// {} can be escaped with {{}}
|
||||||
|
div {
|
||||||
|
"{{}}"
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
// ANCHOR_END: formatting
|
// ANCHOR_END: formatting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub fn Expression(cx: Scope) -> Element {
|
pub fn Expression(cx: Scope) -> Element {
|
||||||
// ANCHOR: expression
|
// ANCHOR: expression
|
||||||
let text = "Dioxus";
|
let text = "Dioxus";
|
||||||
cx.render(rsx!(span {
|
cx.render(rsx!(span {
|
||||||
text.to_uppercase()
|
text.to_uppercase(),
|
||||||
|
// create a list of text from 0 to 9
|
||||||
|
(0..10).map(|i| rsx!{ i.to_string() })
|
||||||
}))
|
}))
|
||||||
// ANCHOR_END: expression
|
// ANCHOR_END: expression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn Loops(cx: Scope) -> Element {
|
||||||
|
// ANCHOR: loops
|
||||||
|
cx.render(rsx!{
|
||||||
|
// use a for loop where the body itself is RSX
|
||||||
|
div {
|
||||||
|
// create a list of text from 0 to 9
|
||||||
|
for i in 0..3 {
|
||||||
|
// NOTE: the body of the loop is RSX not a rust statement
|
||||||
|
div {
|
||||||
|
"{i}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// iterator equivalent
|
||||||
|
div {
|
||||||
|
(0..3).map(|i| rsx!{ div { "{i}" } })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// ANCHOR_END: loops
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn IfStatements(cx: Scope) -> Element {
|
||||||
|
// ANCHOR: ifstatements
|
||||||
|
cx.render(rsx!{
|
||||||
|
// use if statements without an else
|
||||||
|
if true {
|
||||||
|
rsx!(div { "true" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// ANCHOR_END: ifstatements
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ struct ApiResponse {
|
||||||
image_url: String,
|
image_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
// ANCHOR: use_future
|
// ANCHOR: use_future
|
||||||
let future = use_future(cx, (), |_| async move {
|
let future = use_future(cx, (), |_| async move {
|
||||||
|
@ -44,6 +45,7 @@ fn App(cx: Scope) -> Element {
|
||||||
// ANCHOR_END: render
|
// ANCHOR_END: render
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
#[inline_props]
|
#[inline_props]
|
||||||
fn RandomDog(cx: Scope, breed: String) -> Element {
|
fn RandomDog(cx: Scope, breed: String) -> Element {
|
||||||
// ANCHOR: dependency
|
// ANCHOR: dependency
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
- [Getting Started](getting_started/index.md)
|
- [Getting Started](getting_started/index.md)
|
||||||
- [Desktop](getting_started/desktop.md)
|
- [Desktop](getting_started/desktop.md)
|
||||||
- [Web](getting_started/web.md)
|
- [Web](getting_started/web.md)
|
||||||
- [Hot Reload](getting_started/hot_reload.md)
|
|
||||||
- [Server-Side Rendering](getting_started/ssr.md)
|
- [Server-Side Rendering](getting_started/ssr.md)
|
||||||
|
- [Liveview](getting_started/liveview.md)
|
||||||
- [Terminal UI](getting_started/tui.md)
|
- [Terminal UI](getting_started/tui.md)
|
||||||
- [Mobile](getting_started/mobile.md)
|
- [Mobile](getting_started/mobile.md)
|
||||||
|
- [Hot Reloading](getting_started/hot_reload.md)
|
||||||
- [Describing the UI](describing_ui/index.md)
|
- [Describing the UI](describing_ui/index.md)
|
||||||
- [Special Attributes](describing_ui/special_attributes.md)
|
- [Special Attributes](describing_ui/special_attributes.md)
|
||||||
- [Components](describing_ui/components.md)
|
- [Components](describing_ui/components.md)
|
||||||
|
|
|
@ -8,16 +8,6 @@ The `use_future` and `use_coroutine` hooks are useful if you want to uncondition
|
||||||
|
|
||||||
> Note: `spawn` will always spawn a *new* future. You most likely don't want to call it on every render.
|
> Note: `spawn` will always spawn a *new* future. You most likely don't want to call it on every render.
|
||||||
|
|
||||||
The future must be `'static` – so any values captured by the task cannot carry any references to `cx`, such as a `UseState`.
|
|
||||||
|
|
||||||
However, since you'll typically need a way to update the value of a hook, you can use `to_owned` to create a clone of the hook handle. You can then use that clone in the async closure.
|
|
||||||
|
|
||||||
To make this a bit less verbose, Dioxus exports the `to_owned!` macro which will create a binding as shown above, which can be quite helpful when dealing with many values.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
{{#include ../../../examples/spawn.rs:to_owned_macro}}
|
|
||||||
```
|
|
||||||
|
|
||||||
Calling `spawn` will give you a `JoinHandle` which lets you cancel or pause the future.
|
Calling `spawn` will give you a `JoinHandle` which lets you cancel or pause the future.
|
||||||
|
|
||||||
## Spawning Tokio Tasks
|
## Spawning Tokio Tasks
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
# Coroutines
|
# Coroutines
|
||||||
|
|
||||||
Another good tool to keep in your async toolbox are coroutines. Coroutines are futures that can be manually stopped, started, paused, and resumed.
|
Another tool in your async toolbox are coroutines. Coroutines are futures that can be manually stopped, started, paused, and resumed.
|
||||||
|
|
||||||
Like regular futures, code in a Dioxus coroutine will run until the next `await` point before yielding. This low-level control over asynchronous tasks is quite powerful, allowing for infinitely looping tasks like WebSocket polling, background timers, and other periodic actions.
|
Like regular futures, code in a coroutine will run until the next `await` point before yielding. This low-level control over asynchronous tasks is quite powerful, allowing for infinitely looping tasks like WebSocket polling, background timers, and other periodic actions.
|
||||||
|
|
||||||
## `use_coroutine`
|
## `use_coroutine`
|
||||||
|
|
||||||
The basic setup for coroutines is the `use_coroutine` hook. Most coroutines we write will be polling loops using async/await.
|
The `use_coroutine` hook allows you to create a coroutine. Most coroutines we write will be polling loops using async/await.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
|
@ -50,14 +50,50 @@ if sync.is_running() {
|
||||||
|
|
||||||
This pattern is where coroutines are extremely useful – instead of writing all the complicated logic for pausing our async tasks like we would with JavaScript promises, the Rust model allows us to just not poll our future.
|
This pattern is where coroutines are extremely useful – instead of writing all the complicated logic for pausing our async tasks like we would with JavaScript promises, the Rust model allows us to just not poll our future.
|
||||||
|
|
||||||
|
## Yielding Values
|
||||||
|
|
||||||
|
To yield values from a coroutine, simply bring in a `UseState` handle and set the value whenever your coroutine completes its work.
|
||||||
|
|
||||||
|
The future must be `'static` – so any values captured by the task cannot carry any references to `cx`, such as a `UseState`.
|
||||||
|
|
||||||
|
You can use [to_owned](https://doc.rust-lang.org/std/borrow/trait.ToOwned.html#tymethod.to_owned) to create a clone of the hook handle which can be moved into the async closure.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let sync_status = use_state(cx, || Status::Launching);
|
||||||
|
let sync_task = use_coroutine(cx, |rx: UnboundedReceiver<SyncAction>| {
|
||||||
|
let sync_status = sync_status.to_owned();
|
||||||
|
async move {
|
||||||
|
loop {
|
||||||
|
delay_ms(1000).await;
|
||||||
|
sync_status.set(Status::Working);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
To make this a bit less verbose, Dioxus exports the `to_owned!` macro which will create a binding as shown above, which can be quite helpful when dealing with many values.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let sync_status = use_state(cx, || Status::Launching);
|
||||||
|
let load_status = use_state(cx, || Status::Launching);
|
||||||
|
let sync_task = use_coroutine(cx, |rx: UnboundedReceiver<SyncAction>| {
|
||||||
|
to_owned![sync_status, load_status];
|
||||||
|
async move {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
## Sending Values
|
## Sending Values
|
||||||
|
|
||||||
You might've noticed the `use_coroutine` closure takes an argument called `rx`. What is that? Well, a common pattern in complex apps is to handle a bunch of async code at once. With libraries like Redux Toolkit, managing multiple promises at once can be challenging and a common source of bugs.
|
You might've noticed the `use_coroutine` closure takes an argument called `rx`. What is that? Well, a common pattern in complex apps is to handle a bunch of async code at once. With libraries like Redux Toolkit, managing multiple promises at once can be challenging and a common source of bugs.
|
||||||
|
|
||||||
With Coroutines, we have the opportunity to centralize our async logic. The `rx` parameter is an Unbounded Channel for code external to the coroutine to send data *into* the coroutine. Instead of looping on an external service, we can loop on the channel itself, processing messages from within our app without needing to spawn a new future. To send data into the coroutine, we would call "send" on the handle.
|
With Coroutines, we can centralize our async logic. The `rx` parameter is an Channel that allows code external to the coroutine to send data *into* the coroutine. Instead of looping on an external service, we can loop on the channel itself, processing messages from within our app without needing to spawn a new future. To send data into the coroutine, we would call "send" on the handle.
|
||||||
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
|
|
||||||
enum ProfileUpdate {
|
enum ProfileUpdate {
|
||||||
SetUsername(String),
|
SetUsername(String),
|
||||||
SetAge(i32)
|
SetAge(i32)
|
||||||
|
@ -83,6 +119,10 @@ cx.render(rsx!{
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
> Note: In order to use/run the `rx.next().await` statement you will need to extend the [`Stream`] trait (used by [`UnboundedReceiver`]) by adding 'futures_util' as a dependency to your project and adding the `use futures_util::stream::StreamExt;`.
|
||||||
|
|
||||||
|
|
||||||
For sufficiently complex apps, we could build a bunch of different useful "services" that loop on channels to update the app.
|
For sufficiently complex apps, we could build a bunch of different useful "services" that loop on channels to update the app.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
@ -103,7 +143,7 @@ async fn editor_service(rx: UnboundedReceiver<EditorCommand>) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
We can combine coroutines with Fermi to emulate Redux Toolkit's Thunk system with much less headache. This lets us store all of our app's state *within* a task and then simply update the "view" values stored in Atoms. It cannot be understated how powerful this technique is: we get all the perks of native Rust tasks with the optimizations and ergonomics of global state. This means your *actual* state does not need to be tied up in a system like Fermi or Redux – the only Atoms that need to exist are those that are used to drive the display/UI.
|
We can combine coroutines with [Fermi](https://docs.rs/fermi/latest/fermi/index.html) to emulate Redux Toolkit's Thunk system with much less headache. This lets us store all of our app's state *within* a task and then simply update the "view" values stored in Atoms. It cannot be understated how powerful this technique is: we get all the perks of native Rust tasks with the optimizations and ergonomics of global state. This means your *actual* state does not need to be tied up in a system like Fermi or Redux – the only Atoms that need to exist are those that are used to drive the display/UI.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
static USERNAME: Atom<String> = |_| "default".to_string();
|
static USERNAME: Atom<String> = |_| "default".to_string();
|
||||||
|
@ -130,6 +170,8 @@ fn Banner(cx: Scope) -> Element {
|
||||||
Now, in our sync service, we can structure our state however we want. We only need to update the view values when ready.
|
Now, in our sync service, we can structure our state however we want. We only need to update the view values when ready.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
|
|
||||||
enum SyncAction {
|
enum SyncAction {
|
||||||
SetUsername(String),
|
SetUsername(String),
|
||||||
}
|
}
|
||||||
|
@ -152,27 +194,9 @@ async fn sync_service(mut rx: UnboundedReceiver<SyncAction>, atoms: AtomRoot) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Yielding Values
|
|
||||||
|
|
||||||
To yield values from a coroutine, simply bring in a `UseState` handle and set the value whenever your coroutine completes its work.
|
|
||||||
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let sync_status = use_state(cx, || Status::Launching);
|
|
||||||
let sync_task = use_coroutine(cx, |rx: UnboundedReceiver<SyncAction>| {
|
|
||||||
to_owned![sync_status];
|
|
||||||
async move {
|
|
||||||
loop {
|
|
||||||
delay_ms(1000).await;
|
|
||||||
sync_status.set(Status::Working);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Automatic injection into the Context API
|
## Automatic injection into the Context API
|
||||||
|
|
||||||
Coroutine handles are automatically injected through the context API. `use_coroutine_handle` with the message type as a generic can be used to fetch a handle.
|
Coroutine handles are automatically injected through the context API. You can use the `use_coroutine_handle` hook with the message type as a generic to fetch a handle.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn Child(cx: Scope) -> Element {
|
fn Child(cx: Scope) -> Element {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
[`use_future`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_future.html) lets you run an async closure, and provides you with its result.
|
[`use_future`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_future.html) lets you run an async closure, and provides you with its result.
|
||||||
|
|
||||||
For example, we can make an API request inside `use_future`:
|
For example, we can make an API request (using [reqwest](https://docs.rs/reqwest/latest/reqwest/index.html)) inside `use_future`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/use_future.rs:use_future}}
|
{{#include ../../../examples/use_future.rs:use_future}}
|
||||||
|
@ -25,7 +25,7 @@ The `UseFuture` handle provides a `restart` method. It can be used to execute th
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Often, you will need to run the future again every time some value (e.g. a prop) changes. Rather than `.restart`ing it manually, you can provide a tuple of "dependencies" to the hook. It will automatically re-run the future when any of those dependencies change. Example:
|
Often, you will need to run the future again every time some value (e.g. a prop) changes. Rather than calling `restart` manually, you can provide a tuple of "dependencies" to the hook. It will automatically re-run the future when any of those dependencies change. Example:
|
||||||
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Antipatterns
|
# Antipatterns
|
||||||
|
|
||||||
This example shows what not to do and provides a reason why a given pattern is considered an "AntiPattern". Most anti-patterns are considered wrong due performance reasons, or for harming code re-usability.
|
This example shows what not to do and provides a reason why a given pattern is considered an "AntiPattern". Most anti-patterns are considered wrong for performance or code re-usability reasons.
|
||||||
|
|
||||||
## Unnecessarily Nested Fragments
|
## Unnecessarily Nested Fragments
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ Only Component and Fragment nodes are susceptible to this issue. Dioxus mitigate
|
||||||
|
|
||||||
## Incorrect Iterator Keys
|
## Incorrect Iterator Keys
|
||||||
|
|
||||||
As described in the conditional rendering chapter, list items must have unique keys that are associated with the same items across renders. This helps Dioxus associate state with the contained components, and ensures good diffing performance. Do not omit keys, unless you know that the list is static and will never change.
|
As described in the [dynamic rendering chapter](../interactivity/dynamic_rendering.md#the-key-attribute), list items must have unique keys that are associated with the same items across renders. This helps Dioxus associate state with the contained components and ensures good diffing performance. Do not omit keys, unless you know that the list will never change.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/anti_patterns.rs:iter_keys}}
|
{{#include ../../../examples/anti_patterns.rs:iter_keys}}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Development happens in the [Dioxus GitHub repository](https://github.com/DioxusLabs/dioxus). If you've found a bug or have an idea for a feature, please submit an issue (check if someone hasn't [done it already](https://github.com/DioxusLabs/dioxus/issues) though).
|
Development happens in the [Dioxus GitHub repository](https://github.com/DioxusLabs/dioxus). If you've found a bug or have an idea for a feature, please submit an issue (but first check if someone hasn't [done it already](https://github.com/DioxusLabs/dioxus/issues)).
|
||||||
|
|
||||||
[GitHub discussions](https://github.com/DioxusLabs/dioxus/discussions) can be used as a place to ask for help or talk about features. You can also join [our Discord channel](https://discord.gg/XgGxMSkvUM) where some development discussion happens.
|
[GitHub discussions](https://github.com/DioxusLabs/dioxus/discussions) can be used as a place to ask for help or talk about features. You can also join [our Discord channel](https://discord.gg/XgGxMSkvUM) where some development discussion happens.
|
||||||
|
|
||||||
|
@ -10,11 +10,11 @@ If you'd like to improve the docs, PRs are welcome! Both Rust docs ([source](htt
|
||||||
|
|
||||||
## Working on the Ecosystem
|
## Working on the Ecosystem
|
||||||
|
|
||||||
Part of what makes React great is the rich ecosystem. We'd like the same for Dioxus! So if you have a library in mind that you'd like to write, and that you think many people would benefit from, it will be appreciated. You can [browse npm.js](https://www.npmjs.com/search?q=keywords:react-component) for inspiration.
|
Part of what makes React great is the rich ecosystem. We'd like the same for Dioxus! So if you have a library in mind that you'd like to write and many people would benefit from, it will be appreciated. You can [browse npm.js](https://www.npmjs.com/search?q=keywords:react-component) for inspiration.
|
||||||
|
|
||||||
## Bugs & Features
|
## Bugs & Features
|
||||||
|
|
||||||
If you've fixed [an issue that's open](https://github.com/DioxusLabs/dioxus/issues), feel free to submit a PR! You can also take a look at [the roadmap](./roadmap.md) and work on something in there. Consider [reaching out](https://discord.gg/XgGxMSkvUM) to the team first to make sure everyone's on the same page, and you don't do useless work!
|
If you've fixed [an open issue](https://github.com/DioxusLabs/dioxus/issues), feel free to submit a PR! You can also take a look at [the roadmap](./roadmap.md) and work on something in there. Consider [reaching out](https://discord.gg/XgGxMSkvUM) to the team first to make sure everyone's on the same page, and you don't do useless work!
|
||||||
|
|
||||||
All pull requests (including those made by a team member) must be approved by at least one other team member.
|
All pull requests (including those made by a team member) must be approved by at least one other team member.
|
||||||
Larger, more nuanced decisions about design, architecture, breaking changes, trade-offs, etc. are made by team consensus.
|
Larger, more nuanced decisions about design, architecture, breaking changes, trade-offs, etc. are made by team consensus.
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer.
|
Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer.
|
||||||
|
|
||||||
Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but only really require processing `DomEdits` and sending `UserEvents`.
|
Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but only really require processing `Mutations` and sending `UserEvents`.
|
||||||
|
|
||||||
## The specifics:
|
## The specifics:
|
||||||
|
|
||||||
|
@ -11,153 +11,225 @@ Implementing the renderer is fairly straightforward. The renderer needs to:
|
||||||
1. Handle the stream of edits generated by updates to the virtual DOM
|
1. Handle the stream of edits generated by updates to the virtual DOM
|
||||||
2. Register listeners and pass events into the virtual DOM's event system
|
2. Register listeners and pass events into the virtual DOM's event system
|
||||||
|
|
||||||
Essentially, your renderer needs to implement the `RealDom` trait and generate `EventTrigger` objects to update the VirtualDOM. From there, you'll have everything needed to render the VirtualDOM to the screen.
|
Essentially, your renderer needs to process edits and generate events to update the VirtualDOM. From there, you'll have everything needed to render the VirtualDOM to the screen.
|
||||||
|
|
||||||
Internally, Dioxus handles the tree relationship, diffing, memory management, and the event system, leaving as little as possible required for renderers to implement themselves.
|
Internally, Dioxus handles the tree relationship, diffing, memory management, and the event system, leaving as little as possible required for renderers to implement themselves.
|
||||||
|
|
||||||
For reference, check out the javascript interperter or tui renderer as a starting point for your custom renderer.
|
For reference, check out the [javascript interpreter](https://github.com/DioxusLabs/dioxus/tree/master/packages/interpreter) or [tui renderer](https://github.com/DioxusLabs/dioxus/tree/master/packages/tui) as a starting point for your custom renderer.
|
||||||
|
|
||||||
## DomEdits
|
## Templates
|
||||||
|
|
||||||
The "DomEdit" type is a serialized enum that represents an atomic operation occurring on the RealDom. The variants roughly follow this set:
|
Dioxus is built around the concept of [Templates](https://docs.rs/dioxus-core/latest/dioxus_core/prelude/struct.Template.html). Templates describe a UI tree known at compile time with dynamic parts filled at runtime. This is useful internally to make skip diffing static nodes, but it is also useful for the renderer to reuse parts of the UI tree. This can be useful for things like a list of items. Each item could contain some static parts and some dynamic parts. The renderer can use the template to create a static part of the UI once, clone it for each element in the list, and then fill in the dynamic parts.
|
||||||
|
|
||||||
|
## Mutations
|
||||||
|
|
||||||
|
The `Mutation` type is a serialized enum that represents an operation that should be applied to update the UI. The variants roughly follow this set:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
enum DomEdit {
|
enum Mutation {
|
||||||
PushRoot,
|
|
||||||
AppendChildren,
|
AppendChildren,
|
||||||
|
AssignId,
|
||||||
|
CreatePlaceholder,
|
||||||
|
CreateTextNode,
|
||||||
|
HydrateText,
|
||||||
|
LoadTemplate,
|
||||||
ReplaceWith,
|
ReplaceWith,
|
||||||
|
ReplacePlaceholder,
|
||||||
InsertAfter,
|
InsertAfter,
|
||||||
InsertBefore,
|
InsertBefore,
|
||||||
Remove,
|
SetAttribute,
|
||||||
CreateTextNode,
|
SetText,
|
||||||
CreateElement,
|
|
||||||
CreateElementNs,
|
|
||||||
CreatePlaceholder,
|
|
||||||
NewEventListener,
|
NewEventListener,
|
||||||
RemoveEventListener,
|
RemoveEventListener,
|
||||||
SetText,
|
Remove,
|
||||||
SetAttribute,
|
PushRoot,
|
||||||
RemoveAttribute,
|
|
||||||
PopRoot,
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The Dioxus diffing mechanism operates as a [stack machine](https://en.wikipedia.org/wiki/Stack_machine) where the "push_root" method pushes a new "real" DOM node onto the stack and "append_child" and "replace_with" both remove nodes from the stack.
|
The Dioxus diffing mechanism operates as a [stack machine](https://en.wikipedia.org/wiki/Stack_machine) where the [LoadTemplate](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.LoadTemplate), [CreatePlaceholder](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.CreatePlaceholder), and [CreateTextNode](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.CreateTextNode) mutations pushes a new "real" DOM node onto the stack and [AppendChildren](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.AppendChildren), [InsertAfter](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.InsertAfter), [InsertBefore](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.InsertBefore), [ReplacePlaceholder](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.ReplacePlaceholder), and [ReplaceWith](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.ReplaceWith) all remove nodes from the stack.
|
||||||
|
|
||||||
|
## Node storage
|
||||||
|
|
||||||
### An example
|
Dioxus saves and loads elements with IDs. Inside the VirtualDOM, this is just tracked as as a u64.
|
||||||
|
|
||||||
For the sake of understanding, lets consider this example – a very simple UI declaration:
|
Whenever a `CreateElement` edit is generated during diffing, Dioxus increments its node counter and assigns that new element its current NodeCount. The RealDom is responsible for remembering this ID and pushing the correct node when id is used in a mutation. Dioxus reclaims the IDs of elements when removed. To stay in sync with Dioxus you can use a sparse Vec (Vec<Option<T>>) with possibly unoccupied items. You can use the ids as indexes into the Vec for elements, and grow the Vec when an id does not exist.
|
||||||
|
|
||||||
|
### An Example
|
||||||
|
|
||||||
|
For the sake of understanding, let's consider this example – a very simple UI declaration:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
rsx!( h1 {"hello world"} )
|
rsx!( h1 {"count: {x}"} )
|
||||||
```
|
```
|
||||||
|
|
||||||
To get things started, Dioxus must first navigate to the container of this h1 tag. To "navigate" here, the internal diffing algorithm generates the DomEdit `PushRoot` where the ID of the root is the container.
|
#### Building Templates
|
||||||
|
|
||||||
When the renderer receives this instruction, it pushes the actual Node onto its own stack. The real renderer's stack will look like this:
|
The above rsx will create a template that contains one static h1 tag and a placeholder for a dynamic text node. The template contains the static parts of the UI, and ids for the dynamic parts along with the paths to access them.
|
||||||
|
|
||||||
|
The template will look something like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Template {
|
||||||
|
// Some id that is unique for the entire project
|
||||||
|
name: "main.rs:1:1:0",
|
||||||
|
// The root nodes of the template
|
||||||
|
roots: &[
|
||||||
|
TemplateNode::Element {
|
||||||
|
tag: "h1",
|
||||||
|
namespace: None,
|
||||||
|
attrs: &[],
|
||||||
|
children: &[
|
||||||
|
TemplateNode::DynamicText {
|
||||||
|
id: 0
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// the path to each of the dynamic nodes
|
||||||
|
node_paths: &[
|
||||||
|
// the path to dynamic node with a id of 0
|
||||||
|
&[
|
||||||
|
// on the first root node
|
||||||
|
0,
|
||||||
|
// the first child of the root node
|
||||||
|
0,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
// the path to each of the dynamic attributes
|
||||||
|
attr_paths: &'a [&'a [u8]],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> For more detailed docs about the struture of templates see the [Template api docs](https://docs.rs/dioxus-core/latest/dioxus_core/prelude/struct.Template.html)
|
||||||
|
|
||||||
|
This template will be sent to the renderer in the [list of templates](https://docs.rs/dioxus-core/latest/dioxus_core/struct.Mutations.html#structfield.templates) supplied with the mutations the first time it is used. Any time the renderer encounters a [LoadTemplate](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.LoadTemplate) mutation after this, it should clone the template and store it in the given id.
|
||||||
|
|
||||||
|
For dynamic nodes and dynamic text nodes, a placeholder node should be created and inserted into the UI so that the node can be modified later.
|
||||||
|
|
||||||
|
In HTML renderers, this template could look like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1>""</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Applying Mutations
|
||||||
|
|
||||||
|
After the renderer has created all of the new templates, it can begin to process the mutations.
|
||||||
|
|
||||||
|
When the renderer starts, it should contain the Root node on the stack and store the Root node with an id of 0. The Root node is the top-level node of the UI. In HTML, this is the `<div id="main">` element.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
instructions: []
|
||||||
|
stack: [
|
||||||
|
RootNode,
|
||||||
|
]
|
||||||
|
nodes: [
|
||||||
|
RootNode,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The first mutation is a `LoadTemplate` mutation. This tells the renderer to load a root from the template with the given id. The renderer will then push the root node of the template onto the stack and store it with an id for later. In this case, the root node is an h1 element.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
instructions: [
|
instructions: [
|
||||||
PushRoot(Container)
|
LoadTemplate {
|
||||||
|
// the id of the template
|
||||||
|
name: "main.rs:1:1:0",
|
||||||
|
// the index of the root node in the template
|
||||||
|
index: 0,
|
||||||
|
// the id to store
|
||||||
|
id: ElementId(1),
|
||||||
|
}
|
||||||
]
|
]
|
||||||
stack: [
|
stack: [
|
||||||
ContainerNode,
|
RootNode,
|
||||||
|
<h1>""</h1>,
|
||||||
|
]
|
||||||
|
nodes: [
|
||||||
|
RootNode,
|
||||||
|
<h1>""</h1>,
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, Dioxus will encounter the h1 node. The diff algorithm decides that this node needs to be created, so Dioxus will generate the DomEdit `CreateElement`. When the renderer receives this instruction, it will create an unmounted node and push into its own stack:
|
Next, Dioxus will create the dynamic text node. The diff algorithm decides that this node needs to be created, so Dioxus will generate the Mutation `HydrateText`. When the renderer receives this instruction, it will navigate to the placeholder text node in the template and replace it with the new text.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
instructions: [
|
instructions: [
|
||||||
PushRoot(Container),
|
LoadTemplate {
|
||||||
CreateElement(h1),
|
name: "main.rs:1:1:0",
|
||||||
|
index: 0,
|
||||||
|
id: ElementId(1),
|
||||||
|
},
|
||||||
|
HydrateText {
|
||||||
|
// the id to store the text node
|
||||||
|
id: ElementId(2),
|
||||||
|
// the text to set
|
||||||
|
text: "count: 0",
|
||||||
|
}
|
||||||
]
|
]
|
||||||
stack: [
|
stack: [
|
||||||
ContainerNode,
|
RootNode,
|
||||||
h1,
|
<h1>"count: 0"</h1>,
|
||||||
|
]
|
||||||
|
nodes: [
|
||||||
|
RootNode,
|
||||||
|
<h1>"count: 0"</h1>,
|
||||||
|
"count: 0",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
Next, Dioxus sees the text node, and generates the `CreateTextNode` DomEdit:
|
|
||||||
```rust
|
Remember, the h1 node is not attached to anything (it is unmounted) so Dioxus needs to generate an Edit that connects the h1 node to the Root. It depends on the situation, but in this case, we use `AppendChildren`. This pops the text node off the stack, leaving the Root element as the next element on the stack.
|
||||||
instructions: [
|
|
||||||
PushRoot(Container),
|
|
||||||
CreateElement(h1),
|
|
||||||
CreateTextNode("hello world")
|
|
||||||
]
|
|
||||||
stack: [
|
|
||||||
ContainerNode,
|
|
||||||
h1,
|
|
||||||
"hello world"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
Remember, the text node is not attached to anything (it is unmounted) so Dioxus needs to generate an Edit that connects the text node to the h1 element. It depends on the situation, but in this case we use `AppendChildren`. This pops the text node off the stack, leaving the h1 element as the next element in line.
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
instructions: [
|
instructions: [
|
||||||
PushRoot(Container),
|
LoadTemplate {
|
||||||
CreateElement(h1),
|
name: "main.rs:1:1:0",
|
||||||
CreateTextNode("hello world"),
|
index: 0,
|
||||||
AppendChildren(1)
|
id: ElementId(1),
|
||||||
|
},
|
||||||
|
HydrateText {
|
||||||
|
id: ElementId(2),
|
||||||
|
text: "count: 0",
|
||||||
|
},
|
||||||
|
AppendChildren {
|
||||||
|
// the id of the parent node
|
||||||
|
id: ElementId(0),
|
||||||
|
// the number of nodes to pop off the stack and append
|
||||||
|
m: 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
stack: [
|
stack: [
|
||||||
ContainerNode,
|
RootNode,
|
||||||
h1
|
]
|
||||||
|
nodes: [
|
||||||
|
RootNode,
|
||||||
|
<h1>"count: 0"</h1>,
|
||||||
|
"count: 0",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
We call `AppendChildren` again, popping off the h1 node and attaching it to the parent:
|
|
||||||
```rust
|
|
||||||
instructions: [
|
|
||||||
PushRoot(Container),
|
|
||||||
CreateElement(h1),
|
|
||||||
CreateTextNode("hello world"),
|
|
||||||
AppendChildren(1),
|
|
||||||
AppendChildren(1)
|
|
||||||
]
|
|
||||||
stack: [
|
|
||||||
ContainerNode,
|
|
||||||
]
|
|
||||||
```
|
|
||||||
Finally, the container is popped since we don't need it anymore.
|
|
||||||
```rust
|
|
||||||
instructions: [
|
|
||||||
PushRoot(Container),
|
|
||||||
CreateElement(h1),
|
|
||||||
CreateTextNode("hello world"),
|
|
||||||
AppendChildren(1),
|
|
||||||
AppendChildren(1),
|
|
||||||
PopRoot
|
|
||||||
]
|
|
||||||
stack: []
|
|
||||||
```
|
|
||||||
Over time, our stack looked like this:
|
Over time, our stack looked like this:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
[]
|
[Root]
|
||||||
[Container]
|
[Root, <h1>""</h1>]
|
||||||
[Container, h1]
|
[Root, <h1>"count: 0"</h1>]
|
||||||
[Container, h1, "hello world"]
|
[Root]
|
||||||
[Container, h1]
|
|
||||||
[Container]
|
|
||||||
[]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice how our stack is empty once UI has been mounted. Conveniently, this approach completely separates the VirtualDOM and the Real DOM. Additionally, these edits are serializable, meaning we can even manage UIs across a network connection. This little stack machine and serialized edits makes Dioxus independent of platform specifics.
|
Conveniently, this approach completely separates the Virtual DOM and the Real DOM. Additionally, these edits are serializable, meaning we can even manage UIs across a network connection. This little stack machine and serialized edits make Dioxus independent of platform specifics.
|
||||||
|
|
||||||
Dioxus is also really fast. Because Dioxus splits the diff and patch phase, it's able to make all the edits to the RealDOM in a very short amount of time (less than a single frame) making rendering very snappy. It also allows Dioxus to cancel large diffing operations if higher priority work comes in while it's diffing.
|
Dioxus is also really fast. Because Dioxus splits the diff and patch phase, it's able to make all the edits to the RealDOM in a very short amount of time (less than a single frame) making rendering very snappy. It also allows Dioxus to cancel large diffing operations if higher priority work comes in while it's diffing.
|
||||||
|
|
||||||
It's important to note that there _is_ one layer of connectedness between Dioxus and the renderer. Dioxus saves and loads elements (the PushRoot edit) with an ID. Inside the VirtualDOM, this is just tracked as a u64.
|
This little demo serves to show exactly how a Renderer would need to process a mutation stream to build UIs.
|
||||||
|
|
||||||
Whenever a `CreateElement` edit is generated during diffing, Dioxus increments its node counter and assigns that new element its current NodeCount. The RealDom is responsible for remembering this ID and pushing the correct node when PushRoot(ID) is generated. Dioxus reclaims IDs of elements when removed. To stay in sync with Dioxus you can use a sparce Vec (Vec<Option<T>>) with possibly unoccupied items. You can use the ids as indexes into the Vec for elements, and grow the Vec when a id does not exist.
|
|
||||||
|
|
||||||
This little demo serves to show exactly how a Renderer would need to process an edit stream to build UIs. A set of serialized DomEditss for various demos is available for you to test your custom renderer against.
|
|
||||||
|
|
||||||
## Event loop
|
## Event loop
|
||||||
|
|
||||||
Like most GUIs, Dioxus relies on an event loop to progress the VirtualDOM. The VirtualDOM itself can produce events as well, so it's important that your custom renderer can handle those too.
|
Like most GUIs, Dioxus relies on an event loop to progress the VirtualDOM. The VirtualDOM itself can produce events as well, so it's important for your custom renderer can handle those too.
|
||||||
|
|
||||||
The code for the WebSys implementation is straightforward, so we'll add it here to demonstrate how simple an event loop is:
|
The code for the WebSys implementation is straightforward, so we'll add it here to demonstrate how simple an event loop is:
|
||||||
|
|
||||||
```rust
|
```rust, ignore
|
||||||
pub async fn run(&mut self) -> dioxus_core::error::Result<()> {
|
pub async fn run(&mut self) -> dioxus_core::error::Result<()> {
|
||||||
// Push the body element onto the WebsysDom's stack machine
|
// Push the body element onto the WebsysDom's stack machine
|
||||||
let mut websys_dom = crate::new::WebsysDom::new(prepare_websys_dom());
|
let mut websys_dom = crate::new::WebsysDom::new(prepare_websys_dom());
|
||||||
|
@ -185,9 +257,9 @@ pub async fn run(&mut self) -> dioxus_core::error::Result<()> {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
It's important that you decode the real events from your event system into Dioxus' synthetic event system (synthetic meaning abstracted). This simply means matching your event type and creating a Dioxus `UserEvent` type. Right now, the VirtualEvent system is modeled almost entirely around the HTML spec, but we are interested in slimming it down.
|
It's important to decode what the real events are for your event system into Dioxus' synthetic event system (synthetic meaning abstracted). This simply means matching your event type and creating a Dioxus `UserEvent` type. Right now, the virtual event system is modeled almost entirely around the HTML spec, but we are interested in slimming it down.
|
||||||
|
|
||||||
```rust
|
```rust, ignore
|
||||||
fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
|
fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
|
||||||
match event.type_().as_str() {
|
match event.type_().as_str() {
|
||||||
"keydown" => {
|
"keydown" => {
|
||||||
|
@ -219,40 +291,23 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
|
||||||
|
|
||||||
## Custom raw elements
|
## Custom raw elements
|
||||||
|
|
||||||
If you need to go as far as relying on custom elements for your renderer – you totally can. This still enables you to use Dioxus' reactive nature, component system, shared state, and other features, but will ultimately generate different nodes. All attributes and listeners for the HTML and SVG namespace are shuttled through helper structs that essentially compile away (pose no runtime overhead). You can drop in your own elements any time you want, with little hassle. However, you must be absolutely sure your renderer can handle the new type, or it will crash and burn.
|
If you need to go as far as relying on custom elements/attributes for your renderer – you totally can. This still enables you to use Dioxus' reactive nature, component system, shared state, and other features, but will ultimately generate different nodes. All attributes and listeners for the HTML and SVG namespace are shuttled through helper structs that essentially compile away. You can drop in your elements any time you want, with little hassle. However, you must be sure your renderer can handle the new namespace.
|
||||||
|
|
||||||
These custom elements are defined as unit structs with trait implementations.
|
For more examples and information on how to create custom namespaces, see the [`dioxus_html` crate](https://github.com/DioxusLabs/dioxus/blob/master/packages/html/README.md#how-to-extend-it).
|
||||||
|
|
||||||
For example, the `div` element is (approximately!) defined as such:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
struct div;
|
|
||||||
impl div {
|
|
||||||
/// Some glorious documentation about the class property.
|
|
||||||
const TAG_NAME: &'static str = "div";
|
|
||||||
const NAME_SPACE: Option<&'static str> = None;
|
|
||||||
// define the class attribute
|
|
||||||
pub fn class<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
|
|
||||||
cx.attr("class", val, None, false)
|
|
||||||
}
|
|
||||||
// more attributes
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You've probably noticed that many elements in the `rsx!` macros support on-hover documentation. The approach we take to custom elements means that the unit struct is created immediately where the element is used in the macro. When the macro is expanded, the doc comments still apply to the unit struct, giving tons of in-editor feedback, even inside a proc macro.
|
|
||||||
|
|
||||||
# Native Core
|
# Native Core
|
||||||
|
|
||||||
If you are creating a renderer in rust, native core provides some utilites to implement a renderer. It provides an abstraction over DomEdits and handles layout for you.
|
If you are creating a renderer in rust, the [native-core](https://github.com/DioxusLabs/dioxus/tree/master/packages/native-core) crate provides some utilities to implement a renderer. It provides an abstraction over Mutations and Templates and contains helpers that can handle the layout and text editing for you.
|
||||||
|
|
||||||
## RealDom
|
## The RealDom
|
||||||
|
|
||||||
The `RealDom` is a higher level abstraction over updating the Dom. It updates with `DomEdits` and provides a way to incrementally update the state of nodes based on what attributes change.
|
The `RealDom` is a higher-level abstraction over updating the Dom. It uses an entity component system to manage the state of nodes. This system allows you to modify insert and modify arbitrary components on nodes. On top of this, the RealDom provides a way to manage a tree of nodes, and the State trait provides a way to automatically add and update these components when the tree is modified. It also provides a way to apply `Mutations` to the RealDom.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
Let's build a toy renderer with borders, size, and text color.
|
Let's build a toy renderer with borders, size, and text color.
|
||||||
Before we start lets take a look at an example element we can render:
|
Before we start let's take a look at an example element we can render:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
cx.render(rsx!{
|
cx.render(rsx!{
|
||||||
div{
|
div{
|
||||||
|
@ -265,11 +320,11 @@ cx.render(rsx!{
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
In this tree the color depends on the parent's color. The size depends on the childrens size, the current text, and a text size. The border depends on only the current node.
|
In this tree, the color depends on the parent's color. The layout depends on the children's layout, the current text, and the text size. The border depends on only the current node.
|
||||||
|
|
||||||
In the following diagram arrows represent dataflow:
|
In the following diagram arrows represent dataflow:
|
||||||
|
|
||||||
[![](https://mermaid.ink/img/pako:eNqdVNFqgzAU_RXJXizUUZPJmIM-jO0LukdhpCbO0JhIGteW0n9fNK1Oa0brfUnu9VxyzzkXjyCVhIIYZFzu0hwr7X2-JcIzsa3W3wqXuZdKoele22oddfa1Y0Tnfn31muvMfqeCDNq3GmvaNROmaKqZFO1DPTRhP8MOd1fTWYNDvzlmQbBMJZcq9JtjNgY1mLVUhBqQPQeojl3wGCw5PsjqnIe-zXqEL8GZ2Kz0gVMPmoeU3ND4IcuiaLGY2zRouuKncv_qGKv3VodpJe0JVU6QCQ5kgqMyWQVr8hbk4hm1PBcmsuwmnrCVH94rP7xN_ucp8sOB_EPSfz9drYVrkpc_AmH8_yTjJueUc-ntpOJkgt2os9tKjcYlt-DLUiD3UsB2KZCLcwjv3Aq33-g2v0M0xXA0MBy5DUdXi-gcJZriuLmAOSioKjAj5ld8rMsJ0DktaAJicyVYbRKQiJPBVSUx438QpqUCcYb5ls4BrrRcHUTaFizqnWGzR8W5evoFI-bJdw)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqdVNFqgzAU_RXJXizUUZPJmIM-jO0LukdhpCbO0JhIGteW0n9fNK1Oa0brfUnu9VxyzzkXjyCVhIIYZFzu0hwr7X2-JcIzsa3W3wqXuZdKoele22oddfa1Y0Tnfn31muvMfqeCDNq3GmvaNROmaKqZFO1DPTRhP8MOd1fTWYNDvzlmQbBMJZcq9JtjNgY1mLVUhBqQPQeojl3wGCw5PsjqnIe-zXqEL8GZ2Kz0gVMPmoeU3ND4IcuiaLGY2zRouuKncv_qGKv3VodpJe0JVU6QCQ5kgqMyWQVr8hbk4hm1PBcmsuwmnrCVH94rP7xN_ucp8sOB_EPSfz9drYVrkpc_AmH8_yTjJueUc-ntpOJkgt2os9tKjcYlt-DLUiD3UsB2KZCLcwjv3Aq33-g2v0M0xXA0MBy5DUdXi-gcJZriuLmAOSioKjAj5ld8rMsJ0DktaAJicyVYbRKQiJPBVSUx438QpqUCcYb5ls4BrrRcHUTaFizqnWGzR8W5evoFI-bJdw)
|
[![](https://mermaid.ink/img/pako:eNqllV1vgjAUhv8K6W4wkQVa2QdLdrHsdlfukmSptEhjoaSWqTH-9xVwONAKst70g5739JzzlO5BJAgFAYi52EQJlsr6fAszS7d1sVhKnCdWJDJFt6peLVs5-9owohK7HFrVcFJ_pxnpmK8VVvRkTJikkWIiaxy1dhP23bUwW1WW5WbPrrqJ4ziR4EJ6dtVN2ls5y1ZztePUcrWZFCvqVEcPPDffvlyS1XoLIQnVgnVvVPR6FU9Zc-6dV453ojjOPbuetRJ57gIeXQR3cez7rjtteZyZQ2j5MqmjqwE0ZW0VKx9RKtgpFewp1aw3sXXFy6TWgiYlv8mfq1scD8ofbBCAfQg8_AMBOAyBxzEIwA4CxgQ99QbQkjnD2KT7_CfxGF8_9WXQEsq5sDZCcjICOXRCri4h6r3NA38Q6Jdi1EOx5w3DGDYYI6MUvJFjM3VoGHUeGoMd6mBnDmh2E3fo7O4Yhf0x4OkBmIKUyhQzol_GfbkcApXQlIYg0EOC5SoEYXbQ-3ChxHyXRSBQsqBTUOREx_7OsAY3BUGM-VqvUsKUkB_1U6vf05gtweEHTk4_HQ?type=png)](https://mermaid.live/edit#pako:eNqllV1vgjAUhv8K6W4wkQVa2QdLdrHsdlfukmSptEhjoaSWqTH-9xVwONAKst70g5739JzzlO5BJAgFAYi52EQJlsr6fAszS7d1sVhKnCdWJDJFt6peLVs5-9owohK7HFrVcFJ_pxnpmK8VVvRkTJikkWIiaxy1dhP23bUwW1WW5WbPrrqJ4ziR4EJ6dtVN2ls5y1ZztePUcrWZFCvqVEcPPDffvlyS1XoLIQnVgnVvVPR6FU9Zc-6dV453ojjOPbuetRJ57gIeXQR3cez7rjtteZyZQ2j5MqmjqwE0ZW0VKx9RKtgpFewp1aw3sXXFy6TWgiYlv8mfq1scD8ofbBCAfQg8_AMBOAyBxzEIwA4CxgQ99QbQkjnD2KT7_CfxGF8_9WXQEsq5sDZCcjICOXRCri4h6r3NA38Q6Jdi1EOx5w3DGDYYI6MUvJFjM3VoGHUeGoMd6mBnDmh2E3fo7O4Yhf0x4OkBmIKUyhQzol_GfbkcApXQlIYg0EOC5SoEYXbQ-3ChxHyXRSBQsqBTUOREx_7OsAY3BUGM-VqvUsKUkB_1U6vf05gtweEHTk4_HQ)
|
||||||
|
|
||||||
[//]: # "%% mermaid flow chart"
|
[//]: # "%% mermaid flow chart"
|
||||||
[//]: # "flowchart TB"
|
[//]: # "flowchart TB"
|
||||||
|
@ -280,217 +335,79 @@ In the following diagram arrows represent dataflow:
|
||||||
[//]: # " direction TB"
|
[//]: # " direction TB"
|
||||||
[//]: # " subgraph div state"
|
[//]: # " subgraph div state"
|
||||||
[//]: # " direction TB"
|
[//]: # " direction TB"
|
||||||
[//]: # " state1(state)-->color1(color)"
|
[//]: # " state1(state)---color1(color)"
|
||||||
[//]: # " state1-->border1(border)"
|
[//]: # " linkStyle 0 stroke-width:10px;"
|
||||||
|
[//]: # " state1---border1(border)"
|
||||||
|
[//]: # " linkStyle 1 stroke-width:10px;"
|
||||||
[//]: # " text_width-.->layout_width1(layout width)"
|
[//]: # " text_width-.->layout_width1(layout width)"
|
||||||
[//]: # " linkStyle 2 stroke:#ff5500,stroke-width:4px;"
|
[//]: # " linkStyle 2 stroke:#ff5500,stroke-width:4px;"
|
||||||
[//]: # " state1-->layout_width1"
|
[//]: # " state1---layout_width1"
|
||||||
|
[//]: # " linkStyle 3 stroke-width:10px;"
|
||||||
[//]: # " end"
|
[//]: # " end"
|
||||||
[//]: # " subgraph p state"
|
[//]: # " subgraph p state"
|
||||||
[//]: # " direction TB"
|
[//]: # " direction TB"
|
||||||
[//]: # " state2(state)-->color2(color)"
|
[//]: # " state2(state)---color2(color)"
|
||||||
|
[//]: # " linkStyle 4 stroke-width:10px;"
|
||||||
[//]: # " color1-.->color2"
|
[//]: # " color1-.->color2"
|
||||||
[//]: # " linkStyle 5 stroke:#0000ff,stroke-width:4px;"
|
[//]: # " linkStyle 5 stroke:#0000ff,stroke-width:4px;"
|
||||||
[//]: # " state2-->border2(border)"
|
[//]: # " state2---border2(border)"
|
||||||
|
[//]: # " linkStyle 6 stroke-width:10px;"
|
||||||
[//]: # " text_width-.->layout_width2(layout width)"
|
[//]: # " text_width-.->layout_width2(layout width)"
|
||||||
[//]: # " linkStyle 7 stroke:#ff5500,stroke-width:4px;"
|
[//]: # " linkStyle 7 stroke:#ff5500,stroke-width:4px;"
|
||||||
[//]: # " state2-->layout_width2"
|
[//]: # " state2---layout_width2"
|
||||||
|
[//]: # " linkStyle 8 stroke-width:10px;"
|
||||||
[//]: # " layout_width2-.->layout_width1"
|
[//]: # " layout_width2-.->layout_width1"
|
||||||
[//]: # " linkStyle 9 stroke:#00aa00,stroke-width:4px;"
|
[//]: # " linkStyle 9 stroke:#00aa00,stroke-width:4px;"
|
||||||
[//]: # " end"
|
[//]: # " end"
|
||||||
[//]: # " subgraph hello world state"
|
[//]: # " subgraph hello world state"
|
||||||
[//]: # " direction TB"
|
[//]: # " direction TB"
|
||||||
[//]: # " state3(state)-->border3(border)"
|
[//]: # " state3(state)---border3(border)"
|
||||||
[//]: # " state3-->color3(color)"
|
[//]: # " linkStyle 10 stroke-width:10px;"
|
||||||
|
[//]: # " state3---color3(color)"
|
||||||
|
[//]: # " linkStyle 11 stroke-width:10px;"
|
||||||
[//]: # " color2-.->color3"
|
[//]: # " color2-.->color3"
|
||||||
[//]: # " linkStyle 12 stroke:#0000ff,stroke-width:4px;"
|
[//]: # " linkStyle 12 stroke:#0000ff,stroke-width:4px;"
|
||||||
[//]: # " text_width-.->layout_width3(layout width)"
|
[//]: # " text_width-.->layout_width3(layout width)"
|
||||||
[//]: # " linkStyle 13 stroke:#ff5500,stroke-width:4px;"
|
[//]: # " linkStyle 13 stroke:#ff5500,stroke-width:4px;"
|
||||||
[//]: # " state3-->layout_width3"
|
[//]: # " state3---layout_width3"
|
||||||
|
[//]: # " linkStyle 14 stroke-width:10px;"
|
||||||
[//]: # " layout_width3-.->layout_width2"
|
[//]: # " layout_width3-.->layout_width2"
|
||||||
[//]: # " linkStyle 15 stroke:#00aa00,stroke-width:4px;"
|
[//]: # " linkStyle 15 stroke:#00aa00,stroke-width:4px;"
|
||||||
[//]: # " end"
|
[//]: # " end"
|
||||||
[//]: # " end"
|
[//]: # " end"
|
||||||
|
|
||||||
To help in building a Dom, native core provides four traits: State, ChildDepState, ParentDepState, and NodeDepState and a RealDom struct. The ChildDepState, ParentDepState, and NodeDepState provide a way to describe how some information in a node relates to that of its relatives. By providing how to build a single node from its relations, native-core will derive a way to update the state of all nodes for you with ```#[derive(State)]```. Once you have a state you can provide it as a generic to RealDom. RealDom provides all of the methods to interact and update your new dom.
|
To help in building a Dom, native-core provides the State trait and a RealDom struct. The State trait provides a way to describe how states in a node depend on other states in its relatives. By describing how to update a single node from its relations, native-core will derive a way to update the states of all nodes for you. Once you have a state you can provide it as a generic to RealDom. RealDom provides all of the methods to interact and update your new dom.
|
||||||
|
|
||||||
```rust
|
Native Core cannot create all of the required methods for the State trait, but it can derive some of them. To implement the State trait, you must implement the following methods and let the `#[partial_derive_state]` macro handle the rest:
|
||||||
use dioxus_native_core::node_ref::*;
|
|
||||||
use dioxus_native_core::state::{ChildDepState, NodeDepState, ParentDepState, State};
|
|
||||||
use dioxus_native_core_macro::{sorted_str_slice, State};
|
|
||||||
|
|
||||||
#[derive(Default, Copy, Clone)]
|
```rust, ignore
|
||||||
struct Size(f32, f32);
|
{{#include ../../../examples/custom_renderer.rs:derive_state}}
|
||||||
// Size only depends on the current node and its children, so it implements ChildDepState
|
|
||||||
impl ChildDepState for Size {
|
|
||||||
// Size accepts a font size context
|
|
||||||
type Ctx = f32;
|
|
||||||
// Size depends on the Size part of each child
|
|
||||||
type DepState = Self;
|
|
||||||
// Size only cares about the width, height, and text parts of the current node
|
|
||||||
const NODE_MASK: NodeMask =
|
|
||||||
NodeMask::new_with_attrs(AttributeMask::Static(&sorted_str_slice!(["width", "height"]))).with_text();
|
|
||||||
fn reduce<'a>(
|
|
||||||
&mut self,
|
|
||||||
node: NodeView,
|
|
||||||
children: impl Iterator<Item = &'a Self::DepState>,
|
|
||||||
ctx: &Self::Ctx,
|
|
||||||
) -> bool
|
|
||||||
where
|
|
||||||
Self::DepState: 'a,
|
|
||||||
{
|
|
||||||
let mut width;
|
|
||||||
let mut height;
|
|
||||||
if let Some(text) = node.text() {
|
|
||||||
// if the node has text, use the text to size our object
|
|
||||||
width = text.len() as f32 * ctx;
|
|
||||||
height = *ctx;
|
|
||||||
} else {
|
|
||||||
// otherwise, the size is the maximum size of the children
|
|
||||||
width = children
|
|
||||||
.by_ref()
|
|
||||||
.map(|item| item.0)
|
|
||||||
.reduce(|accum, item| if accum >= item { accum } else { item })
|
|
||||||
.unwrap_or(0.0);
|
|
||||||
|
|
||||||
height = children
|
|
||||||
.map(|item| item.1)
|
|
||||||
.reduce(|accum, item| if accum >= item { accum } else { item })
|
|
||||||
.unwrap_or(0.0);
|
|
||||||
}
|
|
||||||
// if the node contains a width or height attribute it overrides the other size
|
|
||||||
for a in node.attributes(){
|
|
||||||
match a.name{
|
|
||||||
"width" => width = a.value.as_float32().unwrap(),
|
|
||||||
"height" => height = a.value.as_float32().unwrap(),
|
|
||||||
// because Size only depends on the width and height, no other attributes will be passed to the member
|
|
||||||
_ => panic!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
|
|
||||||
let changed = (width != self.0) || (height != self.1);
|
|
||||||
*self = Self(width, height);
|
|
||||||
changed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
|
||||||
struct TextColor {
|
|
||||||
r: u8,
|
|
||||||
g: u8,
|
|
||||||
b: u8,
|
|
||||||
}
|
|
||||||
// TextColor only depends on the current node and its parent, so it implements ParentDepState
|
|
||||||
impl ParentDepState for TextColor {
|
|
||||||
type Ctx = ();
|
|
||||||
// TextColor depends on the TextColor part of the parent
|
|
||||||
type DepState = Self;
|
|
||||||
// TextColor only cares about the color attribute of the current node
|
|
||||||
const NODE_MASK: NodeMask = NodeMask::new_with_attrs(AttributeMask::Static(&["color"]));
|
|
||||||
fn reduce(
|
|
||||||
&mut self,
|
|
||||||
node: NodeView,
|
|
||||||
parent: Option<&Self::DepState>,
|
|
||||||
_ctx: &Self::Ctx,
|
|
||||||
) -> bool {
|
|
||||||
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
|
|
||||||
let new = match node.attributes().next().map(|attr| attr.name) {
|
|
||||||
// if there is a color tag, translate it
|
|
||||||
Some("red") => TextColor { r: 255, g: 0, b: 0 },
|
|
||||||
Some("green") => TextColor { r: 0, g: 255, b: 0 },
|
|
||||||
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
|
|
||||||
Some(_) => panic!("unknown color"),
|
|
||||||
// otherwise check if the node has a parent and inherit that color
|
|
||||||
None => match parent {
|
|
||||||
Some(parent) => *parent,
|
|
||||||
None => Self::default(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// check if the member has changed
|
|
||||||
let changed = new != *self;
|
|
||||||
*self = new;
|
|
||||||
changed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
|
||||||
struct Border(bool);
|
|
||||||
// TextColor only depends on the current node, so it implements NodeDepState
|
|
||||||
impl NodeDepState<()> for Border {
|
|
||||||
type Ctx = ();
|
|
||||||
|
|
||||||
// Border does not depended on any other member in the current node
|
|
||||||
const NODE_MASK: NodeMask =
|
|
||||||
NodeMask::new_with_attrs(AttributeMask::Static(&["border"]));
|
|
||||||
fn reduce(&mut self, node: NodeView, _sibling: (), _ctx: &Self::Ctx) -> bool {
|
|
||||||
// check if the node contians a border attribute
|
|
||||||
let new = Self(node.attributes().next().map(|a| a.name == "border").is_some());
|
|
||||||
// check if the member has changed
|
|
||||||
let changed = new != *self;
|
|
||||||
*self = new;
|
|
||||||
changed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// State provides a derive macro, but anotations on the members are needed in the form #[dep_type(dep_member, CtxType)]
|
|
||||||
#[derive(State, Default, Clone)]
|
|
||||||
struct ToyState {
|
|
||||||
// the color member of it's parent and no context
|
|
||||||
#[parent_dep_state(color)]
|
|
||||||
color: TextColor,
|
|
||||||
// depends on the node, and no context
|
|
||||||
#[node_dep_state()]
|
|
||||||
border: Border,
|
|
||||||
// depends on the layout_width member of children and f32 context (for text size)
|
|
||||||
#[child_dep_state(size, f32)]
|
|
||||||
size: Size,
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now that we have our state, we can put it to use in our dom. Re can update the dom with update_state to update the structure of the dom (adding, removing, and chaning properties of nodes) and then apply_mutations to update the ToyState for each of the nodes that changed.
|
Lets take a look at how to implement the State trait for a simple renderer.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn main(){
|
{{#include ../../../examples/custom_renderer.rs:state_impl}}
|
||||||
fn app(cx: Scope) -> Element {
|
```
|
||||||
cx.render(rsx!{
|
|
||||||
div{
|
|
||||||
color: "red",
|
|
||||||
"hello world"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
let vdom = VirtualDom::new(app);
|
|
||||||
let rdom: RealDom<ToyState> = RealDom::new();
|
|
||||||
|
|
||||||
let mutations = dom.rebuild();
|
Now that we have our state, we can put it to use in our RealDom. We can update the RealDom with apply_mutations to update the structure of the dom (adding, removing, and changing properties of nodes) and then update_state to update the States for each of the nodes that changed.
|
||||||
// update the structure of the real_dom tree
|
|
||||||
let to_update = rdom.apply_mutations(vec![mutations]);
|
|
||||||
let mut ctx = AnyMap::new();
|
|
||||||
// set the font size to 3.3
|
|
||||||
ctx.insert(3.3f32);
|
|
||||||
// update the ToyState for nodes in the real_dom tree
|
|
||||||
let _to_rerender = rdom.update_state(&dom, to_update, ctx).unwrap();
|
|
||||||
|
|
||||||
// we need to run the vdom in a async runtime
|
```rust
|
||||||
tokio::runtime::Builder::new_current_thread()
|
{{#include ../../../examples/custom_renderer.rs:rendering}}
|
||||||
.enable_all()
|
|
||||||
.build()?
|
|
||||||
.block_on(async {
|
|
||||||
loop{
|
|
||||||
let wait = vdom.wait_for_work();
|
|
||||||
let mutations = vdom.work_with_deadline(|| false);
|
|
||||||
let to_update = rdom.apply_mutations(mutations);
|
|
||||||
let mut ctx = AnyMap::new();
|
|
||||||
ctx.insert(3.3);
|
|
||||||
let _to_rerender = rdom.update_state(vdom, to_update, ctx).unwrap();
|
|
||||||
|
|
||||||
// render...
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
For most platforms the layout of the Elements will stay the same. The layout_attributes module provides a way to apply html attributes to a stretch layout style.
|
|
||||||
|
For most platforms, the layout of the Elements will stay the same. The [layout_attributes](https://docs.rs/dioxus-native-core/latest/dioxus_native_core/layout_attributes/index.html) module provides a way to apply HTML attributes a [Taffy](https://docs.rs/taffy/latest/taffy/index.html) layout style.
|
||||||
|
|
||||||
|
## Text Editing
|
||||||
|
|
||||||
|
To make it easier to implement text editing in rust renderers, `native-core` also contains a renderer-agnostic cursor system. The cursor can handle text editing, selection, and movement with common keyboard shortcuts integrated.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
{{#include ../../../examples/custom_renderer.rs:cursor}}
|
||||||
|
```
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
That should be it! You should have nearly all the knowledge required on how to implement your own renderer. We're super interested in seeing Dioxus apps brought to custom desktop renderers, mobile renderer, video game UI, and even augmented reality! If you're interesting in contributing to any of the these projects, don't be afraid to reach out or join the community.
|
|
||||||
|
That should be it! You should have nearly all the knowledge required on how to implement your renderer. We're super interested in seeing Dioxus apps brought to custom desktop renderers, mobile renderers, video game UI, and even augmented reality! If you're interested in contributing to any of these projects, don't be afraid to reach out or join the [community](https://discord.gg/XgGxMSkvUM).
|
||||||
|
|
|
@ -9,7 +9,7 @@ Component props are a single struct annotated with `#[derive(Props)]`. For a com
|
||||||
There are 2 flavors of Props structs:
|
There are 2 flavors of Props structs:
|
||||||
- Owned props:
|
- Owned props:
|
||||||
- Don't have an associated lifetime
|
- Don't have an associated lifetime
|
||||||
- Implement `PartialEq`, allowing for memoization (if the props don't change, Dioxus won't re-render the component)
|
- Implement `PartialEq`, allow for memoization (if the props don't change, Dioxus won't re-render the component)
|
||||||
- Borrowed props:
|
- Borrowed props:
|
||||||
- [Borrow](https://doc.rust-lang.org/beta/rust-by-example/scope/borrow.html) from a parent component
|
- [Borrow](https://doc.rust-lang.org/beta/rust-by-example/scope/borrow.html) from a parent component
|
||||||
- Cannot be memoized due to lifetime constraints
|
- Cannot be memoized due to lifetime constraints
|
||||||
|
@ -32,7 +32,7 @@ You can then pass prop values to the component the same way you would pass attri
|
||||||
|
|
||||||
### Borrowed Props
|
### Borrowed Props
|
||||||
|
|
||||||
Owning props works well if your props are easy to copy around – like a single number. But what if we need to pass a larger data type, like a String from an `App` Component to a `TitleCard` subcomponent? A naive solution might be to [`.clone()`](https://doc.rust-lang.org/std/clone/trait.Clone.html) the String, creating a copy of it for the subcomponent – but this would be inefficient, especially for larger Strings.
|
Owned props work well if your props are easy to copy around – like a single number. But what if we need to pass a larger data type, like a String from an `App` Component to a `TitleCard` subcomponent? A naive solution might be to [`.clone()`](https://doc.rust-lang.org/std/clone/trait.Clone.html) the String, creating a copy of it for the subcomponent – but this would be inefficient, especially for larger Strings.
|
||||||
|
|
||||||
Rust allows for something more efficient – borrowing the String as a `&str` – this is what Borrowed Props are for!
|
Rust allows for something more efficient – borrowing the String as a `&str` – this is what Borrowed Props are for!
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@ We can then use the component like this:
|
||||||
```
|
```
|
||||||
![Screenshot: TitleCard component](./images/component_borrowed_props_screenshot.png)
|
![Screenshot: TitleCard component](./images/component_borrowed_props_screenshot.png)
|
||||||
|
|
||||||
|
Borrowed props can be very useful, but they do not allow for memorization so they will *always* rerun when the parent scope is rerendered. Because of this Borrowed Props should be reserved for components that are cheap to rerun or places where cloning data is an issue. Using Borrowed Props everywhere will result in large parts of your app rerunning every interaction.
|
||||||
|
|
||||||
## Prop Options
|
## Prop Options
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Components
|
# Components
|
||||||
|
|
||||||
Just like you wouldn't want to write a complex program in a single, long, `main` function, you shouldn't build a complex UI in a single `App` function. Instead, it would be better to break down the functionality of an app in logical parts called components.
|
Just like you wouldn't want to write a complex program in a single, long, `main` function, you shouldn't build a complex UI in a single `App` function. Instead, you should break down the functionality of an app in logical parts called components.
|
||||||
|
|
||||||
A component is a Rust function, named in UpperCammelCase, that takes a `Scope` parameter and returns an `Element` describing the UI it wants to render. In fact, our `App` function is a component!
|
A component is a Rust function, named in UpperCammelCase, that takes a `Scope` parameter and returns an `Element` describing the UI it wants to render. In fact, our `App` function is a component!
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ A component is a Rust function, named in UpperCammelCase, that takes a `Scope` p
|
||||||
{{#include ../../../examples/hello_world_desktop.rs:component}}
|
{{#include ../../../examples/hello_world_desktop.rs:component}}
|
||||||
```
|
```
|
||||||
|
|
||||||
> You'll probably want to add `#![allow(non_snake_case)]` to the top of your crate to avoid warnings about the function name
|
> You'll probably want to add `#![allow(non_snake_case)]` to the top of your crate to avoid warnings about UpperCammelCase component names
|
||||||
|
|
||||||
A Component is responsible for some rendering task – typically, rendering an isolated part of the user interface. For example, you could have an `About` component that renders a short description of Dioxus Labs:
|
A Component is responsible for some rendering task – typically, rendering an isolated part of the user interface. For example, you could have an `About` component that renders a short description of Dioxus Labs:
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# Describing the UI
|
# Describing the UI
|
||||||
|
|
||||||
Dioxus is a *declarative* framework. This means that instead of telling Dioxus what to do (e.g. to "create an element" or "set color to red") we simply *declare* what we want the UI to look like using RSX.
|
Dioxus is a *declarative* framework. This means that instead of telling Dioxus what to do (e.g. to "create an element" or "set the color to red") we simply *declare* what we want the UI to look like using RSX.
|
||||||
|
|
||||||
You have already seen a simple example or RSX syntax in the "hello world" application:
|
You have already seen a simple example of RSX syntax in the "hello world" application:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/hello_world_desktop.rs:component}}
|
{{#include ../../../examples/hello_world_desktop.rs:component}}
|
||||||
|
@ -21,9 +21,51 @@ RSX is very similar to HTML in that it describes elements with attributes and ch
|
||||||
<div></div>
|
<div></div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Attributes
|
||||||
|
|
||||||
|
Attributes (and [listeners](../interactivity/index.md)) modify the behavior or appearance of the element they are attached to. They are specified inside the `{}` brackets, using the `name: value` syntax. You can provide the value as a literal in the RSX:
|
||||||
|
```rust
|
||||||
|
{{#include ../../../examples/rsx_overview.rs:attributes}}
|
||||||
|
```
|
||||||
|
```html
|
||||||
|
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" class="primary_button" autofocus="true" style="color: red"></a>
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: All attributes defined in `dioxus-html` follow the snake_case naming convention. They transform their `snake_case` names to HTML's `camelCase` attributes.
|
||||||
|
|
||||||
|
> Note: Styles can be used directly outside of the `style:` attribute. In the above example, `color: "red"` is turned into `style="color: red"`.
|
||||||
|
|
||||||
|
#### Custom Attributes
|
||||||
|
|
||||||
|
Dioxus has a pre-configured set of attributes that you can use. RSX is validated at compile time to make sure you didn't specify an invalid attribute. If you want to override this behavior with a custom attribute name, specify the attribute in quotes:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
{{#include ../../../examples/rsx_overview.rs:custom_attributes}}
|
||||||
|
```
|
||||||
|
```html
|
||||||
|
<b customAttribute="value">
|
||||||
|
</b>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interpolation
|
||||||
|
|
||||||
|
Similarly to how you can [format](https://doc.rust-lang.org/rust-by-example/hello/print/fmt.html) Rust strings, you can also interpolate in RSX text. Use `{variable}` to Display the value of a variable in a string, or `{variable:?}` to use the Debug representation:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
{{#include ../../../examples/rsx_overview.rs:formatting}}
|
||||||
|
```
|
||||||
|
```html
|
||||||
|
<div class="country-es" position="(42, 0)">
|
||||||
|
<div>ES</div>
|
||||||
|
<div>42</div>
|
||||||
|
<div>{}</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
### Children
|
### Children
|
||||||
|
|
||||||
To add children to an element, put them inside the `{}` brackets. They can be either other elements, or text. For example, you could have an `ol` (ordered list) element, containing 3 `li` (list item) elements, each of which contains some text:
|
To add children to an element, put them inside the `{}` brackets after all attributes and listeners in the element. They can be other elements, text, or [components](components.md). For example, you could have an `ol` (ordered list) element, containing 3 `li` (list item) elements, each of which contains some text:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/rsx_overview.rs:children}}
|
{{#include ../../../examples/rsx_overview.rs:children}}
|
||||||
|
@ -38,69 +80,51 @@ To add children to an element, put them inside the `{}` brackets. They can be ei
|
||||||
|
|
||||||
### Fragments
|
### Fragments
|
||||||
|
|
||||||
You can also "group" elements by wrapping them in `Fragment {}`. This will not create any additional elements.
|
You can render multiple elements at the top level of `rsx!` and they will be automatically grouped.
|
||||||
|
|
||||||
> Note: you can also render multiple elements at the top level of `rsx!` and they will be automatically grouped – no need for an explicit `Fragment {}` there.
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/rsx_overview.rs:fragments}}
|
{{#include ../../../examples/rsx_overview.rs:manyroots}}
|
||||||
```
|
```
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<p>First Item</p>
|
<p>First Item</p>
|
||||||
<p>Second Item</p>
|
<p>Second Item</p>
|
||||||
<span>a group</span>
|
|
||||||
<span>of three</span>
|
|
||||||
<span>items</span>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Attributes
|
|
||||||
|
|
||||||
Attributes are also specified inside the `{}` brackets, using the `name: value` syntax. You can provide the value as a literal in the RSX:
|
|
||||||
```rust
|
|
||||||
{{#include ../../../examples/rsx_overview.rs:attributes}}
|
|
||||||
```
|
|
||||||
```html
|
|
||||||
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" class="primary_button" autofocus="true">Log In</a>
|
|
||||||
```
|
|
||||||
|
|
||||||
> Note: All attributes defined in `dioxus-html` follow the snake_case naming convention. They transform their `snake_case` names to HTML's `camelCase` attributes.
|
|
||||||
|
|
||||||
#### Custom Attributes
|
|
||||||
|
|
||||||
Dioxus has a pre-configured set of attributes that you can use. RSX is validated at compile time to make sure you didn't specify an invalid attribute. If you want to override this behavior with a custom attribute name, specify the attribute in quotes:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
{{#include ../../../examples/rsx_overview.rs:custom_attributes}}
|
|
||||||
```
|
|
||||||
```html
|
|
||||||
<b customAttribute="value">
|
|
||||||
Rust is cool
|
|
||||||
</b>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Interpolation
|
|
||||||
|
|
||||||
Similarly to how you can [format](https://doc.rust-lang.org/rust-by-example/hello/print/fmt.html) Rust strings, you can also interpolate in RSX text. Use `{variable}` to Display the value of a variable in a string, or `{variable:?}` to use the Debug representation:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
{{#include ../../../examples/rsx_overview.rs:formatting}}
|
|
||||||
```
|
|
||||||
```html
|
|
||||||
|
|
||||||
<div class="country-es">Coordinates: (42, 0)
|
|
||||||
<div>ES</div>
|
|
||||||
<div>42</div>
|
|
||||||
</div>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Expressions
|
### Expressions
|
||||||
|
|
||||||
You can include arbitrary Rust expressions within RSX, but you must escape them in `[]` brackets:
|
You can include arbitrary Rust expressions as children within RSX that implements [IntoDynNode](https://docs.rs/dioxus-core/0.3/dioxus_core/trait.IntoDynNode.html). This is useful for displaying data from an [iterator](https://doc.rust-lang.org/stable/book/ch13-02-iterators.html#processing-a-series-of-items-with-iterators):
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/rsx_overview.rs:expression}}
|
{{#include ../../../examples/rsx_overview.rs:expression}}
|
||||||
```
|
```
|
||||||
```html
|
```html
|
||||||
<span>DIOXUS</span>
|
<span>DIOXUS0123456789</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loops
|
||||||
|
|
||||||
|
In addition to iterators you can also use for loops directly within RSX:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
{{#include ../../../examples/rsx_overview.rs:loops}}
|
||||||
|
```
|
||||||
|
```html
|
||||||
|
<div>0</div>
|
||||||
|
<div>1</div>
|
||||||
|
<div>2</div>
|
||||||
|
<div>0</div>
|
||||||
|
<div>1</div>
|
||||||
|
<div>2</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### If statements
|
||||||
|
|
||||||
|
You can also use if statements without an else branch within RSX:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
{{#include ../../../examples/rsx_overview.rs:ifstatements}}
|
||||||
|
```
|
||||||
|
```html
|
||||||
|
<div>true</div>
|
||||||
```
|
```
|
|
@ -1,20 +1,57 @@
|
||||||
# Desktop Application
|
# Desktop Overview
|
||||||
|
|
||||||
Build a standalone native desktop app that looks and feels the same across operating systems.
|
Build a standalone native desktop app that looks and feels the same across operating systems.
|
||||||
|
|
||||||
Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
|
Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- [File explorer](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer)
|
- [File Explorer](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer)
|
||||||
- [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner)
|
- [WiFi Scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner)
|
||||||
|
|
||||||
[![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer)
|
[![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer)
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
The desktop is a powerful target for Dioxus, but is currently limited in capability when compared to the Web platform. Currently, desktop apps are rendered with the platform's WebView library, but your Rust code is running natively on a native thread. This means that browser APIs are *not* available, so rendering WebGL, Canvas, etc is not as easy as the Web. However, native system APIs *are* accessible, so streaming, WebSockets, filesystem, etc are all viable APIs. In the future, we plan to move to a custom webrenderer-based DOM renderer with WGPU integrations.
|
The desktop is a powerful target for Dioxus but is currently limited in capability when compared to the Web platform. Currently, desktop apps are rendered with the platform's WebView library, but your Rust code is running natively on a native thread. This means that browser APIs are *not* available, so rendering WebGL, Canvas, etc is not as easy as the Web. However, native system APIs *are* accessible, so streaming, WebSockets, filesystem, etc are all viable APIs. In the future, we plan to move to a custom web renderer-based DOM renderer with WGPU integrations.
|
||||||
|
|
||||||
Dioxus Desktop is built off [Tauri](https://tauri.app/). Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri – mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly.
|
Dioxus Desktop is built off [Tauri](https://tauri.app/). Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri – mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly.
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
## Platform-Specific Dependencies
|
||||||
|
Dioxus desktop renders through a web view. Depending on your platform, you might need to install some dependancies.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Windows Desktop apps depend on WebView2 – a library that should be installed in all modern Windows distributions. If you have Edge installed, then Dioxus will work fine. If you *don't* have Webview2, [then you can install it through Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). MS provides 3 options:
|
||||||
|
|
||||||
|
1. A tiny "evergreen" *bootstrapper* that fetches an installer from Microsoft's CDN
|
||||||
|
2. A tiny *installer* that fetches Webview2 from Microsoft's CDN
|
||||||
|
3. A statically linked version of Webview2 in your final binary for offline users
|
||||||
|
|
||||||
|
For development purposes, use Option 1.
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
Webview Linux apps require WebkitGtk. When distributing, this can be part of your dependency tree in your `.rpm` or `.deb`. However, likely, your users will already have WebkitGtk.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
When using Debian/bullseye `libappindicator3-dev` is no longer available but replaced by `libayatana-appindicator3-dev`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# on Debian/bullseye use:
|
||||||
|
sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/v1/guides/getting-started/prerequisites#setting-up-linux).
|
||||||
|
|
||||||
|
|
||||||
|
### MacOS
|
||||||
|
|
||||||
|
Currently – everything for macOS is built right in! However, you might run into an issue if you're using nightly Rust due to some permissions issues in our Tao dependency (which have been resolved but not published).
|
||||||
|
|
||||||
## Creating a Project
|
## Creating a Project
|
||||||
|
|
||||||
|
@ -25,7 +62,7 @@ cargo new --bin demo
|
||||||
cd demo
|
cd demo
|
||||||
```
|
```
|
||||||
|
|
||||||
Add Dioxus and the `desktop` renderer (this will edit `Cargo.toml`):
|
Add Dioxus and the desktop renderer as dependencies (this will edit your `Cargo.toml`):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo add dioxus
|
cargo add dioxus
|
||||||
|
|
|
@ -1,25 +1,49 @@
|
||||||
# Setting Up Hot Reload
|
# Setting Up Hot Reload
|
||||||
|
|
||||||
1. Hot reloading allows much faster iteration times inside of rsx calls by interperting them and streaming the edits.
|
1. Hot reloading allows much faster iteration times inside of rsx calls by interpreting them and streaming the edits.
|
||||||
2. It is useful when changing the styling/layout of a program, but will not help with changing the logic of a program.
|
2. It is useful when changing the styling/layout of a program, but will not help with changing the logic of a program.
|
||||||
3. Currently the cli only implements hot reloading for the web renderer.
|
3. Currently the cli only implements hot reloading for the web renderer. For TUI, desktop, and LiveView you can use the hot reload macro instead.
|
||||||
|
|
||||||
# Setup
|
# Web
|
||||||
|
For the web renderer, you can use the dioxus cli to serve your application with hot reloading enabled.
|
||||||
|
|
||||||
|
## Setup
|
||||||
Install [dioxus-cli](https://github.com/DioxusLabs/cli).
|
Install [dioxus-cli](https://github.com/DioxusLabs/cli).
|
||||||
Enable the hot-reload feature on dioxus:
|
Hot reloading is automatically enabled when using the web renderer on debug builds.
|
||||||
```toml
|
|
||||||
dioxus = { version = "*", features = ["hot-reload"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
# Usage
|
## Usage
|
||||||
1. run:
|
1. Run:
|
||||||
```
|
```bash
|
||||||
dioxus serve --hot-reload
|
dioxus serve --hot-reload
|
||||||
```
|
```
|
||||||
2. change some code within a rsx macro
|
2. Change some code within a rsx or render macro
|
||||||
3. open your localhost in a browser
|
3. Open your localhost in a browser
|
||||||
4. save and watch the style change without recompiling
|
4. Save and watch the style change without recompiling
|
||||||
|
|
||||||
|
# Desktop/Liveview/TUI
|
||||||
|
For desktop, LiveView, and tui, you can place the hot reload macro at the top of your main function to enable hot reloading.
|
||||||
|
Hot reloading is automatically enabled on debug builds.
|
||||||
|
|
||||||
|
For more information about hot reloading on native platforms and configuration options see the [dioxus-hot-reload](https://crates.io/crates/dioxus-hot-reload) crate.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Add the following to your main function:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
hot_reload_init!();
|
||||||
|
// launch your application
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
1. Run:
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
2. Change some code within a rsx or render macro
|
||||||
|
3. Save and watch the style change without recompiling
|
||||||
|
|
||||||
# Limitations
|
# Limitations
|
||||||
1. The interperter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will trigger a full recompile to capture the expression.
|
1. The interpreter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will require a full recompile to capture the expression.
|
||||||
2. Components and Iterators can contain abritary rust code, and will trigger a full recompile when changed.
|
2. Components, Iterators, and some attributes can contain arbitrary rust code and will trigger a full recompile when changed.
|
||||||
|
|
|
@ -12,63 +12,23 @@ Dioxus integrates very well with the [Rust-Analyzer LSP plugin](https://rust-ana
|
||||||
|
|
||||||
Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler.
|
Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler.
|
||||||
|
|
||||||
We strongly recommend going through the [official Rust book](https://doc.rust-lang.org/book/ch01-00-getting-started.html) _completely_. However, our hope is that a Dioxus app can serve as a great first Rust project. With Dioxus, you'll learn about:
|
We strongly recommend going through the [official Rust book](https://doc.rust-lang.org/book/ch01-00-getting-started.html) _completely_. However, we hope that a Dioxus app can serve as a great first Rust project. With Dioxus, you'll learn about:
|
||||||
|
|
||||||
- Error handling
|
- Error handling
|
||||||
- Structs, Functions, Enums
|
- Structs, Functions, Enums
|
||||||
- Closures
|
- Closures
|
||||||
- Macros
|
- Macros
|
||||||
|
|
||||||
We've put a lot of care into making Dioxus syntax familiar and easy to understand, so you won't need deep knowledge on async, lifetimes, or smart pointers until you really start building complex Dioxus apps.
|
We've put a lot of care into making Dioxus syntax familiar and easy to understand, so you won't need deep knowledge of async, lifetimes, or smart pointers until you start building complex Dioxus apps.
|
||||||
|
|
||||||
### Platform-Specific Dependencies
|
|
||||||
|
|
||||||
#### Windows
|
|
||||||
|
|
||||||
Windows Desktop apps depend on WebView2 – a library which should be installed in all modern Windows distributions. If you have Edge installed, then Dioxus will work fine. If you *don't* have Webview2, [then you can install it through Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). MS provides 3 options:
|
|
||||||
|
|
||||||
1. A tiny "evergreen" *bootstrapper* which will fetch an installer from Microsoft's CDN
|
|
||||||
2. A tiny *installer* which will fetch Webview2 from Microsoft's CDN
|
|
||||||
3. A statically linked version of Webview2 in your final binary for offline users
|
|
||||||
|
|
||||||
For development purposes, use Option 1.
|
|
||||||
|
|
||||||
#### Linux
|
|
||||||
|
|
||||||
Webview Linux apps require WebkitGtk. When distributing, this can be part of your dependency tree in your `.rpm` or `.deb`. However, it's very likely that your users will already have WebkitGtk.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
When using Debian/bullseye `libappindicator3-dev` is no longer available but replaced by `libayatana-appindicator3-dev`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# on Debian/bullseye use:
|
|
||||||
sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/v1/guides/getting-started/prerequisites#setting-up-linux).
|
|
||||||
|
|
||||||
|
|
||||||
#### MacOS
|
|
||||||
|
|
||||||
Currently – everything for macOS is built right in! However, you might run into an issue if you're using nightly Rust due to some permissions issues in our Tao dependency (which have been resolved but not published).
|
|
||||||
|
|
||||||
### Suggested Cargo Extensions
|
|
||||||
|
|
||||||
If you want to keep your traditional `npm install XXX` workflow for adding packages, you might want to install `cargo-edit` and a few other fun `cargo` extensions:
|
|
||||||
|
|
||||||
- [cargo-expand](https://github.com/dtolnay/cargo-expand) for expanding macro calls
|
|
||||||
- [cargo tree](https://doc.rust-lang.org/cargo/commands/cargo-tree.html) – an integrated cargo command that lets you inspect your dependency tree
|
|
||||||
|
|
||||||
|
|
||||||
## Setup Guides
|
## Setup Guides
|
||||||
|
|
||||||
Dioxus supports multiple platforms. Depending on what you want, the setup is a bit different.
|
Dioxus supports multiple platforms. Choose the platform you want to target below to get platform-specific setup instructions:
|
||||||
|
|
||||||
- [Web](web.md): running in the browser using WASM
|
- [Web](web.md): runs in the browser through WebAssembly
|
||||||
- [Server Side Rendering](ssr.md): render Dioxus HTML as text
|
- [Server Side Rendering](ssr.md): renders to HTML text on the server
|
||||||
- [Desktop](desktop.md): a standalone app using webview
|
- [Liveview](liveview.md): runs on the server, renders in the browser using WebSockets
|
||||||
- [Mobile](mobile.md)
|
- [Desktop](desktop.md): runs in a web view on desktop
|
||||||
- [Terminal UI](tui.md): terminal text-based graphic interface
|
- [Mobile](mobile.md): runs in a web view on mobile
|
||||||
|
- [Terminal UI](tui.md): renders text-based graphics in the terminal
|
66
docs/guide/src/en/getting_started/liveview.md
Normal file
66
docs/guide/src/en/getting_started/liveview.md
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Liveview
|
||||||
|
|
||||||
|
Liveview allows apps to *run* on the server and *render* in the browser. It uses WebSockets to communicate between the server and the browser.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- [Axum Example](https://github.com/DioxusLabs/dioxus/tree/master/packages/liveview/examples/axum.rs)
|
||||||
|
- [Salvo Example](https://github.com/DioxusLabs/dioxus/tree/master/packages/liveview/examples/salvo.rs)
|
||||||
|
- [Warp Example](https://github.com/DioxusLabs/dioxus/tree/master/packages/liveview/examples/warp.rs)
|
||||||
|
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Liveview is currently limited in capability when compared to the Web platform. Liveview apps run on the server in a native thread. This means that browser APIs are not available, so rendering WebGL, Canvas, etc is not as easy as the Web. However, native system APIs are accessible, so streaming, WebSockets, filesystem, etc are all viable APIs.
|
||||||
|
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
For this guide, we're going to show how to use Dioxus Liveview with [Axum](https://docs.rs/axum/latest/axum/).
|
||||||
|
|
||||||
|
Make sure you have Rust and Cargo installed, and then create a new project:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo new --bin demo
|
||||||
|
cd app
|
||||||
|
```
|
||||||
|
|
||||||
|
Add Dioxus and the liveview renderer with the Axum feature as dependencies:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo add dioxus
|
||||||
|
cargo add dioxus-liveview --features axum
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, add all the Axum dependencies. This will be different if you're using a different Web Framework
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo add tokio --features full
|
||||||
|
cargo add axum
|
||||||
|
```
|
||||||
|
|
||||||
|
Your dependencies should look roughly like this:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.4.5"
|
||||||
|
dioxus = { version = "*" }
|
||||||
|
dioxus-liveview = { version = "*", features = ["axum"] }
|
||||||
|
tokio = { version = "1.15.0", features = ["full"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, set up your Axum app to respond on an endpoint.
|
||||||
|
|
||||||
|
|
||||||
|
```rust
|
||||||
|
{{#include ../../../examples/hello_world_liveview.rs:glue}}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
And then add our app component:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
{{#include ../../../examples/hello_world_liveview.rs:app}}
|
||||||
|
```
|
||||||
|
|
||||||
|
And that's it!
|
||||||
|
|
|
@ -5,12 +5,12 @@ Build a mobile app with Dioxus!
|
||||||
Example: [Todo App](https://github.com/DioxusLabs/example-projects/blob/master/ios_demo)
|
Example: [Todo App](https://github.com/DioxusLabs/example-projects/blob/master/ios_demo)
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
Mobile is currently the least-supported renderer target for Dioxus. Mobile apps are rendered with either the platform's WebView or experimentally through WGPU. WebView doesn't support animations, transparency, and native widgets.
|
Mobile is currently the least-supported renderer target for Dioxus. Mobile apps are rendered with either the platform's WebView or experimentally through [WGPU](https://github.com/DioxusLabs/blitz). WebView doesn't support animations, transparency, and native widgets.
|
||||||
|
|
||||||
|
|
||||||
Mobile support is currently best suited for CRUD-style apps, ideally for internal teams who need to develop quickly but don't care much about animations or native widgets.
|
Mobile support is currently best suited for CRUD-style apps, ideally for internal teams who need to develop quickly but don't care much about animations or native widgets.
|
||||||
|
|
||||||
This guide is primarily targetted for iOS apps, however, you can follow it while using the `android` guide in `cargo-mobile`.
|
This guide is primarily targeted at iOS apps, however, you can follow it while using the `android` guide in `cargo-mobile`.
|
||||||
|
|
||||||
## Getting Set up
|
## Getting Set up
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ We're going to completely clear out the `dependencies` it generates for us, swap
|
||||||
[package]
|
[package]
|
||||||
name = "dioxus-ios-demo"
|
name = "dioxus-ios-demo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
authors = []
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,19 +15,7 @@ When working with web frameworks that require `Send`, it is possible to render a
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
If you just want to render `rsx!` or a VirtualDom to HTML, check out the API docs. It's pretty simple:
|
For this guide, we're going to show how to use Dioxus SSR with [Axum](https://docs.rs/axum/latest/axum/).
|
||||||
|
|
||||||
```rust
|
|
||||||
// We can render VirtualDoms
|
|
||||||
let mut vdom = VirtualDom::new(app);
|
|
||||||
let _ = vdom.rebuild();
|
|
||||||
println!("{}", dioxus_ssr::render_vdom(&vdom));
|
|
||||||
|
|
||||||
// Or we can render rsx! calls directly
|
|
||||||
println!( "{}", dioxus_ssr::render_lazy(rsx! { h1 { "Hello, world!" } } );
|
|
||||||
```
|
|
||||||
|
|
||||||
However, for this guide, we're going to show how to use Dioxus SSR with `Axum`.
|
|
||||||
|
|
||||||
Make sure you have Rust and Cargo installed, and then create a new project:
|
Make sure you have Rust and Cargo installed, and then create a new project:
|
||||||
|
|
||||||
|
@ -36,7 +24,7 @@ cargo new --bin demo
|
||||||
cd app
|
cd app
|
||||||
```
|
```
|
||||||
|
|
||||||
Add Dioxus and the `ssr` renderer feature:
|
Add Dioxus and the ssr renderer as dependencies:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo add dioxus
|
cargo add dioxus
|
||||||
|
@ -86,8 +74,9 @@ And then add our endpoint. We can either render `rsx!` directly:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
async fn app_endpoint() -> Html<String> {
|
async fn app_endpoint() -> Html<String> {
|
||||||
|
// render the rsx! macro to HTML
|
||||||
Html(dioxus_ssr::render_lazy(rsx! {
|
Html(dioxus_ssr::render_lazy(rsx! {
|
||||||
h1 { "hello world!" }
|
div { "hello world!" }
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -96,12 +85,16 @@ Or we can render VirtualDoms.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
async fn app_endpoint() -> Html<String> {
|
async fn app_endpoint() -> Html<String> {
|
||||||
|
// create a component that renders a div with the text "hello world"
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
cx.render(rsx!(h1 { "hello world" }))
|
cx.render(rsx!(div { "hello world" }))
|
||||||
}
|
}
|
||||||
|
// create a VirtualDom with the app component
|
||||||
let mut app = VirtualDom::new(app);
|
let mut app = VirtualDom::new(app);
|
||||||
|
// rebuild the VirtualDom before rendering
|
||||||
let _ = app.rebuild();
|
let _ = app.rebuild();
|
||||||
|
|
||||||
|
// render the VirtualDom to HTML
|
||||||
Html(dioxus_ssr::render_vdom(&app))
|
Html(dioxus_ssr::render_vdom(&app))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -8,12 +8,19 @@ You can build a text-based interface that will run in the terminal using Dioxus.
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
TUI support is currently quite experimental. Even the project name will change. But, if you're willing to venture into the realm of the unknown, this guide will get you started.
|
TUI support is currently quite experimental. But, if you're willing to venture into the realm of the unknown, this guide will get you started.
|
||||||
|
|
||||||
|
- It uses flexbox for the layout
|
||||||
|
- It only supports a subset of the attributes and elements
|
||||||
|
- Regular widgets will not work in the tui render, but the tui renderer has its own widget components that start with a capital letter. See the [widgets example](https://github.com/DioxusLabs/dioxus/blob/master/packages/tui/examples/widgets.rs)
|
||||||
|
- 1px is one character line height. Your regular CSS px does not translate
|
||||||
|
- If your app panics, your terminal is wrecked. This will be fixed eventually
|
||||||
|
|
||||||
|
|
||||||
## Getting Set up
|
## Getting Set up
|
||||||
|
|
||||||
|
|
||||||
Start by making a new package and adding our TUI renderer.
|
Start by making a new package and adding Dioxus and the TUI renderer as dependancies.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo new --bin demo
|
cargo new --bin demo
|
||||||
|
@ -39,10 +46,3 @@ Press "ctrl-c" to close the app. To switch from "ctrl-c" to just "q" to quit yo
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/hello_world_tui_no_ctrl_c.rs}}
|
{{#include ../../../examples/hello_world_tui_no_ctrl_c.rs}}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Our TUI package uses flexbox for layout
|
|
||||||
- Regular widgets will not work in the tui render, but the tui renderer has it's own widget components (see [the widgets example](https://github.com/DioxusLabs/dioxus/blob/master/packages/tui/examples/tui_widgets.rs)).
|
|
||||||
- 1px is one character lineheight. Your regular CSS px does not translate.
|
|
||||||
- If your app panics, your terminal is wrecked. This will be fixed eventually.
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# Web
|
# Web
|
||||||
|
|
||||||
Build single-page applications that run in the browser with Dioxus. To run on the Web, your app must be compiled to WebAssembly and depend on the `dioxus` crate with the `web` feature enabled.
|
Build single-page applications that run in the browser with Dioxus. To run on the Web, your app must be compiled to WebAssembly and depend on the `dioxus` and `dioxus-web` crates.
|
||||||
|
|
||||||
A build of Dioxus for the web will be roughly equivalent to the size of a React build (70kb vs 65kb), but will load significantly faster due to [WebAssembly's StreamingCompile](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/) option.
|
A build of Dioxus for the web will be roughly equivalent to the size of a React build (70kb vs 65kb) but it will load significantly faster because [WebAssembly can be compiled as it is streamed](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/).
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc)
|
- [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc)
|
||||||
|
@ -10,21 +10,24 @@ Examples:
|
||||||
|
|
||||||
[![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc)
|
[![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc)
|
||||||
|
|
||||||
> Note: Because of the limitations of Wasm, not every crate will work with your web apps, so you'll need to make sure that your crates work without native system calls (timers, IO, etc).
|
> Note: Because of the limitations of Wasm, [not every crate will work](https://rustwasm.github.io/docs/book/reference/which-crates-work-with-wasm.html) with your web apps, so you'll need to make sure that your crates work without native system calls (timers, IO, etc).
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
The Web is the best-supported target platform for Dioxus.
|
The Web is the best-supported target platform for Dioxus.
|
||||||
|
|
||||||
|
- Because your app will be compiled to WASM you have access to browser APIs through [wasm-bingen](https://rustwasm.github.io/docs/wasm-bindgen/introduction.html).
|
||||||
|
- Dioxus provides hydration to resume apps that are rendered on the server. See the [hydration example](https://github.com/DioxusLabs/dioxus/blob/master/packages/web/examples/hydrate.rs) for more details.
|
||||||
|
|
||||||
## Tooling
|
## Tooling
|
||||||
|
|
||||||
To develop your Dioxus app for the web, you'll need a tool to build and serve your assets. We recommend using [trunk](https://trunkrs.dev) which includes a build system, Wasm optimization, a dev server, and support for SASS/CSS:
|
To develop your Dioxus app for the web, you'll need a tool to build and serve your assets. We recommend using [dioxus-cli](https://github.com/DioxusLabs/cli) which includes a build system, Wasm optimization, a dev server, and support hot reloading:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo install trunk
|
cargo install dioxus-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
Make sure the `wasm32-unknown-unknown` target is installed:
|
Make sure the `wasm32-unknown-unknown` target for rust is installed:
|
||||||
```shell
|
```shell
|
||||||
rustup target add wasm32-unknown-unknown
|
rustup target add wasm32-unknown-unknown
|
||||||
```
|
```
|
||||||
|
@ -38,28 +41,13 @@ cargo new --bin demo
|
||||||
cd demo
|
cd demo
|
||||||
```
|
```
|
||||||
|
|
||||||
Add Dioxus as a dependency and add the web renderer:
|
Add Dioxus and the web renderer as dependencies (this will edit your `Cargo.toml`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo add dioxus
|
cargo add dioxus
|
||||||
cargo add dioxus-web
|
cargo add dioxus-web
|
||||||
```
|
```
|
||||||
|
|
||||||
Add an `index.html` for Trunk to use. Make sure your "mount point" element has an ID of "main":
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="main"> </div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
Edit your `main.rs`:
|
Edit your `main.rs`:
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/hello_world_web.rs}}
|
{{#include ../../../examples/hello_world_web.rs}}
|
||||||
|
@ -69,5 +57,5 @@ Edit your `main.rs`:
|
||||||
And to serve our app:
|
And to serve our app:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
trunk serve
|
dioxus serve
|
||||||
```
|
```
|
||||||
|
|
|
@ -27,7 +27,7 @@ Dioxus is heavily inspired by React. If you know React, getting started with Dio
|
||||||
- Comprehensive inline documentation – hover and guides for all HTML elements, listeners, and events.
|
- Comprehensive inline documentation – hover and guides for all HTML elements, listeners, and events.
|
||||||
- Extremely memory efficient – 0 global allocations for steady-state components.
|
- Extremely memory efficient – 0 global allocations for steady-state components.
|
||||||
- Multi-channel asynchronous scheduler for first-class async support.
|
- Multi-channel asynchronous scheduler for first-class async support.
|
||||||
- And more! Read the [full release post](https://dioxuslabs.com/blog/introducing-dioxus/.
|
- And more! Read the [full release post](https://dioxuslabs.com/blog/introducing-dioxus/).
|
||||||
|
|
||||||
### Multiplatform
|
### Multiplatform
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ For example, if many components need to access an `AppSettings` struct, you can
|
||||||
|
|
||||||
## Custom Hook Logic
|
## Custom Hook Logic
|
||||||
|
|
||||||
You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.use_hook) to build your own hooks. In fact, this is what all the standard hooks are built on!
|
You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.use_hook) to build your own hooks. In fact, this is what all the standard hooks are built on!
|
||||||
|
|
||||||
`use_hook` accepts a single closure for initializing the hook. It will be only run the first time the component is rendered. The return value of that closure will be used as the value of the hook – Dioxus will take it, and store it for as long as the component is alive. On every render (not just the first one!), you will get a reference to this value.
|
`use_hook` accepts a single closure for initializing the hook. It will be only run the first time the component is rendered. The return value of that closure will be used as the value of the hook – Dioxus will take it, and store it for as long as the component is alive. On every render (not just the first one!), you will get a reference to this value.
|
||||||
|
|
||||||
|
@ -22,5 +22,5 @@ You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.
|
||||||
|
|
||||||
Inside the initialization closure, you will typically make calls to other `cx` methods. For example:
|
Inside the initialization closure, you will typically make calls to other `cx` methods. For example:
|
||||||
|
|
||||||
- The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.schedule_update) to make Dioxus re-render the component whenever it changes.
|
- The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.schedule_update) to make Dioxus re-render the component whenever it changes.
|
||||||
- The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope
|
- The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope
|
||||||
|
|
|
@ -12,10 +12,19 @@ To render different elements based on a condition, you could use an `if-else` st
|
||||||
|
|
||||||
> You could also use `match` statements, or any Rust function to conditionally render different things.
|
> You could also use `match` statements, or any Rust function to conditionally render different things.
|
||||||
|
|
||||||
|
### Improving the `if-else` Example
|
||||||
|
|
||||||
|
You may have noticed some repeated code in the `if-else` example above. Repeating code like this is both bad for maintainability and performance. Dioxus will skip diffing static elements like the button, but when switching between multiple `rsx` calls it cannot perform this optimization. For this example either approach is fine, but for components with large parts that are reused between conditionals, it can be more of an issue.
|
||||||
|
|
||||||
|
We can improve this example by splitting up the dynamic parts and inserting them where they are needed.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
{{#include ../../../examples/conditional_rendering.rs:if_else_improved}}
|
||||||
|
```
|
||||||
|
|
||||||
### Inspecting `Element` props
|
### Inspecting `Element` props
|
||||||
|
|
||||||
Since `Element` is a `Option<VNode>`, components accepting `Element` as a prop can actually inspect its contents, and render different things based on that. Example:
|
Since `Element` is a `Option<VNode>`, components accepting `Element` as a prop can inspect its contents, and render different things based on that. Example:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/component_children_inspect.rs:Clickable}}
|
{{#include ../../../examples/component_children_inspect.rs:Clickable}}
|
||||||
|
@ -43,9 +52,9 @@ Often, you'll want to render a collection of components. For example, you might
|
||||||
For this, Dioxus accepts iterators that produce `Element`s. So we need to:
|
For this, Dioxus accepts iterators that produce `Element`s. So we need to:
|
||||||
|
|
||||||
- Get an iterator over all of our items (e.g., if you have a `Vec` of comments, iterate over it with `iter()`)
|
- Get an iterator over all of our items (e.g., if you have a `Vec` of comments, iterate over it with `iter()`)
|
||||||
- `.map` the iterator to convert each item into a rendered `Element` using `cx.render(rsx!(...))`
|
- `.map` the iterator to convert each item into a `LazyNode` using `rsx!(...)`
|
||||||
- Add a unique `key` attribute to each iterator item
|
- Add a unique `key` attribute to each iterator item
|
||||||
- Include this iterator in the final RSX
|
- Include this iterator in the final RSX (or use it inline)
|
||||||
|
|
||||||
Example: suppose you have a list of comments you want to render. Then, you can render them like this:
|
Example: suppose you have a list of comments you want to render. Then, you can render them like this:
|
||||||
|
|
||||||
|
@ -53,21 +62,26 @@ Example: suppose you have a list of comments you want to render. Then, you can r
|
||||||
{{#include ../../../examples/rendering_lists.rs:render_list}}
|
{{#include ../../../examples/rendering_lists.rs:render_list}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Inline for loops
|
||||||
|
|
||||||
|
Because of how common it is to render a list of items, Dioxus provides a shorthand for this. Instead of using `.iter, `.map`, and `rsx`, you can use a `for` loop with a body of rsx code:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
{{#include ../../../examples/rendering_lists.rs:render_list_for_loop}}
|
||||||
|
```
|
||||||
|
|
||||||
### The `key` Attribute
|
### The `key` Attribute
|
||||||
|
|
||||||
Every time you re-render your list, Dioxus needs to keep track of which item went where, because the order of items in a list might change – items might be added, removed or swapped. Despite that, Dioxus needs to:
|
Every time you re-render your list, Dioxus needs to keep track of which items go where to determine what updates need to be made to the UI.
|
||||||
|
|
||||||
- Keep track of component state
|
|
||||||
- Efficiently figure out what updates need to be made to the UI
|
|
||||||
|
|
||||||
For example, suppose the `CommentComponent` had some state – e.g. a field where the user typed in a reply. If the order of comments suddenly changes, Dioxus needs to correctly associate that state with the same comment – otherwise, the user will end up replying to a different comment!
|
For example, suppose the `CommentComponent` had some state – e.g. a field where the user typed in a reply. If the order of comments suddenly changes, Dioxus needs to correctly associate that state with the same comment – otherwise, the user will end up replying to a different comment!
|
||||||
|
|
||||||
To help Dioxus keep track of list items, we need to associate each item with a unique key. In the example above, we dynamically generated the unique key. In real applications, it's more likely that the key will come from e.g. a database ID. It doesn't really matter where you get the key from, as long as it meets the requirements
|
To help Dioxus keep track of list items, we need to associate each item with a unique key. In the example above, we dynamically generated the unique key. In real applications, it's more likely that the key will come from e.g. a database ID. It doesn't matter where you get the key from, as long as it meets the requirements:
|
||||||
|
|
||||||
- Keys must be unique in a list
|
- Keys must be unique in a list
|
||||||
- The same item should always get associated with the same key
|
- The same item should always get associated with the same key
|
||||||
- Keys should be relatively small (i.e. converting the entire Comment structure to a String would be a pretty bad key) so they can be compared efficiently
|
- Keys should be relatively small (i.e. converting the entire Comment structure to a String would be a pretty bad key) so they can be compared efficiently
|
||||||
|
|
||||||
You might be tempted to use an item's index in the list as its key. In fact, that’s what Dioxus will use if you don’t specify a key at all. This is only acceptable if you can guarantee that the list is constant – i.e., no re-ordering, additions or deletions.
|
You might be tempted to use an item's index in the list as its key. That’s what Dioxus will use if you don’t specify a key at all. This is only acceptable if you can guarantee that the list is constant – i.e., no re-ordering, additions, or deletions.
|
||||||
|
|
||||||
> Note that if you pass the key to a component you've made, it won't receive the key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop.
|
> Note that if you pass the key to a component you've made, it won't receive the key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop.
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
# Event Handlers
|
# Event Handlers
|
||||||
|
|
||||||
Events are interesting things that happen, usually related to something the user has done. For example, some events happen when the user clicks, scrolls, moves the mouse, or types a character. To respond to these actions and make the UI interactive, we need to handle those events.
|
Event handlers are used to respond to user actions. For example, an event handler could be triggered when the user clicks, scrolls, moves the mouse, or types a character.
|
||||||
|
|
||||||
Events are associated with elements. For example, we usually don't care about all the clicks that happen within an app, only those on a particular button. To handle events that happen on an element, we must attach the desired event handler to it.
|
Event handlers are attached to elements. For example, we usually don't care about all the clicks that happen within an app, only those on a particular button.
|
||||||
|
|
||||||
Event handlers are similar to regular attributes, but their name usually starts with `on`- and they accept closures as values. The closure will be called whenever the event is triggered, and will be passed that event.
|
Event handlers are similar to regular attributes, but their name usually starts with `on`- and they accept closures as values. The closure will be called whenever the event it listens for is triggered and will be passed that event.
|
||||||
|
|
||||||
For example, to handle clicks on an element, we can specify an `onclick` handler:
|
For example, to handle clicks on an element, we can specify an `onclick` handler:
|
||||||
|
|
||||||
|
@ -14,20 +14,24 @@ For example, to handle clicks on an element, we can specify an `onclick` handler
|
||||||
|
|
||||||
## The `Event` object
|
## The `Event` object
|
||||||
|
|
||||||
Event handlers receive an [`UiEvent`](https://docs.rs/dioxus-core/latest/dioxus_core/struct.UiEvent.html) object containing information about the event. Different types of events contain different types of data. For example, mouse-related events contain [`MouseData`](https://docs.rs/dioxus/latest/dioxus/events/struct.MouseData.html), which tells you things like where the mouse was clicked and what mouse buttons were used.
|
Event handlers receive an [`Event`](https://docs.rs/dioxus-core/latest/dioxus_core/struct.Event.html) object containing information about the event. Different types of events contain different types of data. For example, mouse-related events contain [`MouseData`](https://docs.rs/dioxus/latest/dioxus/events/struct.MouseData.html), which tells you things like where the mouse was clicked and what mouse buttons were used.
|
||||||
|
|
||||||
In the example above, this event data was logged to the terminal:
|
In the example above, this event data was logged to the terminal:
|
||||||
|
|
||||||
```
|
```
|
||||||
Clicked! Event: UiEvent { data: MouseData { coordinates: Coordinates { screen: (468.0, 109.0), client: (73.0, 25.0), element: (63.0, 15.0), page: (73.0, 25.0) }, modifiers: (empty), held_buttons: EnumSet(), trigger_button: Some(Primary) } }
|
Clicked! Event: UiEvent { bubble_state: Cell { value: true }, data: MouseData { coordinates: Coordinates { screen: (242.0, 256.0), client: (26.0, 17.0), element: (16.0, 7.0), page: (26.0, 17.0) }, modifiers: (empty), held_buttons: EnumSet(), trigger_button: Some(Primary) } }
|
||||||
Clicked! Event: UiEvent { data: MouseData { coordinates: Coordinates { screen: (468.0, 109.0), client: (73.0, 25.0), element: (63.0, 15.0), page: (73.0, 25.0) }, modifiers: (empty), held_buttons: EnumSet(), trigger_button: Some(Primary) } }
|
Clicked! Event: UiEvent { bubble_state: Cell { value: true }, data: MouseData { coordinates: Coordinates { screen: (242.0, 256.0), client: (26.0, 17.0), element: (16.0, 7.0), page: (26.0, 17.0) }, modifiers: (empty), held_buttons: EnumSet(), trigger_button: Some(Primary) } }
|
||||||
```
|
```
|
||||||
|
|
||||||
To learn what the different event types provide, read the [events module docs](https://docs.rs/dioxus/latest/dioxus/events/index.html).
|
To learn what the different event types for HTML provide, read the [events module docs](https://docs.rs/dioxus-html/latest/dioxus_html/events/index.html).
|
||||||
|
|
||||||
### Stopping propagation
|
### Event propagation
|
||||||
|
|
||||||
When you have e.g. a `button` inside a `div`, any click on the `button` is also a click on the `div`. For this reason, Dioxus propagates the click event: first, it is triggered on the target element, then on parent elements. If you want to prevent this behavior, you can call `cancel_bubble()` on the event:
|
Some events will trigger first on the element the event originated at upward. For example, a click event on a `button` inside a `div` would first trigger the button's event listener and then the div's event listener.
|
||||||
|
|
||||||
|
> For more information about event propigation see [the mdn docs on event bubling](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling)
|
||||||
|
|
||||||
|
If you want to prevent this behavior, you can call `stop_propogation()` on the event:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/event_nested.rs:rsx}}
|
{{#include ../../../examples/event_nested.rs:rsx}}
|
||||||
|
@ -45,7 +49,7 @@ In some instances, might want to avoid this default behavior. For this, you can
|
||||||
|
|
||||||
Any event handlers will still be called.
|
Any event handlers will still be called.
|
||||||
|
|
||||||
> Normally, in React or JavaScript, you'd call "preventDefault" on the event in the callback. Dioxus does *not* currently support this behavior. Note: this means you cannot conditionally prevent default behavior.
|
> Normally, in React or JavaScript, you'd call "preventDefault" on the event in the callback. Dioxus does *not* currently support this behavior. Note: this means you cannot conditionally prevent default behavior based on the data in the event.
|
||||||
|
|
||||||
## Handler Props
|
## Handler Props
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
# Hooks and Component State
|
# Hooks and Component State
|
||||||
|
|
||||||
So far our components, being Rust functions, had no state – they were always rendering the same thing. However, in a UI component, it is often useful to have stateful functionality to build user interactions. For example, you might want to track whether the user has openend a drop-down, and render different things accordingly.
|
So far our components have had no state like a normal rust functions. However, in a UI component, it is often useful to have stateful functionality to build user interactions. For example, you might want to track whether the user has opened a drop-down, and render different things accordingly.
|
||||||
|
|
||||||
For stateful logic, you can use hooks. Hooks are Rust functions that take a reference to `ScopeState` (in a component, you can pass `&cx`), and provide you with functionality and state.
|
Hooks allow us to create state in our components. Hooks are Rust functions that take a reference to `ScopeState` (in a component, you can pass `cx`), and provide you with functionality and state.
|
||||||
|
|
||||||
## `use_state` Hook
|
## `use_state` Hook
|
||||||
|
|
||||||
[`use_state`](https://docs.rs/dioxus/latest/dioxus/hooks/fn.use_state.html) is one of the simplest hooks.
|
[`use_state`](https://docs.rs/dioxus/latest/dioxus/prelude/fn.use_state.html) is one of the simplest hooks.
|
||||||
|
|
||||||
- You provide a closure that determines the initial value
|
- You provide a closure that determines the initial value
|
||||||
- `use_state` gives you the current value, and a way to update it by setting it to something else
|
- `use_state` gives you the current value, and a way to update it by setting it to something else
|
||||||
|
@ -21,7 +21,7 @@ For example, you might have seen the counter example, in which state (a number)
|
||||||
|
|
||||||
Every time the component's state changes, it re-renders, and the component function is called, so you can describe what you want the new UI to look like. You don't have to worry about "changing" anything – just describe what you want in terms of the state, and Dioxus will take care of the rest!
|
Every time the component's state changes, it re-renders, and the component function is called, so you can describe what you want the new UI to look like. You don't have to worry about "changing" anything – just describe what you want in terms of the state, and Dioxus will take care of the rest!
|
||||||
|
|
||||||
> `use_state` returns your value wrapped in a smart pointer of type [`UseState`](https://docs.rs/dioxus/latest/dioxus/hooks/struct.UseState.html). This is why you can both read the value and update it, even within a handler.
|
> `use_state` returns your value wrapped in a smart pointer of type [`UseState`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.UseState.html). This is why you can both read the value and update it, even within an event handler.
|
||||||
|
|
||||||
You can use multiple hooks in the same component if you want:
|
You can use multiple hooks in the same component if you want:
|
||||||
|
|
||||||
|
@ -40,13 +40,13 @@ But how can Dioxus differentiate between multiple hooks in the same component? A
|
||||||
{{#include ../../../examples/hooks_counter_two_state.rs:use_state_calls}}
|
{{#include ../../../examples/hooks_counter_two_state.rs:use_state_calls}}
|
||||||
```
|
```
|
||||||
|
|
||||||
This is only possible because the two hooks are always called in the same order, so Dioxus knows which is which. So the order you call hooks matters, which is why you must follow certain rules when using hooks:
|
This is only possible because the two hooks are always called in the same order, so Dioxus knows which is which. Because the order you call hooks matters, you must follow certain rules when using hooks:
|
||||||
|
|
||||||
1. Hooks may be only used in components or other hooks (we'll get to that later)
|
1. Hooks may be only used in components or other hooks (we'll get to that later)
|
||||||
2. On every call to the component function
|
2. On every call to the component function
|
||||||
1. The same hooks must be called
|
1. The same hooks must be called
|
||||||
2. In the same order
|
2. In the same order
|
||||||
3. Hooks name's must start with `use_` so you don't accidentally confuse them with regular functions
|
3. Hooks name's should start with `use_` so you don't accidentally confuse them with regular functions
|
||||||
|
|
||||||
These rules mean that there are certain things you can't do with hooks:
|
These rules mean that there are certain things you can't do with hooks:
|
||||||
|
|
||||||
|
@ -73,11 +73,11 @@ For example, suppose we want to maintain a `Vec` of values. If we stored it with
|
||||||
|
|
||||||
Thankfully, there is another hook for that, `use_ref`! It is similar to `use_state`, but it lets you get a mutable reference to the contained data.
|
Thankfully, there is another hook for that, `use_ref`! It is similar to `use_state`, but it lets you get a mutable reference to the contained data.
|
||||||
|
|
||||||
Here's a simple example that keeps a list of events in a `use_ref`. We can acquire write access to the state with `.write()`, and then just `.push` a new value to the state:
|
Here's a simple example that keeps a list of events in a `use_ref`. We can acquire write access to the state with `.with_mut()`, and then just `.push` a new value to the state:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/hooks_use_ref.rs:component}}
|
{{#include ../../../examples/hooks_use_ref.rs:component}}
|
||||||
```
|
```
|
||||||
|
|
||||||
> The return values of `use_state` and `use_ref`, (`UseState` and `UseRef`, respectively) are in some ways similar to [`Cell`](https://doc.rust-lang.org/std/cell/) and [`RefCell`](https://doc.rust-lang.org/std/cell/struct.RefCell.html) – they provide interior mutability. However, these Dioxus wrappers also ensure that the component gets re-rendered whenever you change the state.
|
> The return values of `use_state` and `use_ref` (`UseState` and `UseRef`, respectively) are in some ways similar to [`Cell`](https://doc.rust-lang.org/std/cell/) and [`RefCell`](https://doc.rust-lang.org/std/cell/struct.RefCell.html) – they provide interior mutability. However, these Dioxus wrappers also ensure that the component gets re-rendered whenever you change the state.
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,23 @@
|
||||||
# Router
|
# Router
|
||||||
|
|
||||||
In many of your apps, you'll want to have different "scenes". For a webpage, these scenes might be the different webpages with their own content.
|
In many of your apps, you'll want to have different "scenes". For a webpage, these scenes might be the different webpages with their own content. For a desktop app, these scenes might be different views in your app.
|
||||||
|
|
||||||
You could write your own scene management solution – quite simply too. However, to save you the effort, Dioxus supports a first-party solution for scene management called Dioxus Router.
|
To unify these platforms, Dioxus provides a first-party solution for scene management called Dioxus Router.
|
||||||
|
|
||||||
|
|
||||||
## What is it?
|
## What is it?
|
||||||
|
|
||||||
For an app like the Dioxus landing page (https://dioxuslabs.com), we want to have different pages. A quick sketch of an app would be something like:
|
For an app like the Dioxus landing page (https://dioxuslabs.com), we want to have several different scenes:
|
||||||
|
|
||||||
- Homepage
|
- Homepage
|
||||||
- Blog
|
- Blog
|
||||||
- Example showcase
|
|
||||||
|
|
||||||
Each of these scenes is independent – we don't want to render both the homepage and blog at the same time.
|
Each of these scenes is independent – we don't want to render both the homepage and blog at the same time.
|
||||||
|
|
||||||
This is where the router crates come in handy. To make sure we're using the router, simply add the `"router"` feature to your dioxus dependency.
|
The Dioxus router makes it easy to create these scenes. To make sure we're using the router, add the `dioxus-router` package to your `Cargo.toml`.
|
||||||
|
|
||||||
```toml
|
```shell
|
||||||
[dependencies]
|
cargo add dioxus-router
|
||||||
dioxus = { version = "*" }
|
|
||||||
dioxus-router = { version = "*" }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,8 +27,11 @@ Unlike other routers in the Rust ecosystem, our router is built declaratively. T
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
rsx!{
|
rsx!{
|
||||||
|
// All of our routes will be rendered inside this Router component
|
||||||
Router {
|
Router {
|
||||||
|
// if the current location is "/home", render the Home component
|
||||||
Route { to: "/home", Home {} }
|
Route { to: "/home", Home {} }
|
||||||
|
// if the current location is "/blog", render the Blog component
|
||||||
Route { to: "/blog", Blog {} }
|
Route { to: "/blog", Blog {} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ rsx!{
|
||||||
Router {
|
Router {
|
||||||
Route { to: "/home", Home {} }
|
Route { to: "/home", Home {} }
|
||||||
Route { to: "/blog", Blog {} }
|
Route { to: "/blog", Blog {} }
|
||||||
|
// if the current location doesn't match any of the above routes, render the NotFound component
|
||||||
Route { to: "", NotFound {} }
|
Route { to: "", NotFound {} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,6 +62,7 @@ rsx!{
|
||||||
Router {
|
Router {
|
||||||
Route { to: "/home", Home {} }
|
Route { to: "/home", Home {} }
|
||||||
Route { to: "/blog", Blog {} }
|
Route { to: "/blog", Blog {} }
|
||||||
|
// if the current location doesn't match any of the above routes, redirect to "/home"
|
||||||
Redirect { from: "", to: "/home" }
|
Redirect { from: "", to: "/home" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,6 +84,4 @@ rsx!{
|
||||||
|
|
||||||
## More reading
|
## More reading
|
||||||
|
|
||||||
This page is just meant to be a very brief overview of the router to show you that there's a powerful solution already built for a very common problem. For more information about the router, definitely check out its book or check out some of the examples.
|
This page is just a very brief overview of the router. For more information, check out [the router book](https://dioxuslabs.com/router/guide/) or some of [the router examples](https://github.com/DioxusLabs/dioxus/blob/master/examples/router.rs).
|
||||||
|
|
||||||
The router has its own documentation! [Available here](https://dioxuslabs.com/router/guide/).
|
|
||||||
|
|
|
@ -6,9 +6,9 @@ Often, multiple components need to access the same state. Depending on your need
|
||||||
|
|
||||||
One approach to share state between components is to "lift" it up to the nearest common ancestor. This means putting the `use_state` hook in a parent component, and passing the needed values down as props.
|
One approach to share state between components is to "lift" it up to the nearest common ancestor. This means putting the `use_state` hook in a parent component, and passing the needed values down as props.
|
||||||
|
|
||||||
For example, suppose we want to build a meme editor. We want to have an input to edit the meme caption, but also a preview of the meme with the caption. Logically, the meme and the input are 2 separate components, but they need access to the same state (the current caption).
|
Suppose we want to build a meme editor. We want to have an input to edit the meme caption, but also a preview of the meme with the caption. Logically, the meme and the input are 2 separate components, but they need access to the same state (the current caption).
|
||||||
|
|
||||||
> Of course, in this simple example, we could write everything in one component – but it is better to split everything out in smaller components to make the code more reusable and easier to maintain (this is even more important for larger, complex apps).
|
> Of course, in this simple example, we could write everything in one component – but it is better to split everything out in smaller components to make the code more reusable, maintainable, and performant (this is even more important for larger, complex apps).
|
||||||
|
|
||||||
We start with a `Meme` component, responsible for rendering a meme with a given caption:
|
We start with a `Meme` component, responsible for rendering a meme with a given caption:
|
||||||
```rust
|
```rust
|
||||||
|
@ -57,7 +57,7 @@ As a result, any child component of `App` (direct or not), can access the `DarkM
|
||||||
{{#include ../../../examples/meme_editor_dark_mode.rs:use_context}}
|
{{#include ../../../examples/meme_editor_dark_mode.rs:use_context}}
|
||||||
```
|
```
|
||||||
|
|
||||||
> `use_context` returns `Option<UseSharedState<DarkMode>>` here. If the context has been provided, the value is `Some(UseSharedState)`, which you can call `.read` or `.write` on, similarly to `UseRef`. Otherwise, the value is `None`.
|
> `use_context` returns `Option<UseSharedState<DarkMode>>` here. If the context has been provided, the value is `Some(UseSharedState<DarkMode>)`, which you can call `.read` or `.write` on, similarly to `UseRef`. Otherwise, the value is `None`.
|
||||||
|
|
||||||
For example, here's how we would implement the dark mode toggle, which both reads the context (to determine what color it should render) and writes to it (to toggle dark mode):
|
For example, here's how we would implement the dark mode toggle, which both reads the context (to determine what color it should render) and writes to it (to toggle dark mode):
|
||||||
```rust
|
```rust
|
||||||
|
|
|
@ -19,7 +19,7 @@ Notice the flexibility – you can:
|
||||||
|
|
||||||
## Uncontrolled Inputs
|
## Uncontrolled Inputs
|
||||||
|
|
||||||
As an alternative to controlled inputs, you can simply let the platform keep track of the input values. If we don't tell a HTML input what content it should have, it be editable anyway (this is built into the webview). This approach can be more performant, but less flexible. For example, it's harder to keep the input in sync with another element.
|
As an alternative to controlled inputs, you can simply let the platform keep track of the input values. If we don't tell a HTML input what content it should have, it will be editable anyway (this is built into the browser). This approach can be more performant, but less flexible. For example, it's harder to keep the input in sync with another element.
|
||||||
|
|
||||||
Since you don't necessarily have the current value of the uncontrolled input in state, you can access it either by listening to `oninput` events (similarly to controlled components), or, if the input is part of a form, you can access the form data in the form events (e.g. `oninput` or `onsubmit`):
|
Since you don't necessarily have the current value of the uncontrolled input in state, you can access it either by listening to `oninput` events (similarly to controlled components), or, if the input is part of a form, you can access the form data in the form events (e.g. `oninput` or `onsubmit`):
|
||||||
|
|
||||||
|
|
|
@ -125,8 +125,10 @@ We are currently working on our own build tool called [Dioxus CLI](https://githu
|
||||||
- ability to publish to github/netlify/vercel
|
- ability to publish to github/netlify/vercel
|
||||||
- bundling for iOS/Desktop/etc
|
- bundling for iOS/Desktop/etc
|
||||||
|
|
||||||
### LiveView / Server Component Support
|
### Server Component Support
|
||||||
|
|
||||||
The internal architecture of Dioxus was designed from day one to support the `LiveView` use-case, where a web server hosts a running app for each connected user. As of today, there is no first-class LiveView support – you'll need to wire this up yourself.
|
|
||||||
|
|
||||||
While not currently fully implemented, the expectation is that LiveView apps can be a hybrid between Wasm and server-rendered where only portions of a page are "live" and the rest of the page is either server-rendered, statically generated, or handled by the host SPA.
|
While not currently fully implemented, the expectation is that LiveView apps can be a hybrid between Wasm and server-rendered where only portions of a page are "live" and the rest of the page is either server-rendered, statically generated, or handled by the host SPA.
|
||||||
|
|
||||||
|
### Native rendering
|
||||||
|
|
||||||
|
We are currently working on a native renderer for Dioxus using WGPU called [Blitz](https://github.com/DioxusLabs/blitz/). This will allow you to build apps that are rendered natively for iOS, Android, and Desktop.
|
||||||
|
|
1
docs/guide/src/pt-br/interactivity/roteador.md
Normal file
1
docs/guide/src/pt-br/interactivity/roteador.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Roteamento
|
|
@ -1 +0,0 @@
|
||||||
this directory is for deploying into
|
|
|
@ -63,7 +63,7 @@ This very site is built with Dioxus, and the source code is available [here](htt
|
||||||
|
|
||||||
To get started with Dioxus, check out any of the "Getting Started" guides for your platform of choice, or check out the GitHub Repository for more details.
|
To get started with Dioxus, check out any of the "Getting Started" guides for your platform of choice, or check out the GitHub Repository for more details.
|
||||||
|
|
||||||
- [Getting Started with Dioxus](https://dioxuslabs.com/guide)
|
- [Getting Started with Dioxus](https://dioxuslabs.com/guide/en)
|
||||||
- [Getting Started with Web](https://dioxuslabs.com/reference/web)
|
- [Getting Started with Web](https://dioxuslabs.com/reference/web)
|
||||||
- [Getting Started with Desktop](https://dioxuslabs.com/reference/desktop)
|
- [Getting Started with Desktop](https://dioxuslabs.com/reference/desktop)
|
||||||
- [Getting Started with Mobile](https://dioxuslabs.com/reference/mobile)
|
- [Getting Started with Mobile](https://dioxuslabs.com/reference/mobile)
|
||||||
|
@ -163,7 +163,7 @@ Today, to publish a Dioxus app, you don't need NPM/WebPack/Parcel/etc. Dioxus si
|
||||||
|
|
||||||
## Show me more
|
## Show me more
|
||||||
|
|
||||||
Here, we'll dive into some features of Dioxus and why it's so fun to use. The [guide](https://dioxuslabs.com/guide/) serves as a deeper and more comprehensive look at what Dioxus can do.
|
Here, we'll dive into some features of Dioxus and why it's so fun to use. The [guide](https://dioxuslabs.com/guide/en/) serves as a deeper and more comprehensive look at what Dioxus can do.
|
||||||
|
|
||||||
## Building a new project is simple
|
## Building a new project is simple
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
**Dioxus** é um framework e ecossistema para desenvolver interfaces rápidas, escaláveis e robustas com a linguagem de Programação Rust. Este guia irá ajudar você a começar com o Dioxus para Web, Desktop, Móvel e mais.
|
**Dioxus** é um framework e ecossistema para desenvolver interfaces rápidas, escaláveis e robustas com a linguagem de Programação Rust. Este guia irá ajudar você a começar com o Dioxus para Web, Desktop, Móvel e mais.
|
||||||
|
|
||||||
> Este livro é a Referência e Guias Avançados para o framework Dioxus. Para um tutorial em como de fato _usar_ o Dioxus, procure o [guia oficial](https://dioxuslabs.com/guide/).
|
> Este livro é a Referência e Guias Avançados para o framework Dioxus. Para um tutorial em como de fato _usar_ o Dioxus, procure o [guia oficial](https://dioxuslabs.com/guide/en/).
|
||||||
|
|
||||||
## Guias e Referência
|
## Guias e Referência
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
|
One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
|
||||||
|
|
||||||
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
|
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
|
||||||
|
|
||||||
## Getting Set up
|
## Getting Set up
|
||||||
|
|
||||||
|
@ -42,4 +42,4 @@ To configure the webview, menubar, and other important desktop-specific features
|
||||||
|
|
||||||
## Future Steps
|
## Future Steps
|
||||||
|
|
||||||
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't!
|
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide/en) if you already haven't!
|
||||||
|
|
|
@ -18,8 +18,6 @@ cargo run --example hello_world
|
||||||
|
|
||||||
[custom_assets](./custom_assets.rs) - Include images
|
[custom_assets](./custom_assets.rs) - Include images
|
||||||
|
|
||||||
[custom_element](./custom_element.rs) - Render webcomponents
|
|
||||||
|
|
||||||
[custom_html](./custom_html.rs) - Customize wrapper HTML
|
[custom_html](./custom_html.rs) - Customize wrapper HTML
|
||||||
|
|
||||||
[eval](./eval.rs) - Evaluate JS expressions
|
[eval](./eval.rs) - Evaluate JS expressions
|
||||||
|
@ -132,36 +130,6 @@ cargo run --example hello_world
|
||||||
|
|
||||||
[todomvc](./todomvc.rs) - Todo task list example
|
[todomvc](./todomvc.rs) - Todo task list example
|
||||||
|
|
||||||
## Terminal UI
|
|
||||||
|
|
||||||
[tui_all_events](../packages/tui/examples/tui_all_events.rs) - All of the terminal events
|
|
||||||
|
|
||||||
[tui_border](../packages/tui/examples/tui_border.rs) - Different styles of borders and corners
|
|
||||||
|
|
||||||
[tui_buttons](../packages/tui/examples/tui_buttons.rs) - A grid of buttons that work demonstrate the focus system
|
|
||||||
|
|
||||||
[tui_color_test](../packages/tui/examples/tui_color_test.rs) - Grid of colors to demonstrate compatablility with different terminal color rendering modes
|
|
||||||
|
|
||||||
[tui_colorpicker](../packages/tui/examples/tui_colorpicker.rs) - A colorpicker
|
|
||||||
|
|
||||||
[tui_components](../packages/tui/examples/tui_components.rs) - Simple component example
|
|
||||||
|
|
||||||
[tui_flex](../packages/tui/examples/tui_flex.rs) - Flexbox support
|
|
||||||
|
|
||||||
[tui_hover](../packages/tui/examples/tui_hover.rs) - Detect hover and mouse events
|
|
||||||
|
|
||||||
[tui_list](../packages/tui/examples/tui_list.rs) - Renders a list of items
|
|
||||||
|
|
||||||
[tui_margin](../packages/tui/examples/tui_margin.rs) - Margins around flexboxes
|
|
||||||
|
|
||||||
[tui_quadrants](../packages/tui/examples/tui_quadrants.rs) - Four quadrants
|
|
||||||
|
|
||||||
[tui_readme](../packages/tui/examples/tui_readme.rs) - The readme example
|
|
||||||
|
|
||||||
[tui_task](../packages/tui/examples/tui_task.rs) - Background tasks
|
|
||||||
|
|
||||||
[tui_text](../packages/tui/examples/tui_text.rs) - Simple text example
|
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
Missing Features
|
Missing Features
|
||||||
- Fine-grained reactivity
|
- Fine-grained reactivity
|
||||||
|
|
18
examples/button.rs
Normal file
18
examples/button.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
button {
|
||||||
|
onclick: |_| async move {
|
||||||
|
println!("hello, desktop!");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
println!("goodbye, desktop!");
|
||||||
|
},
|
||||||
|
"hello, desktop!"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -82,7 +82,7 @@ fn app(cx: Scope) -> Element {
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let temp = calc_val(val.as_str());
|
let temp = calc_val(val.as_str());
|
||||||
if temp > 0.0 {
|
if temp > 0.0 {
|
||||||
val.set(format!("-{}", temp));
|
val.set(format!("-{temp}"));
|
||||||
} else {
|
} else {
|
||||||
val.set(format!("{}", temp.abs()));
|
val.set(format!("{}", temp.abs()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ fn app(cx: Scope) -> Element {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
println!("{:#?}, ", res);
|
println!("{res:#?}, ");
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
|
28
examples/clock.rs
Normal file
28
examples/clock.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
//! Example: README.md showcase
|
||||||
|
//!
|
||||||
|
//! The example from the README.md.
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::{use_init_signal_rt, use_signal};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
use_init_signal_rt(cx);
|
||||||
|
|
||||||
|
let mut count = use_signal(cx, || 0);
|
||||||
|
|
||||||
|
use_future!(cx, || async move {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
count += 1;
|
||||||
|
println!("current: {count}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div { "High-Five counter: {count}" }
|
||||||
|
})
|
||||||
|
}
|
82
examples/compose.rs
Normal file
82
examples/compose.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
//! This example shows how to create a popup window and send data back to the parent window.
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_desktop::use_window;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let window = use_window(cx);
|
||||||
|
let emails_sent = use_ref(cx, Vec::new);
|
||||||
|
|
||||||
|
let tx = use_coroutine(cx, |mut rx: UnboundedReceiver<String>| {
|
||||||
|
to_owned![emails_sent];
|
||||||
|
async move {
|
||||||
|
while let Some(message) = rx.next().await {
|
||||||
|
emails_sent.write().push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
h1 { "This is your email" }
|
||||||
|
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
let dom = VirtualDom::new_with_props(compose, ComposeProps {
|
||||||
|
app_tx: tx.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
// this returns a weak reference to the other window
|
||||||
|
// Be careful not to keep a strong reference to the other window or it will never be dropped
|
||||||
|
// and the window will never close.
|
||||||
|
window.new_window(dom, Default::default());
|
||||||
|
},
|
||||||
|
"Click to compose a new email"
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
emails_sent.read().iter().map(|message| cx.render(rsx! {
|
||||||
|
li {
|
||||||
|
h3 { "email" }
|
||||||
|
span {"{message}"}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ComposeProps {
|
||||||
|
app_tx: Coroutine<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compose(cx: Scope<ComposeProps>) -> Element {
|
||||||
|
let user_input = use_state(cx, String::new);
|
||||||
|
let window = use_window(cx);
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
h1 { "Compose a new email" }
|
||||||
|
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
cx.props.app_tx.send(user_input.get().clone());
|
||||||
|
window.close();
|
||||||
|
},
|
||||||
|
"Click to send"
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
oninput: move |e| {
|
||||||
|
user_input.set(e.value.clone());
|
||||||
|
},
|
||||||
|
value: "{user_input}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
36
examples/counter.rs
Normal file
36
examples/counter.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
//! Comparison example with leptos' counter example
|
||||||
|
//! https://github.com/leptos-rs/leptos/blob/main/examples/counters/src/lib.rs
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let counters = use_state(cx, || vec![0, 0, 0]);
|
||||||
|
let sum: usize = counters.iter().copied().sum();
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div {
|
||||||
|
button { onclick: move |_| counters.make_mut().push(0), "Add counter" }
|
||||||
|
button { onclick: move |_| { counters.make_mut().pop(); }, "Remove counter" }
|
||||||
|
p { "Total: {sum}" }
|
||||||
|
for (i, counter) in counters.iter().enumerate() {
|
||||||
|
li {
|
||||||
|
button { onclick: move |_| counters.make_mut()[i] -= 1, "-1" }
|
||||||
|
input {
|
||||||
|
value: "{counter}",
|
||||||
|
oninput: move |e| {
|
||||||
|
if let Ok(value) = e.value.parse::<usize>() {
|
||||||
|
counters.make_mut()[i] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button { onclick: move |_| counters.make_mut()[i] += 1, "+1" }
|
||||||
|
button { onclick: move |_| { counters.make_mut().remove(i); }, "x" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,8 @@ fn app(cx: Scope) -> Element {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
"This should show an image:"
|
"This should show an image:"
|
||||||
img { src: "examples/assets/logo.png", }
|
img { src: "examples/assets/logo.png" }
|
||||||
|
img { src: "/Users/jonkelley/Desktop/blitz.png" }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
//! This example shows to wrap a webcomponent / custom element with a component.
|
|
||||||
//!
|
|
||||||
//! Oftentimes, a third party library will provide a webcomponent that you want
|
|
||||||
//! to use in your application. This example shows how to create that custom element
|
|
||||||
//! directly with the raw_element method on NodeFactory.
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
let _ = dom.rebuild();
|
|
||||||
|
|
||||||
let output = dioxus_ssr::render(&dom);
|
|
||||||
|
|
||||||
println!("{}", output);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
let g = cx.component(component, (), "component");
|
|
||||||
let c = cx.make_node(g);
|
|
||||||
cx.render(rsx! {
|
|
||||||
div { c }
|
|
||||||
})
|
|
||||||
|
|
||||||
// let nf = NodeFactory::new(cx);
|
|
||||||
|
|
||||||
// let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump());
|
|
||||||
|
|
||||||
// attrs.push(nf.attr("client-id", format_args!("abc123"), None, false));
|
|
||||||
|
|
||||||
// attrs.push(nf.attr("name", format_args!("bob"), None, false));
|
|
||||||
|
|
||||||
// attrs.push(nf.attr("age", format_args!("47"), None, false));
|
|
||||||
|
|
||||||
// Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn component(cx: Scope) -> Element {
|
|
||||||
todo!()
|
|
||||||
}
|
|
|
@ -52,7 +52,7 @@ struct DogApi {
|
||||||
#[inline_props]
|
#[inline_props]
|
||||||
async fn breed_pic(cx: Scope, breed: String) -> Element {
|
async fn breed_pic(cx: Scope, breed: String) -> Element {
|
||||||
let fut = use_future!(cx, |breed| async move {
|
let fut = use_future!(cx, |breed| async move {
|
||||||
reqwest::get(format!("https://dog.ceo/api/breed/{}/images/random", breed))
|
reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.json::<DogApi>()
|
.json::<DogApi>()
|
||||||
|
|
36
examples/drops.rs
Normal file
36
examples/drops.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let count = if cx.generation() % 2 == 0 { 10 } else { 0 };
|
||||||
|
|
||||||
|
println!("Generation: {}", cx.generation());
|
||||||
|
|
||||||
|
if cx.generation() < 10 {
|
||||||
|
cx.needs_update();
|
||||||
|
}
|
||||||
|
|
||||||
|
render! {
|
||||||
|
(0..count).map(|_| rsx!{
|
||||||
|
drop_child {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_child(cx: Scope) -> Element {
|
||||||
|
cx.use_hook(|| Drops);
|
||||||
|
render! {
|
||||||
|
div{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Drops;
|
||||||
|
|
||||||
|
impl Drop for Drops {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
println!("Dropped!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +1,17 @@
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_desktop::EvalResult;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus_desktop::launch(app);
|
dioxus_desktop::launch(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
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);
|
let script = use_state(cx, String::new);
|
||||||
if future.read().is_some() {
|
let output = use_state(cx, String::new);
|
||||||
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 {
|
||||||
|
p { "Output: {output}" }
|
||||||
input {
|
input {
|
||||||
placeholder: "Enter an expression",
|
placeholder: "Enter an expression",
|
||||||
value: "{script}",
|
value: "{script}",
|
||||||
|
@ -27,8 +19,12 @@ fn app(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let fut = eval(script);
|
to_owned![script, eval, output];
|
||||||
future.set(Some(fut));
|
cx.spawn(async move {
|
||||||
|
if let Ok(res) = eval(script.to_string()).await {
|
||||||
|
output.set(res.to_string());
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
"Execute"
|
"Execute"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ fn main() {
|
||||||
static NAME: Atom<String> = |_| "world".to_string();
|
static NAME: Atom<String> = |_| "world".to_string();
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
|
use_init_atom_root(cx);
|
||||||
let name = use_read(cx, NAME);
|
let name = use_read(cx, NAME);
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
|
|
@ -89,7 +89,7 @@ impl Files {
|
||||||
let paths = match std::fs::read_dir(cur_path) {
|
let paths = match std::fs::read_dir(cur_path) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let err = format!("An error occured: {:?}", err);
|
let err = format!("An error occured: {err:?}");
|
||||||
self.err = Some(err);
|
self.err = Some(err);
|
||||||
self.path_stack.pop();
|
self.path_stack.pop();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -3,7 +3,7 @@ use dioxus_desktop::Config;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cfg = Config::new().with_file_drop_handler(|_w, e| {
|
let cfg = Config::new().with_file_drop_handler(|_w, e| {
|
||||||
println!("{:?}", e);
|
println!("{e:?}");
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ fn app(cx: Scope) -> Element {
|
||||||
// so the value of our input event will be either huey, dewey, louie, or true/false (because of the checkboxe)
|
// so the value of our input event will be either huey, dewey, louie, or true/false (because of the checkboxe)
|
||||||
// be mindful in grouping inputs together, as they will all be handled by the same event handler
|
// be mindful in grouping inputs together, as they will all be handled by the same event handler
|
||||||
oninput: move |evt| {
|
oninput: move |evt| {
|
||||||
println!("{:?}", evt);
|
println!("{evt:?}");
|
||||||
},
|
},
|
||||||
div {
|
div {
|
||||||
input {
|
input {
|
||||||
|
@ -104,7 +104,7 @@ fn app(cx: Scope) -> Element {
|
||||||
name: "pdf",
|
name: "pdf",
|
||||||
r#type: "checkbox",
|
r#type: "checkbox",
|
||||||
oninput: move |evt| {
|
oninput: move |evt| {
|
||||||
println!("{:?}", evt);
|
println!("{evt:?}");
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
|
@ -121,7 +121,7 @@ fn app(cx: Scope) -> Element {
|
||||||
r#type: "{field}",
|
r#type: "{field}",
|
||||||
value: "{value}",
|
value: "{value}",
|
||||||
oninput: move |evt: FormEvent| {
|
oninput: move |evt: FormEvent| {
|
||||||
println!("{:?}", evt);
|
println!("{evt:?}");
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
|
|
27
examples/multiwindow.rs
Normal file
27
examples/multiwindow.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let window = dioxus_desktop::use_window(cx);
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
let dom = VirtualDom::new(popup);
|
||||||
|
window.new_window(dom, Default::default());
|
||||||
|
},
|
||||||
|
"New Window"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popup(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div { "This is a popup!" }
|
||||||
|
})
|
||||||
|
}
|
|
@ -16,18 +16,18 @@ fn app(cx: Scope) -> Element {
|
||||||
onclick: move |_| println!("clicked! top"),
|
onclick: move |_| println!("clicked! top"),
|
||||||
"- div"
|
"- div"
|
||||||
button {
|
button {
|
||||||
onclick: move |_| println!("clicked! bottom propoate"),
|
onclick: move |_| println!("clicked! bottom propagate"),
|
||||||
"Propogate"
|
"Propagate"
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
onclick: move |evt| {
|
onclick: move |evt| {
|
||||||
println!("clicked! bottom no bubbling");
|
println!("clicked! bottom no bubbling");
|
||||||
evt.stop_propogation();
|
evt.stop_propagation();
|
||||||
},
|
},
|
||||||
"Dont propogate"
|
"Dont propagate"
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
"Does not handle clicks - only propogate"
|
"Does not handle clicks - only propagate"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
63
examples/overlay.rs
Normal file
63
examples/overlay.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_desktop::{tao::dpi::PhysicalPosition, use_window, LogicalSize, WindowBuilder};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch_cfg(app, make_config());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let window = use_window(cx);
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
background_color: "red",
|
||||||
|
border: "1px solid black",
|
||||||
|
|
||||||
|
div {
|
||||||
|
width: "100%",
|
||||||
|
height: "10px",
|
||||||
|
background_color: "black",
|
||||||
|
onmousedown: move |_| window.drag(),
|
||||||
|
}
|
||||||
|
|
||||||
|
"This is an overlay!"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_config() -> dioxus_desktop::Config {
|
||||||
|
dioxus_desktop::Config::default()
|
||||||
|
.with_window(make_window())
|
||||||
|
.with_custom_head(
|
||||||
|
r#"
|
||||||
|
<style type="text/css">
|
||||||
|
html, body {
|
||||||
|
height: 100px;
|
||||||
|
margin: 0;
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#main, #bodywrap {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
"#
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_window() -> WindowBuilder {
|
||||||
|
WindowBuilder::new()
|
||||||
|
.with_transparent(true)
|
||||||
|
.with_decorations(false)
|
||||||
|
.with_resizable(false)
|
||||||
|
.with_always_on_top(true)
|
||||||
|
.with_position(PhysicalPosition::new(0, 0))
|
||||||
|
.with_max_inner_size(LogicalSize::new(100000, 50))
|
||||||
|
}
|
|
@ -83,7 +83,7 @@ fn app(cx: Scope) -> Element {
|
||||||
div {
|
div {
|
||||||
class: {
|
class: {
|
||||||
const WORD: &str = "expressions";
|
const WORD: &str = "expressions";
|
||||||
format_args!("Arguments can be passed in through curly braces for complex {}", WORD)
|
format_args!("Arguments can be passed in through curly braces for complex {WORD}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,7 +214,7 @@ fn app(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_dollars(dollars: u32, cents: u32) -> String {
|
fn format_dollars(dollars: u32, cents: u32) -> String {
|
||||||
format!("${}.{:02}", dollars, cents)
|
format!("${dollars}.{cents:02}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn helper<'a>(cx: &'a ScopeState, text: &str) -> Element<'a> {
|
fn helper<'a>(cx: &'a ScopeState, text: &str) -> Element<'a> {
|
||||||
|
|
17
examples/shortcut.rs
Normal file
17
examples/shortcut.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_desktop::tao::keyboard::ModifiersState;
|
||||||
|
use dioxus_desktop::use_global_shortcut;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let toggled = use_state(cx, || false);
|
||||||
|
use_global_shortcut(cx, KeyCode::S, ModifiersState::CONTROL, {
|
||||||
|
to_owned![toggled];
|
||||||
|
move || toggled.modify(|t| !*t)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx!("toggle: {toggled.get()}"))
|
||||||
|
}
|
30
examples/signals.rs
Normal file
30
examples/signals.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::{use_init_signal_rt, use_signal};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
use_init_signal_rt(cx);
|
||||||
|
|
||||||
|
let mut count = use_signal(cx, || 0);
|
||||||
|
|
||||||
|
use_future!(cx, || async move {
|
||||||
|
loop {
|
||||||
|
count += 1;
|
||||||
|
tokio::time::sleep(Duration::from_millis(400)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
h1 { "High-Five counter: {count}" }
|
||||||
|
button { onclick: move |_| count += 1, "Up high!" }
|
||||||
|
button { onclick: move |_| count -= 1, "Down low!" }
|
||||||
|
|
||||||
|
if count() > 5 {
|
||||||
|
rsx!{ h2 { "High five!" } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ fn main() {
|
||||||
let mut file = String::new();
|
let mut file = String::new();
|
||||||
let mut renderer = dioxus_ssr::Renderer::default();
|
let mut renderer = dioxus_ssr::Renderer::default();
|
||||||
renderer.render_to(&mut file, &vdom).unwrap();
|
renderer.render_to(&mut file, &vdom).unwrap();
|
||||||
println!("{}", file);
|
println!("{file}");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
|
|
|
@ -9,7 +9,7 @@ fn main() {
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let model = use_state(cx, || String::from("asd"));
|
let model = use_state(cx, || String::from("asd"));
|
||||||
|
|
||||||
println!("{}", model);
|
println!("{model}");
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
textarea {
|
textarea {
|
||||||
|
|
|
@ -7,7 +7,7 @@ fn main() {
|
||||||
dioxus_desktop::launch(app);
|
dioxus_desktop::launch(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum FilterState {
|
pub enum FilterState {
|
||||||
All,
|
All,
|
||||||
Active,
|
Active,
|
||||||
|
@ -39,17 +39,25 @@ pub fn app(cx: Scope<()>) -> Element {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
filtered_todos.sort_unstable();
|
filtered_todos.sort_unstable();
|
||||||
|
|
||||||
let show_clear_completed = todos.values().any(|todo| todo.checked);
|
let active_todo_count = todos.values().filter(|item| !item.checked).count();
|
||||||
let items_left = filtered_todos.len();
|
let active_todo_text = match active_todo_count {
|
||||||
let item_text = match items_left {
|
|
||||||
1 => "item",
|
1 => "item",
|
||||||
_ => "items",
|
_ => "items",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let show_clear_completed = todos.values().any(|todo| todo.checked);
|
||||||
|
|
||||||
|
let selected = |state| {
|
||||||
|
if *filter == state {
|
||||||
|
"selected"
|
||||||
|
} else {
|
||||||
|
"false"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
section { class: "todoapp",
|
section { class: "todoapp",
|
||||||
style { include_str!("./assets/todomvc.css") }
|
style { include_str!("./assets/todomvc.css") }
|
||||||
div {
|
|
||||||
header { class: "header",
|
header { class: "header",
|
||||||
h1 {"todos"}
|
h1 {"todos"}
|
||||||
input {
|
input {
|
||||||
|
@ -76,19 +84,54 @@ pub fn app(cx: Scope<()>) -> Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
section {
|
||||||
|
class: "main",
|
||||||
|
if !todos.is_empty() {
|
||||||
|
rsx! {
|
||||||
|
input {
|
||||||
|
id: "toggle-all",
|
||||||
|
class: "toggle-all",
|
||||||
|
r#type: "checkbox",
|
||||||
|
onchange: move |_| {
|
||||||
|
let check = active_todo_count != 0;
|
||||||
|
for (_, item) in todos.make_mut().iter_mut() {
|
||||||
|
item.checked = check;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checked: if active_todo_count == 0 { "true" } else { "false" },
|
||||||
|
}
|
||||||
|
label { r#for: "toggle-all" }
|
||||||
|
}
|
||||||
|
}
|
||||||
ul { class: "todo-list",
|
ul { class: "todo-list",
|
||||||
filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id, todos: todos }))
|
filtered_todos.iter().map(|id| rsx!(TodoEntry {
|
||||||
|
key: "{id}",
|
||||||
|
id: *id,
|
||||||
|
todos: todos,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
(!todos.is_empty()).then(|| rsx!(
|
(!todos.is_empty()).then(|| rsx!(
|
||||||
footer { class: "footer",
|
footer { class: "footer",
|
||||||
span { class: "todo-count",
|
span { class: "todo-count",
|
||||||
strong {"{items_left} "}
|
strong {"{active_todo_count} "}
|
||||||
span {"{item_text} left"}
|
span {"{active_todo_text} left"}
|
||||||
}
|
}
|
||||||
ul { class: "filters",
|
ul { class: "filters",
|
||||||
li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }}
|
for (state, state_text, url) in [
|
||||||
li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }}
|
(FilterState::All, "All", "#/"),
|
||||||
li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }}
|
(FilterState::Active, "Active", "#/active"),
|
||||||
|
(FilterState::Completed, "Completed", "#/completed"),
|
||||||
|
] {
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
href: url,
|
||||||
|
class: selected(state),
|
||||||
|
onclick: move |_| filter.set(state),
|
||||||
|
prevent_default: "onclick",
|
||||||
|
state_text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
show_clear_completed.then(|| rsx!(
|
show_clear_completed.then(|| rsx!(
|
||||||
button {
|
button {
|
||||||
|
@ -136,13 +179,17 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
||||||
cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap();
|
cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
r#for: "cbg-{todo.id}",
|
r#for: "cbg-{todo.id}",
|
||||||
onclick: move |_| is_editing.set(true),
|
ondblclick: move |_| is_editing.set(true),
|
||||||
prevent_default: "onclick",
|
prevent_default: "onclick",
|
||||||
"{todo.contents}"
|
"{todo.contents}"
|
||||||
}
|
}
|
||||||
|
button {
|
||||||
|
class: "destroy",
|
||||||
|
onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); },
|
||||||
|
prevent_default: "onclick",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is_editing.then(|| rsx!{
|
is_editing.then(|| rsx!{
|
||||||
input {
|
input {
|
||||||
|
|
|
@ -35,13 +35,13 @@ fn app(cx: Scope) -> Element {
|
||||||
nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" }
|
nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" }
|
||||||
button {
|
button {
|
||||||
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
|
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
|
||||||
onmousedown: |evt| evt.stop_propogation(),
|
onmousedown: |evt| evt.stop_propagation(),
|
||||||
onclick: move |_| window.set_minimized(true),
|
onclick: move |_| window.set_minimized(true),
|
||||||
"Minimize"
|
"Minimize"
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
|
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
|
||||||
onmousedown: |evt| evt.stop_propogation(),
|
onmousedown: |evt| evt.stop_propagation(),
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
|
|
||||||
window.set_fullscreen(!**fullscreen);
|
window.set_fullscreen(!**fullscreen);
|
||||||
|
@ -52,7 +52,7 @@ fn app(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
|
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
|
||||||
onmousedown: |evt| evt.stop_propogation(),
|
onmousedown: |evt| evt.stop_propagation(),
|
||||||
onclick: move |_| window.close(),
|
onclick: move |_| window.close(),
|
||||||
"Close"
|
"Close"
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ fn app(cx: Scope) -> Element {
|
||||||
div {
|
div {
|
||||||
button {
|
button {
|
||||||
class: "inline-flex items-center text-white bg-green-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
|
class: "inline-flex items-center text-white bg-green-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
|
||||||
onmousedown: |evt| evt.stop_propogation(),
|
onmousedown: |evt| evt.stop_propagation(),
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
window.set_always_on_top(!always_on_top);
|
window.set_always_on_top(!always_on_top);
|
||||||
always_on_top.set(!always_on_top);
|
always_on_top.set(!always_on_top);
|
||||||
|
@ -77,7 +77,7 @@ fn app(cx: Scope) -> Element {
|
||||||
div {
|
div {
|
||||||
button {
|
button {
|
||||||
class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
|
class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
|
||||||
onmousedown: |evt| evt.stop_propogation(),
|
onmousedown: |evt| evt.stop_propagation(),
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
window.set_decorations(!decorations);
|
window.set_decorations(!decorations);
|
||||||
decorations.set(!decorations);
|
decorations.set(!decorations);
|
||||||
|
@ -88,7 +88,7 @@ fn app(cx: Scope) -> Element {
|
||||||
div {
|
div {
|
||||||
button {
|
button {
|
||||||
class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
|
class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
|
||||||
onmousedown: |evt| evt.stop_propogation(),
|
onmousedown: |evt| evt.stop_propagation(),
|
||||||
onclick: move |_| window.set_title("Dioxus Application"),
|
onclick: move |_| window.set_title("Dioxus Application"),
|
||||||
"Change Title"
|
"Change Title"
|
||||||
}
|
}
|
||||||
|
|
42
examples/window_focus.rs
Normal file
42
examples/window_focus.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_desktop::tao::event::WindowEvent;
|
||||||
|
use dioxus_desktop::use_wry_event_handler;
|
||||||
|
use dioxus_desktop::wry::application::event::Event as WryEvent;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let focused = use_state(cx, || false);
|
||||||
|
|
||||||
|
use_wry_event_handler(cx, {
|
||||||
|
to_owned![focused];
|
||||||
|
move |event, _| {
|
||||||
|
if let WryEvent::WindowEvent {
|
||||||
|
event: WindowEvent::Focused(new_focused),
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
focused.set(*new_focused);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flex_direction: "column",
|
||||||
|
align_items: "center",
|
||||||
|
{
|
||||||
|
if *focused.get() {
|
||||||
|
"This window is focused!"
|
||||||
|
} else {
|
||||||
|
"This window is not focused!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -7,16 +7,17 @@ fn main() {
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let window = use_window(cx);
|
let window = use_window(cx);
|
||||||
|
|
||||||
let level = use_state(cx, || 1.0);
|
let level = use_state(cx, || 1.0);
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
input {
|
input {
|
||||||
r#type: "number",
|
r#type: "number",
|
||||||
value: "{level}",
|
value: "{level}",
|
||||||
oninput: |e| {
|
oninput: |e| {
|
||||||
let num = e.value.parse::<f64>().unwrap_or(1.0);
|
if let Ok(new_zoom) = e.value.parse::<f64>() {
|
||||||
level.set(num);
|
level.set(new_zoom);
|
||||||
window.set_zoom_level(num);
|
window.webview.zoom(new_zoom);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/DioxusLabs/example-projects"> 代码示例 </a>
|
<a href="https://github.com/DioxusLabs/example-projects"> 代码示例 </a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://dioxuslabs.com/guide"> 开发指南 </a>
|
<a href="https://dioxuslabs.com/guide/en"> 开发指南 </a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/DioxusLabs/dioxus/blob/master/README.md"> English </a>
|
<a href="https://github.com/DioxusLabs/dioxus/blob/master/README.md"> English </a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
|
|
|
@ -1,494 +0,0 @@
|
||||||
# Solved problems while building Dioxus
|
|
||||||
|
|
||||||
focuses:
|
|
||||||
|
|
||||||
- ergonomics
|
|
||||||
- render agnostic
|
|
||||||
- remote coupling
|
|
||||||
- memory efficient
|
|
||||||
- concurrent
|
|
||||||
- global context
|
|
||||||
- scheduled updates
|
|
||||||
-
|
|
||||||
|
|
||||||
## FC Macro for more elegant components
|
|
||||||
|
|
||||||
Originally the syntax of the FC macro was meant to look like:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[fc]
|
|
||||||
fn example(cx: &Context<{ name: String }>) -> DomTree {
|
|
||||||
html! { <div> "Hello, {name}!" </div> }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`Context` was originally meant to be more obviously parameterized around a struct definition. However, while this works with rustc, this does not work well with Rust Analyzer. Instead, the new form was chosen which works with Rust Analyzer and happens to be more ergonomic.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[fc]
|
|
||||||
fn example(cx: &Context, name: String) -> DomTree {
|
|
||||||
html! { <div> "Hello, {name}!" </div> }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Anonymous Components
|
|
||||||
|
|
||||||
In Yew, the function_component macro turns a struct into a Trait `impl` with associated type `props`. Like so:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Properties)]
|
|
||||||
struct Props {
|
|
||||||
// some props
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SomeComponent;
|
|
||||||
impl FunctionProvider for SomeComponent {
|
|
||||||
type TProps = Props;
|
|
||||||
|
|
||||||
fn run(&mut self, props: &Props) -> Html {
|
|
||||||
// user's functional component goes here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SomeComponent = FunctionComponent<function_name>;
|
|
||||||
```
|
|
||||||
|
|
||||||
By default, the underlying component is defined as a "functional" implementation of the `Component` trait with all the lifecycle methods. In Dioxus, we don't allow components as structs, and instead take a "hooks-only" approach. However, we still need cx. To get these without dealing with traits, we just assume functional components are modules. This lets the macros assume an FC is a module, and `FC::Props` is its props and `FC::component` is the component. Yew's method does a similar thing, but with associated types on traits.
|
|
||||||
|
|
||||||
Perhaps one day we might use traits instead.
|
|
||||||
|
|
||||||
The FC macro needs to work like this to generate a final module signature:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// "Example" can be used directly
|
|
||||||
// The "associated types" are just children of the module
|
|
||||||
// That way, files can just be components (yay, no naming craziness)
|
|
||||||
mod Example {
|
|
||||||
// Associated metadata important for liveview
|
|
||||||
static NAME: &'static str = "Example";
|
|
||||||
|
|
||||||
struct Props {
|
|
||||||
name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
fn component(cx: &Context<Props>) -> DomTree {
|
|
||||||
html! { <div> "Hello, {name}!" </div> }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// or, Example.rs
|
|
||||||
|
|
||||||
static NAME: &'static str = "Example";
|
|
||||||
|
|
||||||
struct Props {
|
|
||||||
name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
fn component(cx: &Context<Props>) -> DomTree {
|
|
||||||
html! { <div> "Hello, {name}!" </div> }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
These definitions might be ugly, but the fc macro cleans it all up. The fc macro also allows some configuration
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[fc]
|
|
||||||
fn example(cx: &Context, name: String) -> DomTree {
|
|
||||||
html! { <div> "Hello, {name}!" </div> }
|
|
||||||
}
|
|
||||||
|
|
||||||
// .. expands to
|
|
||||||
|
|
||||||
mod Example {
|
|
||||||
use super::*;
|
|
||||||
static NAME: &'static str = "Example";
|
|
||||||
struct Props {
|
|
||||||
name: String
|
|
||||||
}
|
|
||||||
fn component(cx: &Context<Props>) -> DomTree {
|
|
||||||
html! { <div> "Hello, {name}!" </div> }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Live Components
|
|
||||||
|
|
||||||
Live components are a very important part of the Dioxus ecosystem. However, the goal with live components was to constrain their implementation purely to APIs available through Context (concurrency, context, subscription).
|
|
||||||
|
|
||||||
From a certain perspective, live components are simply server-side-rendered components that update when their props change. Here's more-or-less how live components work:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[fc]
|
|
||||||
static LiveFc: FC = |cx, refresh_handler: impl FnOnce| {
|
|
||||||
// Grab the "live context"
|
|
||||||
let live_context = cx.use_context::<LiveContext>();
|
|
||||||
|
|
||||||
// Ensure this component is registered as "live"
|
|
||||||
live_context.register_scope();
|
|
||||||
|
|
||||||
// send our props to the live context and get back a future
|
|
||||||
let vnodes = live_context.request_update(cx);
|
|
||||||
|
|
||||||
// Suspend the rendering of this component until the vnodes are finished arriving
|
|
||||||
// Render them once available
|
|
||||||
cx.suspend(async move {
|
|
||||||
let output = vnodes.await;
|
|
||||||
|
|
||||||
// inject any listener handles (ie button clicks, views, etc) to the parsed nodes
|
|
||||||
output[1].add_listener("onclick", refresh_handler);
|
|
||||||
|
|
||||||
// Return these nodes
|
|
||||||
// Nodes skip diffing and go straight to rendering
|
|
||||||
output
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Notice that LiveComponent receivers (the client-side interpretation of a LiveComponent) are simply suspended components waiting for updates from the LiveContext (the context that wraps the app to make it "live").
|
|
||||||
|
|
||||||
## Allocation Strategy (ie incorporating Dodrio research)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
The `VNodeTree` type is a very special type that allows VNodes to be created using a pluggable allocator. The html! macro creates something that looks like:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub static Example: Component = |cx| {
|
|
||||||
html! { <div> "blah" </div> }
|
|
||||||
};
|
|
||||||
|
|
||||||
// expands to...
|
|
||||||
|
|
||||||
pub static Example: Component = |cx| {
|
|
||||||
// This function converts a Fn(allocator) -> DomTree closure to a VNode struct that will later be evaluated.
|
|
||||||
html_macro_to_vnodetree(move |allocator| {
|
|
||||||
let mut node0 = allocator.alloc(VElement::div);
|
|
||||||
let node1 = allocator.alloc_text("blah");
|
|
||||||
node0.children = [node1];
|
|
||||||
node0
|
|
||||||
})
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
At runtime, the new closure is created that captures references to `cx`. Therefore, this closure can only be evaluated while `cx` is borrowed and in scope. However, this closure can only be evaluated with an `allocator`. Currently, the global and Bumpalo allocators are available, though in the future we will add support for creating a VDom with any allocator or arena system (IE Jemalloc, wee-alloc, etc). The intention here is to allow arena allocation of VNodes (no need to box nested VNodes). Between diffing phases, the arena will be overwritten as old nodes are replaced with new nodes. This saves allocation time and enables bump allocators.
|
|
||||||
|
|
||||||
## Context and lifetimes
|
|
||||||
|
|
||||||
We want components to be able to fearlessly "use_context" for use in state management solutions.
|
|
||||||
|
|
||||||
However, we cannot provide these guarantees without compromising the references. If a context mutates, it cannot lend out references.
|
|
||||||
|
|
||||||
Functionally, this can be solved with UnsafeCell and runtime dynamics. Essentially, if a context mutates, then any affected components would need to be updated, even if they themselves aren't updated. Otherwise, a reference would be pointing at data that could have potentially been moved.
|
|
||||||
|
|
||||||
To do this safely is a pretty big challenge. We need to provide a method of sharing data that is safe, ergonomic, and that fits the abstraction model.
|
|
||||||
|
|
||||||
Enter, the "ContextGuard".
|
|
||||||
|
|
||||||
The "ContextGuard" is very similar to a Ref/RefMut from the RefCell implementation, but one that derefs into actual underlying value.
|
|
||||||
|
|
||||||
However, derefs of the ContextGuard are a bit more sophisticated than the Ref model.
|
|
||||||
|
|
||||||
For RefCell, when a Ref is taken, the RefCell is now "locked." This means you cannot take another `borrow_mut` instance while the Ref is still active. For our purposes, our modification phase is very limited, so we can make more assumptions about what is safe.
|
|
||||||
|
|
||||||
1. We can pass out ContextGuards from any use of use_context. These don't actually lock the value until used.
|
|
||||||
2. The ContextGuards only lock the data while the component is executing and when a callback is running.
|
|
||||||
3. Modifications of the underlying context occur after a component is rendered and after the event has been run.
|
|
||||||
|
|
||||||
With the knowledge that usage of ContextGuard can only be achieved in a component context and the above assumptions, we can design a guard that prevents any poor usage but also is ergonomic.
|
|
||||||
|
|
||||||
As such, the design of the ContextGuard must:
|
|
||||||
|
|
||||||
- be /copy/ for easy moves into closures
|
|
||||||
- never point to invalid data (no dereferencing of raw pointers after movable data has been changed (IE a vec has been resized))
|
|
||||||
- not allow references of underlying data to leak into closures
|
|
||||||
|
|
||||||
To solve this, we can be clever with lifetimes to ensure that any data is protected, but context is still ergonomic.
|
|
||||||
|
|
||||||
1. As such, deref context guard returns an element with a lifetime bound to the borrow of the guard.
|
|
||||||
2. Because we cannot return locally borrowed data AND we consume context, this borrow cannot be moved into a closure.
|
|
||||||
3. ContextGuard is _copy_ so the guard itself can be moved into closures
|
|
||||||
4. ContextGuard derefs with its unique lifetime _inside_ closures
|
|
||||||
5. Derefing a ContextGuard evaluates the underlying selector to ensure safe temporary access to underlying data
|
|
||||||
|
|
||||||
```rust
|
|
||||||
struct ExampleContext {
|
|
||||||
// unpinnable objects with dynamic sizing
|
|
||||||
items: Vec<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
fn Example<'src>(cx: Scope<'src, ()>) -> DomTree<'src> {
|
|
||||||
let val: &'b ContextGuard<ExampleContext> = (&'b cx).use_context(|context: &'other ExampleContext| {
|
|
||||||
// always select the last element
|
|
||||||
context.items.last()
|
|
||||||
});
|
|
||||||
|
|
||||||
let handler1 = move |_| println!("Value is {}", val); // deref coercion performed here for printing
|
|
||||||
let handler2 = move |_| println!("Value is {}", val); // deref coercion performed here for printing
|
|
||||||
|
|
||||||
cx.render(html! {
|
|
||||||
<div>
|
|
||||||
<button onclick={handler1}> "Echo value with h1" </button>
|
|
||||||
<button onclick={handler2}> "Echo value with h2" </button>
|
|
||||||
<div>
|
|
||||||
<p> "Value is: {val}" </p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
A few notes:
|
|
||||||
|
|
||||||
- this does _not_ protect you from data races!!!
|
|
||||||
- this does _not_ force rendering of components
|
|
||||||
- this _does_ protect you from invalid + UB use of data
|
|
||||||
- this approach leaves lots of room for fancy state management libraries
|
|
||||||
- this approach is fairly quick, especially if borrows can be cached during usage phases
|
|
||||||
|
|
||||||
## Concurrency
|
|
||||||
|
|
||||||
For Dioxus, concurrency is built directly into the VirtualDOM lifecycle and event system. Suspended components prove "no changes" while diffing, and will cause a lifecycle update later. This is considered a "trigger" and will cause targeted diffs and re-renders. Renderers will need to await the Dioxus suspense queue if they want to process these updates. This will typically involve joining the suspense queue and event listeners together like:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// wait for an even either from the suspense queue or our own custom listener system
|
|
||||||
let (left, right) = join!(vdom.suspense_queue, self.custom_event_listener);
|
|
||||||
```
|
|
||||||
|
|
||||||
LiveView is built on this model, and updates from the WebSocket connection to the host server are treated as external updates. This means any renderer can feed targeted EditLists (the underlying message of this event) directly into the VirtualDOM.
|
|
||||||
|
|
||||||
## Execution Model
|
|
||||||
|
|
||||||
<!-- todo -->
|
|
||||||
|
|
||||||
## Diffing
|
|
||||||
|
|
||||||
Diffing is an interesting story. Since we don't re-render the entire DOM, we need a way to patch up the DOM without visiting every component. To get this working, we need to think in cycles, queues, and stacks. Most of the original logic is pulled from Dodrio as Dioxus and Dodrio share much of the same DNA.
|
|
||||||
|
|
||||||
When an event is triggered, we find the callback that installed the listener and run it. We then record all components affected by the running of the "subscription" primitive. In practice, many hooks will initiate a subscription, so it is likely that many components throughout the entire tree will need to be re-rendered. For each component, we attach its index and the type of update it needs.
|
|
||||||
|
|
||||||
In practice, removals trump prop updates which trump subscription updates. Therefore, we only process updates where props are directly changed first, as this will likely flow into child components.
|
|
||||||
|
|
||||||
Roughly, the flow looks like:
|
|
||||||
|
|
||||||
- Process the initiating event
|
|
||||||
- Mark components affected by the subscription API (the only way of causing forward updates)
|
|
||||||
- Descend from the root into children, ignoring those not affected by the subscription API. (walking the tree until we hit the first affected component, or choosing the highest component)
|
|
||||||
- Run this component and then immediately diff its output, marking any children that also need to be updated and putting them into the immediate queue
|
|
||||||
- Mark this component as already-ran and remove it from the need_to_diff list, instead moving it into the "already diffed list"
|
|
||||||
- Run the marked children until the immediate queue is empty
|
|
||||||
|
|
||||||
```rust
|
|
||||||
struct DiffMachine {
|
|
||||||
immediate_queue: Vec<Index>,
|
|
||||||
diffed: HashSet<Index>,
|
|
||||||
need_to_diff: HashSet<Index>
|
|
||||||
marked_for_removal: Vec<Index>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
On the actual diffing level, we're using the diffing algorithm pulled from Dodrio, but plan to move to a dedicated crate that implements Meyers/Patience for us. During the diffing phase, we track our current position using a "Traversal" which implements the "MoveTo". When "MoveTo" is combined with "Edit", it is possible for renderers to fully interpret a series of Moves and Edits together to update their internal node structures for rendering.
|
|
||||||
|
|
||||||
## Patch Stream
|
|
||||||
|
|
||||||
One of the most important parts of Dioxus is the ability to stream patches from server to client. However, this inherently has challenges where raw VNodes attach listeners to themselves, and are therefore not serializable.
|
|
||||||
|
|
||||||
### How do properties work?
|
|
||||||
|
|
||||||
How should properties passing work? Should we directly call the child? Should we box the props? Should we replace the pops inside the box?
|
|
||||||
|
|
||||||
Here's (generally) my head is at:
|
|
||||||
|
|
||||||
Components need to store their props on them if they want to be updated remotely. These props _can_ be updated after the fact.
|
|
||||||
|
|
||||||
Perf concerns:
|
|
||||||
unnecessary function runs - list-y components - hook calls? - making vnodes?
|
|
||||||
|
|
||||||
Does any of this matter?
|
|
||||||
Should we just run any component we see, immediately and imperatively? That will cause checks throughout the whole tree, no matter where the update occurred
|
|
||||||
|
|
||||||
https://calendar.perfplanet.com/2013/diff/
|
|
||||||
|
|
||||||
Here's how react does it:
|
|
||||||
|
|
||||||
Any "dirty" node causes an entire subtree render. Calling "setState" at the very top will cascade all the way down. This is particularly bad for this component design:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
static APP: Component = |cx| {
|
|
||||||
let title = use_context(Title);
|
|
||||||
cx.render(html!{
|
|
||||||
<div>
|
|
||||||
<h1> "{title}"</h1>
|
|
||||||
<HeavyList /> // VComponent::new(|| (FC, PropsForFc)) -> needs a context to immediately update the component's props imperatively? store the props in a box on bump? store the props on the child?
|
|
||||||
// if props didnt change, then let the refernece stay invalid?.... no, cant do that, bump gets reset
|
|
||||||
// immediately update props on the child component if it can be found? -> interesting, feels wrong, but faster, at the very least.
|
|
||||||
// can box on bump for the time being (fast enough), and then move it over? during the comparison phase? props only need to matter
|
|
||||||
// cant downcast (can with transmute, but yikes)
|
|
||||||
// how does chain borrowing work? a -> b -> c -> d
|
|
||||||
// if b gets marked as dirty, then c and d are invalidated (semantically, UB, but not *bad* UB, just data races)
|
|
||||||
// make props static? -> easy to move, gross to use
|
|
||||||
//
|
|
||||||
// treat like a context selector?
|
|
||||||
// use_props::<P>(2)
|
|
||||||
// child_props: Map<Scope, Box<dyn Props>>
|
|
||||||
// vs children: BTreeSet<Scope> -> to get nth
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
};
|
|
||||||
static HEAVY_LIST: Component = |cx| {
|
|
||||||
cx.render({
|
|
||||||
{0.100.map(i => <BigElement >)}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
An update to the use_context subscription will mark the node as dirty. The node then is forced to re-analyze HeavyList, even though HeavyList did not change. We should automatically implement this suppression knowing that props are immutable and can be partialeq.
|
|
||||||
|
|
||||||
## FC Layout
|
|
||||||
|
|
||||||
The FC layout was altered to make life easier for us inside the VirtualDom. The "view" function returns an unbounded VNode object. Calling the "view" function is unsafe under the hood, but prevents lifetimes from leaking out of the function call. Plus, it's easier to write. Because there are no lifetimes on the output (occur purely under the hood), we can escape needing to annotate them.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn component(cx: Scope<Props>) -> DomTree {
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The VNode object purely represents a viewable "key". It also forces components to use the "view" function as there is no other way to generate the VNode object. Because the VNode is a required type of FC, we can guarantee the same usage and flow patterns for all components.
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
Events are finally in! To do events properly, we are abstracting over the event source with synthetic events. This forces 3rd party renderers to create the appropriate cross-platform event
|
|
||||||
|
|
||||||
## Optional Props on Components
|
|
||||||
|
|
||||||
A major goal here is ergonomics. Any field that is Option<T> should default to none.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
|
|
||||||
rsx! {
|
|
||||||
Example { /* args go here */ a: 10, b: 20 }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Properties)]
|
|
||||||
struct Props {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static Component: Component<Props> = |cx| {
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[fc]
|
|
||||||
static Component: FC = |cx, name: &str| {
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Noderefs
|
|
||||||
|
|
||||||
How do we resolve noderefs in a world of patches? Patches _must_ be serializable, so if we do something like `Option<&RefCell<Slot>>`, then that must serialize as _something_ to indicate to a remote host that access to the node itself is desired. Our `Slot` type will need to be somewhat abstract.
|
|
||||||
|
|
||||||
If we add a new patch type called "BindRef" we could do something like:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
enum Patch {
|
|
||||||
//...
|
|
||||||
BindAsRef { raw_node: &RefCell<Option<Slot>> }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let node_ref = use_node_ref(cx);
|
|
||||||
use_effect(cx, || {
|
|
||||||
|
|
||||||
}, []);
|
|
||||||
div { ref: node_ref,
|
|
||||||
"hello me"
|
|
||||||
h3 {"yo dom"}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
refs only work when you're native to the platform. it doesn't make sense to gain a ref when you're not native.
|
|
||||||
|
|
||||||
## In-sync or separate?
|
|
||||||
|
|
||||||
React makes refs - and protection against dom manipulation - work by modifying the real dom while diffing the virtual dom. This lets it bind real dom elements to the virtual dom elements. Dioxus currently does not do this, instead creating a list of changes for an interpreter to apply once diffing has completed.
|
|
||||||
|
|
||||||
This behavior fit dodrio well as all dom manipulations would occur batched. The original intention for this approach was to make it faster to read out of Wasm and into JS. Dodrio is essentially performing the Wasm job that Wasm<->JS for strings does. In theory, this particular optimization is not necessary.
|
|
||||||
|
|
||||||
https://github.com/fitzgen/dodrio/issues/77
|
|
||||||
|
|
||||||
This issue/pr on the dodrio repository points to a future where elements are held on to by the virtualdom.
|
|
||||||
|
|
||||||
Can we solve events, refs, and protection against 3rd party dom mutation all in one shot?
|
|
||||||
|
|
||||||
I think we can....
|
|
||||||
|
|
||||||
every node gets a globally unique ID
|
|
||||||
|
|
||||||
abstract the real dom
|
|
||||||
|
|
||||||
```rust
|
|
||||||
|
|
||||||
struct VirtualDom<Dom: RealDom>
|
|
||||||
|
|
||||||
trait RealDom {
|
|
||||||
type Node: RealNode;
|
|
||||||
fn get_node(&self, id: u32) -> &Self::Node;
|
|
||||||
fn get_node_mut(&mut self, id: u32) -> &mut Self::Node;
|
|
||||||
fn replace_node();
|
|
||||||
fn create_text_node();
|
|
||||||
fn create_element();
|
|
||||||
fn create_element_ns();
|
|
||||||
}
|
|
||||||
|
|
||||||
trait RealNode {
|
|
||||||
fn add_listener(&mut self, event: &str);
|
|
||||||
fn set_inner_text(&mut self, text: &str);
|
|
||||||
fn set_attr(&mut self, name, value);
|
|
||||||
fn set_class(&mut self);
|
|
||||||
fn remove_attr(&mut self);
|
|
||||||
// We can't have a generic type in trait objects, so instead we provide the inner as Any
|
|
||||||
fn raw_node_as_any_mut(&mut self) -> &mut dyn Any;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VirtualDom<Dom: RealDom> {
|
|
||||||
fn diff<Dom: RealDom>() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enum VNode<'bump, 'realdom, RealDom> {
|
|
||||||
VElement {
|
|
||||||
real: &RealDom::Node
|
|
||||||
}
|
|
||||||
VText {
|
|
||||||
real: &RealDom::Node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let mut real_dom = websys::Document();
|
|
||||||
let virtual_dom = Dioxus::VirtualDom::new();
|
|
||||||
|
|
||||||
virtual_dom.rebuild(&mut real_dom);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let event = switch! {
|
|
||||||
real_dom.events.await => event,
|
|
||||||
virtual_dom.inner_events.await => event
|
|
||||||
};
|
|
||||||
|
|
||||||
virtual_dom.apply_event(&mut real_dom, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
|
@ -1,18 +1,23 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dioxus-autofmt"
|
name = "dioxus-autofmt"
|
||||||
version = "0.0.0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
authors = ["Jonathan Kelley"]
|
||||||
|
description = "Autofomatter for Dioxus RSX"
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||||
|
homepage = "https://dioxuslabs.com"
|
||||||
|
documentation = "https://dioxuslabs.com"
|
||||||
|
keywords = ["dom", "ui", "gui", "react"]
|
||||||
# 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-rsx = { path = "../rsx" }
|
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
|
||||||
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
||||||
triple_accel = "0.4.0"
|
|
||||||
serde = { version = "1.0.136", features = ["derive"] }
|
serde = { version = "1.0.136", features = ["derive"] }
|
||||||
prettyplease = { git = "https://github.com/DioxusLabs/prettyplease-macro-fmt.git", features = [
|
prettyplease = { package = "prettier-please", version = "0.1.16", features = [
|
||||||
"verbatim",
|
"verbatim",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
|
|
@ -1,88 +1,49 @@
|
||||||
# This crate autofmts blocks of rsx!
|
# dioxus-autofmt
|
||||||
|
|
||||||
This crate formats rsx! by parsing call bodies and pretty-printing them back out.
|
|
||||||
|
|
||||||
|
|
||||||
|
[![Crates.io][crates-badge]][crates-url]
|
||||||
|
[![MIT licensed][mit-badge]][mit-url]
|
||||||
|
[![Build Status][actions-badge]][actions-url]
|
||||||
|
[![Discord chat][discord-badge]][discord-url]
|
||||||
|
|
||||||
# Todo:
|
[crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg
|
||||||
Sorted roughly in order of what's possible
|
[crates-url]: https://crates.io/crates/dioxus-autofmt
|
||||||
|
|
||||||
- [x] Oneline rsx! calls - blocker because this wrecks formatting
|
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||||
- [ ] Nested RSX calls (important) - unnecessary but desirable
|
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||||
- [ ] RSX edits overstepping each other
|
|
||||||
- [ ] Collapse components and elements under syntax -
|
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||||
- [ ] Don't eat comments in exprs
|
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||||
- [ ] Format regular exprs
|
|
||||||
- [ ] Fix prettyplease around chaining
|
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||||
- [ ] Don't eat comments in prettyplease
|
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||||
|
|
||||||
|
[Website](https://dioxuslabs.com) |
|
||||||
|
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||||
|
[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus_autofmt) |
|
||||||
|
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||||
|
|
||||||
|
|
||||||
# Technique
|
## Overview
|
||||||
|
|
||||||
|
`dioxus-autofmt` provides a pretty printer for the `rsx` syntax tree.
|
||||||
|
|
||||||
|
|
||||||
div {
|
This is done manually with a via set of formatting rules. The output is not guaranteed to be stable between minor versions of the crate as we might tweak the output.
|
||||||
div {}
|
|
||||||
div {}
|
`dioxus-autofmt` provides an API to perform precision edits as well as just spit out a block of formatted RSX from any RSX syntax tree. This is used by the `rsx-rosetta` crate which can accept various input languages and output valid RSX.
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
div
|
## Contributing
|
||||||
|
|
||||||
possible line break
|
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||||
div
|
- Join the discord and ask questions!
|
||||||
div
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
This project is licensed under the [MIT license].
|
||||||
|
|
||||||
|
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||||
|
|
||||||
string of possible items within a nesting
|
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||||
div {
|
for inclusion in Dioxus by you shall be licensed as MIT without any additional
|
||||||
attr_pair
|
terms or conditions.
|
||||||
expr
|
|
||||||
text
|
|
||||||
comment
|
|
||||||
}
|
|
||||||
a nesting is either a component or an element
|
|
||||||
|
|
||||||
idea:
|
|
||||||
collect all items into a queue
|
|
||||||
q
|
|
||||||
```rust
|
|
||||||
section {
|
|
||||||
div {
|
|
||||||
h1 { p { "asdasd" } }
|
|
||||||
h1 { p { "asdasd" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section {}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
// space
|
|
||||||
// space
|
|
||||||
// space
|
|
||||||
|
|
||||||
|
|
||||||
3 - section
|
|
||||||
3 - section div
|
|
||||||
3 - section div h1
|
|
||||||
3 - section div h1 p
|
|
||||||
3 - section div h1 p text
|
|
||||||
3 - section
|
|
||||||
3 - section div
|
|
||||||
3 - section div h1
|
|
||||||
3 - section div h1 p
|
|
||||||
3 - section div h1 p text
|
|
||||||
|
|
||||||
block
|
|
||||||
|
|
||||||
- when we hit the end of a trail, we can make a decision what needs to be hard breaked
|
|
||||||
- most nestings cannot be merged into a single one, so at some point we need to write the line break
|
|
||||||
- this is the scan section. we scan forward until it's obvious where to place a hard break
|
|
||||||
- when a line is finished, we can print it out by unloading our queued items
|
|
||||||
- never double nested
|
|
||||||
|
|
||||||
|
|
||||||
Terms
|
|
||||||
- break is a whitespace than can flex, dependent on the situation
|
|
||||||
- ‹›
|
|
||||||
|
|
|
@ -1,33 +1,18 @@
|
||||||
use std::{
|
//! The output buffer that supports some helpful methods
|
||||||
collections::{HashMap, VecDeque},
|
//! These are separate from the input so we can lend references between the two
|
||||||
fmt::{Result, Write},
|
//!
|
||||||
};
|
//!
|
||||||
|
//!
|
||||||
|
|
||||||
use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, IfmtInput};
|
use std::fmt::{Result, Write};
|
||||||
use proc_macro2::{LineColumn, Span};
|
|
||||||
use syn::{spanned::Spanned, Expr};
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
use dioxus_rsx::IfmtInput;
|
||||||
|
|
||||||
|
/// The output buffer that tracks indent and string
|
||||||
|
#[derive(Debug, Default)]
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
pub src: Vec<String>,
|
|
||||||
pub cached_formats: HashMap<Location, String>,
|
|
||||||
pub buf: String,
|
pub buf: String,
|
||||||
pub indent: usize,
|
pub indent: usize,
|
||||||
pub comments: VecDeque<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Location {
|
|
||||||
pub line: usize,
|
|
||||||
pub col: usize,
|
|
||||||
}
|
|
||||||
impl Location {
|
|
||||||
pub fn new(start: LineColumn) -> Self {
|
|
||||||
Self {
|
|
||||||
line: start.line,
|
|
||||||
col: start.column,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Buffer {
|
impl Buffer {
|
||||||
|
@ -62,160 +47,14 @@ impl Buffer {
|
||||||
writeln!(self.buf)
|
writeln!(self.buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expects to be written directly into place
|
|
||||||
pub fn write_ident(&mut self, node: &BodyNode) -> Result {
|
|
||||||
match node {
|
|
||||||
BodyNode::Element(el) => self.write_element(el),
|
|
||||||
BodyNode::Component(component) => self.write_component(component),
|
|
||||||
BodyNode::Text(text) => self.write_text(text),
|
|
||||||
BodyNode::RawExpr(exp) => self.write_raw_expr(exp),
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_text(&mut self, text: &IfmtInput) -> Result {
|
pub fn write_text(&mut self, text: &IfmtInput) -> Result {
|
||||||
write!(self.buf, "\"{}\"", text.source.as_ref().unwrap().value())
|
write!(self.buf, "\"{}\"", text.source.as_ref().unwrap().value())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn consume(self) -> Option<String> {
|
|
||||||
Some(self.buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_comments(&mut self, child: Span) -> Result {
|
|
||||||
// collect all comments upwards
|
|
||||||
let start = child.start();
|
|
||||||
let line_start = start.line - 1;
|
|
||||||
|
|
||||||
for (id, line) in self.src[..line_start].iter().enumerate().rev() {
|
|
||||||
if line.trim().starts_with("//") || line.is_empty() {
|
|
||||||
if id != 0 {
|
|
||||||
self.comments.push_front(id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut last_was_empty = false;
|
|
||||||
while let Some(comment_line) = self.comments.pop_front() {
|
|
||||||
let line = &self.src[comment_line];
|
|
||||||
if line.is_empty() {
|
|
||||||
if !last_was_empty {
|
|
||||||
self.new_line()?;
|
|
||||||
}
|
|
||||||
last_was_empty = true;
|
|
||||||
} else {
|
|
||||||
last_was_empty = false;
|
|
||||||
self.tabbed_line()?;
|
|
||||||
write!(self.buf, "{}", self.src[comment_line].trim())?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Write for Buffer {
|
||||||
|
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||||
|
self.buf.push_str(s);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push out the indent level and write each component, line by line
|
|
||||||
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
|
|
||||||
self.indent += 1;
|
|
||||||
|
|
||||||
self.write_body_no_indent(children)?;
|
|
||||||
|
|
||||||
self.indent -= 1;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result {
|
|
||||||
let last_child = children.len();
|
|
||||||
|
|
||||||
for (idx, child) in children.iter().enumerate() {
|
|
||||||
match child {
|
|
||||||
// check if the expr is a short
|
|
||||||
BodyNode::RawExpr { .. } => {
|
|
||||||
self.tabbed_line()?;
|
|
||||||
self.write_ident(child)?;
|
|
||||||
if idx != last_child - 1 {
|
|
||||||
write!(self.buf, ",")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if self.current_span_is_primary(child.span()) {
|
|
||||||
self.write_comments(child.span())?;
|
|
||||||
}
|
|
||||||
self.tabbed_line()?;
|
|
||||||
self.write_ident(child)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
|
|
||||||
let mut total = 0;
|
|
||||||
|
|
||||||
for attr in attributes {
|
|
||||||
if self.current_span_is_primary(attr.attr.flart()) {
|
|
||||||
'line: for line in self.src[..attr.attr.flart().start().line - 1].iter().rev() {
|
|
||||||
match (line.trim().starts_with("//"), line.is_empty()) {
|
|
||||||
(true, _) => return 100000,
|
|
||||||
(_, true) => continue 'line,
|
|
||||||
_ => break 'line,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
total += match &attr.attr {
|
|
||||||
ElementAttr::AttrText { value, name } => {
|
|
||||||
value.source.as_ref().unwrap().value().len() + name.span().line_length() + 3
|
|
||||||
}
|
|
||||||
ElementAttr::AttrExpression { name, value } => {
|
|
||||||
value.span().line_length() + name.span().line_length() + 3
|
|
||||||
}
|
|
||||||
ElementAttr::CustomAttrText { value, name } => {
|
|
||||||
value.source.as_ref().unwrap().value().len() + name.value().len() + 3
|
|
||||||
}
|
|
||||||
ElementAttr::CustomAttrExpression { name, value } => {
|
|
||||||
name.value().len() + value.span().line_length() + 3
|
|
||||||
}
|
|
||||||
ElementAttr::EventTokens { tokens, name } => {
|
|
||||||
let location = Location::new(tokens.span().start());
|
|
||||||
|
|
||||||
let len = if let std::collections::hash_map::Entry::Vacant(e) =
|
|
||||||
self.cached_formats.entry(location)
|
|
||||||
{
|
|
||||||
let formatted = prettyplease::unparse_expr(tokens);
|
|
||||||
let len = if formatted.contains('\n') {
|
|
||||||
10000
|
|
||||||
} else {
|
|
||||||
formatted.len()
|
|
||||||
};
|
|
||||||
e.insert(formatted);
|
|
||||||
len
|
|
||||||
} else {
|
|
||||||
self.cached_formats[&location].len()
|
|
||||||
};
|
|
||||||
|
|
||||||
len + name.span().line_length() + 3
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
total
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str {
|
|
||||||
self.cached_formats
|
|
||||||
.entry(Location::new(expr.span().start()))
|
|
||||||
.or_insert_with(|| prettyplease::unparse_expr(expr))
|
|
||||||
.as_str()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait SpanLength {
|
|
||||||
fn line_length(&self) -> usize;
|
|
||||||
}
|
|
||||||
impl SpanLength for Span {
|
|
||||||
fn line_length(&self) -> usize {
|
|
||||||
self.end().line - self.start().line
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
198
packages/autofmt/src/collect_macros.rs
Normal file
198
packages/autofmt/src/collect_macros.rs
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
//! Collect macros from a file
|
||||||
|
//!
|
||||||
|
//! Returns all macros that match a pattern. You can use this information to autoformat them later
|
||||||
|
|
||||||
|
use proc_macro2::LineColumn;
|
||||||
|
use syn::{Block, Expr, File, Item, Macro, Stmt};
|
||||||
|
|
||||||
|
type CollectedMacro<'a> = &'a Macro;
|
||||||
|
|
||||||
|
pub fn collect_from_file<'a>(file: &'a File, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||||
|
for item in file.items.iter() {
|
||||||
|
collect_from_item(item, macros);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_from_item<'a>(item: &'a Item, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||||
|
match item {
|
||||||
|
Item::Fn(f) => collect_from_block(&f.block, macros),
|
||||||
|
|
||||||
|
// Ignore macros if they're not rsx or render
|
||||||
|
Item::Macro(macro_) => {
|
||||||
|
if macro_.mac.path.segments[0].ident == "rsx"
|
||||||
|
|| macro_.mac.path.segments[0].ident == "render"
|
||||||
|
{
|
||||||
|
macros.push(¯o_.mac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently disabled since we're not focused on autoformatting these
|
||||||
|
Item::Impl(_imp) => {}
|
||||||
|
Item::Trait(_) => {}
|
||||||
|
|
||||||
|
// Global-ish things
|
||||||
|
Item::Static(f) => collect_from_expr(&f.expr, macros),
|
||||||
|
Item::Const(f) => collect_from_expr(&f.expr, macros),
|
||||||
|
Item::Mod(s) => {
|
||||||
|
if let Some((_, block)) = &s.content {
|
||||||
|
for item in block {
|
||||||
|
collect_from_item(item, macros);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// None of these we can really do anything with at the item level
|
||||||
|
Item::Macro2(_)
|
||||||
|
| Item::Enum(_)
|
||||||
|
| Item::ExternCrate(_)
|
||||||
|
| Item::ForeignMod(_)
|
||||||
|
| Item::TraitAlias(_)
|
||||||
|
| Item::Type(_)
|
||||||
|
| Item::Struct(_)
|
||||||
|
| Item::Union(_)
|
||||||
|
| Item::Use(_)
|
||||||
|
| Item::Verbatim(_) => {}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_from_block<'a>(block: &'a Block, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||||
|
for stmt in &block.stmts {
|
||||||
|
match stmt {
|
||||||
|
Stmt::Item(item) => collect_from_item(item, macros),
|
||||||
|
Stmt::Local(local) => {
|
||||||
|
if let Some((_eq, init)) = &local.init {
|
||||||
|
collect_from_expr(init, macros);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Stmt::Expr(exp) | Stmt::Semi(exp, _) => collect_from_expr(exp, macros),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_from_expr<'a>(expr: &'a Expr, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||||
|
// collect an expr from the exprs, descending into blocks
|
||||||
|
match expr {
|
||||||
|
Expr::Macro(macro_) => {
|
||||||
|
if macro_.mac.path.segments[0].ident == "rsx"
|
||||||
|
|| macro_.mac.path.segments[0].ident == "render"
|
||||||
|
{
|
||||||
|
macros.push(¯o_.mac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::MethodCall(e) => {
|
||||||
|
collect_from_expr(&e.receiver, macros);
|
||||||
|
for expr in e.args.iter() {
|
||||||
|
collect_from_expr(expr, macros);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Assign(exp) => {
|
||||||
|
collect_from_expr(&exp.left, macros);
|
||||||
|
collect_from_expr(&exp.right, macros);
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::Async(b) => collect_from_block(&b.block, macros),
|
||||||
|
Expr::Block(b) => collect_from_block(&b.block, macros),
|
||||||
|
Expr::Closure(c) => collect_from_expr(&c.body, macros),
|
||||||
|
Expr::Let(l) => collect_from_expr(&l.expr, macros),
|
||||||
|
Expr::Unsafe(u) => collect_from_block(&u.block, macros),
|
||||||
|
Expr::Loop(l) => collect_from_block(&l.body, macros),
|
||||||
|
|
||||||
|
Expr::Call(c) => {
|
||||||
|
collect_from_expr(&c.func, macros);
|
||||||
|
for expr in c.args.iter() {
|
||||||
|
collect_from_expr(expr, macros);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::ForLoop(b) => {
|
||||||
|
collect_from_expr(&b.expr, macros);
|
||||||
|
collect_from_block(&b.body, macros);
|
||||||
|
}
|
||||||
|
Expr::If(f) => {
|
||||||
|
collect_from_expr(&f.cond, macros);
|
||||||
|
collect_from_block(&f.then_branch, macros);
|
||||||
|
if let Some((_, else_branch)) = &f.else_branch {
|
||||||
|
collect_from_expr(else_branch, macros);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Yield(y) => {
|
||||||
|
if let Some(expr) = &y.expr {
|
||||||
|
collect_from_expr(expr, macros);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::Return(r) => {
|
||||||
|
if let Some(expr) = &r.expr {
|
||||||
|
collect_from_expr(expr, macros);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::Match(l) => {
|
||||||
|
collect_from_expr(&l.expr, macros);
|
||||||
|
for arm in l.arms.iter() {
|
||||||
|
if let Some((_, expr)) = &arm.guard {
|
||||||
|
collect_from_expr(expr, macros);
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_from_expr(&arm.body, macros);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::While(w) => {
|
||||||
|
collect_from_expr(&w.cond, macros);
|
||||||
|
collect_from_block(&w.body, macros);
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't both formatting these for now
|
||||||
|
Expr::Array(_)
|
||||||
|
| Expr::AssignOp(_)
|
||||||
|
| Expr::Await(_)
|
||||||
|
| Expr::Binary(_)
|
||||||
|
| Expr::Box(_)
|
||||||
|
| Expr::Break(_)
|
||||||
|
| Expr::Cast(_)
|
||||||
|
| Expr::Continue(_)
|
||||||
|
| Expr::Field(_)
|
||||||
|
| Expr::Group(_)
|
||||||
|
| Expr::Index(_)
|
||||||
|
| Expr::Lit(_)
|
||||||
|
| Expr::Paren(_)
|
||||||
|
| Expr::Path(_)
|
||||||
|
| Expr::Range(_)
|
||||||
|
| Expr::Reference(_)
|
||||||
|
| Expr::Repeat(_)
|
||||||
|
| Expr::Struct(_)
|
||||||
|
| Expr::Try(_)
|
||||||
|
| Expr::TryBlock(_)
|
||||||
|
| Expr::Tuple(_)
|
||||||
|
| Expr::Type(_)
|
||||||
|
| Expr::Unary(_)
|
||||||
|
| Expr::Verbatim(_) => {}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn byte_offset(input: &str, location: LineColumn) -> usize {
|
||||||
|
let mut offset = 0;
|
||||||
|
for _ in 1..location.line {
|
||||||
|
offset += input[offset..].find('\n').unwrap() + 1;
|
||||||
|
}
|
||||||
|
offset
|
||||||
|
+ input[offset..]
|
||||||
|
.chars()
|
||||||
|
.take(location.column)
|
||||||
|
.map(char::len_utf8)
|
||||||
|
.sum::<usize>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_file_and_collects_rsx_macros() {
|
||||||
|
let contents = include_str!("../tests/samples/long.rsx");
|
||||||
|
let parsed = syn::parse_file(contents).unwrap();
|
||||||
|
let mut macros = vec![];
|
||||||
|
collect_from_file(&parsed, &mut macros);
|
||||||
|
assert_eq!(macros.len(), 3);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{buffer::Location, Buffer};
|
use crate::{writer::Location, Writer};
|
||||||
use dioxus_rsx::*;
|
use dioxus_rsx::*;
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use std::fmt::{Result, Write};
|
use std::fmt::{Result, Write};
|
||||||
|
@ -19,7 +19,7 @@ enum ShortOptimization {
|
||||||
NoOpt,
|
NoOpt,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Buffer {
|
impl Writer<'_> {
|
||||||
pub fn write_component(
|
pub fn write_component(
|
||||||
&mut self,
|
&mut self,
|
||||||
Component {
|
Component {
|
||||||
|
@ -28,6 +28,7 @@ impl Buffer {
|
||||||
children,
|
children,
|
||||||
manual_props,
|
manual_props,
|
||||||
prop_gen_args,
|
prop_gen_args,
|
||||||
|
..
|
||||||
}: &Component,
|
}: &Component,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
self.write_component_name(name, prop_gen_args)?;
|
self.write_component_name(name, prop_gen_args)?;
|
||||||
|
@ -82,46 +83,46 @@ impl Buffer {
|
||||||
match opt_level {
|
match opt_level {
|
||||||
ShortOptimization::Empty => {}
|
ShortOptimization::Empty => {}
|
||||||
ShortOptimization::Oneliner => {
|
ShortOptimization::Oneliner => {
|
||||||
write!(self.buf, " ")?;
|
write!(self.out, " ")?;
|
||||||
|
|
||||||
self.write_component_fields(fields, manual_props, true)?;
|
self.write_component_fields(fields, manual_props, true)?;
|
||||||
|
|
||||||
if !children.is_empty() && !fields.is_empty() {
|
if !children.is_empty() && !fields.is_empty() {
|
||||||
write!(self.buf, ", ")?;
|
write!(self.out, ", ")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for child in children {
|
for child in children {
|
||||||
self.write_ident(child)?;
|
self.write_ident(child)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(self.buf, " ")?;
|
write!(self.out, " ")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShortOptimization::PropsOnTop => {
|
ShortOptimization::PropsOnTop => {
|
||||||
write!(self.buf, " ")?;
|
write!(self.out, " ")?;
|
||||||
self.write_component_fields(fields, manual_props, true)?;
|
self.write_component_fields(fields, manual_props, true)?;
|
||||||
|
|
||||||
if !children.is_empty() && !fields.is_empty() {
|
if !children.is_empty() && !fields.is_empty() {
|
||||||
write!(self.buf, ",")?;
|
write!(self.out, ",")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.write_body_indented(children)?;
|
self.write_body_indented(children)?;
|
||||||
self.tabbed_line()?;
|
self.out.tabbed_line()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShortOptimization::NoOpt => {
|
ShortOptimization::NoOpt => {
|
||||||
self.write_component_fields(fields, manual_props, false)?;
|
self.write_component_fields(fields, manual_props, false)?;
|
||||||
|
|
||||||
if !children.is_empty() && !fields.is_empty() {
|
if !children.is_empty() && !fields.is_empty() {
|
||||||
write!(self.buf, ",")?;
|
write!(self.out, ",")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.write_body_indented(children)?;
|
self.write_body_indented(children)?;
|
||||||
self.tabbed_line()?;
|
self.out.tabbed_line()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(self.buf, "}}")?;
|
write!(self.out, "}}")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,16 +134,16 @@ impl Buffer {
|
||||||
let mut name = name.to_token_stream().to_string();
|
let mut name = name.to_token_stream().to_string();
|
||||||
name.retain(|c| !c.is_whitespace());
|
name.retain(|c| !c.is_whitespace());
|
||||||
|
|
||||||
write!(self.buf, "{name}")?;
|
write!(self.out, "{name}")?;
|
||||||
|
|
||||||
if let Some(generics) = generics {
|
if let Some(generics) = generics {
|
||||||
let mut written = generics.to_token_stream().to_string();
|
let mut written = generics.to_token_stream().to_string();
|
||||||
written.retain(|c| !c.is_whitespace());
|
written.retain(|c| !c.is_whitespace());
|
||||||
|
|
||||||
write!(self.buf, "{}", written)?;
|
write!(self.out, "{written}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(self.buf, " {{")?;
|
write!(self.out, " {{")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -157,18 +158,18 @@ impl Buffer {
|
||||||
|
|
||||||
while let Some(field) = field_iter.next() {
|
while let Some(field) = field_iter.next() {
|
||||||
if !sameline {
|
if !sameline {
|
||||||
self.indented_tabbed_line()?;
|
self.out.indented_tabbed_line()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = &field.name;
|
let name = &field.name;
|
||||||
match &field.content {
|
match &field.content {
|
||||||
ContentField::ManExpr(exp) => {
|
ContentField::ManExpr(exp) => {
|
||||||
let out = prettyplease::unparse_expr(exp);
|
let out = prettyplease::unparse_expr(exp);
|
||||||
write!(self.buf, "{}: {}", name, out)?;
|
write!(self.out, "{name}: {out}")?;
|
||||||
}
|
}
|
||||||
ContentField::Formatted(s) => {
|
ContentField::Formatted(s) => {
|
||||||
write!(
|
write!(
|
||||||
self.buf,
|
self.out,
|
||||||
"{}: \"{}\"",
|
"{}: \"{}\"",
|
||||||
name,
|
name,
|
||||||
s.source.as_ref().unwrap().value()
|
s.source.as_ref().unwrap().value()
|
||||||
|
@ -178,27 +179,27 @@ impl Buffer {
|
||||||
let out = prettyplease::unparse_expr(exp);
|
let out = prettyplease::unparse_expr(exp);
|
||||||
let mut lines = out.split('\n').peekable();
|
let mut lines = out.split('\n').peekable();
|
||||||
let first = lines.next().unwrap();
|
let first = lines.next().unwrap();
|
||||||
write!(self.buf, "{}: {}", name, first)?;
|
write!(self.out, "{name}: {first}")?;
|
||||||
for line in lines {
|
for line in lines {
|
||||||
self.new_line()?;
|
self.out.new_line()?;
|
||||||
self.indented_tab()?;
|
self.out.indented_tab()?;
|
||||||
write!(self.buf, "{}", line)?;
|
write!(self.out, "{line}")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if field_iter.peek().is_some() || manual_props.is_some() {
|
if field_iter.peek().is_some() || manual_props.is_some() {
|
||||||
write!(self.buf, ",")?;
|
write!(self.out, ",")?;
|
||||||
|
|
||||||
if sameline {
|
if sameline {
|
||||||
write!(self.buf, " ")?;
|
write!(self.out, " ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(exp) = manual_props {
|
if let Some(exp) = manual_props {
|
||||||
if !sameline {
|
if !sameline {
|
||||||
self.indented_tabbed_line()?;
|
self.out.indented_tabbed_line()?;
|
||||||
}
|
}
|
||||||
self.write_manual_props(exp)?;
|
self.write_manual_props(exp)?;
|
||||||
}
|
}
|
||||||
|
@ -258,10 +259,10 @@ impl Buffer {
|
||||||
|
|
||||||
let first_line = lines.next().unwrap();
|
let first_line = lines.next().unwrap();
|
||||||
|
|
||||||
write!(self.buf, "..{first_line}")?;
|
write!(self.out, "..{first_line}")?;
|
||||||
for line in lines {
|
for line in lines {
|
||||||
self.indented_tabbed_line()?;
|
self.out.indented_tabbed_line()?;
|
||||||
write!(self.buf, "{line}")?;
|
write!(self.out, "{line}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,35 +1,56 @@
|
||||||
use crate::Buffer;
|
use crate::Writer;
|
||||||
use dioxus_rsx::*;
|
use dioxus_rsx::*;
|
||||||
use proc_macro2::Span;
|
use proc_macro2::Span;
|
||||||
use std::{fmt::Result, fmt::Write};
|
use std::{
|
||||||
use syn::{spanned::Spanned, Expr};
|
fmt::Result,
|
||||||
|
fmt::{self, Write},
|
||||||
|
};
|
||||||
|
use syn::{spanned::Spanned, token::Brace, Expr};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum ShortOptimization {
|
enum ShortOptimization {
|
||||||
// Special because we want to print the closing bracket immediately
|
/// Special because we want to print the closing bracket immediately
|
||||||
|
///
|
||||||
|
/// IE
|
||||||
|
/// `div {}` instead of `div { }`
|
||||||
Empty,
|
Empty,
|
||||||
|
|
||||||
// Special optimization to put everything on the same line
|
/// Special optimization to put everything on the same line and add some buffer spaces
|
||||||
|
///
|
||||||
|
/// IE
|
||||||
|
///
|
||||||
|
/// `div { "asdasd" }` instead of a multiline variant
|
||||||
Oneliner,
|
Oneliner,
|
||||||
|
|
||||||
// Optimization where children flow but props remain fixed on top
|
/// Optimization where children flow but props remain fixed on top
|
||||||
PropsOnTop,
|
PropsOnTop,
|
||||||
|
|
||||||
// The noisiest optimization where everything flows
|
/// The noisiest optimization where everything flows
|
||||||
NoOpt,
|
NoOpt,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Buffer {
|
/*
|
||||||
pub fn write_element(
|
// whitespace
|
||||||
&mut self,
|
div {
|
||||||
Element {
|
// some whitespace
|
||||||
|
class: "asdasd"
|
||||||
|
|
||||||
|
// whjiot
|
||||||
|
asdasd // whitespace
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
impl Writer<'_> {
|
||||||
|
pub fn write_element(&mut self, el: &Element) -> Result {
|
||||||
|
let Element {
|
||||||
name,
|
name,
|
||||||
key,
|
key,
|
||||||
attributes,
|
attributes,
|
||||||
children,
|
children,
|
||||||
_is_static,
|
_is_static,
|
||||||
}: &Element,
|
brace,
|
||||||
) -> Result {
|
} = el;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
1. Write the tag
|
1. Write the tag
|
||||||
2. Write the key
|
2. Write the key
|
||||||
|
@ -37,7 +58,7 @@ impl Buffer {
|
||||||
4. Write the children
|
4. Write the children
|
||||||
*/
|
*/
|
||||||
|
|
||||||
write!(self.buf, "{name} {{")?;
|
write!(self.out, "{name} {{")?;
|
||||||
|
|
||||||
// decide if we have any special optimizations
|
// decide if we have any special optimizations
|
||||||
// Default with none, opt the cases in one-by-one
|
// Default with none, opt the cases in one-by-one
|
||||||
|
@ -45,8 +66,9 @@ impl Buffer {
|
||||||
|
|
||||||
// check if we have a lot of attributes
|
// check if we have a lot of attributes
|
||||||
let attr_len = self.is_short_attrs(attributes);
|
let attr_len = self.is_short_attrs(attributes);
|
||||||
let is_short_attr_list = attr_len < 80;
|
let is_short_attr_list = (attr_len + self.out.indent * 4) < 80;
|
||||||
let is_small_children = self.is_short_children(children).is_some();
|
let children_len = self.is_short_children(children);
|
||||||
|
let is_small_children = children_len.is_some();
|
||||||
|
|
||||||
// if we have few attributes and a lot of children, place the attrs on top
|
// if we have few attributes and a lot of children, place the attrs on top
|
||||||
if is_short_attr_list && !is_small_children {
|
if is_short_attr_list && !is_small_children {
|
||||||
|
@ -64,12 +86,19 @@ impl Buffer {
|
||||||
|
|
||||||
// if we have few children and few attributes, make it a one-liner
|
// if we have few children and few attributes, make it a one-liner
|
||||||
if is_short_attr_list && is_small_children {
|
if is_short_attr_list && is_small_children {
|
||||||
|
if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
|
||||||
opt_level = ShortOptimization::Oneliner;
|
opt_level = ShortOptimization::Oneliner;
|
||||||
|
} else {
|
||||||
|
opt_level = ShortOptimization::PropsOnTop;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's nothing at all, empty optimization
|
// If there's nothing at all, empty optimization
|
||||||
if attributes.is_empty() && children.is_empty() && key.is_none() {
|
if attributes.is_empty() && children.is_empty() && key.is_none() {
|
||||||
opt_level = ShortOptimization::Empty;
|
opt_level = ShortOptimization::Empty;
|
||||||
|
|
||||||
|
// Write comments if they exist
|
||||||
|
self.write_todo_body(brace)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// multiline handlers bump everything down
|
// multiline handlers bump everything down
|
||||||
|
@ -80,55 +109,56 @@ impl Buffer {
|
||||||
match opt_level {
|
match opt_level {
|
||||||
ShortOptimization::Empty => {}
|
ShortOptimization::Empty => {}
|
||||||
ShortOptimization::Oneliner => {
|
ShortOptimization::Oneliner => {
|
||||||
write!(self.buf, " ")?;
|
write!(self.out, " ")?;
|
||||||
|
|
||||||
self.write_attributes(attributes, key, true)?;
|
self.write_attributes(attributes, key, true)?;
|
||||||
|
|
||||||
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
||||||
write!(self.buf, ", ")?;
|
write!(self.out, ", ")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (id, child) in children.iter().enumerate() {
|
for (id, child) in children.iter().enumerate() {
|
||||||
self.write_ident(child)?;
|
self.write_ident(child)?;
|
||||||
if id != children.len() - 1 && children.len() > 1 {
|
if id != children.len() - 1 && children.len() > 1 {
|
||||||
write!(self.buf, ", ")?;
|
write!(self.out, ", ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(self.buf, " ")?;
|
write!(self.out, " ")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShortOptimization::PropsOnTop => {
|
ShortOptimization::PropsOnTop => {
|
||||||
if !attributes.is_empty() || key.is_some() {
|
if !attributes.is_empty() || key.is_some() {
|
||||||
write!(self.buf, " ")?;
|
write!(self.out, " ")?;
|
||||||
}
|
}
|
||||||
self.write_attributes(attributes, key, true)?;
|
self.write_attributes(attributes, key, true)?;
|
||||||
|
|
||||||
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
||||||
write!(self.buf, ",")?;
|
write!(self.out, ",")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !children.is_empty() {
|
if !children.is_empty() {
|
||||||
self.write_body_indented(children)?;
|
self.write_body_indented(children)?;
|
||||||
}
|
}
|
||||||
self.tabbed_line()?;
|
self.out.tabbed_line()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShortOptimization::NoOpt => {
|
ShortOptimization::NoOpt => {
|
||||||
self.write_attributes(attributes, key, false)?;
|
self.write_attributes(attributes, key, false)?;
|
||||||
|
|
||||||
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
||||||
write!(self.buf, ",")?;
|
write!(self.out, ",")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !children.is_empty() {
|
if !children.is_empty() {
|
||||||
self.write_body_indented(children)?;
|
self.write_body_indented(children)?;
|
||||||
}
|
}
|
||||||
self.tabbed_line()?;
|
|
||||||
|
self.out.tabbed_line()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(self.buf, "}}")?;
|
write!(self.out, "}}")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -143,39 +173,39 @@ impl Buffer {
|
||||||
|
|
||||||
if let Some(key) = key {
|
if let Some(key) = key {
|
||||||
if !sameline {
|
if !sameline {
|
||||||
self.indented_tabbed_line()?;
|
self.out.indented_tabbed_line()?;
|
||||||
}
|
}
|
||||||
write!(
|
write!(
|
||||||
self.buf,
|
self.out,
|
||||||
"key: \"{}\"",
|
"key: \"{}\"",
|
||||||
key.source.as_ref().unwrap().value()
|
key.source.as_ref().unwrap().value()
|
||||||
)?;
|
)?;
|
||||||
if !attributes.is_empty() {
|
if !attributes.is_empty() {
|
||||||
write!(self.buf, ",")?;
|
write!(self.out, ",")?;
|
||||||
if sameline {
|
if sameline {
|
||||||
write!(self.buf, " ")?;
|
write!(self.out, " ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while let Some(attr) = attr_iter.next() {
|
while let Some(attr) = attr_iter.next() {
|
||||||
self.indent += 1;
|
self.out.indent += 1;
|
||||||
if !sameline {
|
if !sameline {
|
||||||
self.write_comments(attr.attr.flart())?;
|
self.write_comments(attr.attr.start())?;
|
||||||
}
|
}
|
||||||
self.indent -= 1;
|
self.out.indent -= 1;
|
||||||
|
|
||||||
if !sameline {
|
if !sameline {
|
||||||
self.indented_tabbed_line()?;
|
self.out.indented_tabbed_line()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.write_attribute(attr)?;
|
self.write_attribute(attr)?;
|
||||||
|
|
||||||
if attr_iter.peek().is_some() {
|
if attr_iter.peek().is_some() {
|
||||||
write!(self.buf, ",")?;
|
write!(self.out, ",")?;
|
||||||
|
|
||||||
if sameline {
|
if sameline {
|
||||||
write!(self.buf, " ")?;
|
write!(self.out, " ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,19 +217,19 @@ impl Buffer {
|
||||||
match &attr.attr {
|
match &attr.attr {
|
||||||
ElementAttr::AttrText { name, value } => {
|
ElementAttr::AttrText { name, value } => {
|
||||||
write!(
|
write!(
|
||||||
self.buf,
|
self.out,
|
||||||
"{name}: \"{value}\"",
|
"{name}: \"{value}\"",
|
||||||
value = value.source.as_ref().unwrap().value()
|
value = value.source.as_ref().unwrap().value()
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
ElementAttr::AttrExpression { name, value } => {
|
ElementAttr::AttrExpression { name, value } => {
|
||||||
let out = prettyplease::unparse_expr(value);
|
let out = prettyplease::unparse_expr(value);
|
||||||
write!(self.buf, "{}: {}", name, out)?;
|
write!(self.out, "{name}: {out}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ElementAttr::CustomAttrText { name, value } => {
|
ElementAttr::CustomAttrText { name, value } => {
|
||||||
write!(
|
write!(
|
||||||
self.buf,
|
self.out,
|
||||||
"\"{name}\": \"{value}\"",
|
"\"{name}\": \"{value}\"",
|
||||||
name = name.value(),
|
name = name.value(),
|
||||||
value = value.source.as_ref().unwrap().value()
|
value = value.source.as_ref().unwrap().value()
|
||||||
|
@ -208,7 +238,7 @@ impl Buffer {
|
||||||
|
|
||||||
ElementAttr::CustomAttrExpression { name, value } => {
|
ElementAttr::CustomAttrExpression { name, value } => {
|
||||||
let out = prettyplease::unparse_expr(value);
|
let out = prettyplease::unparse_expr(value);
|
||||||
write!(self.buf, "\"{}\": {}", name.value(), out)?;
|
write!(self.out, "\"{}\": {}", name.value(), out)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ElementAttr::EventTokens { name, tokens } => {
|
ElementAttr::EventTokens { name, tokens } => {
|
||||||
|
@ -220,17 +250,17 @@ impl Buffer {
|
||||||
// a one-liner for whatever reason
|
// a one-liner for whatever reason
|
||||||
// Does not need a new line
|
// Does not need a new line
|
||||||
if lines.peek().is_none() {
|
if lines.peek().is_none() {
|
||||||
write!(self.buf, "{}: {}", name, first)?;
|
write!(self.out, "{name}: {first}")?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(self.buf, "{}: {}", name, first)?;
|
writeln!(self.out, "{name}: {first}")?;
|
||||||
|
|
||||||
while let Some(line) = lines.next() {
|
while let Some(line) = lines.next() {
|
||||||
self.indented_tab()?;
|
self.out.indented_tab()?;
|
||||||
write!(self.buf, "{}", line)?;
|
write!(self.out, "{line}")?;
|
||||||
if lines.peek().is_none() {
|
if lines.peek().is_none() {
|
||||||
write!(self.buf, "")?;
|
write!(self.out, "")?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(self.buf)?;
|
writeln!(self.out)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,7 +276,7 @@ impl Buffer {
|
||||||
let start = location.start();
|
let start = location.start();
|
||||||
let line_start = start.line - 1;
|
let line_start = start.line - 1;
|
||||||
|
|
||||||
let this_line = self.src[line_start].as_str();
|
let this_line = self.src[line_start];
|
||||||
|
|
||||||
let beginning = if this_line.len() > start.column {
|
let beginning = if this_line.len() > start.column {
|
||||||
this_line[..start.column].trim()
|
this_line[..start.column].trim()
|
||||||
|
@ -254,8 +284,6 @@ impl Buffer {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
|
|
||||||
// dbg!(beginning);
|
|
||||||
|
|
||||||
beginning.is_empty()
|
beginning.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,6 +296,10 @@ impl Buffer {
|
||||||
if children.is_empty() {
|
if children.is_empty() {
|
||||||
// todo: allow elements with comments but no children
|
// todo: allow elements with comments but no children
|
||||||
// like div { /* comment */ }
|
// like div { /* comment */ }
|
||||||
|
// or
|
||||||
|
// div {
|
||||||
|
// // some helpful
|
||||||
|
// }
|
||||||
return Some(0);
|
return Some(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,10 +328,8 @@ impl Buffer {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[BodyNode::RawExpr(ref expr)] => {
|
|
||||||
// TODO: let rawexprs to be inlined
|
// TODO: let rawexprs to be inlined
|
||||||
get_expr_length(expr)
|
[BodyNode::RawExpr(ref expr)] => get_expr_length(expr),
|
||||||
}
|
|
||||||
[BodyNode::Element(ref el)] => {
|
[BodyNode::Element(ref el)] => {
|
||||||
let attr_len = self.is_short_attrs(&el.attributes);
|
let attr_len = self.is_short_attrs(&el.attributes);
|
||||||
|
|
||||||
|
@ -333,8 +363,8 @@ impl Buffer {
|
||||||
Some(len) => total_count += len,
|
Some(len) => total_count += len,
|
||||||
None => return None,
|
None => return None,
|
||||||
},
|
},
|
||||||
BodyNode::ForLoop(_) => todo!(),
|
BodyNode::ForLoop(_forloop) => return None,
|
||||||
BodyNode::IfChain(_) => todo!(),
|
BodyNode::IfChain(_chain) => return None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,6 +372,35 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// empty everything except for some comments
|
||||||
|
fn write_todo_body(&mut self, brace: &Brace) -> fmt::Result {
|
||||||
|
let span = brace.span.span();
|
||||||
|
let start = span.start();
|
||||||
|
let end = span.end();
|
||||||
|
|
||||||
|
if start.line == end.line {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(self.out)?;
|
||||||
|
|
||||||
|
for idx in start.line..end.line {
|
||||||
|
let line = &self.src[idx];
|
||||||
|
if line.trim().starts_with("//") {
|
||||||
|
for _ in 0..self.out.indent + 1 {
|
||||||
|
write!(self.out, " ")?
|
||||||
|
}
|
||||||
|
writeln!(self.out, "{}", line.trim()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..self.out.indent {
|
||||||
|
write!(self.out, " ")?
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_expr_length(expr: &Expr) -> Option<usize> {
|
fn get_expr_length(expr: &Expr) -> Option<usize> {
|
||||||
|
|
|
@ -1,48 +1,26 @@
|
||||||
//! pretty printer for rsx!
|
//! pretty printer for rsx!
|
||||||
use std::fmt::{Result, Write};
|
use std::fmt::{Result, Write};
|
||||||
|
|
||||||
use crate::Buffer;
|
use proc_macro2::Span;
|
||||||
|
|
||||||
impl Buffer {
|
use crate::{collect_macros::byte_offset, Writer};
|
||||||
pub fn write_raw_expr(&mut self, exp: &syn::Expr) -> Result {
|
|
||||||
|
impl Writer<'_> {
|
||||||
|
pub fn write_raw_expr(&mut self, placement: Span) -> Result {
|
||||||
/*
|
/*
|
||||||
We want to normalize the expr to the appropriate indent level.
|
We want to normalize the expr to the appropriate indent level.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// in a perfect world, just fire up the rust pretty printer
|
|
||||||
// pretty_print_rust_code_as_if_it_were_rustfmt()
|
|
||||||
|
|
||||||
use syn::spanned::Spanned;
|
|
||||||
let placement = exp.span();
|
|
||||||
let start = placement.start();
|
let start = placement.start();
|
||||||
let end = placement.end();
|
let end = placement.end();
|
||||||
// let num_spaces_desired = (self.indent * 4) as isize;
|
|
||||||
|
|
||||||
// print comments
|
|
||||||
// let mut queued_comments = vec![];
|
|
||||||
// let mut offset = 2;
|
|
||||||
// loop {
|
|
||||||
// let line = &self.src[start.line - offset];
|
|
||||||
// if line.trim_start().starts_with("//") {
|
|
||||||
// queued_comments.push(line);
|
|
||||||
// } else {
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// offset += 1;
|
|
||||||
// }
|
|
||||||
// let had_comments = !queued_comments.is_empty();
|
|
||||||
// for comment in queued_comments.into_iter().rev() {
|
|
||||||
// writeln!(self.buf, "{}", comment)?;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if the expr is on one line, just write it directly
|
// if the expr is on one line, just write it directly
|
||||||
if start.line == end.line {
|
if start.line == end.line {
|
||||||
write!(
|
// split counting utf8 chars
|
||||||
self.buf,
|
let start = byte_offset(self.raw_src, start);
|
||||||
"{}",
|
let end = byte_offset(self.raw_src, end);
|
||||||
&self.src[start.line - 1][start.column - 1..end.column].trim()
|
let row = self.raw_src[start..end].trim();
|
||||||
)?;
|
write!(self.out, "{row}")?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +28,7 @@ impl Buffer {
|
||||||
// This involves unshifting the first line if it's aligned
|
// This involves unshifting the first line if it's aligned
|
||||||
let first_line = &self.src[start.line - 1];
|
let first_line = &self.src[start.line - 1];
|
||||||
write!(
|
write!(
|
||||||
self.buf,
|
self.out,
|
||||||
"{}",
|
"{}",
|
||||||
&first_line[start.column - 1..first_line.len()].trim()
|
&first_line[start.column - 1..first_line.len()].trim()
|
||||||
)?;
|
)?;
|
||||||
|
@ -66,7 +44,7 @@ impl Buffer {
|
||||||
};
|
};
|
||||||
|
|
||||||
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
|
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
|
||||||
writeln!(self.buf)?;
|
writeln!(self.out)?;
|
||||||
// trim the leading whitespace
|
// trim the leading whitespace
|
||||||
let line = match id {
|
let line = match id {
|
||||||
x if x == (end.line - start.line) - 1 => &line[..end.column],
|
x if x == (end.line - start.line) - 1 => &line[..end.column],
|
||||||
|
@ -75,52 +53,17 @@ impl Buffer {
|
||||||
|
|
||||||
if offset < 0 {
|
if offset < 0 {
|
||||||
for _ in 0..-offset {
|
for _ in 0..-offset {
|
||||||
write!(self.buf, " ")?;
|
write!(self.out, " ")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(self.buf, "{}", line)?;
|
write!(self.out, "{line}")?;
|
||||||
} else {
|
} else {
|
||||||
let offset = offset as usize;
|
let offset = offset as usize;
|
||||||
let right = &line[offset..];
|
let right = &line[offset..];
|
||||||
write!(self.buf, "{}", right)?;
|
write!(self.out, "{right}")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// let first = &self.src[start.line - 1];
|
|
||||||
// let num_spaces_real = first.chars().take_while(|c| c.is_whitespace()).count() as isize;
|
|
||||||
// let offset = num_spaces_real - num_spaces_desired;
|
|
||||||
|
|
||||||
// for (row, line) in self.src[start.line - 1..end.line].iter().enumerate() {
|
|
||||||
// let line = match row {
|
|
||||||
// 0 => &line[start.column - 1..],
|
|
||||||
// a if a == (end.line - start.line) => &line[..end.column - 1],
|
|
||||||
// _ => line,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// writeln!(self.buf)?;
|
|
||||||
// // trim the leading whitespace
|
|
||||||
// if offset < 0 {
|
|
||||||
// for _ in 0..-offset {
|
|
||||||
// write!(self.buf, " ")?;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// write!(self.buf, "{}", line)?;
|
|
||||||
// } else {
|
|
||||||
// let offset = offset as usize;
|
|
||||||
// let right = &line[offset..];
|
|
||||||
// write!(self.buf, "{}", right)?;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// :(
|
|
||||||
// fn pretty_print_rust_code_as_if_it_were_rustfmt(code: &str) -> String {
|
|
||||||
// let formatted = prettyplease::unparse_expr(exp);
|
|
||||||
// for line in formatted.lines() {
|
|
||||||
// write!(self.buf, "{}", line)?;
|
|
||||||
// self.new_line()?;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
use crate::buffer::*;
|
use crate::writer::*;
|
||||||
use crate::util::*;
|
use collect_macros::byte_offset;
|
||||||
|
use dioxus_rsx::{BodyNode, CallBody};
|
||||||
|
use proc_macro2::LineColumn;
|
||||||
|
use syn::{ExprMacro, MacroDelimiter};
|
||||||
|
|
||||||
mod buffer;
|
mod buffer;
|
||||||
|
mod collect_macros;
|
||||||
mod component;
|
mod component;
|
||||||
mod element;
|
mod element;
|
||||||
mod expr;
|
mod expr;
|
||||||
mod util;
|
mod writer;
|
||||||
|
|
||||||
/// A modification to the original file to be applied by an IDE
|
/// A modification to the original file to be applied by an IDE
|
||||||
///
|
///
|
||||||
|
@ -31,87 +35,141 @@ pub struct FormattedBlock {
|
||||||
/// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting.
|
/// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting.
|
||||||
///
|
///
|
||||||
/// This function expects a complete file, not just a block of code. To format individual rsx! blocks, use fmt_block instead.
|
/// This function expects a complete file, not just a block of code. To format individual rsx! blocks, use fmt_block instead.
|
||||||
|
///
|
||||||
|
/// The point here is to provide precise modifications of a source file so an accompanying IDE tool can map these changes
|
||||||
|
/// back to the file precisely.
|
||||||
|
///
|
||||||
|
/// Nested blocks of RSX will be handled automatically
|
||||||
pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||||
let mut formatted_blocks = Vec::new();
|
let mut formatted_blocks = Vec::new();
|
||||||
let mut last_bracket_end = 0;
|
|
||||||
|
|
||||||
use triple_accel::{levenshtein_search, Match};
|
let parsed = syn::parse_file(contents).unwrap();
|
||||||
|
|
||||||
for Match { end, start, k } in levenshtein_search(b"rsx! {", contents.as_bytes()) {
|
let mut macros = vec![];
|
||||||
if k > 1 {
|
collect_macros::collect_from_file(&parsed, &mut macros);
|
||||||
|
|
||||||
|
// No macros, no work to do
|
||||||
|
if macros.is_empty() {
|
||||||
|
return formatted_blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut writer = Writer::new(contents);
|
||||||
|
|
||||||
|
// Dont parse nested macros
|
||||||
|
let mut end_span = LineColumn { column: 0, line: 0 };
|
||||||
|
for item in macros {
|
||||||
|
let macro_path = &item.path.segments[0].ident;
|
||||||
|
|
||||||
|
// this macro is inside the last macro we parsed, skip it
|
||||||
|
if macro_path.span().start() < end_span {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure the marker is not nested
|
// item.parse_body::<CallBody>();
|
||||||
if start < last_bracket_end {
|
let body = item.parse_body::<CallBody>().unwrap();
|
||||||
continue;
|
|
||||||
|
let rsx_start = macro_path.span().start();
|
||||||
|
|
||||||
|
writer.out.indent = &writer.src[rsx_start.line - 1]
|
||||||
|
.chars()
|
||||||
|
.take_while(|c| *c == ' ')
|
||||||
|
.count()
|
||||||
|
/ 4;
|
||||||
|
|
||||||
|
write_body(&mut writer, &body);
|
||||||
|
|
||||||
|
// writing idents leaves the final line ended at the end of the last ident
|
||||||
|
if writer.out.buf.contains('\n') {
|
||||||
|
writer.out.new_line().unwrap();
|
||||||
|
writer.out.tab().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut indent_level = {
|
let span = match item.delimiter {
|
||||||
// walk backwards from start until we find a new line
|
MacroDelimiter::Paren(b) => b.span,
|
||||||
let mut lines = contents[..start].lines().rev();
|
MacroDelimiter::Brace(b) => b.span,
|
||||||
match lines.next() {
|
MacroDelimiter::Bracket(b) => b.span,
|
||||||
Some(line) => {
|
|
||||||
if line.starts_with("//") || line.starts_with("///") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
line.chars().take_while(|c| *c == ' ').count() / 4
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let remaining = &contents[end - 1..];
|
let mut formatted = String::new();
|
||||||
let bracket_end = find_bracket_end(remaining).unwrap();
|
|
||||||
let sub_string = &contents[end..bracket_end + end - 1];
|
|
||||||
last_bracket_end = bracket_end + end - 1;
|
|
||||||
|
|
||||||
let mut new = fmt_block(sub_string, indent_level).unwrap();
|
std::mem::swap(&mut formatted, &mut writer.out.buf);
|
||||||
|
|
||||||
if new.len() <= 80 && !new.contains('\n') {
|
let start = byte_offset(contents, span.start()) + 1;
|
||||||
new = format!(" {new} ");
|
let end = byte_offset(contents, span.end()) - 1;
|
||||||
|
|
||||||
// if the new string is not multiline, don't try to adjust the marker ending
|
// Rustfmt will remove the space between the macro and the opening paren if the macro is a single expression
|
||||||
// We want to trim off any indentation that there might be
|
let body_is_solo_expr = body.roots.len() == 1
|
||||||
indent_level = 0;
|
&& matches!(body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_));
|
||||||
|
|
||||||
|
if formatted.len() <= 80 && !formatted.contains('\n') && !body_is_solo_expr {
|
||||||
|
formatted = format!(" {formatted} ");
|
||||||
}
|
}
|
||||||
|
|
||||||
let end_marker = end + bracket_end - indent_level * 4 - 1;
|
end_span = span.end();
|
||||||
|
|
||||||
if new == contents[end..end_marker] {
|
if contents[start..end] == formatted {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted_blocks.push(FormattedBlock {
|
formatted_blocks.push(FormattedBlock {
|
||||||
formatted: new,
|
formatted,
|
||||||
start: end,
|
start,
|
||||||
end: end_marker,
|
end,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted_blocks
|
formatted_blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
|
pub fn write_block_out(body: CallBody) -> Option<String> {
|
||||||
let mut buf = Buffer {
|
let mut buf = Writer::new("");
|
||||||
src: block.lines().map(|f| f.to_string()).collect(),
|
|
||||||
indent: indent_level,
|
|
||||||
..Buffer::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
|
write_body(&mut buf, &body);
|
||||||
|
|
||||||
|
buf.consume()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_body(buf: &mut Writer, body: &CallBody) {
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
// Oneliner optimization
|
|
||||||
if buf.is_short_children(&body.roots).is_some() {
|
if buf.is_short_children(&body.roots).is_some() {
|
||||||
buf.write_ident(&body.roots[0]).unwrap();
|
// write all the indents with spaces and commas between
|
||||||
|
for idx in 0..body.roots.len() - 1 {
|
||||||
|
let ident = &body.roots[idx];
|
||||||
|
buf.write_ident(ident).unwrap();
|
||||||
|
write!(&mut buf.out.buf, ", ").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the last ident without a comma
|
||||||
|
let ident = &body.roots[body.roots.len() - 1];
|
||||||
|
buf.write_ident(ident).unwrap();
|
||||||
} else {
|
} else {
|
||||||
buf.write_body_indented(&body.roots).unwrap();
|
buf.write_body_indented(&body.roots).unwrap();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
|
||||||
|
let body = syn::parse2::<CallBody>(expr.mac.tokens).unwrap();
|
||||||
|
|
||||||
|
let mut buf = Writer::new(raw);
|
||||||
|
|
||||||
|
write_body(&mut buf, &body);
|
||||||
|
|
||||||
|
buf.consume()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
|
||||||
|
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
|
||||||
|
|
||||||
|
let mut buf = Writer::new(block);
|
||||||
|
|
||||||
|
buf.out.indent = indent_level;
|
||||||
|
|
||||||
|
write_body(&mut buf, &body);
|
||||||
|
|
||||||
// writing idents leaves the final line ended at the end of the last ident
|
// writing idents leaves the final line ended at the end of the last ident
|
||||||
if buf.buf.contains('\n') {
|
if buf.out.buf.contains('\n') {
|
||||||
buf.new_line().unwrap();
|
buf.out.new_line().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.consume()
|
buf.consume()
|
||||||
|
@ -124,8 +182,6 @@ pub fn apply_format(input: &str, block: FormattedBlock) -> String {
|
||||||
let (left, _) = input.split_at(start);
|
let (left, _) = input.split_at(start);
|
||||||
let (_, right) = input.split_at(end);
|
let (_, right) = input.split_at(end);
|
||||||
|
|
||||||
// dbg!(&block.formatted);
|
|
||||||
|
|
||||||
format!("{}{}{}", left, block.formatted, right)
|
format!("{}{}{}", left, block.formatted, right)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue