Merge branch 'master' into make-widgets-cross-framework
|
@ -0,0 +1,8 @@
|
|||
ARG VARIANT="nightly-bookworm-slim"
|
||||
FROM rustlang/rust:${VARIANT}
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get -qq install build-essential libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
|
|
@ -0,0 +1,26 @@
|
|||
# Dev Container
|
||||
|
||||
A dev container in the most simple context allows one to create a consistent development environment within a docker container that can easily be opened locally or remotely via codespaces such that contributors don't need to install anything to contribute.
|
||||
|
||||
## Useful Links
|
||||
|
||||
- <https://code.visualstudio.com/docs/devcontainers/containers>
|
||||
- <https://containers.dev/>
|
||||
- <https://github.com/devcontainers>
|
||||
- <https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers>
|
||||
|
||||
## Using A Dev Container
|
||||
|
||||
### Locally
|
||||
|
||||
To use this dev container locally, make sure Docker is installed and in VSCode install the `ms-vscode-remote.remote-containers` extension. Then from the root of Dioxus you can type `Ctrl + Shift + P`, then choose `Dev Containers: Rebuild and Reopen in Devcontainer`.
|
||||
|
||||
### Codespaces
|
||||
|
||||
[Codespaces Setup](https://docs.github.com/en/codespaces/developing-in-codespaces/creating-a-codespace-for-a-repository#creating-a-codespace-for-a-repository)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If having difficulty commiting with github, and you use ssh or gpg keys, you may need to ensure that the keys are being shared properly between your host and VSCode.
|
||||
|
||||
Though VSCode does a pretty good job sharing credentials between host and devcontainer, to save some time you can always just reopen the container locally to commit with `Ctrl + Shift + P`, then choose `Dev Containers: Reopen Folder Locally`
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "dioxus",
|
||||
"remoteUser": "vscode",
|
||||
"build": {
|
||||
"dockerfile": "./Dockerfile",
|
||||
"context": "."
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/common-utils:2": {
|
||||
"installZsh": "true",
|
||||
"username": "vscode",
|
||||
"uid": "1000",
|
||||
"gid": "1000",
|
||||
"upgradePackages": "true"
|
||||
}
|
||||
},
|
||||
"containerEnv": {
|
||||
"RUST_LOG": "INFO"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"files.watcherExclude": {
|
||||
"**/target/**": true
|
||||
},
|
||||
"[rust]": {
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
},
|
||||
"extensions": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"tamasfe.even-better-toml",
|
||||
"serayuzgur.crates"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.2
|
||||
with:
|
||||
branch: gh-pages # The branch the action should deploy to.
|
||||
folder: docs/nightly # The folder the action should deploy.
|
||||
|
|
|
@ -34,7 +34,7 @@ jobs:
|
|||
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.2
|
||||
with:
|
||||
branch: gh-pages # The branch the action should deploy to.
|
||||
folder: docs/nightly # The folder the action should deploy.
|
||||
|
|
|
@ -30,14 +30,7 @@ jobs:
|
|||
name: Test Suite
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all --tests
|
||||
- run: cargo test --all --tests
|
||||
|
|
|
@ -32,30 +32,19 @@ jobs:
|
|||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: sudo apt-get update
|
||||
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all --examples --tests
|
||||
- run: cargo check --all --examples --tests
|
||||
|
||||
test:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: sudo apt-get update
|
||||
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
|
||||
|
@ -63,48 +52,31 @@ jobs:
|
|||
- uses: browser-actions/setup-firefox@latest
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: make
|
||||
args: tests
|
||||
- run: cargo make tests
|
||||
|
||||
fmt:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: sudo apt-get update
|
||||
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
|
||||
- run: rustup component add clippy
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace --examples --tests -- -D warnings
|
||||
- run: cargo clippy --workspace --examples --tests -- -D warnings
|
||||
|
||||
# Coverage is disabled until we can fix it
|
||||
# coverage:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Jonathan Kelley <jkelleyrtp@gmail.com> <jkelleyrtp@gmail.com>
|
58
Cargo.toml
|
@ -22,9 +22,53 @@ members = [
|
|||
"packages/rsx-rosetta",
|
||||
"packages/signals",
|
||||
"packages/hot-reload",
|
||||
"packages/fullstack",
|
||||
"packages/fullstack/server-macro",
|
||||
"packages/fullstack/examples/axum-hello-world",
|
||||
"packages/fullstack/examples/axum-router",
|
||||
"packages/fullstack/examples/axum-desktop",
|
||||
"packages/fullstack/examples/salvo-hello-world",
|
||||
"packages/fullstack/examples/warp-hello-world",
|
||||
"docs/guide",
|
||||
# Full project examples
|
||||
"examples/tailwind",
|
||||
"examples/PWA-example",
|
||||
]
|
||||
|
||||
# dependencies that are shared across packages
|
||||
[workspace.dependencies]
|
||||
dioxus = { path = "packages/dioxus" }
|
||||
dioxus-core = { path = "packages/core" }
|
||||
dioxus-core-macro = { path = "packages/core-macro" }
|
||||
dioxus-router = { path = "packages/router" }
|
||||
dioxus-html = { path = "packages/html" }
|
||||
dioxus-hooks = { path = "packages/hooks" }
|
||||
dioxus-web = { path = "packages/web" }
|
||||
dioxus-ssr = { path = "packages/ssr" }
|
||||
dioxus-desktop = { path = "packages/desktop" }
|
||||
dioxus-mobile = { path = "packages/mobile" }
|
||||
dioxus-interpreter-js = { path = "packages/interpreter" }
|
||||
fermi = { path = "packages/fermi" }
|
||||
dioxus-liveview = { path = "packages/liveview" }
|
||||
dioxus-autofmt = { path = "packages/autofmt" }
|
||||
dioxus-rsx = { path = "packages/rsx" }
|
||||
dioxus-tui = { path = "packages/dioxus-tui" }
|
||||
rink = { path = "packages/rink" }
|
||||
dioxus-native-core = { path = "packages/native-core" }
|
||||
dioxus-native-core-macro = { path = "packages/native-core-macro" }
|
||||
dioxus-rsx-rosetta = { path = "packages/rsx-rosetta" }
|
||||
dioxus-signals = { path = "packages/signals" }
|
||||
dioxus-hot-reload = { path = "packages/hot-reload" }
|
||||
dioxus-fullstack = { path = "packages/fullstack" }
|
||||
dioxus_server_macro = { path = "packages/fullstack/server-macro" }
|
||||
log = "0.4.19"
|
||||
tokio = "1.28"
|
||||
slab = "0.4.2"
|
||||
futures-channel = "0.3.21"
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
rustc-hash = "1.1.0"
|
||||
wasm-bindgen = "0.2.79"
|
||||
|
||||
# This is a "virtual package"
|
||||
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
|
||||
[package]
|
||||
|
@ -42,12 +86,12 @@ rust-version = "1.60.0"
|
|||
publish = false
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { path = "./packages/dioxus" }
|
||||
dioxus-desktop = { path = "./packages/desktop", features = ["transparent"] }
|
||||
dioxus-ssr = { path = "./packages/ssr" }
|
||||
dioxus-router = { path = "./packages/router" }
|
||||
dioxus-signals = { path = "./packages/signals" }
|
||||
fermi = { path = "./packages/fermi" }
|
||||
dioxus = { workspace = true }
|
||||
dioxus-desktop = { workspace = true, features = ["transparent"] }
|
||||
dioxus-ssr = { workspace = true }
|
||||
dioxus-router = { workspace = true }
|
||||
dioxus-signals = { workspace = true }
|
||||
fermi = { workspace = true }
|
||||
futures-util = "0.3.21"
|
||||
log = "0.4.14"
|
||||
num-format = "0.4.0"
|
||||
|
@ -61,7 +105,7 @@ tokio = { version = "1.16.1", features = ["full"] }
|
|||
reqwest = { version = "0.11.9", features = ["json"] }
|
||||
fern = { version = "0.6.0", features = ["colored"] }
|
||||
thiserror = "1.0.30"
|
||||
env_logger = "0.9.0"
|
||||
env_logger = "0.10.0"
|
||||
simple_logger = "4.0.0"
|
||||
|
||||
[profile.release]
|
||||
|
|
|
@ -42,10 +42,10 @@ private = true
|
|||
[tasks.test]
|
||||
dependencies = ["build"]
|
||||
command = "cargo"
|
||||
args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router"]
|
||||
args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router", "--exclude", "dioxus-desktop"]
|
||||
private = true
|
||||
|
||||
[tasks.test-with-browser]
|
||||
env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router"] }
|
||||
env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router", "**/packages/desktop"] }
|
||||
private = true
|
||||
workspace = true
|
||||
|
|
|
@ -64,7 +64,7 @@ fn app(cx: Scope) -> Element {
|
|||
}
|
||||
```
|
||||
|
||||
Dioxus can be used to deliver webapps, desktop apps, static sites, mobile apps, TUI apps, liveview apps, and more. Dioxus is entirely renderer agnostic and can be used as platform for any renderer.
|
||||
Dioxus can be used to deliver webapps, desktop apps, static sites, mobile apps, TUI apps, liveview apps, and more. Dioxus is entirely renderer agnostic and can be used as a platform for any renderer.
|
||||
|
||||
If you know React, then you already know Dioxus.
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# Building the Documentation
|
||||
|
||||
Dioxus uses a fork of MdBook with multilanguage support. To build the documentation, you will need to install the forked version of MdBook.
|
||||
|
||||
```sh
|
||||
cargo install mdbook --git https://github.com/Demonthos/mdBook.git --branch master
|
||||
```
|
||||
|
||||
Then, you can build the documentation by running:
|
||||
|
||||
```sh
|
||||
cd docs
|
||||
cd guide
|
||||
mdbook build -d ../nightly/guide
|
||||
cd ..
|
||||
cd router
|
||||
mdbook build -d ../nightly/router
|
||||
cd ../../
|
||||
```
|
|
@ -16,12 +16,12 @@ dioxus-native-core-macro = { path = "../../packages/native-core-macro" }
|
|||
dioxus-router = { path = "../../packages/router" }
|
||||
dioxus-liveview = { path = "../../packages/liveview", features = ["axum"] }
|
||||
dioxus-tui = { path = "../../packages/dioxus-tui" }
|
||||
dioxus-fullstack = { path = "../../packages/fullstack" }
|
||||
# dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] }
|
||||
fermi = { path = "../../packages/fermi" }
|
||||
shipyard = "0.6.2"
|
||||
|
||||
|
||||
# dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] }
|
||||
serde = { version = "1.0.138", features=["derive"] }
|
||||
reqwest = { version = "0.11.11", features = ["json"] }
|
||||
tokio = { version = "1.19.2" , features=[]}
|
||||
tokio = { version = "1.19.2", features = ["full"] }
|
||||
axum = { version = "0.6.1", features = ["ws"] }
|
||||
gloo-storage = "0.2.2"
|
||||
|
|
|
@ -10,8 +10,7 @@ fn App(cx: Scope) -> Element {
|
|||
// ANCHOR: prevent_default
|
||||
cx.render(rsx! {
|
||||
input {
|
||||
prevent_default: "oninput",
|
||||
prevent_default: "onclick",
|
||||
prevent_default: "oninput onclick",
|
||||
}
|
||||
})
|
||||
// ANCHOR_END: prevent_default
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
#![allow(unused)]
|
||||
#![allow(non_snake_case)]
|
||||
// ANCHOR: all
|
||||
|
||||
// ANCHOR: main
|
||||
#![allow(non_snake_case)]
|
||||
use axum::{response::Html, routing::get, Router};
|
||||
// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
println!("listening on http://{}", addr);
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
Router::new()
|
||||
.route("/", get(app_endpoint))
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// ANCHOR_END: main
|
||||
|
||||
// ANCHOR: endpoint
|
||||
async fn app_endpoint() -> Html<String> {
|
||||
// render the rsx! macro to HTML
|
||||
Html(dioxus_ssr::render_lazy(rsx! {
|
||||
div { "hello world!" }
|
||||
}))
|
||||
}
|
||||
// ANCHOR_END: endpoint
|
||||
|
||||
// ANCHOR: second_endpoint
|
||||
async fn second_app_endpoint() -> Html<String> {
|
||||
// create a component that renders a div with the text "hello world"
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx!(div { "hello world" }))
|
||||
}
|
||||
// create a VirtualDom with the app component
|
||||
let mut app = VirtualDom::new(app);
|
||||
// rebuild the VirtualDom before rendering
|
||||
let _ = app.rebuild();
|
||||
|
||||
// render the VirtualDom to HTML
|
||||
Html(dioxus_ssr::render(&app))
|
||||
}
|
||||
// ANCHOR_END: second_endpoint
|
||||
|
||||
// ANCHOR: component
|
||||
// define a component that renders a div with the text "Hello, world!"
|
||||
fn App(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
"Hello, world!"
|
||||
}
|
||||
})
|
||||
}
|
||||
// ANCHOR_END: component
|
||||
// ANCHOR_END: all
|
|
@ -0,0 +1,38 @@
|
|||
#![allow(unused)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {}
|
||||
|
||||
// ANCHOR: non_clone_state
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct UseState<'a, T> {
|
||||
value: &'a RefCell<T>,
|
||||
update: Arc<dyn Fn()>,
|
||||
}
|
||||
|
||||
fn my_use_state<T: 'static>(cx: &ScopeState, init: impl FnOnce() -> T) -> UseState<T> {
|
||||
// The update function will trigger a re-render in the component cx is attached to
|
||||
let update = cx.schedule_update();
|
||||
// Create the initial state
|
||||
let value = cx.use_hook(|| RefCell::new(init()));
|
||||
|
||||
UseState { value, update }
|
||||
}
|
||||
|
||||
impl<T: Clone> UseState<'_, T> {
|
||||
fn get(&self) -> T {
|
||||
self.value.borrow().clone()
|
||||
}
|
||||
|
||||
fn set(&self, value: T) {
|
||||
// Update the state
|
||||
*self.value.borrow_mut() = value;
|
||||
// Trigger a re-render on the component the state is from
|
||||
(self.update)();
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: non_clone_state
|
|
@ -11,3 +11,57 @@ fn use_settings(cx: &ScopeState) -> &UseSharedState<AppSettings> {
|
|||
use_shared_state::<AppSettings>(cx).expect("App settings not provided")
|
||||
}
|
||||
// ANCHOR_END: wrap_context
|
||||
|
||||
// ANCHOR: use_storage
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
/// A persistent storage hook that can be used to store data across application reloads.
|
||||
#[allow(clippy::needless_return)]
|
||||
pub fn use_persistent<T: Serialize + DeserializeOwned + Default + 'static>(
|
||||
cx: &ScopeState,
|
||||
// A unique key for the storage entry
|
||||
key: impl ToString,
|
||||
// A function that returns the initial value if the storage entry is empty
|
||||
init: impl FnOnce() -> T,
|
||||
) -> &UsePersistent<T> {
|
||||
// Use the use_ref hook to create a mutable state for the storage entry
|
||||
let state = use_ref(cx, move || {
|
||||
// This closure will run when the hook is created
|
||||
let key = key.to_string();
|
||||
let value = LocalStorage::get(key.as_str()).ok().unwrap_or_else(init);
|
||||
StorageEntry { key, value }
|
||||
});
|
||||
|
||||
// Wrap the state in a new struct with a custom API
|
||||
// Note: We use use_hook here so that this hook is easier to use in closures in the rsx. Any values with the same lifetime as the ScopeState can be used in the closure without cloning.
|
||||
cx.use_hook(|| UsePersistent {
|
||||
inner: state.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
struct StorageEntry<T> {
|
||||
key: String,
|
||||
value: T,
|
||||
}
|
||||
|
||||
/// Storage that persists across application reloads
|
||||
pub struct UsePersistent<T: 'static> {
|
||||
inner: UseRef<StorageEntry<T>>,
|
||||
}
|
||||
|
||||
impl<T: Serialize + DeserializeOwned + Clone + 'static> UsePersistent<T> {
|
||||
/// Returns a reference to the value
|
||||
pub fn get(&self) -> T {
|
||||
self.inner.read().value.clone()
|
||||
}
|
||||
|
||||
/// Sets the value
|
||||
pub fn set(&self, value: T) {
|
||||
let mut inner = self.inner.write();
|
||||
// Write the new value to local storage
|
||||
LocalStorage::set(inner.key.as_str(), &value);
|
||||
inner.value = value;
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: use_storage
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
#![allow(unused)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {}
|
||||
|
||||
// ANCHOR: use_state
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct UseState<T> {
|
||||
value: Rc<RefCell<T>>,
|
||||
update: Arc<dyn Fn()>,
|
||||
}
|
||||
|
||||
fn my_use_state<T: 'static>(cx: &ScopeState, init: impl FnOnce() -> T) -> &UseState<T> {
|
||||
cx.use_hook(|| {
|
||||
// The update function will trigger a re-render in the component cx is attached to
|
||||
let update = cx.schedule_update();
|
||||
// Create the initial state
|
||||
let value = Rc::new(RefCell::new(init()));
|
||||
|
||||
UseState { value, update }
|
||||
})
|
||||
}
|
||||
|
||||
impl<T: Clone> UseState<T> {
|
||||
fn get(&self) -> T {
|
||||
self.value.borrow().clone()
|
||||
}
|
||||
|
||||
fn set(&self, value: T) {
|
||||
// Update the state
|
||||
*self.value.borrow_mut() = value;
|
||||
// Trigger a re-render on the component the state is from
|
||||
(self.update)();
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: use_state
|
||||
|
||||
// ANCHOR: use_context
|
||||
pub fn use_context<T: 'static + Clone>(cx: &ScopeState) -> Option<&T> {
|
||||
cx.use_hook(|| cx.consume_context::<T>()).as_ref()
|
||||
}
|
||||
|
||||
pub fn use_context_provider<T: 'static + Clone>(cx: &ScopeState, f: impl FnOnce() -> T) -> &T {
|
||||
cx.use_hook(|| {
|
||||
let val = f();
|
||||
// Provide the context state to the scope
|
||||
cx.provide_context(val.clone());
|
||||
val
|
||||
})
|
||||
}
|
||||
|
||||
// ANCHOR_END: use_context
|
|
@ -0,0 +1,34 @@
|
|||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use dioxus_fullstack::prelude::*;
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
axum::Router::new()
|
||||
.serve_dioxus_application("", ServeConfigBuilder::new(app, ()))
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut count = use_state(cx, || 0);
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
})
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
dioxus_web::launch_with_props(
|
||||
app,
|
||||
// Get the root props from the document
|
||||
get_root_props_from_document().unwrap_or_default(),
|
||||
dioxus_web::Config::new().hydrate(true),
|
||||
);
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use axum::extract::Path;
|
||||
use axum::extract::State;
|
||||
use axum::routing::get;
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
axum::Router::new()
|
||||
// Serve the dist folder with the static javascript and WASM files created by the dixous CLI
|
||||
.serve_static_assets("./dist")
|
||||
// Register server functions
|
||||
.register_server_fns("")
|
||||
// Connect to the hot reload server in debug mode
|
||||
.connect_hot_reload()
|
||||
// Render the application. This will serialize the root props (the intial count) into the HTML
|
||||
.route(
|
||||
"/",
|
||||
get(move | State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
|
||||
ssr_state.render(
|
||||
&ServeConfigBuilder::new(
|
||||
app,
|
||||
0,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
)}),
|
||||
)
|
||||
// Render the application with a different intial count
|
||||
.route(
|
||||
"/:initial_count",
|
||||
get(move |Path(intial_count): Path<usize>, State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
|
||||
ssr_state.render(
|
||||
&ServeConfigBuilder::new(
|
||||
app,
|
||||
intial_count,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
)}),
|
||||
)
|
||||
.with_state(SSRState::default())
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope<usize>) -> Element {
|
||||
let mut count = use_state(cx, || *cx.props);
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
})
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut count = use_state(cx, || 0);
|
||||
|
||||
cx.render(
|
||||
// rsx expands to LazyNodes::new
|
||||
::dioxus::core::LazyNodes::new(
|
||||
move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode {
|
||||
// The template is every static part of the rsx
|
||||
static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template {
|
||||
// This is the source location of the rsx that generated this template. This is used to make hot rsx reloading work. Hot rsx reloading just replaces the template with a new one generated from the rsx by the CLI.
|
||||
name: "examples\\readme.rs:14:15:250",
|
||||
// The root nodes are the top level nodes of the rsx
|
||||
roots: &[
|
||||
// The h1 node
|
||||
::dioxus::core::TemplateNode::Element {
|
||||
// Find the built in h1 tag in the dioxus_elements crate exported by the dioxus html crate
|
||||
tag: dioxus_elements::h1::TAG_NAME,
|
||||
namespace: dioxus_elements::h1::NAME_SPACE,
|
||||
attrs: &[],
|
||||
// The children of the h1 node
|
||||
children: &[
|
||||
// The dynamic count text node
|
||||
// Any nodes that are dynamic have a dynamic placeholder with a unique index
|
||||
::dioxus::core::TemplateNode::DynamicText {
|
||||
// This index is used to find what element in `dynamic_nodes` to use instead of the placeholder
|
||||
id: 0usize,
|
||||
},
|
||||
],
|
||||
},
|
||||
// The up high button node
|
||||
::dioxus::core::TemplateNode::Element {
|
||||
tag: dioxus_elements::button::TAG_NAME,
|
||||
namespace: dioxus_elements::button::NAME_SPACE,
|
||||
attrs: &[
|
||||
// The dynamic onclick listener attribute
|
||||
// Any attributes that are dynamic have a dynamic placeholder with a unique index.
|
||||
::dioxus::core::TemplateAttribute::Dynamic {
|
||||
// Similar to dynamic nodes, dynamic attributes have a unique index used to find the attribute in `dynamic_attrs` to use instead of the placeholder
|
||||
id: 0usize,
|
||||
},
|
||||
],
|
||||
children: &[::dioxus::core::TemplateNode::Text { text: "Up high!" }],
|
||||
},
|
||||
// The down low button node
|
||||
::dioxus::core::TemplateNode::Element {
|
||||
tag: dioxus_elements::button::TAG_NAME,
|
||||
namespace: dioxus_elements::button::NAME_SPACE,
|
||||
attrs: &[
|
||||
// The dynamic onclick listener attribute
|
||||
::dioxus::core::TemplateAttribute::Dynamic { id: 1usize },
|
||||
],
|
||||
children: &[::dioxus::core::TemplateNode::Text { text: "Down low!" }],
|
||||
},
|
||||
],
|
||||
// Node paths is a list of paths to every dynamic node in the rsx
|
||||
node_paths: &[
|
||||
// The first node path is the path to the dynamic node with an id of 0 (the count text node)
|
||||
&[
|
||||
// Go to the index 0 root node
|
||||
0u8,
|
||||
//
|
||||
// Go to the first child of the root node
|
||||
0u8,
|
||||
],
|
||||
],
|
||||
// Attr paths is a list of paths to every dynamic attribute in the rsx
|
||||
attr_paths: &[
|
||||
// The first attr path is the path to the dynamic attribute with an id of 0 (the up high button onclick listener)
|
||||
&[
|
||||
// Go to the index 1 root node
|
||||
1u8,
|
||||
],
|
||||
// The second attr path is the path to the dynamic attribute with an id of 1 (the down low button onclick listener)
|
||||
&[
|
||||
// Go to the index 2 root node
|
||||
2u8,
|
||||
],
|
||||
],
|
||||
};
|
||||
// The VNode is a reference to the template with the dynamic parts of the rsx
|
||||
::dioxus::core::VNode {
|
||||
parent: None,
|
||||
key: None,
|
||||
// The static template this node will use. The template is stored in a Cell so it can be replaced with a new template when hot rsx reloading is enabled
|
||||
template: std::cell::Cell::new(TEMPLATE),
|
||||
root_ids: Default::default(),
|
||||
dynamic_nodes: __cx.bump().alloc([
|
||||
// The dynamic count text node (dynamic node id 0)
|
||||
__cx.text_node(format_args!("High-Five counter: {0}", count)),
|
||||
]),
|
||||
dynamic_attrs: __cx.bump().alloc([
|
||||
// The dynamic up high button onclick listener (dynamic attribute id 0)
|
||||
dioxus_elements::events::onclick(__cx, move |_| count += 1),
|
||||
// The dynamic down low button onclick listener (dynamic attribute id 1)
|
||||
dioxus_elements::events::onclick(__cx, move |_| count -= 1),
|
||||
]),
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use dioxus_fullstack::prelude::*;
|
||||
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
axum::Router::new()
|
||||
.serve_dioxus_application("", ServeConfigBuilder::new(app, ()))
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut count = use_state(cx, || 0);
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
})
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
dioxus_web::launch_with_props(
|
||||
app,
|
||||
// Get the root props from the document
|
||||
get_root_props_from_document().unwrap_or_default(),
|
||||
dioxus_web::Config::new().hydrate(true),
|
||||
);
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use axum::extract::Path;
|
||||
use axum::extract::State;
|
||||
use axum::routing::get;
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
axum::Router::new()
|
||||
// Serve the dist folder with the static javascript and WASM files created by the dixous CLI
|
||||
.serve_static_assets("./dist")
|
||||
// Register server functions
|
||||
.register_server_fns("")
|
||||
// Connect to the hot reload server in debug mode
|
||||
.connect_hot_reload()
|
||||
// Render the application. This will serialize the root props (the intial count) into the HTML
|
||||
.route(
|
||||
"/",
|
||||
get(move |State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
|
||||
ssr_state.render(
|
||||
&ServeConfigBuilder::new(
|
||||
app,
|
||||
0,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
)}),
|
||||
)
|
||||
// Render the application with a different intial count
|
||||
.route(
|
||||
"/:initial_count",
|
||||
get(move |Path(intial_count): Path<usize>, State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
|
||||
ssr_state.render(
|
||||
&ServeConfigBuilder::new(
|
||||
app,
|
||||
intial_count,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
)}),
|
||||
)
|
||||
.with_state(SSRState::default())
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope<usize>) -> Element {
|
||||
let mut count = use_state(cx, || *cx.props);
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![count];
|
||||
let sc = cx.sc();
|
||||
async move {
|
||||
// Call the server function just like a local async function
|
||||
if let Ok(new_count) = double_server(sc, *count.current()).await {
|
||||
count.set(new_count);
|
||||
}
|
||||
}
|
||||
},
|
||||
"Double"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// We use the "getcbor" encoding to make caching easier
|
||||
#[server(DoubleServer, "", "getcbor")]
|
||||
async fn double_server(cx: DioxusServerContext, number: usize) -> Result<usize, ServerFnError> {
|
||||
// Perform some expensive computation or access a database on the server
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
let result = number * 2;
|
||||
|
||||
println!(
|
||||
"User Agent {:?}",
|
||||
cx.request_parts().headers.get("User-Agent")
|
||||
);
|
||||
|
||||
// Set the cache control header to 1 hour on the post request
|
||||
cx.response_headers_mut()
|
||||
.insert("Cache-Control", "max-age=3600".parse().unwrap());
|
||||
|
||||
println!("server calculated {result}");
|
||||
|
||||
Ok(result)
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[derive(Default, Clone)]
|
||||
struct ServerFunctionState {
|
||||
call_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
dioxus_web::launch_with_props(
|
||||
app,
|
||||
// Get the root props from the document
|
||||
get_root_props_from_document().unwrap_or_default(),
|
||||
dioxus_web::Config::new().hydrate(true),
|
||||
);
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use axum::body::Body;
|
||||
use axum::extract::Path;
|
||||
use axum::extract::State;
|
||||
use axum::http::Request;
|
||||
use axum::routing::get;
|
||||
use std::sync::Arc;
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
axum::Router::new()
|
||||
// Serve the dist folder with the static javascript and WASM files created by the dixous CLI
|
||||
.serve_static_assets("./dist")
|
||||
// Register server functions
|
||||
.register_server_fns_with_handler("", |func| {
|
||||
move |State(server_fn_state): State<ServerFunctionState>, req: Request<Body>| async move {
|
||||
let (parts, body) = req.into_parts();
|
||||
let parts: Arc<RequestParts> = Arc::new(parts.into());
|
||||
let mut server_context = DioxusServerContext::new(parts.clone());
|
||||
server_context.insert(server_fn_state);
|
||||
server_fn_handler(server_context, func.clone(), parts, body).await
|
||||
}
|
||||
})
|
||||
.with_state(ServerFunctionState::default())
|
||||
// Connect to the hot reload server in debug mode
|
||||
.connect_hot_reload()
|
||||
// Render the application. This will serialize the root props (the intial count) into the HTML
|
||||
.route(
|
||||
"/",
|
||||
get(move |State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
|
||||
ssr_state.render(
|
||||
&ServeConfigBuilder::new(
|
||||
app,
|
||||
0,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
)}),
|
||||
)
|
||||
// Render the application with a different intial count
|
||||
.route(
|
||||
"/:initial_count",
|
||||
get(move |Path(intial_count): Path<usize>, State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
|
||||
ssr_state.render(
|
||||
&ServeConfigBuilder::new(
|
||||
app,
|
||||
intial_count,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
)}),
|
||||
)
|
||||
.with_state(SSRState::default())
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope<usize>) -> Element {
|
||||
let mut count = use_state(cx, || *cx.props);
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![count];
|
||||
let sc = cx.sc();
|
||||
async move {
|
||||
// Call the server function just like a local async function
|
||||
if let Ok(new_count) = double_server(sc, *count.current()).await {
|
||||
count.set(new_count);
|
||||
}
|
||||
}
|
||||
},
|
||||
"Double"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// We use the "getcbor" encoding to make caching easier
|
||||
#[server(DoubleServer, "", "getcbor")]
|
||||
async fn double_server(cx: DioxusServerContext, number: usize) -> Result<usize, ServerFnError> {
|
||||
// Perform some expensive computation or access a database on the server
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
let result = number * 2;
|
||||
|
||||
println!(
|
||||
"User Agent {:?}",
|
||||
cx.request_parts().headers.get("User-Agent")
|
||||
);
|
||||
|
||||
// Set the cache control header to 1 hour on the post request
|
||||
cx.response_headers_mut()
|
||||
.insert("Cache-Control", "max-age=3600".parse().unwrap());
|
||||
|
||||
// Get the server function state
|
||||
let server_fn_state = cx.get::<ServerFunctionState>().unwrap();
|
||||
let call_count = server_fn_state
|
||||
.call_count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
println!("server functions have been called {call_count} times");
|
||||
|
||||
println!("server calculated {result}");
|
||||
|
||||
Ok(result)
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
dioxus_web::launch_with_props(
|
||||
app,
|
||||
// Get the root props from the document
|
||||
get_root_props_from_document().unwrap_or_default(),
|
||||
dioxus_web::Config::new().hydrate(true),
|
||||
);
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use axum::extract::Path;
|
||||
use axum::extract::State;
|
||||
use axum::routing::get;
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
axum::Router::new()
|
||||
// Serve the dist folder with the static javascript and WASM files created by the dixous CLI
|
||||
.serve_static_assets("./dist")
|
||||
// Register server functions
|
||||
.register_server_fns("")
|
||||
// Connect to the hot reload server in debug mode
|
||||
.connect_hot_reload()
|
||||
// Render the application. This will serialize the root props (the intial count) into the HTML
|
||||
.route(
|
||||
"/",
|
||||
get(move |Path(intial_count): Path<usize>, State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
|
||||
ssr_state.render(
|
||||
&ServeConfigBuilder::new(
|
||||
app,
|
||||
intial_count,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
)}),
|
||||
)
|
||||
// Render the application with a different intial count
|
||||
.route(
|
||||
"/:initial_count",
|
||||
get(move |Path(intial_count): Path<usize>, State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
|
||||
ssr_state.render(
|
||||
&ServeConfigBuilder::new(
|
||||
app,
|
||||
intial_count,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
)}),
|
||||
)
|
||||
.with_state(SSRState::default())
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope<usize>) -> Element {
|
||||
let mut count = use_state(cx, || *cx.props);
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![count];
|
||||
async move {
|
||||
// Call the server function just like a local async function
|
||||
if let Ok(new_count) = double_server(*count.current()).await {
|
||||
count.set(new_count);
|
||||
}
|
||||
}
|
||||
},
|
||||
"Double"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[server(DoubleServer)]
|
||||
async fn double_server(number: usize) -> Result<usize, ServerFnError> {
|
||||
// Perform some expensive computation or access a database on the server
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
let result = number * 2;
|
||||
println!("server calculated {result}");
|
||||
Ok(result)
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
- [Desktop](getting_started/desktop.md)
|
||||
- [Web](getting_started/web.md)
|
||||
- [Server-Side Rendering](getting_started/ssr.md)
|
||||
- [Fullstack](getting_started/fullstack.md)
|
||||
- [Liveview](getting_started/liveview.md)
|
||||
- [Terminal UI](getting_started/tui.md)
|
||||
- [Mobile](getting_started/mobile.md)
|
||||
|
@ -31,14 +32,24 @@
|
|||
- [Error Handling](best_practices/error_handling.md)
|
||||
- [Antipatterns](best_practices/antipatterns.md)
|
||||
- [Publishing](publishing/index.md)
|
||||
|
||||
- [Desktop](publishing/desktop.md)
|
||||
- [Web](publishing/web.md)
|
||||
|
||||
---
|
||||
|
||||
- [Fullstack](fullstack/index.md)
|
||||
- [Getting Started](fullstack/getting_started.md)
|
||||
- [Communicating with the Server](fullstack/server_functions.md)
|
||||
|
||||
---
|
||||
|
||||
- [Custom Renderer](custom_renderer/index.md)
|
||||
|
||||
---
|
||||
|
||||
[Roadmap](roadmap.md)
|
||||
[Contributing](contributing.md)
|
||||
- [Contributing](contributing/index.md)
|
||||
- [Project Structure](contributing/project_structure.md)
|
||||
- [Walkthrough of Internals](contributing/walkthrough_readme.md)
|
||||
- [Guiding Principles](contributing/guiding_principles.md)
|
||||
- [Roadmap](contributing/roadmap.md)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
# Overall Goals
|
||||
|
||||
This document outlines some of the overall goals for Dioxus. These goals are not set in stone, but they represent general guidelines for the project.
|
||||
|
||||
The goal of Dioxus is to make it easy to build **cross-platform applications that scale**.
|
||||
|
||||
## Cross-Platform
|
||||
|
||||
Dioxus is designed to be cross-platform by default. This means that it should be easy to build applications that run on the web, desktop, and mobile. However, Dioxus should also be flexible enough to allow users to opt into platform-specific features when needed. The `use_eval` is one example of this. By default, Dioxus does not assume that the platform supports JavaScript, but it does provide a hook that allows users to opt into JavaScript when needed.
|
||||
|
||||
## Performance
|
||||
|
||||
As Dioxus applications grow, they should remain relatively performant without the need for manual optimizations. There will be cases where manual optimizations are needed, but Dioxus should try to make these cases as rare as possible.
|
||||
|
||||
One of the benefits of the core architecture of Dioxus is that it delivers reasonable performance even when components are rerendered often. It is based on a Virtual Dom which performs diffing which should prevent unnecessary re-renders even when large parts of the component tree are rerun. On top of this, Dioxus groups static parts of the RSX tree together to skip diffing them entirely.
|
||||
|
||||
## Type Safety
|
||||
|
||||
As teams grow, the Type safety of Rust is a huge advantage. Dioxus should leverage this advantage to make it easy to build applications with large teams.
|
||||
|
||||
To take full advantage of Rust's type system, Dioxus should try to avoid exposing public `Any` types and string-ly typed APIs where possible.
|
||||
|
||||
## Developer Experience
|
||||
|
||||
Dioxus should be easy to learn and ergonomic to use.
|
||||
|
||||
- The API of Dioxus attempts to remain close to React's API where possible. This makes it easier for people to learn Dioxus if they already know React
|
||||
|
||||
- We can avoid the tradeoff between simplicity and flexibility by providing multiple layers of API: One for the very common use case, one for low-level control
|
||||
|
||||
- Hooks: the hooks crate has the most common use cases, but `cx.hook` provides a way to access the underlying persistent reference if needed.
|
||||
- The builder pattern in platform Configs: The builder pattern is used to default to the most common use case, but users can change the defaults if needed.
|
||||
|
||||
- Documentation:
|
||||
- All public APIs should have rust documentation
|
||||
- Examples should be provided for all public features. These examples both serve as documentation and testing. They are checked by CI to ensure that they continue to compile
|
||||
- The most common workflows should be documented in the guide
|
|
@ -10,7 +10,7 @@ If you'd like to improve the docs, PRs are welcome! Both Rust docs ([source](htt
|
|||
|
||||
## 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 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. Once you are done, add your library to the [awesome dioxus](https://github.com/DioxusLabs/awesome-dioxus) list or share it in the `#I-made-a-thing` channel on [Discord](https://discord.gg/XgGxMSkvUM).
|
||||
|
||||
## Bugs & Features
|
||||
|
||||
|
@ -18,3 +18,34 @@ If you've fixed [an open issue](https://github.com/DioxusLabs/dioxus/issues), fe
|
|||
|
||||
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.
|
||||
|
||||
## Tools
|
||||
|
||||
The following tools can be helpful when developing Dioxus. Many of these tools are used in the CI pipeline. Running them locally before submitting a PR instead of waiting for CI can save time.
|
||||
|
||||
- All code is tested with [cargo test](https://doc.rust-lang.org/cargo/commands/cargo-test.html)
|
||||
|
||||
```sh
|
||||
cargo fmt --all
|
||||
```
|
||||
|
||||
- All code is formatted with [rustfmt](https://github.com/rust-lang/rustfmt)
|
||||
|
||||
```sh
|
||||
cargo check --workspace --examples --tests
|
||||
```
|
||||
|
||||
- All code is linted with [Clippy](https://doc.rust-lang.org/clippy/)
|
||||
|
||||
```sh
|
||||
cargo clippy --workspace --examples --tests -- -D warnings
|
||||
```
|
||||
|
||||
- Crates that use unsafe are checked for undefined behavior with [MIRI](https://github.com/rust-lang/miri). MIRI can be helpful to debug what unsafe code is causing issues. Only code that does not interact with system calls can be checked with MIRI. Currently, this is used for the two MIRI tests in `dioxus-core` and `dioxus-native-core`.
|
||||
|
||||
```sh
|
||||
cargo miri test --package dioxus-core --test miri_stress
|
||||
cargo miri test --package dioxus-native-core --test miri_native
|
||||
```
|
||||
|
||||
- [Rust analyzer](https://rust-analyzer.github.io/) can be very helpful for quick feedback in your IDE.
|
|
@ -0,0 +1,50 @@
|
|||
# Project Struture
|
||||
|
||||
There are many packages in the Dioxus organization. This document will help you understand the purpose of each package and how they fit together.
|
||||
|
||||
## Renderers
|
||||
|
||||
- [Desktop](https://github.com/DioxusLabs/dioxus/tree/master/packages/desktop): A Render that Runs Dioxus applications natively, but renders them with the system webview
|
||||
- [Mobile](https://github.com/DioxusLabs/dioxus/tree/master/packages/mobile): A Render that Runs Dioxus applications natively, but renders them with the system webview. This is currently a copy of the desktop render
|
||||
- [Web](https://github.com/DioxusLabs/dioxus/tree/master/packages/Web): Renders Dioxus applications in the browser by compiling to WASM and manipulating the DOM
|
||||
- [Liveview](https://github.com/DioxusLabs/dioxus/tree/master/packages/liveview): A Render that Runs on the server, and renders using a websocket proxy in the browser
|
||||
- [Rink](https://github.com/DioxusLabs/dioxus/tree/master/packages/rink): A Renderer that renders a HTML-like tree into a terminal
|
||||
- [TUI](https://github.com/DioxusLabs/dioxus/tree/master/packages/dioxus-tui): A Renderer that uses Rink to render a Dioxus application in a terminal
|
||||
- [Blitz-Core](https://github.com/DioxusLabs/blitz/tree/master/blitz-core): An experimental native renderer that renders a HTML-like tree using WGPU.
|
||||
- [Blitz](https://github.com/DioxusLabs/blitz): An experimental native renderer that uses Blitz-Core to render a Dioxus application using WGPU.
|
||||
- [SSR](https://github.com/DioxusLabs/dioxus/tree/master/packages/ssr): A Render that Runs Dioxus applications on the server, and renders them to HTML
|
||||
|
||||
## State Management/Hooks
|
||||
|
||||
- [Hooks](https://github.com/DioxusLabs/dioxus/tree/master/packages/hooks): A collection of common hooks for Dioxus applications
|
||||
- [Signals](https://github.com/DioxusLabs/dioxus/tree/master/packages/signals): A experimental state management library for Dioxus applications. This currently contains a `Copy` version of UseRef
|
||||
- [Dioxus STD](https://github.com/DioxusLabs/dioxus-std): A collection of platform agnostic hooks to interact with system interfaces (The clipboard, camera, etc.).
|
||||
- [Fermi](https://github.com/DioxusLabs/dioxus/tree/master/packages/fermi): A global state management library for Dioxus applications.
|
||||
[Router](https://github.com/DioxusLabs/dioxus/tree/master/packages/router): A client-side router for Dioxus applications
|
||||
|
||||
## Core utilities
|
||||
|
||||
- [core](https://github.com/DioxusLabs/dioxus/tree/master/packages/core): The core virtual dom implementation every Dioxus application uses
|
||||
- You can read more about the archetecture of the core [in this blog post](https://dioxuslabs.com/blog/templates-diffing/) and the [custom renderer section of the guide](../custom_renderer/index.md)
|
||||
- [RSX](https://github.com/DioxusLabs/dioxus/tree/master/packages/RSX): The core parsing for RSX used for hot reloading, autoformatting, and the macro
|
||||
- [core-macro](https://github.com/DioxusLabs/dioxus/tree/master/packages/core-macro): The rsx! macro used to write Dioxus applications. (This is a wrapper over the RSX crate)
|
||||
- [HTML macro](https://github.com/DioxusLabs/dioxus-html-macro): A html-like alternative to the RSX macro
|
||||
|
||||
## Native Renderer Utilities
|
||||
|
||||
- [native-core](https://github.com/DioxusLabs/dioxus/tree/master/packages/native-core): Incrementally computed tree of states (mostly styles)
|
||||
- You can read more about how native-core can help you build native renderers in the [custom renderer section of the guide](../custom_renderer/index.html#native-core)
|
||||
- [native-core-macro](https://github.com/DioxusLabs/dioxus/tree/master/packages/native-core-macro): A helper macro for native core
|
||||
- [Taffy](https://github.com/DioxusLabs/taffy): Layout engine powering Blitz-Core, Rink, and Bevy UI
|
||||
|
||||
## Web renderer tooling
|
||||
|
||||
- [HTML](https://github.com/DioxusLabs/dioxus/tree/master/packages/html): defines html specific elements, events, and attributes
|
||||
- [Interpreter](https://github.com/DioxusLabs/dioxus/tree/master/packages/interpreter): defines browser bindings used by the web and desktop renderers
|
||||
|
||||
## Developer tooling
|
||||
|
||||
- [hot-reload](https://github.com/DioxusLabs/dioxus/tree/master/packages/hot-reload): Macro that uses the RSX crate to hot reload static parts of any rsx! macro. This macro works with any non-web renderer with an [integration](https://crates.io/crates/dioxus-hot-reload)
|
||||
- [autofmt](https://github.com/DioxusLabs/dioxus/tree/master/packages/autofmt): Formats RSX code
|
||||
- [rsx-rosetta](https://github.com/DioxusLabs/dioxus/tree/master/packages/RSX-rosetta): Handles conversion between HTML and RSX
|
||||
- [CLI](https://github.com/DioxusLabs/cli): A Command Line Interface and VSCode extension to assist with Dioxus usage
|
|
@ -17,6 +17,7 @@ Generally, here's the status of each platform:
|
|||
- **LiveView**: LiveView support is very young. You'll be figuring things out as you go. Thankfully, none of it is too hard and any work can be upstreamed into Dioxus.
|
||||
|
||||
## Features
|
||||
|
||||
---
|
||||
|
||||
| Feature | Status | Description |
|
||||
|
@ -59,11 +60,12 @@ Generally, here's the status of each platform:
|
|||
- 🛠 = actively being worked on
|
||||
- 👀 = not yet implemented or being worked on
|
||||
|
||||
|
||||
## Roadmap
|
||||
|
||||
These Features are planned for the future of Dioxus:
|
||||
|
||||
### Core
|
||||
|
||||
- [x] Release of Dioxus Core
|
||||
- [x] Upgrade documentation to include more theory and be more comprehensive
|
||||
- [x] Support for HTML-side templates for lightning-fast dom manipulation
|
||||
|
@ -72,16 +74,18 @@ These Features are planned for the future of Dioxus:
|
|||
- [ ] Support for Portals
|
||||
|
||||
### SSR
|
||||
|
||||
- [x] SSR Support + Hydration
|
||||
- [ ] Integrated suspense support for SSR
|
||||
|
||||
### Desktop
|
||||
|
||||
- [ ] Declarative window management
|
||||
- [ ] Templates for building/bundling
|
||||
- [ ] Fully native renderer
|
||||
- [ ] Access to Canvas/WebGL context natively
|
||||
|
||||
### Mobile
|
||||
|
||||
- [ ] Mobile standard library
|
||||
- [ ] GPS
|
||||
- [ ] Camera
|
||||
|
@ -92,9 +96,9 @@ These Features are planned for the future of Dioxus:
|
|||
- [ ] Notifications
|
||||
- [ ] Clipboard
|
||||
- [ ] Animations
|
||||
- [ ] Native Renderer
|
||||
|
||||
### Bundling (CLI)
|
||||
|
||||
- [x] Translation from HTML into RSX
|
||||
- [x] Dev server
|
||||
- [x] Live reload
|
||||
|
@ -106,11 +110,11 @@ These Features are planned for the future of Dioxus:
|
|||
- [ ] Image pipeline
|
||||
|
||||
### Essential hooks
|
||||
|
||||
- [x] Router
|
||||
- [x] Global state management
|
||||
- [ ] Resize observer
|
||||
|
||||
|
||||
## Work in Progress
|
||||
|
||||
### Build Tool
|
|
@ -0,0 +1,126 @@
|
|||
# Walkthrough of the Hello World Example Internals
|
||||
|
||||
This walkthrough will take you through the internals of the Hello World example program. It will explain how major parts of Dioxus internals interact with each other to take the readme example from a source file to a running application. This guide should serve as a high-level overview of the internals of Dioxus. It is not meant to be a comprehensive guide.
|
||||
|
||||
## The Source File
|
||||
|
||||
We start will a hello world program. This program renders a desktop app with the text "Hello World" in a webview.
|
||||
|
||||
```rust
|
||||
{{#include ../../../../../examples/readme.rs}}
|
||||
```
|
||||
|
||||
[![](https://mermaid.ink/img/pako:eNqNkT1vwyAQhv8KvSlR48HphtQtqjK0S6tuSBGBS0CxwcJHk8rxfy_YVqxKVdR3ug_u4YXrQHmNwOFQ-bMyMhB7fReOJbVxfwyyMSy0l7GSpW1ARda727ksUy5MuSyKgvBC5ULA1h5N8WK_kCkfHWHgrBuiXsBynrvdsY9E3u1iM_eyvFOVVadMnELOap-o1911JLPHZ1b-YqLTc3LjTt7WifTZMJPsPdx1ov3Z_ellfcdL8R8vmTy5eUqsTUpZ-vzZzjAEK6gx1NLqtJwuNwSQwRoF8BRqGU4ChOvTORnJf3w7BZxCxBXERkvCjZXpQTXwg6zaVEVtyYe3cdvD0vsf4bucgw?type=png)](https://mermaid.live/edit#pako:eNqNkT1vwyAQhv8KvSlR48HphtQtqjK0S6tuSBGBS0CxwcJHk8rxfy_YVqxKVdR3ug_u4YXrQHmNwOFQ-bMyMhB7fReOJbVxfwyyMSy0l7GSpW1ARda727ksUy5MuSyKgvBC5ULA1h5N8WK_kCkfHWHgrBuiXsBynrvdsY9E3u1iM_eyvFOVVadMnELOap-o1911JLPHZ1b-YqLTc3LjTt7WifTZMJPsPdx1ov3Z_ellfcdL8R8vmTy5eUqsTUpZ-vzZzjAEK6gx1NLqtJwuNwSQwRoF8BRqGU4ChOvTORnJf3w7BZxCxBXERkvCjZXpQTXwg6zaVEVtyYe3cdvD0vsf4bucgw)
|
||||
|
||||
## The rsx! Macro
|
||||
|
||||
Before the Rust compiler runs the program, it will expand all macros. Here is what the hello world example looks like expanded:
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/readme_expanded.rs}}
|
||||
```
|
||||
|
||||
The rsx macro separates the static parts of the rsx (the template) and the dynamic parts (the dynamic_nodes and dynamic_attributes).
|
||||
|
||||
The static template only contains the parts of the rsx that cannot change at runtime with holes for the dynamic parts:
|
||||
|
||||
[![](https://mermaid.ink/img/pako:eNqdksFuwjAMhl8l8wkkKtFx65njdtm0E0GVSQKJoEmVOgKEeHecUrXStO0wn5Lf9u8vcm6ggjZQwf4UzspiJPH2Ib3g6NLuELG1oiMkp0TsLs9EDu2iUeSCH8tz2HJmy3lRFPrqsXGq9mxeLzcbCU6LZSUGXWRdwnY7tY7Tdoko-Dq1U64fODgiUfzJMeuOe7_ZGq-ny2jNhGQu9DqT8NUK6w72RcL8dxgdzv4PnHLAKf-Fk80HoBUDrfkqeBkTUd8EC2hMbNBpXtYtJySQNQ0PqPioMR4lSH_nOkwUPq9eQUUxmQWkViOZtUN-UwPVHk8dq0Y7CvH9uf3-E9wfrmuk1A?type=png)](https://mermaid.live/edit#pako:eNqdksFuwjAMhl8l8wkkKtFx65njdtm0E0GVSQKJoEmVOgKEeHecUrXStO0wn5Lf9u8vcm6ggjZQwf4UzspiJPH2Ib3g6NLuELG1oiMkp0TsLs9EDu2iUeSCH8tz2HJmy3lRFPrqsXGq9mxeLzcbCU6LZSUGXWRdwnY7tY7Tdoko-Dq1U64fODgiUfzJMeuOe7_ZGq-ny2jNhGQu9DqT8NUK6w72RcL8dxgdzv4PnHLAKf-Fk80HoBUDrfkqeBkTUd8EC2hMbNBpXtYtJySQNQ0PqPioMR4lSH_nOkwUPq9eQUUxmQWkViOZtUN-UwPVHk8dq0Y7CvH9uf3-E9wfrmuk1A)
|
||||
|
||||
The dynamic_nodes and dynamic_attributes are the parts of the rsx that can change at runtime:
|
||||
|
||||
[![](https://mermaid.ink/img/pako:eNp1UcFOwzAM_RXLVzZpvUbighDiABfgtkxTlnirtSaZUgc0df130hZEEcwny35-79nu0EZHqHDfxA9bmyTw9KIDlGjz7pDMqQZ3DsazhVCQ7dQbwnEiKxwDvN3NqhN4O4C3q_VaIztYKXjkQ7184HcCG3MQSgq6Mes1bjbTPAV3RdqIJN5l-V__2_Fcf5iY68dgG7ZHBT4WD5ftZfIBN7dQ_Tj4w1B9MVTXGZa_GMYdcIGekjfsymW7oaFRavKkUZXUmXTUqENfcCZLfD0Hi0pSpgXmkzNC92zKATyqvWnaUiXHEtPz9KrxY_0nzYOPmA?type=png)](https://mermaid.live/edit#pako:eNp1UcFOwzAM_RXLVzZpvUbighDiABfgtkxTlnirtSaZUgc0df130hZEEcwny35-79nu0EZHqHDfxA9bmyTw9KIDlGjz7pDMqQZ3DsazhVCQ7dQbwnEiKxwDvN3NqhN4O4C3q_VaIztYKXjkQ7184HcCG3MQSgq6Mes1bjbTPAV3RdqIJN5l-V__2_Fcf5iY68dgG7ZHBT4WD5ftZfIBN7dQ_Tj4w1B9MVTXGZa_GMYdcIGekjfsymW7oaFRavKkUZXUmXTUqENfcCZLfD0Hi0pSpgXmkzNC92zKATyqvWnaUiXHEtPz9KrxY_0nzYOPmA)
|
||||
|
||||
## Launching the App
|
||||
|
||||
The app is launched by calling the `launch` function with the root component. Internally, this function will create a new web view using [wry](https://docs.rs/wry/latest/wry/) and create a virtual dom with the root component. This guide will not explain the renderer in-depth, but you can read more about it in the [custom renderer](/guide/custom-renderer) section.
|
||||
|
||||
## The Virtual DOM
|
||||
|
||||
Before we dive into the initial render in the virtual dom, we need to discuss what the virtual dom is. The virtual dom is a representation of the dom that is used to diff the current dom from the new dom. This diff is then used to create a list of mutations that need to be applied to the dom.
|
||||
|
||||
The Virtual Dom roughly looks like this:
|
||||
|
||||
```rust
|
||||
pub struct VirtualDom {
|
||||
// All the templates that have been created or set durring hot reloading
|
||||
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
|
||||
|
||||
// A slab of all the scopes that have been created
|
||||
pub(crate) scopes: ScopeSlab,
|
||||
|
||||
// All scopes that have been marked as dirty
|
||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||
|
||||
// Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
|
||||
pub(crate) elements: Slab<ElementRef>,
|
||||
|
||||
// This receiver is used to receive messages from hooks about what scopes need to be marked as dirty
|
||||
pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
|
||||
|
||||
// The changes queued up to be sent to the renderer
|
||||
pub(crate) mutations: Mutations<'static>,
|
||||
}
|
||||
```
|
||||
|
||||
> What is a [slab](https://docs.rs/slab/latest/slab/)?
|
||||
> A slab acts like a hashmap with integer keys if you don't care about the value of the keys. It is internally backed by a dense vector which makes it more efficient than a hashmap. When you insert a value into a slab, it returns an integer key that you can use to retrieve the value later.
|
||||
|
||||
> How does Dioxus use slabs?
|
||||
> Dioxus uses "synchronized slabs" to communicate between the renderer and the VDOM. When an node is created in the Virtual Dom, a ElementId is passed along with the mutation to the renderer to identify the node. These ids are used by the Virtual Dom to reference that nodes in future mutations like setting an attribute on a node or removing a node.
|
||||
> When the renderer sends an event to the Virtual Dom, it sends the ElementId of the node that the event was triggered on. The Virtual Dom uses this id to find the node in the slab and then run the necessary event handlers.
|
||||
|
||||
The virtual dom is a tree of scopes. A new scope is created for every component when it is first rendered and recycled when the component is unmounted.
|
||||
|
||||
Scopes serve three main purposes:
|
||||
|
||||
1. They store the state of hooks used by the component
|
||||
2. They store the state for the context API
|
||||
3. They store the current and previous VNode that was rendered for diffing
|
||||
|
||||
### The Initial Render
|
||||
|
||||
The root scope is created and rebuilt:
|
||||
|
||||
1. The root component is run
|
||||
2. The root component returns a VNode
|
||||
3. Mutations for the VNode are created and added to the mutation list (this may involve creating new child components)
|
||||
4. The VNode is stored in the root scope
|
||||
|
||||
After the root scope is built, the mutations are sent to the renderer to be applied to the dom.
|
||||
|
||||
After the initial render, the root scope looks like this:
|
||||
|
||||
[![](https://mermaid.ink/img/pako:eNqtVE1P4zAQ_SuzPrWikRpWXCLtBRDisItWsOxhCaqM7RKricdyJrQV8N93QtvQNCkfEnOynydv3nxkHoVCbUQipjnOVSYDwc_L1AFbWd3dB-kzuEQkuFLoDUwDFkCZAek9nGDh0RlHK__atA1GkUUHf45f0YbppAqB_aOzIAvz-t7-chN_Y-1bw1WSJKsglIu2w9tktWXxIIuHURT5XCqTYa5NmDguw2R8c5MKq2GcgF46WTB_jafi9rZL0yi5q4jQTSrf9altO4okCn1Ratwyz55Qxuku2ITlTMgs6HCQimsPmb3PvqVi-L5gjXP3QcnxWnL8JZLrwGvR31n0KV-Bx6-r-oVkT_-3G1S-NQLbk9i8rj7udP2cixed2QcDCitHJiQw7ub3EVlNecrPjudG2-6soFO5VbMECmR9T5OnlUY4-AFxfw9aTFst3McU9TK1Otm6NEn_DubBYlX2_dglLXOz48FgwJmJ5lZTlhz6xWgNaFnyDgpymcARHO0W2a9J_l5w2wYXvHuGPcqaQ-rESBQmFNJq3nCPNZoK3l4sUSR81DLMUpG6Z_aTFeHV0imRUKjMSFReSzKnVnKGhUimMi8ZNdoShl-rlfmyOUfCS_cPcePz_B_Wl4pc?type=png)](https://mermaid.live/edit#pako:eNqtVE1P4zAQ_SuzPrWikRpWXCLtBRDisItWsOxhCaqM7RKricdyJrQV8N93QtvQNCkfEnOynydv3nxkHoVCbUQipjnOVSYDwc_L1AFbWd3dB-kzuEQkuFLoDUwDFkCZAek9nGDh0RlHK__atA1GkUUHf45f0YbppAqB_aOzIAvz-t7-chN_Y-1bw1WSJKsglIu2w9tktWXxIIuHURT5XCqTYa5NmDguw2R8c5MKq2GcgF46WTB_jafi9rZL0yi5q4jQTSrf9altO4okCn1Ratwyz55Qxuku2ITlTMgs6HCQimsPmb3PvqVi-L5gjXP3QcnxWnL8JZLrwGvR31n0KV-Bx6-r-oVkT_-3G1S-NQLbk9i8rj7udP2cixed2QcDCitHJiQw7ub3EVlNecrPjudG2-6soFO5VbMECmR9T5OnlUY4-AFxfw9aTFst3McU9TK1Otm6NEn_DubBYlX2_dglLXOz48FgwJmJ5lZTlhz6xWgNaFnyDgpymcARHO0W2a9J_l5w2wYXvHuGPcqaQ-rESBQmFNJq3nCPNZoK3l4sUSR81DLMUpG6Z_aTFeHV0imRUKjMSFReSzKnVnKGhUimMi8ZNdoShl-rlfmyOUfCS_cPcePz_B_Wl4pc)
|
||||
|
||||
### Waiting for Events
|
||||
|
||||
The Virtual Dom will only ever rerender a scope if it is marked as dirty. Each hook is responsible for marking the scope as dirty if the state has changed. Hooks can mark a scope as dirty by sending a message to the Virtual Dom's channel.
|
||||
|
||||
There are generally two ways a scope is marked as dirty:
|
||||
|
||||
1. The renderer triggers an event: This causes an event listener to be called if needed which may mark a component as dirty
|
||||
2. The renderer calls wait for work: This polls futures which may mark a component as dirty
|
||||
|
||||
Once at least one scope is marked as dirty, the renderer can call `render_with_deadline` to diff the dirty scopes.
|
||||
|
||||
### Diffing Scopes
|
||||
|
||||
If the user clicked the "up high" button, the root scope would be marked as dirty by the use_state hook. Once the desktop renderer calls `render_with_deadline`, the root scope would be diffed.
|
||||
|
||||
To start the diffing process, the component is run. After the root component is run it will look like this:
|
||||
|
||||
[![](https://mermaid.ink/img/pako:eNrFVlFP2zAQ_iuen0BrpCaIl0i8AEJ72KQJtpcRFBnbJVYTn-U4tBXw33dpG5M2CetoBfdkny_ffb67fPIT5SAkjekkhxnPmHXk-3WiCVpZ3T9YZjJyDeDIDQcjycRCQVwmCTOGXEBhQEvtVvG1CWUldwo0-XX-6vVIF5W1GB9cWVbI1_PNL5v8jW3uPFbpmFOc2HK-GfA2WG1ZeJSFx0EQmJxxmUEupE01liEd394mVAkyjolYaFYgfu1P6N1dF8Yzua-cA51WphtTWzsLc872Zan9CnEGUkktuk6fFm_i5NxFRwn9bUimHrIvCT3-N2EBM70j5XBNOTwI5TrxmvQJkr7ELcHx67Jeggz0v92g8q0RaE-iP1193On6NyxecKUeJeFQaSdtTMLu_Xah5ctT_u94Nty2ZwU0zxWfxqQA5PecPq84kq9nfRw7SK0WDiEFZ4O37d34S_-08lFBVfb92KVb5HIrAp0WpjKYKeGyODLz0dohWIkaZNkiJqfkdLvIH6oRaTSoEmm0n06k0a5K0ZdpL61Io0Yt0nfpxc7UQ0_9cJrhyZ8syX-6brS706Mc489Vjja7fbWj3cxDqIdfJJqOaCFtwZTAV8hT7U0ovjBQRmiMS8HsNKGJfsE4Vjm4WWhOY2crOaKVEczJS8WwgAWNJywv0SuFcmB_rJ41y9fNiBqm_wA0MS9_AUuAiy0?type=png)](https://mermaid.live/edit#pako:eNrFVlFP2zAQ_iuen0BrpCaIl0i8AEJ72KQJtpcRFBnbJVYTn-U4tBXw33dpG5M2CetoBfdkny_ffb67fPIT5SAkjekkhxnPmHXk-3WiCVpZ3T9YZjJyDeDIDQcjycRCQVwmCTOGXEBhQEvtVvG1CWUldwo0-XX-6vVIF5W1GB9cWVbI1_PNL5v8jW3uPFbpmFOc2HK-GfA2WG1ZeJSFx0EQmJxxmUEupE01liEd394mVAkyjolYaFYgfu1P6N1dF8Yzua-cA51WphtTWzsLc872Zan9CnEGUkktuk6fFm_i5NxFRwn9bUimHrIvCT3-N2EBM70j5XBNOTwI5TrxmvQJkr7ELcHx67Jeggz0v92g8q0RaE-iP1193On6NyxecKUeJeFQaSdtTMLu_Xah5ctT_u94Nty2ZwU0zxWfxqQA5PecPq84kq9nfRw7SK0WDiEFZ4O37d34S_-08lFBVfb92KVb5HIrAp0WpjKYKeGyODLz0dohWIkaZNkiJqfkdLvIH6oRaTSoEmm0n06k0a5K0ZdpL61Io0Yt0nfpxc7UQ0_9cJrhyZ8syX-6brS706Mc489Vjja7fbWj3cxDqIdfJJqOaCFtwZTAV8hT7U0ovjBQRmiMS8HsNKGJfsE4Vjm4WWhOY2crOaKVEczJS8WwgAWNJywv0SuFcmB_rJ41y9fNiBqm_wA0MS9_AUuAiy0)
|
||||
|
||||
Next, the Virtual Dom will compare the new VNode with the previous VNode and only update the parts of the tree that have changed.
|
||||
|
||||
When a component is re-rendered, the Virtual Dom will compare the new VNode with the previous VNode and only update the parts of the tree that have changed.
|
||||
|
||||
The diffing algorithm goes through the list of dynamic attributes and nodes and compares them to the previous VNode. If the attribute or node has changed, a mutation that describes the change is added to the mutation list.
|
||||
|
||||
Here is what the diffing algorithm looks like for the root scope (red lines indicate that a mutation was generated, and green lines indicate that no mutation was generated)
|
||||
|
||||
[![](https://mermaid.ink/img/pako:eNrFlFFPwjAQx7_KpT7Kko2Elya8qCE-aGLAJ5khpe1Yw9Zbug4k4He3OJjbGPig0T5t17tf_nf777aEo5CEkijBNY-ZsfAwDjW4kxfzhWFZDGNECxOOmYTIYAo2lsCyDG4xzVBLbcv8_RHKSG4V6orSIN0Wxrh8b2RYKr_uTyubd1W92GiWKg7aac6bOU3G803HbVk82xfP_Ok0JEqAT-FeLWJvpFYSOBbaSkMhCMnra5MgtfhWFrPWqHlhL2urT6atbU-oa0PNE8WXFFJ0-nazXakRroddGk9IwYEUnCd5w7Pddr5UTT8ZuVJY5F0fM7ebRLYyXNDgUnprJWxM-9lb7xAQLHe-M2xDYQCD9pD_2hez_kVn-P_rjLq6n3qjYv2iO5qz9DyvPdyv1ETp5eTTJ_7BGvQq8v1TVtl5jXUcRRcrqFh-dI4VtFlBN6t_ynLNkh5JpUmZEm5rbvfhkLiN6H4BQt2jYGYZklC_uzxWWJxsNCfUmkL2SJEJZuWdYs4cKaERS3IXlUJZNI_lGv7cxj2SMf2CeMx5_wBcbK19?type=png)](https://mermaid.live/edit#pako:eNrFlFFPwjAQx7_KpT7Kko2Elya8qCE-aGLAJ5khpe1Yw9Zbug4k4He3OJjbGPig0T5t17tf_nf777aEo5CEkijBNY-ZsfAwDjW4kxfzhWFZDGNECxOOmYTIYAo2lsCyDG4xzVBLbcv8_RHKSG4V6orSIN0Wxrh8b2RYKr_uTyubd1W92GiWKg7aac6bOU3G803HbVk82xfP_Ok0JEqAT-FeLWJvpFYSOBbaSkMhCMnra5MgtfhWFrPWqHlhL2urT6atbU-oa0PNE8WXFFJ0-nazXakRroddGk9IwYEUnCd5w7Pddr5UTT8ZuVJY5F0fM7ebRLYyXNDgUnprJWxM-9lb7xAQLHe-M2xDYQCD9pD_2hez_kVn-P_rjLq6n3qjYv2iO5qz9DyvPdyv1ETp5eTTJ_7BGvQq8v1TVtl5jXUcRRcrqFh-dI4VtFlBN6t_ynLNkh5JpUmZEm5rbvfhkLiN6H4BQt2jYGYZklC_uzxWWJxsNCfUmkL2SJEJZuWdYs4cKaERS3IXlUJZNI_lGv7cxj2SMf2CeMx5_wBcbK19)
|
||||
|
||||
## Conclusion
|
||||
|
||||
This is only a brief overview of how the Virtual Dom works. There are several aspects not yet covered in this guide including how the Virtual Dom handles async-components, keyed diffing, and how it uses [bump allocation](https://github.com/fitzgen/bumpalo) to efficiently allocate VNodes. If need more information about the Virtual Dom, you can read the code of the [core](https://github.com/DioxusLabs/dioxus/tree/master/packages/core) crate or reach out to us on [Discord](https://discord.gg/XgGxMSkvUM).
|
|
@ -15,7 +15,7 @@ Essentially, your renderer needs to process edits and generate events to update
|
|||
|
||||
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 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.
|
||||
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/dioxus-tui) as a starting point for your custom renderer.
|
||||
|
||||
## Templates
|
||||
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
> This guide assumes you read the [Web](web.md) guide and installed the [Dioxus-cli](https://github.com/DioxusLabs/cli)
|
||||
|
||||
# Getting Started
|
||||
|
||||
## Setup
|
||||
|
||||
For this guide, we're going to show how to use Dioxus with [Axum](https://docs.rs/axum/latest/axum/), but `dioxus-fullstack` also integrates with the [Warp](https://docs.rs/warp/latest/warp/) and [Salvo](https://docs.rs/salvo/latest/salvo/) web frameworks.
|
||||
|
||||
Make sure you have Rust and Cargo installed, and then create a new project:
|
||||
|
||||
```shell
|
||||
cargo new --bin demo
|
||||
cd demo
|
||||
```
|
||||
|
||||
Add `dioxus` and `dioxus-fullstack` as dependencies:
|
||||
|
||||
```shell
|
||||
cargo add dioxus
|
||||
cargo add dioxus-fullstack --features axum, ssr
|
||||
```
|
||||
|
||||
Next, add all the Axum dependencies. This will be different if you're using a different Web Framework
|
||||
|
||||
```shell
|
||||
cargo add tokio --features full
|
||||
cargo add axum
|
||||
```
|
||||
|
||||
Your dependencies should look roughly like this:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
axum = "*"
|
||||
dioxus = { version = "*" }
|
||||
dioxus-fullstack = { version = "*", features = ["axum", "ssr"] }
|
||||
tokio = { version = "*", features = ["full"] }
|
||||
```
|
||||
|
||||
Now, set up your Axum app to serve the Dioxus app.
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/server_basic.rs}}
|
||||
```
|
||||
|
||||
Now, run your app with `cargo run` and open `http://localhost:8080` in your browser. You should see a server-side rendered page with a counter.
|
||||
|
||||
## Hydration
|
||||
|
||||
Right now, the page is static. We can't interact with the buttons. To fix this, we can hydrate the page with `dioxus-web`.
|
||||
|
||||
First, modify your `Cargo.toml` to include two features, one for the server called `ssr`, and one for the client called `web`.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
# Common dependancies
|
||||
dioxus = { version = "*" }
|
||||
dioxus-fullstack = { version = "*" }
|
||||
|
||||
# Web dependancies
|
||||
dioxus-web = { version = "*", features=["hydrate"], optional = true }
|
||||
|
||||
# Server dependancies
|
||||
axum = { version = "0.6.12", optional = true }
|
||||
tokio = { version = "1.27.0", features = ["full"], optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
ssr = ["axum", "tokio", "dioxus-fullstack/axum"]
|
||||
web = ["dioxus-web"]
|
||||
```
|
||||
|
||||
Next, we need to modify our `main.rs` to use either hydrate on the client or render on the server depending on the active features.
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/hydration.rs}}
|
||||
```
|
||||
|
||||
Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. You should see the same page as before, but now you can interact with the buttons!
|
||||
|
||||
## Sycronizing props between the server and client
|
||||
|
||||
Let's make the initial count of the counter dynamic based on the current page.
|
||||
|
||||
### Modifying the server
|
||||
|
||||
To do this, we must remove the serve_dioxus_application and replace it with a custom implementation of its four key functions:
|
||||
|
||||
- Serve static WASM and JS files with serve_static_assets
|
||||
- Register server functions with register_server_fns (more information on server functions later)
|
||||
- Connect to the hot reload server with connect_hot_reload
|
||||
- A custom route that uses SSRState to server-side render the application
|
||||
|
||||
### Modifying the client
|
||||
|
||||
The only thing we need to change on the client is the props. `dioxus-fullstack` will automatically serialize the props it uses to server render the app and send them to the client. In the client section of `main.rs`, we need to add `get_root_props_from_document` to deserialize the props before we hydrate the app.
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/hydration_props.rs}}
|
||||
```
|
||||
|
||||
Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. Navigate to `http://localhost:8080/1` and you should see the counter start at 1. Navigate to `http://localhost:8080/2` and you should see the counter start at 2.
|
|
@ -0,0 +1,59 @@
|
|||
# Fullstack development
|
||||
|
||||
So far you have learned about three different approaches to target the web with Dioxus:
|
||||
|
||||
- [Client-side rendering with dioxus-web](../getting_started/web.md)
|
||||
- [Server-side rendering with dioxus-liveview](../getting_started/liveview.md)
|
||||
- [Server-side static HTML generation with dioxus-ssr](../getting_started/ssr.md)
|
||||
|
||||
## Summary of Existing Approaches
|
||||
|
||||
Each approach has its tradeoffs:
|
||||
|
||||
### Client-side rendering
|
||||
|
||||
- With Client side rendering, you send the entire content of your application to the client, and then the client generates all of the HTML of the page dynamically.
|
||||
|
||||
- This means that the page will be blank until the JavaScript bundle has loaded and the application has initialized. This can result in **slower first render times and makes the page less SEO-friendly**.
|
||||
|
||||
> SEO stands for Search Engine Optimization. It refers to the practice of making your website more likely to appear in search engine results. Search engines like Google and Bing use web crawlers to index the content of websites. Most of these crawlers are not able to run JavaScript, so they will not be able to index the content of your page if it is rendered client-side.
|
||||
|
||||
- Client-side rendered applications need to use **weakly typed requests to communicate with the server**
|
||||
|
||||
> Client-side rendering is a good starting point for most applications. It is well supported and makes it easy to communicate with the client/browser APIs
|
||||
|
||||
### Liveview
|
||||
|
||||
- Liveview rendering communicates with the server over a WebSocket connection. It essentially moves all of the work that Client-side rendering does to the server.
|
||||
|
||||
- This makes it **easy to communicate with the server, but more difficult to communicate with the client/browser APIS**.
|
||||
|
||||
- Each interaction also requires a message to be sent to the server and back which can cause **issues with latency**.
|
||||
|
||||
- Because Liveview uses a websocket to render, the page will be blank until the WebSocket connection has been established and the first renderer has been sent form the websocket. Just like with client side rendering, this can make the page **less SEO-friendly**.
|
||||
|
||||
- Because the page is rendered on the server and the page is sent to the client piece by piece, you never need to send the entire application to the client. The initial load time can be faster than client-side rendering with large applications because Liveview only needs to send a constant small websocket script regardless of the size of the application.
|
||||
|
||||
> Liveview is a good fit for applications that already need to communicate with the server frequently (like real time collaborative apps), but don't need to communicate with as many client/browser APIs
|
||||
|
||||
### Server-side rendering
|
||||
|
||||
- Server-side rendering generates all of the HTML of the page on the server before the page is sent to the client. This means that the page will be fully rendered when it is sent to the client. This results in a faster first render time and makes the page more SEO-friendly. However, it **only works for static pages**.
|
||||
|
||||
> Server-side rendering is not a good fit for purely static sites like a blog
|
||||
|
||||
## A New Approach
|
||||
|
||||
Each of these approaches has its tradeoffs. What if we could combine the best parts of each approach?
|
||||
|
||||
- **Fast initial render** time like SSR
|
||||
- **Works well with SEO** like SSR
|
||||
- **Type safe easy communication with the server** like Liveview
|
||||
- **Access to the client/browser APIs** like Client-side rendering
|
||||
- **Fast interactivity** like Client-side rendering
|
||||
|
||||
We can achieve this by rendering the initial page on the server (SSR) and then taking over rendering on the client (Client-side rendering). Taking over rendering on the client is called **hydration**.
|
||||
|
||||
Finally, we can use [server functions](server_functions.md) to communicate with the server in a type-safe way.
|
||||
|
||||
This approach uses both the dioxus-web and dioxus-ssr crates. To integrate those two packages and `axum`, `warp`, or `salvo`, Dioxus provides the `dioxus-fullstack` crate.
|
|
@ -0,0 +1,31 @@
|
|||
# Communicating with the server
|
||||
|
||||
`dixous-server` provides server functions that allow you to call an automatically generated API on the server from the client as if it were a local function.
|
||||
|
||||
To make a server function, simply add the `#[server(YourUniqueType)]` attribute to a function. The function must:
|
||||
|
||||
- Be an async function
|
||||
- Have arguments and a return type that both implement serialize and deserialize (with [serde](https://serde.rs/)).
|
||||
- Return a `Result` with an error type of ServerFnError
|
||||
|
||||
You must call `register` on the type you passed into the server macro in your main function before starting your server to tell Dioxus about the server function.
|
||||
|
||||
Let's continue building on the app we made in the [getting started](./getting_started.md) guide. We will add a server function to our app that allows us to double the count on the server.
|
||||
|
||||
First, add serde as a dependency:
|
||||
|
||||
```shell
|
||||
cargo add serde
|
||||
```
|
||||
|
||||
Next, add the server function to your `main.rs`:
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/server_function.rs}}
|
||||
```
|
||||
|
||||
Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. You should see a new button that multiplies the count by 2.
|
||||
|
||||
## Conclusion
|
||||
|
||||
That's it! You've created a full-stack Dioxus app. You can find more examples of full-stack apps and information about how to integrate with other frameworks and desktop renderers in the [dioxus-fullstack examples directory](https://github.com/DioxusLabs/dioxus/tree/master/packages/server/examples).
|
|
@ -0,0 +1 @@
|
|||
# Fullstack
|
|
@ -5,28 +5,35 @@
|
|||
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.
|
||||
|
||||
# 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).
|
||||
Hot reloading is automatically enabled when using the web renderer on debug builds.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Run:
|
||||
|
||||
```bash
|
||||
dioxus serve --hot-reload
|
||||
```
|
||||
|
||||
2. Change some code within a rsx or render macro
|
||||
3. Open your localhost in a browser
|
||||
4. Save and watch the style change without recompiling
|
||||
|
||||
# Desktop/Liveview/TUI
|
||||
# Desktop/Liveview/TUI/Server
|
||||
|
||||
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
|
||||
|
@ -37,13 +44,17 @@ fn main() {
|
|||
```
|
||||
|
||||
## 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
|
||||
|
||||
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, Iterators, and some attributes can contain arbitrary rust code and will trigger a full recompile when changed.
|
||||
|
|
|
@ -1,17 +1,6 @@
|
|||
# Server-Side Rendering
|
||||
|
||||
The Dioxus VirtualDom can be rendered server-side.
|
||||
|
||||
[Example: Dioxus DocSite](https://github.com/dioxusLabs/docsite)
|
||||
|
||||
## Multithreaded Support
|
||||
|
||||
The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. This means you can't easily use Dioxus with most web frameworks like Tide, Rocket, Axum, etc.
|
||||
|
||||
To solve this, you'll want to spawn a VirtualDom on its own thread and communicate with it via channels.
|
||||
|
||||
When working with web frameworks that require `Send`, it is possible to render a VirtualDom immediately to a String – but you cannot hold the VirtualDom across an await point. For retained-state SSR (essentially LiveView), you'll need to create a pool of VirtualDoms.
|
||||
|
||||
For lower-level control over the rendering process, you can use the `dioxus-ssr` crate directly. This can be useful when integrating with a web framework that `dioxus-server` does not support, or pre-rendering pages.
|
||||
|
||||
## Setup
|
||||
|
||||
|
@ -21,7 +10,7 @@ Make sure you have Rust and Cargo installed, and then create a new project:
|
|||
|
||||
```shell
|
||||
cargo new --bin demo
|
||||
cd app
|
||||
cd demo
|
||||
```
|
||||
|
||||
Add Dioxus and the ssr renderer as dependencies:
|
||||
|
@ -51,54 +40,32 @@ tokio = { version = "1.15.0", features = ["full"] }
|
|||
Now, set up your Axum app to respond on an endpoint.
|
||||
|
||||
```rust
|
||||
use axum::{response::Html, routing::get, Router};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
println!("listening on http://{}", addr);
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
Router::new()
|
||||
.route("/", get(app_endpoint))
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
{{#include ../../../examples/hello_world_ssr.rs:main}}
|
||||
```
|
||||
|
||||
And then add our endpoint. We can either render `rsx!` directly:
|
||||
|
||||
```rust
|
||||
async fn app_endpoint() -> Html<String> {
|
||||
// render the rsx! macro to HTML
|
||||
Html(dioxus_ssr::render_lazy(rsx! {
|
||||
div { "hello world!" }
|
||||
}))
|
||||
}
|
||||
{{#include ../../../examples/hello_world_ssr.rs:endpoint}}
|
||||
```
|
||||
|
||||
Or we can render VirtualDoms.
|
||||
|
||||
```rust
|
||||
async fn app_endpoint() -> Html<String> {
|
||||
// create a component that renders a div with the text "hello world"
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx!(div { "hello world" }))
|
||||
}
|
||||
// create a VirtualDom with the app component
|
||||
let mut app = VirtualDom::new(app);
|
||||
// rebuild the VirtualDom before rendering
|
||||
let _ = app.rebuild();
|
||||
{{#include ../../../examples/hello_world_ssr.rs:second_endpoint}}
|
||||
```
|
||||
|
||||
// render the VirtualDom to HTML
|
||||
Html(dioxus_ssr::render_vdom(&app))
|
||||
}
|
||||
And then add our app component:
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/hello_world_ssr.rs:component}}
|
||||
```
|
||||
|
||||
And that's it!
|
||||
|
||||
> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it _must_ remain on the thread it started. We are working on loosening this requirement.
|
||||
|
||||
## Multithreaded Support
|
||||
|
||||
The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe.
|
||||
When working with web frameworks that require `Send`, it is possible to render a VirtualDom immediately to a String – but you cannot hold the VirtualDom across an await point. For retained-state SSR (essentially LiveView), you'll need to spawn a VirtualDom on its own thread and communicate with it via channels or create a pool of VirtualDoms.
|
||||
You might notice that you cannot hold the VirtualDom across an await point. Because Dioxus is currently not ThreadSafe, it _must_ remain on the thread it started. We are working on loosening this requirement.
|
||||
|
|
|
@ -12,14 +12,12 @@ TUI support is currently quite experimental. But, if you're willing to venture i
|
|||
|
||||
- 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)
|
||||
- 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/dioxus-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
|
||||
|
||||
|
||||
Start by making a new package and adding Dioxus and the TUI renderer as dependancies.
|
||||
|
||||
```shell
|
||||
|
|
|
@ -5,6 +5,7 @@ Build single-page applications that run in the browser with Dioxus. To run on th
|
|||
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:
|
||||
|
||||
- [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc)
|
||||
- [ECommerce](https://github.com/DioxusLabs/example-projects/tree/master/ecommerce-site)
|
||||
|
||||
|
@ -17,7 +18,7 @@ Examples:
|
|||
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.
|
||||
- Dioxus provides hydration to resume apps that are rendered on the server. See the [fullstack](fullstack.md) getting started guide for more information.
|
||||
|
||||
## Tooling
|
||||
|
||||
|
@ -28,6 +29,7 @@ cargo install dioxus-cli
|
|||
```
|
||||
|
||||
Make sure the `wasm32-unknown-unknown` target for rust is installed:
|
||||
|
||||
```shell
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
@ -49,11 +51,11 @@ cargo add dioxus-web
|
|||
```
|
||||
|
||||
Edit your `main.rs`:
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/hello_world_web.rs}}
|
||||
```
|
||||
|
||||
|
||||
And to serve our app:
|
||||
|
||||
```bash
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
Hooks are a great way to encapsulate business logic. If none of the existing hooks work for your problem, you can write your own.
|
||||
|
||||
When writing your hook, you can make a function that accepts `cx: &ScopeState` as a parameter to accept a scope with any Props.
|
||||
|
||||
## Composing Hooks
|
||||
|
||||
To avoid repetition, you can encapsulate business logic based on existing hooks to create a new hook.
|
||||
|
@ -12,6 +14,12 @@ For example, if many components need to access an `AppSettings` struct, you can
|
|||
{{#include ../../../examples/hooks_composed.rs:wrap_context}}
|
||||
```
|
||||
|
||||
Or if you want to wrap a hook that persists reloads with the storage API, you can build on top of the use_ref hook to work with mutable state:
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/hooks_composed.rs:use_storage}}
|
||||
```
|
||||
|
||||
## Custom Hook Logic
|
||||
|
||||
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!
|
||||
|
@ -23,4 +31,61 @@ 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:
|
||||
|
||||
- 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.
|
||||
|
||||
Here is a simplified implementation of the `use_state` hook:
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/hooks_custom_logic.rs:use_state}}
|
||||
```
|
||||
|
||||
- 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
|
||||
|
||||
Here is an implementation of the `use_context` and `use_context_provider` hooks:
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/hooks_custom_logic.rs:use_context}}
|
||||
```
|
||||
|
||||
## Hook Anti-Patterns
|
||||
|
||||
When writing a custom hook, you should avoid the following anti-patterns:
|
||||
|
||||
- !Clone Hooks: To allow hooks to be used within async blocks, the hooks must be Clone. To make a hook clone, you can wrap data in Rc or Arc and avoid lifetimes in hooks.
|
||||
|
||||
This version of use_state may seem more efficient, but it is not cloneable:
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/hooks_anti_patterns.rs:non_clone_state}}
|
||||
```
|
||||
|
||||
If we try to use this hook in an async block, we will get a compile error:
|
||||
|
||||
```rust
|
||||
fn FutureComponent(cx: &ScopeState) -> Element {
|
||||
let my_state = my_use_state(cx, || 0);
|
||||
cx.spawn({
|
||||
to_owned![my_state];
|
||||
async move {
|
||||
my_state.set(1);
|
||||
}
|
||||
});
|
||||
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
But with the original version, we can use it in an async block:
|
||||
|
||||
```rust
|
||||
fn FutureComponent(cx: &ScopeState) -> Element {
|
||||
let my_state = use_state(cx, || 0);
|
||||
cx.spawn({
|
||||
to_owned![my_state];
|
||||
async move {
|
||||
my_state.set(1);
|
||||
}
|
||||
});
|
||||
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
|
|
@ -64,7 +64,7 @@ Example: suppose you have a list of comments you want to render. Then, you can r
|
|||
|
||||
### 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:
|
||||
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}}
|
||||
|
|
|
@ -29,9 +29,9 @@ To learn what the different event types for HTML provide, read the [events modul
|
|||
|
||||
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)
|
||||
> For more information about event propagation see [the mdn docs on event bubbling](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:
|
||||
If you want to prevent this behavior, you can call `stop_propagation()` on the event:
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/event_nested.rs:rsx}}
|
||||
|
@ -41,7 +41,7 @@ If you want to prevent this behavior, you can call `stop_propogation()` on the e
|
|||
|
||||
Some events have a default behavior. For keyboard events, this might be entering the typed character. For mouse events, this might be selecting some text.
|
||||
|
||||
In some instances, might want to avoid this default behavior. For this, you can add the `prevent_default` attribute with the name of the handler whose default behavior you want to stop. This attribute is special: you can attach it multiple times for multiple attributes:
|
||||
In some instances, might want to avoid this default behavior. For this, you can add the `prevent_default` attribute with the name of the handler whose default behavior you want to stop. This attribute can be used for multiple handlers using their name separated by spaces:
|
||||
|
||||
```rust
|
||||
{{#include ../../../examples/event_prevent_default.rs:prevent_default}}
|
||||
|
|
|
@ -44,7 +44,7 @@ This is only possible because the two hooks are always called in the same order,
|
|||
|
||||
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
|
||||
1. The same hooks must be called
|
||||
1. The same hooks must be called (except in the case of early returns, as explained later in the [Error Handling chapter](../best_practices/error_handling.md))
|
||||
2. In the same order
|
||||
3. Hooks name's should start with `use_` so you don't accidentally confuse them with regular functions
|
||||
|
||||
|
|
|
@ -84,4 +84,4 @@ rsx!{
|
|||
|
||||
## More reading
|
||||
|
||||
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).
|
||||
This page is just a very brief overview of the router. For more information, check out [the router book](https://dioxuslabs.com/docs/0.3/router/) or some of [the router examples](https://github.com/DioxusLabs/dioxus/blob/master/examples/router.rs).
|
||||
|
|
|
@ -80,4 +80,4 @@ rsx!{
|
|||
|
||||
Esta página é apenas uma breve visão geral do roteador para mostrar que existe uma solução poderosa já construída para um problema muito comum. Para obter mais informações sobre o roteador, confira seu livro ou confira alguns dos exemplos.
|
||||
|
||||
O roteador tem sua própria documentação! [Disponível aqui](https://dioxuslabs.com/router/guide/).
|
||||
O roteador tem sua própria documentação! [Disponível aqui](https://dioxuslabs.com/docs/0.3/router/).
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "dioxus-pwa-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Antonio Curavalea <one.kyonblack@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { path = "../../packages/dioxus", version = "^0.3.0"}
|
||||
dioxus-web = { path = "../../packages/web", version = "^0.3.0"}
|
||||
|
||||
log = "0.4.6"
|
||||
|
||||
# WebAssembly Debug
|
||||
wasm-logger = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
|
@ -0,0 +1,42 @@
|
|||
[application]
|
||||
|
||||
# App (Project) Name
|
||||
name = "dioxus-pwa-example"
|
||||
|
||||
# Dioxus App Default Platform
|
||||
# desktop, web, mobile, ssr
|
||||
default_platform = "web"
|
||||
|
||||
# `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (public) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "dioxus | ⛺"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
# when watcher trigger, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# which files or dirs will be watcher monitoring
|
||||
watch_path = ["src", "public"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
script = []
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Dioxus
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,44 @@
|
|||
# Dioxus PWA example
|
||||
|
||||
This is a basic example of a progressive web app (PWA) using Dioxus and Dioxus CLI.
|
||||
Currently PWA functionality requires the use of a service worker and manifest file, so this isn't 100% Rust yet.
|
||||
|
||||
It is also very much usable as a template for your projects, if you're aiming to create a PWA.
|
||||
|
||||
## Try the example
|
||||
|
||||
Make sure you have Dioxus CLI installed (if you're unsure, run `cargo install dioxus-cli`).
|
||||
|
||||
You can run `dioxus serve` in this directory to start the web server locally, or run
|
||||
`dioxus build --release` to build the project so you can deploy it on a separate web-server.
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
├── Cargo.toml
|
||||
├── Dioxus.toml
|
||||
├── index.html // Custom HTML is needed for this, to load the SW and manifest.
|
||||
├── LICENSE
|
||||
├── public
|
||||
│ ├── favicon.ico
|
||||
│ ├── logo_192.png
|
||||
│ ├── logo_512.png
|
||||
│ ├── manifest.json // The manifest file - edit this as you need to.
|
||||
│ └── sw.js // The service worker - you must edit this for actual projects.
|
||||
├── README.md
|
||||
└── src
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
If you're just getting started with PWAs, here are some useful resources:
|
||||
|
||||
* [PWABuilder docs](https://docs.pwabuilder.com/#/)
|
||||
* [MDN article on PWAs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
|
||||
|
||||
For service worker scripting (in JavaScript):
|
||||
|
||||
* [Service worker guide from PWABuilder](https://docs.pwabuilder.com/#/home/sw-intro)
|
||||
* [Service worker examples, also from PWABuilder](https://github.com/pwa-builder/pwabuilder-serviceworkers)
|
||||
|
||||
If you want to stay as close to 100% Rust as possible, you can try using [wasi-worker](https://github.com/dunnock/wasi-worker) to replace the JS service worker file. The JSON manifest will still be required though.
|
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{app_title}</title>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register(
|
||||
'/sw.js'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8" />
|
||||
{style_include}
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script type="module">
|
||||
import init from "/{base_path}/assets/dioxus/{app_name}.js";
|
||||
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {
|
||||
if (wasm.__wbindgen_start == undefined) {
|
||||
wasm.main();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{script_include}
|
||||
</body>
|
||||
</html>
|
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 47 KiB |
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "Dioxus",
|
||||
"icons": [
|
||||
{
|
||||
"src": "logo_192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo_512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "logo_512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "any",
|
||||
"purpose": "any"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"id": "/",
|
||||
"display": "standalone",
|
||||
"display_override": ["window-control-overlay", "standalone"],
|
||||
"scope": "/",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff",
|
||||
"short_name": "Dioxus",
|
||||
"description": "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust.",
|
||||
"dir": "ltr",
|
||||
"lang": "en",
|
||||
"orientation": "portrait"
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
"use strict";
|
||||
|
||||
//console.log('WORKER: executing.');
|
||||
|
||||
/* A version number is useful when updating the worker logic,
|
||||
allowing you to remove outdated cache entries during the update.
|
||||
*/
|
||||
var version = 'v1.0.0::';
|
||||
|
||||
/* These resources will be downloaded and cached by the service worker
|
||||
during the installation process. If any resource fails to be downloaded,
|
||||
then the service worker won't be installed either.
|
||||
*/
|
||||
var offlineFundamentals = [
|
||||
// add here the files you want to cache
|
||||
'favicon.ico'
|
||||
];
|
||||
|
||||
/* The install event fires when the service worker is first installed.
|
||||
You can use this event to prepare the service worker to be able to serve
|
||||
files while visitors are offline.
|
||||
*/
|
||||
self.addEventListener("install", function (event) {
|
||||
//console.log('WORKER: install event in progress.');
|
||||
/* Using event.waitUntil(p) blocks the installation process on the provided
|
||||
promise. If the promise is rejected, the service worker won't be installed.
|
||||
*/
|
||||
event.waitUntil(
|
||||
/* The caches built-in is a promise-based API that helps you cache responses,
|
||||
as well as finding and deleting them.
|
||||
*/
|
||||
caches
|
||||
/* You can open a cache by name, and this method returns a promise. We use
|
||||
a versioned cache name here so that we can remove old cache entries in
|
||||
one fell swoop later, when phasing out an older service worker.
|
||||
*/
|
||||
.open(version + 'fundamentals')
|
||||
.then(function (cache) {
|
||||
/* After the cache is opened, we can fill it with the offline fundamentals.
|
||||
The method below will add all resources in `offlineFundamentals` to the
|
||||
cache, after making requests for them.
|
||||
*/
|
||||
return cache.addAll(offlineFundamentals);
|
||||
})
|
||||
.then(function () {
|
||||
//console.log('WORKER: install completed');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/* The fetch event fires whenever a page controlled by this service worker requests
|
||||
a resource. This isn't limited to `fetch` or even XMLHttpRequest. Instead, it
|
||||
comprehends even the request for the HTML page on first load, as well as JS and
|
||||
CSS resources, fonts, any images, etc.
|
||||
*/
|
||||
self.addEventListener("fetch", function (event) {
|
||||
//console.log('WORKER: fetch event in progress.');
|
||||
|
||||
/* We should only cache GET requests, and deal with the rest of method in the
|
||||
client-side, by handling failed POST,PUT,PATCH,etc. requests.
|
||||
*/
|
||||
if (event.request.method !== 'GET') {
|
||||
/* If we don't block the event as shown below, then the request will go to
|
||||
the network as usual.
|
||||
*/
|
||||
//console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
|
||||
return;
|
||||
}
|
||||
/* Similar to event.waitUntil in that it blocks the fetch event on a promise.
|
||||
Fulfillment result will be used as the response, and rejection will end in a
|
||||
HTTP response indicating failure.
|
||||
*/
|
||||
event.respondWith(
|
||||
caches
|
||||
/* This method returns a promise that resolves to a cache entry matching
|
||||
the request. Once the promise is settled, we can then provide a response
|
||||
to the fetch request.
|
||||
*/
|
||||
.match(event.request)
|
||||
.then(function (cached) {
|
||||
/* Even if the response is in our cache, we go to the network as well.
|
||||
This pattern is known for producing "eventually fresh" responses,
|
||||
where we return cached responses immediately, and meanwhile pull
|
||||
a network response and store that in the cache.
|
||||
|
||||
Read more:
|
||||
https://ponyfoo.com/articles/progressive-networking-serviceworker
|
||||
*/
|
||||
var networked = fetch(event.request)
|
||||
// We handle the network request with success and failure scenarios.
|
||||
.then(fetchedFromNetwork, unableToResolve)
|
||||
// We should catch errors on the fetchedFromNetwork handler as well.
|
||||
.catch(unableToResolve);
|
||||
|
||||
/* We return the cached response immediately if there is one, and fall
|
||||
back to waiting on the network as usual.
|
||||
*/
|
||||
//console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
|
||||
return cached || networked;
|
||||
|
||||
function fetchedFromNetwork(response) {
|
||||
/* We copy the response before replying to the network request.
|
||||
This is the response that will be stored on the ServiceWorker cache.
|
||||
*/
|
||||
var cacheCopy = response.clone();
|
||||
|
||||
//console.log('WORKER: fetch response from network.', event.request.url);
|
||||
|
||||
caches
|
||||
// We open a cache to store the response for this request.
|
||||
.open(version + 'pages')
|
||||
.then(function add(cache) {
|
||||
/* We store the response for this request. It'll later become
|
||||
available to caches.match(event.request) calls, when looking
|
||||
for cached responses.
|
||||
*/
|
||||
cache.put(event.request, cacheCopy);
|
||||
})
|
||||
.then(function () {
|
||||
//console.log('WORKER: fetch response stored in cache.', event.request.url);
|
||||
});
|
||||
|
||||
// Return the response so that the promise is settled in fulfillment.
|
||||
return response;
|
||||
}
|
||||
|
||||
/* When this method is called, it means we were unable to produce a response
|
||||
from either the cache or the network. This is our opportunity to produce
|
||||
a meaningful response even when all else fails. It's the last chance, so
|
||||
you probably want to display a "Service Unavailable" view or a generic
|
||||
error response.
|
||||
*/
|
||||
function unableToResolve() {
|
||||
/* There's a couple of things we can do here.
|
||||
- Test the Accept header and then return one of the `offlineFundamentals`
|
||||
e.g: `return caches.match('/some/cached/image.png')`
|
||||
- You should also consider the origin. It's easier to decide what
|
||||
"unavailable" means for requests against your origins than for requests
|
||||
against a third party, such as an ad provider.
|
||||
- Generate a Response programmaticaly, as shown below, and return that.
|
||||
*/
|
||||
|
||||
//console.log('WORKER: fetch request failed in both cache and network.');
|
||||
|
||||
/* Here we're creating a response programmatically. The first parameter is the
|
||||
response body, and the second one defines the options for the response.
|
||||
*/
|
||||
return new Response('<h1>Service Unavailable</h1>', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'text/html'
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/* The activate event fires after a service worker has been successfully installed.
|
||||
It is most useful when phasing out an older version of a service worker, as at
|
||||
this point you know that the new worker was installed correctly. In this example,
|
||||
we delete old caches that don't match the version in the worker we just finished
|
||||
installing.
|
||||
*/
|
||||
self.addEventListener("activate", function (event) {
|
||||
/* Just like with the install event, event.waitUntil blocks activate on a promise.
|
||||
Activation will fail unless the promise is fulfilled.
|
||||
*/
|
||||
//console.log('WORKER: activate event in progress.');
|
||||
|
||||
event.waitUntil(
|
||||
caches
|
||||
/* This method returns a promise which will resolve to an array of available
|
||||
cache keys.
|
||||
*/
|
||||
.keys()
|
||||
.then(function (keys) {
|
||||
// We return a promise that settles when all outdated caches are deleted.
|
||||
return Promise.all(
|
||||
keys
|
||||
.filter(function (key) {
|
||||
// Filter by keys that don't start with the latest version prefix.
|
||||
return !key.startsWith(version);
|
||||
})
|
||||
.map(function (key) {
|
||||
/* Return a promise that's fulfilled
|
||||
when each outdated cache is deleted.
|
||||
*/
|
||||
return caches.delete(key);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function () {
|
||||
//console.log('WORKER: activate completed.');
|
||||
})
|
||||
);
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
// init debug tool for WebAssembly
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
dioxus_web::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! (
|
||||
div {
|
||||
style: "text-align: center;",
|
||||
h1 { "🌗 Dioxus 🚀" }
|
||||
h3 { "Frontend that scales." }
|
||||
p { "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust." }
|
||||
}
|
||||
))
|
||||
}
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,44 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let elements: &UseRef<Vec<Rc<MountedData>>> = use_ref(cx, Vec::new);
|
||||
let running = use_state(cx, || true);
|
||||
|
||||
use_future!(cx, |(elements, running)| async move {
|
||||
let mut focused = 0;
|
||||
if *running.current() {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
if let Some(element) = elements.read().get(focused) {
|
||||
element.set_focus(true);
|
||||
} else {
|
||||
focused = 0;
|
||||
}
|
||||
focused += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
h1 { "Input Roulette" }
|
||||
for i in 0..100 {
|
||||
input {
|
||||
value: "{i}",
|
||||
onmounted: move |cx| {
|
||||
elements.write().push(cx.inner().clone());
|
||||
},
|
||||
oninput: move |_| {
|
||||
running.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
|
@ -7,9 +7,10 @@ fn main() {
|
|||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
p {
|
||||
"This should show an image:"
|
||||
}
|
||||
img { src: "examples/assets/logo.png" }
|
||||
img { src: "/Users/jonkelley/Desktop/blitz.png" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(App);
|
||||
}
|
||||
|
||||
fn App(cx: Scope) -> Element {
|
||||
let files_uploaded: &UseRef<Vec<String>> = use_ref(cx, Vec::new);
|
||||
|
||||
cx.render(rsx! {
|
||||
input {
|
||||
r#type: "file",
|
||||
accept: ".txt, .rs",
|
||||
multiple: true,
|
||||
onchange: |evt| {
|
||||
to_owned![files_uploaded];
|
||||
async move {
|
||||
if let Some(file_engine) = &evt.files {
|
||||
let files = file_engine.files();
|
||||
for file_name in &files {
|
||||
if let Some(file) = file_engine.read_file_to_string(file_name).await{
|
||||
files_uploaded.write().push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
ul {
|
||||
for file in files_uploaded.read().iter() {
|
||||
li { "{file}" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
|
@ -5,9 +7,20 @@ fn main() {
|
|||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! { generic_child::<i32>{} })
|
||||
cx.render(rsx! { generic_child {
|
||||
data: 0i32
|
||||
} })
|
||||
}
|
||||
|
||||
fn generic_child<T>(cx: Scope) -> Element {
|
||||
cx.render(rsx! { div {} })
|
||||
#[derive(PartialEq, Props)]
|
||||
struct GenericChildProps<T: Display + PartialEq> {
|
||||
data: T,
|
||||
}
|
||||
|
||||
fn generic_child<T: Display + PartialEq>(cx: Scope<GenericChildProps<T>>) -> Element {
|
||||
let data = &cx.props.data;
|
||||
|
||||
cx.render(rsx! { div {
|
||||
"{data}"
|
||||
} })
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ const FIELDS: &[(&str, &str)] = &[
|
|||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div { margin_left: "30px",
|
||||
select_example(cx),
|
||||
div {
|
||||
// handling inputs on divs will catch all input events below
|
||||
// so the value of our input event will be either huey, dewey, louie, or true/false (because of the checkboxe)
|
||||
|
@ -134,3 +135,34 @@ fn app(cx: Scope) -> Element {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn select_example(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
select {
|
||||
id: "selection",
|
||||
name: "selection",
|
||||
multiple: true,
|
||||
oninput: move |evt| {
|
||||
println!("{evt:?}");
|
||||
},
|
||||
option {
|
||||
value : "Option 1",
|
||||
label : "Option 1",
|
||||
}
|
||||
option {
|
||||
value : "Option 2",
|
||||
label : "Option 2",
|
||||
selected : true,
|
||||
},
|
||||
option {
|
||||
value : "Option 3",
|
||||
label : "Option 3",
|
||||
}
|
||||
}
|
||||
label {
|
||||
r#for: "selection",
|
||||
"select element"
|
||||
}
|
||||
}})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
#![allow(clippy::await_holding_refcell_ref)]
|
||||
use std::rc::Rc;
|
||||
|
||||
use dioxus::{html::geometry::euclid::Rect, prelude::*};
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch_cfg(
|
||||
app,
|
||||
dioxus_desktop::Config::default().with_custom_head(
|
||||
r#"
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
#main {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
"#
|
||||
.to_owned(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let div_element: &UseRef<Option<Rc<MountedData>>> = use_ref(cx, || None);
|
||||
|
||||
let dimentions = use_ref(cx, Rect::zero);
|
||||
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
background_color: "red",
|
||||
onmounted: move |cx| {
|
||||
div_element.set(Some(cx.inner().clone()));
|
||||
},
|
||||
"This element is {dimentions.read():?}"
|
||||
}
|
||||
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![div_element, dimentions];
|
||||
async move {
|
||||
let read = div_element.read();
|
||||
let client_rect = read.as_ref().map(|el| el.get_client_rect());
|
||||
if let Some(client_rect) = client_rect {
|
||||
if let Ok(rect) = client_rect.await {
|
||||
dimentions.set(rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Read dimentions"
|
||||
}
|
||||
))
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let header_element = use_ref(cx, || None);
|
||||
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
h1 {
|
||||
onmounted: move |cx| {
|
||||
header_element.set(Some(cx.inner().clone()));
|
||||
},
|
||||
"Scroll to top example"
|
||||
}
|
||||
|
||||
for i in 0..100 {
|
||||
div { "Item {i}" }
|
||||
}
|
||||
|
||||
button {
|
||||
onclick: move |_| {
|
||||
if let Some(header) = header_element.read().as_ref() {
|
||||
header.scroll_to(ScrollBehavior::Smooth);
|
||||
}
|
||||
},
|
||||
"Scroll to top"
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
|
@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element {
|
|||
onclick: move |_| {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
val.set(rng.gen_range(1..6));
|
||||
val.set(rng.gen_range(1..=6));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "dioxus-tailwind"
|
||||
version = "0.0.0"
|
||||
authors = []
|
||||
edition = "2021"
|
||||
description = "A tailwindcss example using Dioxus"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
rust-version = "1.60.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { path = "../../packages/dioxus" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
dioxus-desktop = { path = "../../packages/desktop" }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
dioxus-web = { path = "../../packages/web" }
|
|
@ -0,0 +1,46 @@
|
|||
[application]
|
||||
|
||||
# App (Project) Name
|
||||
name = "Tailwind CSS + Dioxus"
|
||||
|
||||
# Dioxus App Default Platform
|
||||
# desktop, web, mobile, ssr
|
||||
default_platform = "web"
|
||||
|
||||
# `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (public) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "dioxus | ⛺"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
# when watcher trigger, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# which files or dirs will be watcher monitoring
|
||||
watch_path = ["src", "public"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = ["tailwind.css"]
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# serve: [dev-server] only
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
|
@ -0,0 +1,136 @@
|
|||
Example: Basic Tailwind usage
|
||||
|
||||
This example shows how an app might be styled with TailwindCSS.
|
||||
|
||||
# Setup
|
||||
|
||||
1. Install the Dioxus CLI:
|
||||
|
||||
```bash
|
||||
cargo install --git https://github.com/DioxusLabs/cli
|
||||
```
|
||||
|
||||
2. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
|
||||
3. Install the tailwind css cli: https://tailwindcss.com/docs/installation
|
||||
4. Initialize the tailwind css project:
|
||||
|
||||
```bash
|
||||
npx tailwindcss init
|
||||
```
|
||||
|
||||
This should create a `tailwind.config.js` file in the root of the project.
|
||||
|
||||
5. Edit the `tailwind.config.js` file to include rust files:
|
||||
|
||||
```json
|
||||
module.exports = {
|
||||
mode: "all",
|
||||
content: [
|
||||
// include all rust, html and css files in the src directory
|
||||
"./src/**/*.{rs,html,css}",
|
||||
// include all html files in the output (dist) directory
|
||||
"./dist/**/*.html",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
```
|
||||
|
||||
6. Create a `input.css` file with the following content:
|
||||
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
```
|
||||
|
||||
7. Create a `Dioxus.toml` file with the following content that links to the `tailwind.css` file:
|
||||
|
||||
```toml
|
||||
[application]
|
||||
|
||||
# App (Project) Name
|
||||
name = "Tailwind CSS + Dioxus"
|
||||
|
||||
# Dioxus App Default Platform
|
||||
# desktop, web, mobile, ssr
|
||||
default_platform = "web"
|
||||
|
||||
# `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (public) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "dioxus | ⛺"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
# when watcher trigger, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# which files or dirs will be watcher monitoring
|
||||
watch_path = ["src", "public"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = ["tailwind.css"]
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# serve: [dev-server] only
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
```
|
||||
|
||||
## Bonus Steps
|
||||
|
||||
8. Install the tailwind css vs code extension
|
||||
9. Go to the settings for the extension and find the experimental regex support section. Edit the setting.json file to look like this:
|
||||
|
||||
```json
|
||||
"tailwindCSS.experimental.classRegex": ["class: \"(.*)\""],
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"rust": "html"
|
||||
},
|
||||
```
|
||||
|
||||
# Development
|
||||
|
||||
1. Run the following command in the root of the project to start the tailwind css compiler:
|
||||
|
||||
```bash
|
||||
npx tailwindcss -i ./input.css -o ./public/tailwind.css --watch
|
||||
```
|
||||
|
||||
## Web
|
||||
|
||||
- Run the following command in the root of the project to start the dioxus dev server:
|
||||
|
||||
```bash
|
||||
dioxus serve --hot-reload
|
||||
```
|
||||
|
||||
- Open the browser to http://localhost:8080
|
||||
|
||||
## Desktop
|
||||
|
||||
- Launch the dioxus desktop app
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -0,0 +1,833 @@
|
|||
/*
|
||||
! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: 1536px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mb-16 {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-8 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-3 {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.mr-5 {
|
||||
margin-right: 1.25rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.h-10 {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.w-10 {
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.w-5\/6 {
|
||||
width: 83.333333%;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-full {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.border-0 {
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
.bg-gray-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-900 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-indigo-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(99 102 241 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.object-cover {
|
||||
-o-object-fit: cover;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.object-center {
|
||||
-o-object-position: center;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.p-5 {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.px-5 {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.px-6 {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.py-1 {
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.py-24 {
|
||||
padding-top: 6rem;
|
||||
padding-bottom: 6rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.leading-relaxed {
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-indigo-600:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(79 70 229 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-white:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.focus\:outline-none:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:mb-0 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.md\:ml-auto {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.md\:mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.md\:w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.md\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.md\:items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.md\:pr-16 {
|
||||
padding-right: 4rem;
|
||||
}
|
||||
|
||||
.md\:text-left {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.lg\:w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lg\:max-w-lg {
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.lg\:flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.lg\:pr-24 {
|
||||
padding-right: 6rem;
|
||||
}
|
||||
}
|
|
@ -1,22 +1,16 @@
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
//! Example: Basic Tailwind usage
|
||||
//!
|
||||
//! This example shows how an app might be styled with TailwindCSS.
|
||||
//!
|
||||
//! To minify your tailwind bundle, currently you need to use npm. Follow these instructions:
|
||||
//!
|
||||
//! https://dev.to/arctic_hen7/how-to-set-up-tailwind-css-with-yew-and-trunk-il9
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::Config;
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
dioxus_desktop::launch_cfg(
|
||||
app,
|
||||
Config::new()
|
||||
.with_custom_head("<script src=\"https://cdn.tailwindcss.com\"></script>".to_string()),
|
||||
dioxus_desktop::Config::new()
|
||||
.with_custom_head(r#"<link rel="stylesheet" href="public/tailwind.css">"#.to_string()),
|
||||
);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
dioxus_web::launch(app);
|
||||
}
|
||||
|
||||
pub fn app(cx: Scope) -> Element {
|
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
mode: "all",
|
||||
content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
web-component {
|
||||
"my-prop": "5%",
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# Contributing
|
||||
|
||||
On Linux, install the following packages:
|
||||
|
||||
```bash
|
||||
sudo apt install libgdk3.0-cil libatk1.0-dev libcairo2-dev libpango1.0-dev libgdk-pixbuf2.0-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
cargo test --workspace --tests
|
||||
```
|
|
@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
|
||||
dioxus-rsx = { workspace = true }
|
||||
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
||||
|
|
|
@ -8,6 +8,8 @@ use std::fmt::{Result, Write};
|
|||
|
||||
use dioxus_rsx::IfmtInput;
|
||||
|
||||
use crate::write_ifmt;
|
||||
|
||||
/// The output buffer that tracks indent and string
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Buffer {
|
||||
|
@ -48,7 +50,7 @@ impl Buffer {
|
|||
}
|
||||
|
||||
pub fn write_text(&mut self, text: &IfmtInput) -> Result {
|
||||
write!(self.buf, "\"{}\"", text.source.as_ref().unwrap().value())
|
||||
write_ifmt(text, &mut self.buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{writer::Location, Writer};
|
||||
use crate::{ifmt_to_string, writer::Location, Writer};
|
||||
use dioxus_rsx::*;
|
||||
use quote::ToTokens;
|
||||
use std::fmt::{Result, Write};
|
||||
|
@ -170,9 +170,9 @@ impl Writer<'_> {
|
|||
ContentField::Formatted(s) => {
|
||||
write!(
|
||||
self.out,
|
||||
"{}: \"{}\"",
|
||||
"{}: {}",
|
||||
name,
|
||||
s.source.as_ref().unwrap().value()
|
||||
s.source.as_ref().unwrap().to_token_stream()
|
||||
)?;
|
||||
}
|
||||
ContentField::OnHandlerRaw(exp) => {
|
||||
|
@ -215,7 +215,7 @@ impl Writer<'_> {
|
|||
let attr_len = fields
|
||||
.iter()
|
||||
.map(|field| match &field.content {
|
||||
ContentField::Formatted(s) => s.source.as_ref().unwrap().value().len() ,
|
||||
ContentField::Formatted(s) => ifmt_to_string(s).len() ,
|
||||
ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => {
|
||||
let formatted = prettyplease::unparse_expr(exp);
|
||||
let len = if formatted.contains('\n') {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::Writer;
|
||||
use crate::{ifmt_to_string, Writer};
|
||||
use dioxus_rsx::*;
|
||||
use proc_macro2::Span;
|
||||
use quote::ToTokens;
|
||||
use std::{
|
||||
fmt::Result,
|
||||
fmt::{self, Write},
|
||||
|
@ -175,11 +176,7 @@ impl Writer<'_> {
|
|||
if !sameline {
|
||||
self.out.indented_tabbed_line()?;
|
||||
}
|
||||
write!(
|
||||
self.out,
|
||||
"key: \"{}\"",
|
||||
key.source.as_ref().unwrap().value()
|
||||
)?;
|
||||
write!(self.out, "key: {}", ifmt_to_string(key))?;
|
||||
if !attributes.is_empty() {
|
||||
write!(self.out, ",")?;
|
||||
if sameline {
|
||||
|
@ -216,11 +213,7 @@ impl Writer<'_> {
|
|||
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
|
||||
match &attr.attr {
|
||||
ElementAttr::AttrText { name, value } => {
|
||||
write!(
|
||||
self.out,
|
||||
"{name}: \"{value}\"",
|
||||
value = value.source.as_ref().unwrap().value()
|
||||
)?;
|
||||
write!(self.out, "{name}: {value}", value = ifmt_to_string(value))?;
|
||||
}
|
||||
ElementAttr::AttrExpression { name, value } => {
|
||||
let out = prettyplease::unparse_expr(value);
|
||||
|
@ -230,15 +223,15 @@ impl Writer<'_> {
|
|||
ElementAttr::CustomAttrText { name, value } => {
|
||||
write!(
|
||||
self.out,
|
||||
"\"{name}\": \"{value}\"",
|
||||
name = name.value(),
|
||||
value = value.source.as_ref().unwrap().value()
|
||||
"{name}: {value}",
|
||||
name = name.to_token_stream(),
|
||||
value = ifmt_to_string(value)
|
||||
)?;
|
||||
}
|
||||
|
||||
ElementAttr::CustomAttrExpression { name, value } => {
|
||||
let out = prettyplease::unparse_expr(value);
|
||||
write!(self.out, "\"{}\": {}", name.value(), out)?;
|
||||
write!(self.out, "{}: {}", name.to_token_stream(), out)?;
|
||||
}
|
||||
|
||||
ElementAttr::EventTokens { name, tokens } => {
|
||||
|
@ -276,13 +269,12 @@ impl Writer<'_> {
|
|||
let start = location.start();
|
||||
let line_start = start.line - 1;
|
||||
|
||||
let this_line = self.src[line_start];
|
||||
|
||||
let beginning = if this_line.len() > start.column {
|
||||
this_line[..start.column].trim()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let beginning = self
|
||||
.src
|
||||
.get(line_start)
|
||||
.filter(|this_line| this_line.len() > start.column)
|
||||
.map(|this_line| this_line[..start.column].trim())
|
||||
.unwrap_or_default();
|
||||
|
||||
beginning.is_empty()
|
||||
}
|
||||
|
@ -316,7 +308,7 @@ impl Writer<'_> {
|
|||
}
|
||||
|
||||
match children {
|
||||
[BodyNode::Text(ref text)] => Some(text.source.as_ref().unwrap().value().len()),
|
||||
[BodyNode::Text(ref text)] => Some(ifmt_to_string(text).len()),
|
||||
[BodyNode::Component(ref comp)] => {
|
||||
let attr_len = self.field_len(&comp.fields, &comp.manual_props);
|
||||
|
||||
|
@ -339,7 +331,7 @@ impl Writer<'_> {
|
|||
|
||||
if el.children.len() == 1 {
|
||||
if let BodyNode::Text(ref text) = el.children[0] {
|
||||
let value = text.source.as_ref().unwrap().value();
|
||||
let value = ifmt_to_string(text);
|
||||
|
||||
if value.len() + el.name.to_string().len() + attr_len < 80 {
|
||||
return Some(value.len() + el.name.to_string().len() + attr_len);
|
||||
|
@ -357,7 +349,7 @@ impl Writer<'_> {
|
|||
match item {
|
||||
BodyNode::Component(_) | BodyNode::Element(_) => return None,
|
||||
BodyNode::Text(text) => {
|
||||
total_count += text.source.as_ref().unwrap().value().len()
|
||||
total_count += ifmt_to_string(text).len();
|
||||
}
|
||||
BodyNode::RawExpr(expr) => match get_expr_length(expr) {
|
||||
Some(len) => total_count += len,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use std::fmt::{Display, Write};
|
||||
|
||||
use crate::writer::*;
|
||||
use collect_macros::byte_offset;
|
||||
use dioxus_rsx::{BodyNode, CallBody};
|
||||
use dioxus_rsx::{BodyNode, CallBody, IfmtInput};
|
||||
use proc_macro2::LineColumn;
|
||||
use quote::ToTokens;
|
||||
use syn::{ExprMacro, MacroDelimiter};
|
||||
|
||||
mod buffer;
|
||||
|
@ -130,8 +133,6 @@ pub fn write_block_out(body: CallBody) -> Option<String> {
|
|||
}
|
||||
|
||||
fn write_body(buf: &mut Writer, body: &CallBody) {
|
||||
use std::fmt::Write;
|
||||
|
||||
if buf.is_short_children(&body.roots).is_some() {
|
||||
// write all the indents with spaces and commas between
|
||||
for idx in 0..body.roots.len() - 1 {
|
||||
|
@ -208,3 +209,24 @@ pub fn apply_formats(input: &str, blocks: Vec<FormattedBlock>) -> String {
|
|||
|
||||
out
|
||||
}
|
||||
|
||||
struct DisplayIfmt<'a>(&'a IfmtInput);
|
||||
|
||||
impl Display for DisplayIfmt<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let inner_tokens = self.0.source.as_ref().unwrap().to_token_stream();
|
||||
inner_tokens.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ifmt_to_string(input: &IfmtInput) -> String {
|
||||
let mut buf = String::new();
|
||||
let display = DisplayIfmt(input);
|
||||
write!(&mut buf, "{}", display).unwrap();
|
||||
buf
|
||||
}
|
||||
|
||||
pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::fmt::Result {
|
||||
let display = DisplayIfmt(input);
|
||||
write!(writable, "{}", display)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use std::{
|
|||
use syn::{spanned::Spanned, Expr, ExprIf};
|
||||
|
||||
use crate::buffer::Buffer;
|
||||
use crate::ifmt_to_string;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Writer<'a> {
|
||||
|
@ -147,16 +148,16 @@ impl<'a> Writer<'a> {
|
|||
|
||||
total += match &attr.attr {
|
||||
ElementAttr::AttrText { value, name } => {
|
||||
value.source.as_ref().unwrap().value().len() + name.span().line_length() + 6
|
||||
ifmt_to_string(value).len() + name.span().line_length() + 6
|
||||
}
|
||||
ElementAttr::AttrExpression { name, value } => {
|
||||
value.span().line_length() + name.span().line_length() + 6
|
||||
}
|
||||
ElementAttr::CustomAttrText { value, name } => {
|
||||
value.source.as_ref().unwrap().value().len() + name.value().len() + 6
|
||||
ifmt_to_string(value).len() + name.to_token_stream().to_string().len() + 6
|
||||
}
|
||||
ElementAttr::CustomAttrExpression { name, value } => {
|
||||
name.value().len() + value.span().line_length() + 6
|
||||
name.to_token_stream().to_string().len() + value.span().line_length() + 6
|
||||
}
|
||||
ElementAttr::EventTokens { tokens, name } => {
|
||||
let location = Location::new(tokens.span().start());
|
||||
|
|
|
@ -4,8 +4,8 @@ macro_rules! twoway {
|
|||
|
||||
// doc attrs
|
||||
$( #[doc = $doc:expr] )*
|
||||
$name:ident
|
||||
),*
|
||||
$name:ident,
|
||||
)*
|
||||
) => {
|
||||
$(
|
||||
$( #[doc = $doc] )*
|
||||
|
@ -14,6 +14,9 @@ macro_rules! twoway {
|
|||
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
|
||||
let formatted = dioxus_autofmt::fmt_file(src);
|
||||
let out = dioxus_autofmt::apply_formats(src, formatted);
|
||||
// normalize line endings
|
||||
let out = out.replace("\r", "");
|
||||
let src = src.replace("\r", "");
|
||||
pretty_assertions::assert_eq!(&src, &out);
|
||||
}
|
||||
)*
|
||||
|
@ -21,24 +24,25 @@ macro_rules! twoway {
|
|||
}
|
||||
|
||||
twoway![
|
||||
simple,
|
||||
comments,
|
||||
attributes,
|
||||
manual_props,
|
||||
collapse_expr,
|
||||
comments,
|
||||
commentshard,
|
||||
complex,
|
||||
emoji,
|
||||
ifchain_forloop,
|
||||
immediate_expr,
|
||||
key,
|
||||
long_exprs,
|
||||
long,
|
||||
manual_props,
|
||||
messy_indent,
|
||||
multirsx,
|
||||
raw_strings,
|
||||
reallylong,
|
||||
simple,
|
||||
t2,
|
||||
tiny,
|
||||
tinynoopt,
|
||||
long,
|
||||
key,
|
||||
multirsx,
|
||||
commentshard,
|
||||
emoji,
|
||||
messy_indent,
|
||||
long_exprs,
|
||||
ifchain_forloop,
|
||||
t2,
|
||||
reallylong,
|
||||
immediate_expr,
|
||||
collapse_expr,
|
||||
trailing_expr
|
||||
trailing_expr,
|
||||
];
|
||||
|
|
|
@ -13,7 +13,9 @@ rsx! {
|
|||
|
||||
// Complex nesting with handlers
|
||||
li {
|
||||
Link { class: "flex items-center pl-3 py-3 pr-4 {active_class} rounded", to: "{to}",
|
||||
Link {
|
||||
class: "flex items-center pl-3 py-3 pr-4 {active_class} rounded",
|
||||
to: "{to}",
|
||||
span { class: "inline-block mr-3", icons::icon_0 {} }
|
||||
span { "{name}" }
|
||||
children.is_some().then(|| rsx! {
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
rsx! {
|
||||
// Raw text strings
|
||||
button { r#"Click me"#, r##"Click me"##, r######"Click me"######, r#"dynamic {1}"# }
|
||||
|
||||
// Raw attribute strings
|
||||
div {
|
||||
width: r#"10px"#,
|
||||
height: r##"{10}px"##,
|
||||
"raw-attr": r###"raw-attr"###,
|
||||
"raw-attr2": r###"{100}"###
|
||||
}
|
||||
}
|
|
@ -6,6 +6,11 @@ macro_rules! twoway {
|
|||
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
|
||||
let formatted = dioxus_autofmt::fmt_file(src_wrong);
|
||||
let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
|
||||
|
||||
// normalize line endings
|
||||
let out = out.replace("\r", "");
|
||||
let src_right = src_right.replace("\r", "");
|
||||
|
||||
pretty_assertions::assert_eq!(&src_right, &out);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ proc-macro = true
|
|||
proc-macro2 = { version = "1.0" }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
|
||||
dioxus-rsx = { workspace = true }
|
||||
|
||||
# testing
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus-core"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
|
@ -18,23 +18,23 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
|
||||
|
||||
# faster hashmaps
|
||||
rustc-hash = "1.1.0"
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
# Used in diffing
|
||||
longest-increasing-subsequence = "0.1.0"
|
||||
|
||||
futures-util = { version = "0.3", default-features = false, features = [
|
||||
futures-util = { workspace = true, default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
|
||||
slab = "0.4"
|
||||
slab = { workspace = true }
|
||||
|
||||
futures-channel = "0.3.21"
|
||||
futures-channel = { workspace = true }
|
||||
|
||||
indexmap = "1.7"
|
||||
|
||||
smallbox = "0.8.1"
|
||||
log = "0.4.17"
|
||||
log = { workspace = true }
|
||||
|
||||
# Serialize the Edits for use in Webview/Liveview instances
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
@ -42,8 +42,8 @@ serde = { version = "1", features = ["derive"], optional = true }
|
|||
bumpslab = { version = "0.2.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
dioxus = { path = "../dioxus" }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
dioxus = { workspace = true }
|
||||
pretty_assertions = "1.3.0"
|
||||
rand = "0.8.5"
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::{
|
|||
nodes::{DynamicNode, VNode},
|
||||
scopes::ScopeId,
|
||||
virtual_dom::VirtualDom,
|
||||
Attribute, AttributeValue, TemplateNode,
|
||||
Attribute, TemplateNode,
|
||||
};
|
||||
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
@ -125,10 +125,8 @@ impl<'b> VirtualDom {
|
|||
.mounted_element
|
||||
.set(left_attr.mounted_element.get());
|
||||
|
||||
// We want to make sure anything listener that gets pulled is valid
|
||||
if let AttributeValue::Listener(_) = right_attr.value {
|
||||
// We want to make sure anything that gets pulled is valid
|
||||
self.update_template(left_attr.mounted_element.get(), right_template);
|
||||
}
|
||||
|
||||
// If the attributes are different (or volatile), we need to update them
|
||||
if left_attr.value != right_attr.value || left_attr.volatile {
|
||||
|
|
|
@ -284,6 +284,17 @@ where
|
|||
Ok(&*Box::leak(deserialized))
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
fn deserialize_option_leaky<'a, 'de, D>(deserializer: D) -> Result<Option<&'static str>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::Deserialize;
|
||||
|
||||
let deserialized = Option::<String>::deserialize(deserializer)?;
|
||||
Ok(deserialized.map(|deserialized| &*Box::leak(deserialized.into_boxed_str())))
|
||||
}
|
||||
|
||||
impl<'a> Template<'a> {
|
||||
/// Is this template worth caching at all, since it's completely runtime?
|
||||
///
|
||||
|
@ -319,6 +330,10 @@ pub enum TemplateNode<'a> {
|
|||
///
|
||||
/// In HTML, this would be a valid URI that defines a namespace for all elements below it
|
||||
/// SVG is an example of this namespace
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
serde(deserialize_with = "deserialize_option_leaky")
|
||||
)]
|
||||
namespace: Option<&'a str>,
|
||||
|
||||
/// A list of possibly dynamic attribues for this element
|
||||
|
|
|
@ -384,10 +384,13 @@ impl VirtualDom {
|
|||
data,
|
||||
};
|
||||
|
||||
// Loop through each dynamic attribute in this template before moving up to the template's parent.
|
||||
// If the event bubbles, we traverse through the tree until we find the target element.
|
||||
if bubbles {
|
||||
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
|
||||
while let Some(el_ref) = parent_path {
|
||||
// safety: we maintain references of all vnodes in the element slab
|
||||
let template = unsafe { el_ref.template.unwrap().as_ref() };
|
||||
if let Some(template) = el_ref.template {
|
||||
let template = unsafe { template.as_ref() };
|
||||
let node_template = template.template.get();
|
||||
let target_path = el_ref.path;
|
||||
|
||||
|
@ -400,11 +403,6 @@ impl VirtualDom {
|
|||
{
|
||||
listeners.push(&attr.value);
|
||||
|
||||
// Break if the event doesn't bubble anyways
|
||||
if !bubbles {
|
||||
break;
|
||||
}
|
||||
|
||||
// Break if this is the exact target element.
|
||||
// This means we won't call two listeners with the same name on the same element. This should be
|
||||
// documented, or be rejected from the rsx! macro outright
|
||||
|
@ -429,6 +427,36 @@ impl VirtualDom {
|
|||
}
|
||||
|
||||
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise, we just call the listener on the target element
|
||||
if let Some(el_ref) = parent_path {
|
||||
// safety: we maintain references of all vnodes in the element slab
|
||||
if let Some(template) = el_ref.template {
|
||||
let template = unsafe { template.as_ref() };
|
||||
let node_template = template.template.get();
|
||||
let target_path = el_ref.path;
|
||||
|
||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||
let this_path = node_template.attr_paths[idx];
|
||||
|
||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||
// Only call the listener if this is the exact target element.
|
||||
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
||||
if let AttributeValue::Listener(listener) = &attr.value {
|
||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||
cb(uievent.clone());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,30 +12,33 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
|
||||
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-html = { workspace = true, features = ["serialize", "native-bind"] }
|
||||
dioxus-interpreter-js = { workspace = true }
|
||||
dioxus-hot-reload = { workspace = true, optional = true }
|
||||
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
thiserror = "1.0.30"
|
||||
log = "0.4.14"
|
||||
log = { workspace = true }
|
||||
wry = { version = "0.27.2" }
|
||||
futures-channel = "0.3.21"
|
||||
tokio = { version = "1.16.1", features = [
|
||||
futures-channel = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"rt",
|
||||
"time",
|
||||
"macros",
|
||||
], optional = true, default-features = false }
|
||||
"fs",
|
||||
], optional = true }
|
||||
webbrowser = "0.8.0"
|
||||
infer = "0.11.0"
|
||||
dunce = "1.0.2"
|
||||
slab = "0.4"
|
||||
slab = { workspace = true }
|
||||
|
||||
futures-util = "0.3.25"
|
||||
futures-util = { workspace = true }
|
||||
rfd = "0.11.3"
|
||||
urlencoding = "2.1.2"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
|
@ -53,6 +56,19 @@ tray = ["wry/tray"]
|
|||
hot-reload = ["dioxus-hot-reload"]
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
dioxus-hooks = { path = "../hooks" }
|
||||
# image = "0.24.0" # enable this when generating a new desktop image
|
||||
dioxus-core-macro = { workspace = true }
|
||||
dioxus-hooks = { workspace = true }
|
||||
dioxus = { workspace = true }
|
||||
exitcode = "1.1.2"
|
||||
scraper = "0.16.0"
|
||||
|
||||
# These tests need to be run on the main thread, so they cannot use rust's test harness.
|
||||
[[test]]
|
||||
name = "check_events"
|
||||
path = "headless_tests/events.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "check_rendering"
|
||||
path = "headless_tests/rendering.rs"
|
||||
harness = false
|
||||
|
|
|
@ -0,0 +1,351 @@
|
|||
use dioxus::html::geometry::euclid::Vector3D;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::DesktopContext;
|
||||
|
||||
pub(crate) fn check_app_exits(app: Component) {
|
||||
use dioxus_desktop::tao::window::WindowBuilder;
|
||||
use dioxus_desktop::Config;
|
||||
// This is a deadman's switch to ensure that the app exits
|
||||
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||
let should_panic_clone = should_panic.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_secs(100));
|
||||
if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
std::process::exit(exitcode::SOFTWARE);
|
||||
}
|
||||
});
|
||||
|
||||
dioxus_desktop::launch_cfg(
|
||||
app,
|
||||
Config::new().with_window(WindowBuilder::new().with_visible(false)),
|
||||
);
|
||||
|
||||
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
check_app_exits(app);
|
||||
}
|
||||
|
||||
fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) {
|
||||
use_effect(cx, (), |_| {
|
||||
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
||||
async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
desktop_context.eval(&format!(
|
||||
r#"let element = document.getElementById('{}');
|
||||
// Dispatch a synthetic event
|
||||
const event = {};
|
||||
console.log(element, event);
|
||||
element.dispatchEvent(event);
|
||||
"#,
|
||||
id, value
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
fn app(cx: Scope) -> Element {
|
||||
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
||||
let recieved_events = use_state(cx, || 0);
|
||||
|
||||
// button
|
||||
mock_event(
|
||||
cx,
|
||||
"button",
|
||||
r#"new MouseEvent("click", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
button: 0,
|
||||
})"#,
|
||||
);
|
||||
// mouse_move_div
|
||||
mock_event(
|
||||
cx,
|
||||
"mouse_move_div",
|
||||
r#"new MouseEvent("mousemove", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
buttons: 2,
|
||||
})"#,
|
||||
);
|
||||
// mouse_click_div
|
||||
mock_event(
|
||||
cx,
|
||||
"mouse_click_div",
|
||||
r#"new MouseEvent("click", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
buttons: 2,
|
||||
button: 2,
|
||||
})"#,
|
||||
);
|
||||
// mouse_dblclick_div
|
||||
mock_event(
|
||||
cx,
|
||||
"mouse_dblclick_div",
|
||||
r#"new MouseEvent("dblclick", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
buttons: 1|2,
|
||||
button: 2,
|
||||
})"#,
|
||||
);
|
||||
// mouse_down_div
|
||||
mock_event(
|
||||
cx,
|
||||
"mouse_down_div",
|
||||
r#"new MouseEvent("mousedown", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
buttons: 2,
|
||||
button: 2,
|
||||
})"#,
|
||||
);
|
||||
// mouse_up_div
|
||||
mock_event(
|
||||
cx,
|
||||
"mouse_up_div",
|
||||
r#"new MouseEvent("mouseup", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
buttons: 0,
|
||||
button: 0,
|
||||
})"#,
|
||||
);
|
||||
// wheel_div
|
||||
mock_event(
|
||||
cx,
|
||||
"wheel_div",
|
||||
r#"new WheelEvent("wheel", {
|
||||
view: window,
|
||||
deltaX: 1.0,
|
||||
deltaY: 2.0,
|
||||
deltaZ: 3.0,
|
||||
deltaMode: 0x00,
|
||||
bubbles: true,
|
||||
})"#,
|
||||
);
|
||||
// key_down_div
|
||||
mock_event(
|
||||
cx,
|
||||
"key_down_div",
|
||||
r#"new KeyboardEvent("keydown", {
|
||||
key: "a",
|
||||
code: "KeyA",
|
||||
location: 0,
|
||||
repeat: true,
|
||||
keyCode: 65,
|
||||
charCode: 97,
|
||||
char: "a",
|
||||
charCode: 0,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
isComposing: false,
|
||||
which: 65,
|
||||
bubbles: true,
|
||||
})"#,
|
||||
);
|
||||
// key_up_div
|
||||
mock_event(
|
||||
cx,
|
||||
"key_up_div",
|
||||
r#"new KeyboardEvent("keyup", {
|
||||
key: "a",
|
||||
code: "KeyA",
|
||||
location: 0,
|
||||
repeat: false,
|
||||
keyCode: 65,
|
||||
charCode: 97,
|
||||
char: "a",
|
||||
charCode: 0,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
isComposing: false,
|
||||
which: 65,
|
||||
bubbles: true,
|
||||
})"#,
|
||||
);
|
||||
// key_press_div
|
||||
mock_event(
|
||||
cx,
|
||||
"key_press_div",
|
||||
r#"new KeyboardEvent("keypress", {
|
||||
key: "a",
|
||||
code: "KeyA",
|
||||
location: 0,
|
||||
repeat: false,
|
||||
keyCode: 65,
|
||||
charCode: 97,
|
||||
char: "a",
|
||||
charCode: 0,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
isComposing: false,
|
||||
which: 65,
|
||||
bubbles: true,
|
||||
})"#,
|
||||
);
|
||||
// focus_in_div
|
||||
mock_event(
|
||||
cx,
|
||||
"focus_in_div",
|
||||
r#"new FocusEvent("focusin", {bubbles: true})"#,
|
||||
);
|
||||
// focus_out_div
|
||||
mock_event(
|
||||
cx,
|
||||
"focus_out_div",
|
||||
r#"new FocusEvent("focusout",{bubbles: true})"#,
|
||||
);
|
||||
|
||||
if **recieved_events == 12 {
|
||||
println!("all events recieved");
|
||||
desktop_context.close();
|
||||
}
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
button {
|
||||
id: "button",
|
||||
onclick: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().is_empty());
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
},
|
||||
}
|
||||
div {
|
||||
id: "mouse_move_div",
|
||||
onmousemove: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
},
|
||||
}
|
||||
div {
|
||||
id: "mouse_click_div",
|
||||
onclick: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
},
|
||||
}
|
||||
div{
|
||||
id: "mouse_dblclick_div",
|
||||
ondblclick: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div{
|
||||
id: "mouse_down_div",
|
||||
onmousedown: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div{
|
||||
id: "mouse_up_div",
|
||||
onmouseup: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().is_empty());
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div{
|
||||
id: "wheel_div",
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
background_color: "red",
|
||||
onwheel: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{
|
||||
panic!("Expected delta to be in pixels")
|
||||
};
|
||||
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
id: "key_down_div",
|
||||
onkeydown: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert_eq!(event.data.key().to_string(), "a");
|
||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location, 0);
|
||||
assert!(event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
id: "key_up_div",
|
||||
onkeyup: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert_eq!(event.data.key().to_string(), "a");
|
||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location, 0);
|
||||
assert!(!event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
id: "key_press_div",
|
||||
onkeypress: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert_eq!(event.data.key().to_string(), "a");
|
||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location, 0);
|
||||
assert!(!event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
id: "focus_in_div",
|
||||
onfocusin: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
id: "focus_out_div",
|
||||
onfocusout: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::DesktopContext;
|
||||
|
||||
pub(crate) fn check_app_exits(app: Component) {
|
||||
use dioxus_desktop::tao::window::WindowBuilder;
|
||||
use dioxus_desktop::Config;
|
||||
// This is a deadman's switch to ensure that the app exits
|
||||
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||
let should_panic_clone = should_panic.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_secs(100));
|
||||
if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
std::process::exit(exitcode::SOFTWARE);
|
||||
}
|
||||
});
|
||||
|
||||
dioxus_desktop::launch_cfg(
|
||||
app,
|
||||
Config::new().with_window(WindowBuilder::new().with_visible(false)),
|
||||
);
|
||||
|
||||
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
check_app_exits(check_html_renders);
|
||||
}
|
||||
|
||||
fn use_inner_html(cx: &ScopeState, id: &'static str) -> Option<String> {
|
||||
let value: &UseRef<Option<String>> = use_ref(cx, || None);
|
||||
use_effect(cx, (), |_| {
|
||||
to_owned![value];
|
||||
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
||||
async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
let html = desktop_context
|
||||
.eval(&format!(
|
||||
r#"let element = document.getElementById('{}');
|
||||
return element.innerHTML;"#,
|
||||
id
|
||||
))
|
||||
.await;
|
||||
if let Ok(serde_json::Value::String(html)) = html {
|
||||
println!("html: {}", html);
|
||||
value.set(Some(html));
|
||||
}
|
||||
}
|
||||
});
|
||||
value.read().clone()
|
||||
}
|
||||
|
||||
const EXPECTED_HTML: &str = r#"<div id="5" style="width: 100px; height: 100px; color: rgb(0, 0, 0);"><input type="checkbox"><h1>text</h1><div><p>hello world</p></div></div>"#;
|
||||
|
||||
fn check_html_renders(cx: Scope) -> Element {
|
||||
let inner_html = use_inner_html(cx, "main_div");
|
||||
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
||||
|
||||
if let Some(raw_html) = inner_html.as_deref() {
|
||||
let fragment = scraper::Html::parse_fragment(raw_html);
|
||||
println!("fragment: {:?}", fragment.html());
|
||||
let expected = scraper::Html::parse_fragment(EXPECTED_HTML);
|
||||
println!("fragment: {:?}", expected.html());
|
||||
if fragment == expected {
|
||||
println!("html matches");
|
||||
desktop_context.close();
|
||||
}
|
||||
}
|
||||
|
||||
let dyn_value = 0;
|
||||
let dyn_element = rsx! {
|
||||
div {
|
||||
dangerous_inner_html: "<p>hello world</p>",
|
||||
}
|
||||
};
|
||||
|
||||
render! {
|
||||
div {
|
||||
id: "main_div",
|
||||
div {
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
color: "rgb({dyn_value}, {dyn_value}, {dyn_value})",
|
||||
id: 5,
|
||||
input {
|
||||
"type": "checkbox",
|
||||
},
|
||||
h1 {
|
||||
"text"
|
||||
}
|
||||
dyn_element
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 14 KiB |
|
@ -23,6 +23,7 @@ pub struct Config {
|
|||
pub(crate) custom_head: Option<String>,
|
||||
pub(crate) custom_index: Option<String>,
|
||||
pub(crate) root_name: String,
|
||||
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
|
||||
}
|
||||
|
||||
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
||||
|
@ -50,6 +51,7 @@ impl Config {
|
|||
custom_head: None,
|
||||
custom_index: None,
|
||||
root_name: "main".to_string(),
|
||||
background_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,6 +139,14 @@ impl Config {
|
|||
self.root_name = name.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background color of the WebView.
|
||||
/// This will be set before the HTML is rendered and can be used to prevent flashing when the page loads.
|
||||
/// Accepts a color in RGBA format
|
||||
pub fn with_background_color(mut self, color: (u8, u8, u8, u8)) -> Self {
|
||||
self.background_color = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
|
|
@ -5,6 +5,7 @@ use std::rc::Weak;
|
|||
use crate::create_new_window;
|
||||
use crate::eval::EvalResult;
|
||||
use crate::events::IpcMessage;
|
||||
use crate::query::QueryEngine;
|
||||
use crate::shortcut::IntoKeyCode;
|
||||
use crate::shortcut::IntoModifersState;
|
||||
use crate::shortcut::ShortcutId;
|
||||
|
@ -16,7 +17,6 @@ use dioxus_core::ScopeState;
|
|||
use dioxus_core::VirtualDom;
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use serde_json::Value;
|
||||
use slab::Slab;
|
||||
use wry::application::event::Event;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
|
@ -59,8 +59,8 @@ pub struct DesktopContext {
|
|||
/// The proxy to the event loop
|
||||
pub proxy: ProxyType,
|
||||
|
||||
/// The receiver for eval results since eval is async
|
||||
pub(super) eval: tokio::sync::broadcast::Sender<Value>,
|
||||
/// The receiver for queries about the current window
|
||||
pub(super) query: QueryEngine,
|
||||
|
||||
pub(super) pending_windows: WebviewQueue,
|
||||
|
||||
|
@ -96,7 +96,7 @@ impl DesktopContext {
|
|||
webview,
|
||||
proxy,
|
||||
event_loop,
|
||||
eval: tokio::sync::broadcast::channel(8).0,
|
||||
query: Default::default(),
|
||||
pending_windows: webviews,
|
||||
event_handlers,
|
||||
shortcut_manager,
|
||||
|
@ -210,28 +210,10 @@ impl DesktopContext {
|
|||
|
||||
/// Evaluate a javascript expression
|
||||
pub fn eval(&self, code: &str) -> EvalResult {
|
||||
// Embed the return of the eval in a function so we can send it back to the main thread
|
||||
let script = format!(
|
||||
r#"
|
||||
window.ipc.postMessage(
|
||||
JSON.stringify({{
|
||||
"method":"eval_result",
|
||||
"params": (
|
||||
function(){{
|
||||
{code}
|
||||
}}
|
||||
)()
|
||||
}})
|
||||
);
|
||||
"#
|
||||
);
|
||||
// the query id lets us keep track of the eval result and send it back to the main thread
|
||||
let query = self.query.new_query(code, &self.webview);
|
||||
|
||||
if let Err(e) = self.webview.evaluate_script(&script) {
|
||||
// send an error to the eval receiver
|
||||
log::warn!("Eval script error: {e}");
|
||||
}
|
||||
|
||||
EvalResult::new(self.eval.clone())
|
||||
EvalResult::new(query)
|
||||
}
|
||||
|
||||
/// Create a wry event handler that listens for wry events.
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use dioxus_core::ElementId;
|
||||
use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
|
||||
use wry::webview::WebView;
|
||||
|
||||
use crate::query::QueryEngine;
|
||||
|
||||
/// A mounted element passed to onmounted events
|
||||
pub struct DesktopElement {
|
||||
id: ElementId,
|
||||
webview: Rc<WebView>,
|
||||
query: QueryEngine,
|
||||
}
|
||||
|
||||
impl DesktopElement {
|
||||
pub(crate) fn new(id: ElementId, webview: Rc<WebView>, query: QueryEngine) -> Self {
|
||||
Self { id, webview, query }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderedElementBacking for DesktopElement {
|
||||
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn get_client_rect(
|
||||
&self,
|
||||
) -> std::pin::Pin<
|
||||
Box<
|
||||
dyn futures_util::Future<
|
||||
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
|
||||
>,
|
||||
>,
|
||||
> {
|
||||
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
.new_query::<Option<Rect<f64, f64>>>(&script, &self.webview)
|
||||
.resolve();
|
||||
Box::pin(async move {
|
||||
match fut.await {
|
||||
Ok(Some(rect)) => Ok(rect),
|
||||
Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
|
||||
Box::new(DesktopQueryError::FailedToQuery),
|
||||
)),
|
||||
Err(err) => {
|
||||
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn scroll_to(
|
||||
&self,
|
||||
behavior: dioxus_html::ScrollBehavior,
|
||||
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
|
||||
let script = format!(
|
||||
"return window.interpreter.ScrollTo({}, {});",
|
||||
self.id.0,
|
||||
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
|
||||
);
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
.new_query::<bool>(&script, &self.webview)
|
||||
.resolve();
|
||||
Box::pin(async move {
|
||||
match fut.await {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
|
||||
Box::new(DesktopQueryError::FailedToQuery),
|
||||
)),
|
||||
Err(err) => {
|
||||
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_focus(
|
||||
&self,
|
||||
focus: bool,
|
||||
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
|
||||
let script = format!(
|
||||
"return window.interpreter.SetFocus({}, {});",
|
||||
self.id.0, focus
|
||||
);
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
.new_query::<bool>(&script, &self.webview)
|
||||
.resolve();
|
||||
|
||||
Box::pin(async move {
|
||||
match fut.await {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
|
||||
Box::new(DesktopQueryError::FailedToQuery),
|
||||
)),
|
||||
Err(err) => {
|
||||
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DesktopQueryError {
|
||||
FailedToQuery,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DesktopQueryError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DesktopQueryError {}
|
|
@ -1,36 +1,32 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use crate::query::Query;
|
||||
use crate::query::QueryError;
|
||||
use crate::use_window;
|
||||
use dioxus_core::ScopeState;
|
||||
use serde::de::Error;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A future that resolves to the result of a JavaScript evaluation.
|
||||
pub struct EvalResult {
|
||||
pub(crate) broadcast: tokio::sync::broadcast::Sender<serde_json::Value>,
|
||||
pub(crate) query: Query<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl EvalResult {
|
||||
pub(crate) fn new(sender: tokio::sync::broadcast::Sender<serde_json::Value>) -> Self {
|
||||
Self { broadcast: sender }
|
||||
pub(crate) fn new(query: Query<serde_json::Value>) -> Self {
|
||||
Self { query }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFuture for EvalResult {
|
||||
type Output = Result<serde_json::Value, serde_json::Error>;
|
||||
type Output = Result<serde_json::Value, QueryError>;
|
||||
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut reciever = self.broadcast.subscribe();
|
||||
match reciever.recv().await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err(serde_json::Error::custom("No result returned")),
|
||||
}
|
||||
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
|
||||
Box::pin(self.query.resolve())
|
||||
as Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct FileDiologRequest {
|
||||
#[serde(default)]
|
||||
accept: Option<String>,
|
||||
multiple: bool,
|
||||
pub event: String,
|
||||
pub target: usize,
|
||||
pub bubbles: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn get_file_event(request: &FileDiologRequest) -> Vec<PathBuf> {
|
||||
let mut dialog = rfd::FileDialog::new();
|
||||
|
||||
let filters: Vec<_> = request
|
||||
.accept
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.filter_map(|s| Filters::from_str(s).ok())
|
||||
.collect();
|
||||
|
||||
let file_extensions: Vec<_> = filters
|
||||
.iter()
|
||||
.flat_map(|f| f.as_extensions().into_iter())
|
||||
.collect();
|
||||
|
||||
dialog = dialog.add_filter("name", file_extensions.as_slice());
|
||||
|
||||
let files: Vec<_> = if request.multiple {
|
||||
dialog.pick_files().into_iter().flatten().collect()
|
||||
} else {
|
||||
dialog.pick_file().into_iter().collect()
|
||||
};
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
enum Filters {
|
||||
Extension(String),
|
||||
Mime(String),
|
||||
Audio,
|
||||
Video,
|
||||
Image,
|
||||
}
|
||||
|
||||
impl Filters {
|
||||
fn as_extensions(&self) -> Vec<&str> {
|
||||
match self {
|
||||
Filters::Extension(extension) => vec![extension.as_str()],
|
||||
Filters::Mime(_) => vec![],
|
||||
Filters::Audio => vec!["mp3", "wav", "ogg"],
|
||||
Filters::Video => vec!["mp4", "webm"],
|
||||
Filters::Image => vec!["png", "jpg", "jpeg", "gif", "webp"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Filters {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Some(extension) = s.strip_prefix('.') {
|
||||
Ok(Filters::Extension(extension.to_string()))
|
||||
} else {
|
||||
match s {
|
||||
"audio/*" => Ok(Filters::Audio),
|
||||
"video/*" => Ok(Filters::Video),
|
||||
"image/*" => Ok(Filters::Image),
|
||||
_ => Ok(Filters::Mime(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|