Merge pull request #2084 from DioxusLabs/jk/split-out-tui

Move TUI renderer into blitz repo
This commit is contained in:
Jonathan Kelley 2024-03-14 19:00:17 -07:00 committed by GitHub
commit a9a8619489
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 37 additions and 17047 deletions

303
Cargo.lock generated
View file

@ -203,12 +203,6 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344"
[[package]]
name = "anymap"
version = "1.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1f8f5a6f3d50d89e3797d7593a50f96bb2aaa20ca0cc7be1fb673232c91d72"
[[package]]
name = "anymap2"
version = "0.13.0"
@ -1309,12 +1303,6 @@ dependencies = [
"toml 0.8.10",
]
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cast"
version = "0.3.0"
@ -1890,47 +1878,6 @@ version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crossterm"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
dependencies = [
"bitflags 1.3.2",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.4.2",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "crunchy"
version = "0.2.2"
@ -2201,7 +2148,7 @@ version = "0.5.0-alpha.0"
dependencies = [
"criterion 0.3.6",
"dioxus-config-macro",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-core-macro",
"dioxus-desktop",
"dioxus-fullstack",
@ -2213,7 +2160,6 @@ dependencies = [
"dioxus-router",
"dioxus-signals",
"dioxus-ssr",
"dioxus-tui",
"dioxus-web",
"env_logger",
"futures-util",
@ -2268,7 +2214,7 @@ dependencies = [
"dioxus-autofmt",
"dioxus-check",
"dioxus-cli-config",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-hot-reload",
"dioxus-html",
"dioxus-rsx",
@ -2357,6 +2303,20 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "dioxus-core"
version = "0.5.0-alpha.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f39bef0484998f724622964f04468ddfde7fd550f5f0f1aef9394306e9caba44"
dependencies = [
"futures-channel",
"futures-util",
"longest-increasing-subsequence",
"rustc-hash",
"slab",
"tracing",
]
[[package]]
name = "dioxus-core-macro"
version = "0.5.0-alpha.0"
@ -2387,7 +2347,7 @@ dependencies = [
"core-foundation",
"dioxus",
"dioxus-cli-config",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-hooks",
"dioxus-hot-reload",
"dioxus-html",
@ -2449,7 +2409,7 @@ dependencies = [
name = "dioxus-fullstack"
version = "0.5.0-alpha.0"
dependencies = [
"anymap 0.12.1",
"anymap",
"async-trait",
"axum",
"base64",
@ -2488,7 +2448,7 @@ name = "dioxus-hooks"
version = "0.5.0-alpha.0"
dependencies = [
"dioxus",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-debug-cell",
"dioxus-signals",
"futures-channel",
@ -2506,7 +2466,7 @@ name = "dioxus-hot-reload"
version = "0.5.0-alpha.0"
dependencies = [
"chrono",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-html",
"dioxus-rsx",
"execute",
@ -2523,7 +2483,7 @@ name = "dioxus-html"
version = "0.5.0-alpha.0"
dependencies = [
"async-trait",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-html-internal-macro",
"dioxus-rsx",
"enumset",
@ -2556,7 +2516,7 @@ dependencies = [
name = "dioxus-interpreter-js"
version = "0.5.0-alpha.0"
dependencies = [
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-html",
"js-sys",
"md5",
@ -2572,7 +2532,7 @@ name = "dioxus-lib"
version = "0.5.0-alpha.0"
dependencies = [
"dioxus-config-macro",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-core-macro",
"dioxus-hooks",
"dioxus-html",
@ -2587,7 +2547,7 @@ dependencies = [
"axum",
"dioxus",
"dioxus-cli-config",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-hot-reload",
"dioxus-html",
"dioxus-interpreter-js",
@ -2614,40 +2574,6 @@ dependencies = [
"dioxus-desktop",
]
[[package]]
name = "dioxus-native-core"
version = "0.5.0-alpha.0"
dependencies = [
"anymap 1.0.0-beta.2",
"dashmap",
"dioxus",
"dioxus-core",
"dioxus-native-core",
"dioxus-native-core-macro",
"keyboard-types",
"lightningcss",
"parking_lot",
"rand 0.8.5",
"rustc-hash",
"shipyard",
"smallvec",
"taffy",
"tokio",
]
[[package]]
name = "dioxus-native-core-macro"
version = "0.5.0-alpha.0"
dependencies = [
"anymap 0.12.1",
"dioxus",
"dioxus-native-core",
"quote",
"rustc-hash",
"smallvec",
"syn 2.0.51",
]
[[package]]
name = "dioxus-playwright-fullstack-test"
version = "0.1.0"
@ -2730,7 +2656,7 @@ dependencies = [
name = "dioxus-rsx"
version = "0.5.0-alpha.0"
dependencies = [
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"internment",
"krates",
"proc-macro2",
@ -2745,7 +2671,7 @@ name = "dioxus-signals"
version = "0.5.0-alpha.0"
dependencies = [
"dioxus",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"futures-channel",
"futures-util",
"generational-box",
@ -2769,7 +2695,7 @@ dependencies = [
"async-trait",
"chrono",
"dioxus",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-html",
"dioxus-signals",
"fern",
@ -2793,24 +2719,6 @@ dependencies = [
"manganis",
]
[[package]]
name = "dioxus-tui"
version = "0.5.0-alpha.0"
dependencies = [
"criterion 0.3.6",
"crossterm 0.26.1",
"dioxus",
"dioxus-core",
"dioxus-hot-reload",
"dioxus-html",
"dioxus-native-core",
"dioxus-native-core-macro",
"futures",
"plasmo",
"taffy",
"tokio",
]
[[package]]
name = "dioxus-web"
version = "0.5.0-alpha.0"
@ -2818,7 +2726,7 @@ dependencies = [
"async-trait",
"console_error_panic_hook",
"dioxus",
"dioxus-core",
"dioxus-core 0.5.0-alpha.0",
"dioxus-html",
"dioxus-interpreter-js",
"dioxus-ssr",
@ -4293,12 +4201,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "grid"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c"
[[package]]
name = "gtk"
version = "0.18.1"
@ -5152,15 +5054,6 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.12.1"
@ -5701,10 +5594,11 @@ dependencies = [
[[package]]
name = "manganis"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c10916db4ed51967d92545eb5629ce1e1cc05c74da2cd2aab6ab6c57c2e838b6"
checksum = "a42db80aa639f70d6bf1d2ef93612d2ffbb33f6ea17d473b91bfeb3f67bc24bc"
dependencies = [
"dioxus-core 0.5.0-alpha.0 (registry+https://github.com/rust-lang/crates.io-index)",
"manganis-macro",
]
@ -5737,9 +5631,9 @@ dependencies = [
[[package]]
name = "manganis-common"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82ee0f9002a9f5d7e3b662e55562996c60401bae16da23f3790c52e0823216ea"
checksum = "176dfb4bb5592b0d130176add9893af527ab565fc1bcf58ece88acd6276688d1"
dependencies = [
"anyhow",
"base64",
@ -5753,9 +5647,9 @@ dependencies = [
[[package]]
name = "manganis-macro"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da50cc6074480dc6c255a09c9d48dd4a379e6162feb5d308ec33d5ba9da4e579"
checksum = "ab2c2e11190c2f3d6133cffda5c955b463a7e90b7ba866e71e7dfa65fa97ddfc"
dependencies = [
"manganis-common",
"proc-macro2",
@ -6917,28 +6811,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "plasmo"
version = "0.5.0-alpha.0"
dependencies = [
"anyhow",
"anymap 1.0.0-beta.2",
"criterion 0.3.6",
"crossterm 0.26.1",
"dioxus-html",
"dioxus-native-core",
"dioxus-native-core-macro",
"futures",
"futures-channel",
"once_cell",
"ratatui",
"rustc-hash",
"shipyard",
"smallvec",
"taffy",
"tokio",
]
[[package]]
name = "plist"
version = "1.6.0"
@ -7354,24 +7226,6 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "ratatui"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425"
dependencies = [
"bitflags 2.4.2",
"cassowary",
"crossterm 0.27.0",
"indoc",
"itertools 0.11.0",
"lru",
"paste",
"strum",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "rav1e"
version = "0.6.6"
@ -8362,50 +8216,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "shipyard"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3511ae730f2e1c3d62a9025e2f9b2acbf130968057f1b3caab6d74a54a5e0e56"
dependencies = [
"hashbrown 0.12.3",
"lock_api",
"rayon",
"shipyard_proc",
]
[[package]]
name = "shipyard_proc"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eb847f4b9582e468198b5cfb5731b65cc67fe5e535acc9cbf3c11703d15f08c"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@ -8511,15 +8321,6 @@ dependencies = [
"rustc-hash",
]
[[package]]
name = "slotmap"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "1.13.1"
@ -8920,28 +8721,6 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.51",
]
[[package]]
name = "subprocess"
version = "0.2.9"
@ -9042,18 +8821,6 @@ dependencies = [
"version-compare",
]
[[package]]
name = "taffy"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c2287b6d7f721ada4cddf61ade5e760b2c6207df041cac9bfaa192897362fd3"
dependencies = [
"arrayvec",
"grid",
"num-traits",
"slotmap",
]
[[package]]
name = "tao"
version = "0.24.1"

View file

@ -23,10 +23,6 @@ members = [
"packages/autofmt",
"packages/check",
"packages/rsx",
"packages/dioxus-tui",
"packages/plasmo",
"packages/native-core",
"packages/native-core-macro",
"packages/rsx-rosetta",
"packages/generational-box",
"packages/signals",
@ -74,10 +70,6 @@ dioxus-liveview = { path = "packages/liveview", version = "0.5.0-alpha.0" }
dioxus-autofmt = { path = "packages/autofmt", version = "0.5.0-alpha.0" }
dioxus-check = { path = "packages/check", version = "0.5.0-alpha.0" }
dioxus-rsx = { path = "packages/rsx", version = "0.5.0-alpha.0" }
dioxus-tui = { path = "packages/dioxus-tui", version = "0.5.0-alpha.0" }
plasmo = { path = "packages/plasmo", version = "0.5.0-alpha.0" }
dioxus-native-core = { path = "packages/native-core", version = "0.5.0-alpha.0" }
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.5.0-alpha.0" }
rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.5.0-alpha.0" }
dioxus-signals = { path = "packages/signals", version = "0.5.0-alpha.0" }
dioxus-cli-config = { path = "packages/cli-config", version = "0.5.0-alpha.0" }

View file

@ -24,4 +24,3 @@ mobile = []
web = []
ssr = []
liveview = []
tui = []

View file

@ -23,7 +23,7 @@ pub fn server_only(input: TokenStream) -> TokenStream {
#[proc_macro]
pub fn client(input: TokenStream) -> TokenStream {
if cfg!(any(feature = "desktop", feature = "web", feature = "tui")) {
if cfg!(any(feature = "desktop", feature = "web")) {
let input = TokenStream2::from(input);
quote! {
#input
@ -110,18 +110,3 @@ pub fn liveview(input: TokenStream) -> TokenStream {
}
.into()
}
#[proc_macro]
pub fn tui(input: TokenStream) -> TokenStream {
if cfg!(feature = "tui") {
let input = TokenStream2::from(input);
quote! {
#input
}
} else {
quote! {
|| {}
}
}
.into()
}

View file

@ -1,2 +0,0 @@
/target
Cargo.lock

View file

@ -1,2 +0,0 @@
esque
Tui

View file

@ -1,36 +0,0 @@
[package]
name = "dioxus-tui"
version = { workspace = true }
authors = ["Jonathan Kelley, Evan Almloff"]
edition = "2021"
description = "TUI-based renderer for Dioxus"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com/learn/0.4/getting_started/tui"
keywords = ["dom", "ui", "gui", "react", "terminal"]
license = "MIT OR Apache-2.0"
[dependencies]
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true }
dioxus-native-core = { workspace = true, features = ["dioxus"] }
dioxus-native-core-macro = { workspace = true }
dioxus-hot-reload = { workspace = true, optional = true }
plasmo = { workspace = true }
crossterm = "0.26.0"
tokio = { workspace = true, features = ["full"] }
futures = "0.3.19"
taffy = "0.3.12"
[dev-dependencies]
dioxus = { workspace = true }
tokio = { version = "1" }
criterion = "0.3.5"
[[bench]]
name = "update"
harness = false
[features]
default = ["hot-reload"]
hot-reload = ["dioxus-hot-reload"]

View file

@ -1,95 +0,0 @@
<div align="center">
<h1>Dioxus TUI</h1>
<p>
<strong>Beautiful terminal user interfaces in Rust with <a href="https://dioxuslabs.com/">Dioxus </a>.</strong>
</p>
</div>
<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
alt="Crates.io version" />
</a>
<!-- Downloads -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
alt="Download" />
</a>
<!-- docs -->
<a href="https://docs.rs/dioxus">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
<!-- CI -->
<a href="https://github.com/jkelleyrtp/dioxus/actions">
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
alt="CI status" />
</a>
<!--Awesome -->
<a href="https://github.com/dioxuslabs/awesome-dioxus">
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Page" />
</a>
<!-- Discord -->
<a href="https://discord.gg/XgGxMSkvUM">
<img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
</a>
</div>
<br/>
Leverage React-like patterns, CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces with Dioxus.
```rust
fn app() -> Element {
rsx!{
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
"Hello world!"
}
})
}
```
![demo app](examples/example.png)
## Background
You can use Html-like semantics with inline styles, tree hierarchy, components, and more in your [`text-based user interface (TUI)`](https://en.wikipedia.org/wiki/Text-based_user_interface) application.
Dioxus TUI is essentially a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/) and [`Dioxus`](https://dioxuslabs.com/). Dioxus TUI doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful.
## Limitations
- **Subset of Html**
Terminals can only render a subset of HTML. We support as much as we can.
- **Particular frontend design**
Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers.
## Status
**WARNING: Dioxus TUI is currently under construction!**
Rendering a VirtualDom works fine, but the ecosystem of hooks is not yet ready. Additionally, some bugs in the flexbox implementation might be quirky at times.
## Features
Dioxus TUI features:
- [x] Flexbox-based layout system
- [ ] CSS selectors
- [x] inline CSS support
- [x] Built-in focusing system
* [x] Widgets<sup>1</sup>
* [ ] Support for events, hooks, and callbacks<sup>2</sup>
* [ ] Html tags<sup>3</sup>
<sup>1</sup> Currently only a subset of the input element is implemented as a component (not an element). The `Input` component supports sliders, text, numbers, passwords, buttons, and checkboxes.
<sup>2</sup> Basic keyboard, mouse, and focus events are implemented.
<sup>3</sup> Currently, most HTML tags don't translate into any meaning inside of Dioxus TUI. So an `input` _element_ won't mean anything nor does it have any additional functionality.

View file

@ -1,155 +0,0 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use dioxus::prelude::*;
use dioxus_tui::{Config, TuiContext};
criterion_group!(mbenches, tui_update);
criterion_main!(mbenches);
/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time.
fn tui_update(c: &mut Criterion) {
{
let mut group = c.benchmark_group("Update boxes");
for size in 1..=20usize {
let parameter_string = format!("{}", (size).pow(2));
group.bench_with_input(
BenchmarkId::new("size", parameter_string),
&size,
|b, size| {
b.iter(|| {
dioxus_tui::launch_cfg_with_props(
app,
GridProps {
size: *size,
update_count: 1,
},
Config::default().with_headless(),
)
})
},
);
}
}
{
let mut group = c.benchmark_group("Update many boxes");
for update_count in 1..=20usize {
let update_count = update_count * 20;
let parameter_string = update_count.to_string();
group.bench_with_input(
BenchmarkId::new("update count", parameter_string),
&update_count,
|b, update_count| {
b.iter(|| {
dioxus_tui::launch_cfg_with_props(
app,
GridProps {
size: 20,
update_count: *update_count,
},
Config::default().with_headless(),
)
})
},
);
}
}
}
#[derive(Props, PartialEq, Clone)]
struct BoxProps {
x: usize,
y: usize,
hue: f32,
alpha: f32,
}
#[allow(non_snake_case)]
fn Box(props: BoxProps) -> Element {
let count = use_signal(|| 0);
let x = props.x * 2;
let y = props.y * 2;
let hue = props.hue;
let display_hue = props.hue as u32 / 10;
let count = count();
let alpha = props.alpha + (count % 100) as f32;
rsx! {
div {
left: "{x}%",
top: "{y}%",
width: "100%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
align_items: "center",
p{"{display_hue:03}"}
}
}
}
#[derive(Props, PartialEq, Clone)]
struct GridProps {
size: usize,
update_count: usize,
}
#[allow(non_snake_case)]
fn Grid(props: GridProps) -> Element {
let size = props.size;
let mut count = use_signal(|| 0);
let mut counts = use_signal(|| vec![0; size * size]);
let ctx: TuiContext = consume_context();
if count() + props.update_count >= (size * size) {
ctx.quit();
} else {
for _ in 0..props.update_count {
counts.with_mut(|c| {
let i = count();
c[i] += 1;
c[i] %= 360;
});
count.with_mut(|i| {
*i += 1;
*i %= size * size;
});
}
}
rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
for x in 0..size {
div {
width: "100%",
height: "100%",
flex_direction: "row",
for y in 0..size {
Box {
key: "{x}-{y}",
x: x,
y: y,
alpha: 100.0,
hue: y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32,
}
}
}
}
}
}
}
fn app(props: GridProps) -> Element {
rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: props.size,
update_count: props.update_count,
}
}
}
}

View file

@ -1,64 +0,0 @@
use dioxus::prelude::*;
use std::{fmt::Debug, rc::Rc};
fn main() {
dioxus_tui::launch(app);
}
const MAX_EVENTS: usize = 8;
fn app() -> Element {
let mut events = use_signal(|| Vec::new() as Vec<Rc<dyn Debug>>);
let mut log_event = move |event: Rc<dyn Debug>| events.write().push(event);
rsx! {
div { width: "100%", height: "100%", flex_direction: "column",
div {
width: "80%",
height: "50%",
border_width: "1px",
justify_content: "center",
align_items: "center",
background_color: "hsl(248, 53%, 58%)",
// Mosue
onmousemove: move |event| log_event(event.data()),
onclick: move |event| log_event(event.data()),
ondoubleclick: move |event| log_event(event.data()),
onmousedown: move |event| log_event(event.data()),
onmouseup: move |event| log_event(event.data()),
// Scroll
onwheel: move |event| log_event(event.data()),
// Keyboard
onkeydown: move |event| log_event(event.data()),
onkeyup: move |event| log_event(event.data()),
onkeypress: move |event| log_event(event.data()),
// Focus
onfocusin: move |event| log_event(event.data()),
onfocusout: move |event| log_event(event.data()),
"Hover, click, type or scroll to see the info down below"
}
div { width: "80%", height: "50%", flex_direction: "column",
// A trailing iterator of the last MAX_EVENTS events
// The index actually is a fine key here, since events are append-only and therefore stable
for (index, event) in events.read().iter().enumerate().rev().take(MAX_EVENTS).rev() {
p { key: "{index}",
{
// TUI panics if text overflows (https://github.com/DioxusLabs/dioxus/issues/371)
// temporary hack: just trim the strings (and make sure viewport is big enough)
// todo: remove
let mut trimmed = format!("{event:?}");
trimmed.truncate(200);
trimmed
}
}
}
}
}
}
}

View file

@ -1,27 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
let mut radius = use_signal(|| 0);
rsx! {
div {
width: "100%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "hsl(248, 53%, 58%)",
onwheel: move |w| radius.with_mut(|r| *r = (*r + w.delta().strip_units().y as i8).abs()),
border_style: "solid none solid double",
border_width: "thick",
border_radius: "{radius}px",
border_color: "#0000FF #FF00FF #FF0000 #00FF00",
"{radius}"
}
}
}

View file

@ -1,72 +0,0 @@
use dioxus::prelude::*;
use dioxus_html::input_data::keyboard_types::Code;
fn main() {
dioxus_tui::launch(app);
}
#[component]
fn Button(color_offset: u32, layer: u16) -> Element {
let mut toggle = use_signal(|| false);
let mut hovered = use_signal(|| false);
let hue = color_offset % 255;
let saturation = if toggle() { 50 } else { 25 } + if hovered() { 50 } else { 25 };
let brightness = saturation / 2;
let color = format!("hsl({hue}, {saturation}, {brightness})");
rsx! {
div{
width: "100%",
height: "100%",
background_color: "{color}",
tabindex: "{layer}",
onkeydown: move |e| {
if let Code::Space = e.code() {
toggle.toggle();
}
},
onclick: move |_| toggle.toggle(),
onmouseenter: move |_| hovered.set(true),
onmouseleave: move |_| hovered.set(false),
justify_content: "center",
align_items: "center",
display: "flex",
flex_direction: "column",
p { "tabindex: {layer}" }
}
}
}
fn app() -> Element {
rsx! {
div {
display: "flex",
flex_direction: "column",
width: "100%",
height: "100%",
for y in 1..8 {
div {
display: "flex",
flex_direction: "row",
width: "100%",
height: "100%",
for x in 1..8 {
if (x + y) % 2 == 0 {
div {
width: "100%",
height: "100%",
background_color: "rgb(100, 100, 100)",
}
} else {
Button {
color_offset: x * y,
layer: ((x + y) % 3) as u16,
}
}
}
}
}
}
}
}

View file

@ -1,38 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch_cfg(
app,
dioxus_tui::Config::default().with_rendering_mode(dioxus_tui::RenderingMode::Ansi),
);
}
fn app() -> Element {
let steps = 50;
rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
for x in 0..=steps {
div { width: "100%", height: "100%", flex_direction: "row",
for y in 0..=steps {
{
let hue = x as f32*360.0/steps as f32;
let alpha = y as f32*100.0/steps as f32;
rsx! {
div {
left: "{x}px",
top: "{y}px",
width: "10%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
}
}
}
}
}
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

View file

@ -1,82 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
// justify_content: "center",
// align_items: "center",
// flex_direction: "row",
// background_color: "red",
p {
background_color: "black",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"hi"
"hi"
"hi"
}
li {
background_color: "red",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
}
li {
background_color: "blue",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
}
p {
background_color: "yellow",
"asd"
}
p {
background_color: "green",
"asd"
}
p {
background_color: "white",
"asd"
}
p {
background_color: "cyan",
"asd"
}
}
}
}

View file

@ -1,125 +0,0 @@
use dioxus::{events::MouseData, prelude::*};
use dioxus_core::Event;
use std::convert::TryInto;
use std::fmt::Write;
use std::rc::Rc;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
fn to_str(c: &[i32; 3]) -> String {
let mut result = String::new();
result += "#";
for c in c.iter() {
write!(result, "{c:02X?}").unwrap();
}
result
}
fn get_brightness(m: Rc<MouseData>) -> i32 {
let b: i32 = m.held_buttons().len().try_into().unwrap();
127 * b
}
let mut q1_color = use_signal(|| [200; 3]);
let mut q2_color = use_signal(|| [200; 3]);
let mut q3_color = use_signal(|| [200; 3]);
let mut q4_color = use_signal(|| [200; 3]);
let mut page_coordinates = use_signal(|| "".to_string());
let mut element_coordinates = use_signal(|| "".to_string());
let mut buttons = use_signal(|| "".to_string());
let mut modifiers = use_signal(|| "".to_string());
let update_data = move |event: Event<MouseData>| {
page_coordinates.set(format!("{:?}", event.page_coordinates()));
element_coordinates.set(format!("{:?}", event.element_coordinates()));
// Note: client coordinates are also available, but they would be the same as the page coordinates in this example, because there is no scrolling.
// There are also screen coordinates, but they are currently the same as client coordinates due to technical limitations
buttons.set(format!("{:?}", event.held_buttons()));
modifiers.set(format!("{:?}", event.modifiers()));
};
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
border_width: "1px",
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: to_str(&q1_color()),
onmouseenter: move |m| q1_color.set([get_brightness(m.data()), 0, 0]),
onmousedown: move |m| q1_color.set([get_brightness(m.data()), 0, 0]),
onmouseup: move |m| q1_color.set([get_brightness(m.data()), 0, 0]),
onwheel: move |w| q1_color.set([q1_color()[0] + (10.0 * w.delta().strip_units().y) as i32, 0, 0]),
onmouseleave: move |_| q1_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: to_str(&q2_color()),
onmouseenter: move |m| q2_color.set([get_brightness(m.data()); 3]),
onmousedown: move |m| q2_color.set([get_brightness(m.data()); 3]),
onmouseup: move |m| q2_color.set([get_brightness(m.data()); 3]),
onwheel: move |w| q2_color.set([q2_color()[0] + (10.0 * w.delta().strip_units().y) as i32; 3]),
onmouseleave: move |_| q2_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: to_str(&q3_color()),
onmouseenter: move |m| q3_color.set([0, get_brightness(m.data()), 0]),
onmousedown: move |m| q3_color.set([0, get_brightness(m.data()), 0]),
onmouseup: move |m| q3_color.set([0, get_brightness(m.data()), 0]),
onwheel: move |w| q3_color.set([0, q3_color()[1] + (10.0 * w.delta().strip_units().y) as i32, 0]),
onmouseleave: move |_| q3_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: to_str(&q4_color()),
onmouseenter: move |m| q4_color.set([0, 0, get_brightness(m.data())]),
onmousedown: move |m| q4_color.set([0, 0, get_brightness(m.data())]),
onmouseup: move |m| q4_color.set([0, 0, get_brightness(m.data())]),
onwheel: move |w| q4_color.set([0, 0, q4_color()[2] + (10.0 * w.delta().strip_units().y) as i32]),
onmouseleave: move |_| q4_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
}
div { "Page coordinates: {page_coordinates}" }
div { "Element coordinates: {element_coordinates}" }
div { "Buttons: {buttons}" }
div { "Modifiers: {modifiers}" }
}
}
}

View file

@ -1,28 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
border_width: "1px",
h1 { height: "2px", color: "green",
"that's awesome!"
}
ul {
flex_direction: "column",
padding_left: "3px",
for i in 0..10 {
"> hello {i}"
}
}
}
}
}

View file

@ -1,96 +0,0 @@
use dioxus::prelude::*;
use dioxus_tui::{Config, TuiContext};
/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time.
fn main() {
for size in 1..=20usize {
for _ in 0..10 {
let dom = VirtualDom::new(app).with_root_context(size);
dioxus_tui::launch_vdom_cfg(dom, Config::default().with_headless())
}
}
}
fn app() -> Element {
let size = use_context::<usize>();
rsx! {
div { width: "100%", height: "100%", Grid { size } }
}
}
#[component]
fn Box(x: usize, y: usize, hue: f32, alpha: f32) -> Element {
let count = use_signal(|| 0);
let x = x * 2;
let y = y * 2;
let hue = hue;
let display_hue = hue as u32 / 10;
let alpha = alpha + (count() % 100) as f32;
rsx! {
div {
left: "{x}%",
top: "{y}%",
width: "100%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
align_items: "center",
p{"{display_hue:03}"}
}
}
}
#[component]
fn Grid(size: usize) -> Element {
let size = size;
let mut count = use_signal(|| 0);
let mut counts = use_signal(|| vec![0; size * size]);
let ctx: TuiContext = consume_context();
if count() + 1 >= (size * size) {
ctx.quit();
} else {
counts.with_mut(|c| {
let i = count();
c[i] += 1;
c[i] %= 360;
});
count.with_mut(|i| {
*i += 1;
*i %= size * size;
});
}
rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
for x in 0..size {
div{
width: "100%",
height: "100%",
flex_direction: "row",
for y in 0..size {
{
let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32;
let key = format!("{}-{}", x, y);
rsx! {
Box {
x: x,
y: y,
alpha: 100.0,
hue: alpha,
key: "{key}",
}
}
}
}
}
}
}
}
}

View file

@ -1,50 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
background_color: "black",
margin_right: "10px",
div {
width: "70%",
height: "70%",
background_color: "green",
margin_left: "4px",
div {
width: "100%",
height: "100%",
margin_top: "2px",
margin_bottom: "2px",
margin_left: "2px",
margin_right: "2px",
flex_shrink: "0",
background_color: "red",
justify_content: "center",
align_items: "center",
flex_direction: "column",
padding_top: "2px",
padding_bottom: "2px",
padding_left: "4px",
padding_right: "4px",
"[A]"
"[A]"
"[A]"
"[A]"
}
}
}
}
}

View file

@ -1,58 +0,0 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch_cfg(app, Default::default());
}
#[component]
fn Quadrant(color: String, text: String) -> Element {
rsx! {
div {
border_width: "1px",
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "{color}",
"{text}"
}
}
}
fn app() -> Element {
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrant {
color: "red".to_string(),
text: "[A]".to_string()
},
Quadrant {
color: "black".to_string(),
text: "[B]".to_string()
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrant {
color: "green".to_string(),
text: "[C]".to_string()
},
Quadrant {
color: "blue".to_string(),
text: "[D]".to_string()
}
}
}
}
}

View file

@ -1,19 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
rsx! {
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
"Hello world!"
}
}
}

View file

@ -1,28 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
let mut count = use_signal(|| 0);
use_future(move || async move {
loop {
count += 1;
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
schedule_update();
}
});
rsx! {
div { width: "100%",
div { width: "50%", height: "5px", background_color: "blue", justify_content: "center", align_items: "center",
"Hello {count}!"
}
div { width: "50%", height: "10px", background_color: "red", justify_content: "center", align_items: "center",
"Hello {count}!"
}
}
}
}

View file

@ -1,113 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
let mut alpha = use_signal(|| 100);
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
onwheel: move |evt| alpha.set((alpha() + evt.delta().strip_units().y as i64).clamp(0, 100)),
p {
background_color: "black",
flex_direction: "column",
justify_content: "center",
align_items: "center",
color: "green",
"hi"
"hi"
"hi"
}
li {
background_color: "red",
flex_direction: "column",
justify_content: "center",
align_items: "center",
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
}
li {
background_color: "blue",
flex_direction: "column",
justify_content: "center",
align_items: "center",
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
}
p {
background_color: "yellow",
"asd"
}
p {
background_color: "green",
"asd"
}
p {
background_color: "white",
"asd"
}
p {
background_color: "cyan",
"asd"
}
div {
font_weight: "bold",
color: "#666666",
p {
"bold"
}
p {
font_weight: "normal",
" normal"
}
}
p {
font_style: "italic",
color: "red",
"italic"
}
p {
text_decoration: "underline",
color: "rgba(255, 255, 255)",
"underline"
}
p {
text_decoration: "line-through",
color: "hsla(10, 100%, 70%)",
"line-through"
}
div{
position: "absolute",
top: "1px",
background_color: "rgba(255, 0, 0, 50%)",
width: "100%",
p {
color: "rgba(255, 255, 255, {alpha}%)",
background_color: "rgba(100, 100, 100, {alpha}%)",
"rgba(255, 255, 255, {alpha}%)"
}
p {
color: "rgba(255, 255, 255, 100%)",
"rgba(255, 255, 255, 100%)"
}
}
}
}
}

View file

@ -1,90 +0,0 @@
use dioxus::prelude::*;
use dioxus_tui::Config;
fn main() {
dioxus_tui::launch_cfg(app, Config::new());
}
fn app() -> Element {
let mut bg_green = use_signal(|| false);
let color = if bg_green() { "green" } else { "red" };
rsx! {
div {
width: "100%",
background_color: "{color}",
flex_direction: "column",
align_items: "center",
justify_content: "center",
input {
oninput: move |data| if data.value() == "good" {
bg_green.set(true);
} else{
bg_green.set(false);
},
r#type: "checkbox",
value: "good",
width: "50%",
height: "10%",
checked: "true",
}
input {
oninput: move |data| if &data.value() == "hello world"{
bg_green.set(true);
} else {
bg_green.set(false);
},
width: "50%",
height: "10%",
maxlength: "11",
}
input {
oninput: move |data| {
if (data.value().parse::<f32>().unwrap() - 40.0).abs() < 5.0 {
bg_green.set(true);
} else{
bg_green.set(false);
}
},
r#type: "range",
width: "50%",
height: "10%",
min: "20",
max: "80",
}
input {
oninput: move |data| {
if data.value() == "10"{
bg_green.set(true);
} else {
bg_green.set(false);
}
},
r#type: "number",
width: "50%",
height: "10%",
maxlength: "4",
}
input {
oninput: move |data| {
if data.value() == "hello world"{
bg_green.set(true);
} else{
bg_green.set(false);
}
},
r#type: "password",
width: "50%",
height: "10%",
maxlength: "11",
}
input {
oninput: move |_| { bg_green.set(true) },
r#type: "button",
value: "green",
width: "50%",
height: "10%",
}
}
}
}

View file

@ -1,158 +0,0 @@
use std::{
any::Any,
fmt::{Display, Formatter},
};
use dioxus_core::{ElementId, WriteMutations};
use dioxus_html::{
geometry::euclid::{Point2D, Rect, Size2D},
MountedData, MountedError, RenderedElementBacking,
};
use dioxus_native_core::{dioxus::DioxusNativeCoreMutationWriter, NodeId};
use plasmo::query::{ElementRef, Query};
pub(crate) struct DioxusTUIMutationWriter<'a> {
pub(crate) query: Query,
pub(crate) events: &'a mut Vec<(ElementId, &'static str, Box<dyn Any>, bool)>,
pub(crate) native_core_writer: DioxusNativeCoreMutationWriter<'a>,
}
impl WriteMutations for DioxusTUIMutationWriter<'_> {
fn register_template(&mut self, template: dioxus_core::prelude::Template) {
self.native_core_writer.register_template(template)
}
fn append_children(&mut self, id: ElementId, m: usize) {
self.native_core_writer.append_children(id, m)
}
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) {
self.native_core_writer.assign_node_id(path, id)
}
fn create_placeholder(&mut self, id: ElementId) {
self.native_core_writer.create_placeholder(id)
}
fn create_text_node(&mut self, value: &str, id: ElementId) {
self.native_core_writer.create_text_node(value, id)
}
fn hydrate_text_node(&mut self, path: &'static [u8], value: &str, id: ElementId) {
self.native_core_writer.hydrate_text_node(path, value, id)
}
fn load_template(&mut self, name: &'static str, index: usize, id: ElementId) {
self.native_core_writer.load_template(name, index, id)
}
fn replace_node_with(&mut self, id: ElementId, m: usize) {
self.native_core_writer.replace_node_with(id, m)
}
fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) {
self.native_core_writer
.replace_placeholder_with_nodes(path, m)
}
fn insert_nodes_after(&mut self, id: ElementId, m: usize) {
self.native_core_writer.insert_nodes_after(id, m)
}
fn insert_nodes_before(&mut self, id: ElementId, m: usize) {
self.native_core_writer.insert_nodes_before(id, m)
}
fn set_attribute(
&mut self,
name: &'static str,
ns: Option<&'static str>,
value: &dioxus_core::AttributeValue,
id: ElementId,
) {
self.native_core_writer.set_attribute(name, ns, value, id)
}
fn set_node_text(&mut self, value: &str, id: ElementId) {
self.native_core_writer.set_node_text(value, id)
}
fn create_event_listener(&mut self, name: &'static str, id: ElementId) {
if name == "mounted" {
let element = TuiElement {
query: self.query.clone(),
id: self.native_core_writer.state.element_to_node_id(id),
};
self.events
.push((id, "mounted", Box::new(MountedData::new(element)), false));
} else {
self.native_core_writer.create_event_listener(name, id)
}
}
fn remove_event_listener(&mut self, name: &'static str, id: ElementId) {
self.native_core_writer.remove_event_listener(name, id)
}
fn remove_node(&mut self, id: ElementId) {
self.native_core_writer.remove_node(id)
}
fn push_root(&mut self, id: ElementId) {
self.native_core_writer.push_root(id)
}
}
#[derive(Clone)]
pub(crate) struct TuiElement {
query: Query,
id: NodeId,
}
impl TuiElement {
pub(crate) fn element(&self) -> ElementRef {
self.query.get(self.id)
}
}
impl RenderedElementBacking for TuiElement {
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let layout = self.element().layout();
Box::pin(async move {
match layout {
Some(layout) => {
let x = layout.location.x as f64;
let y = layout.location.y as f64;
let width = layout.size.width as f64;
let height = layout.size.height as f64;
Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height)))
}
None => Err(MountedError::OperationFailed(Box::new(TuiElementNotFound))),
}
})
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Debug)]
struct TuiElementNotFound;
impl Display for TuiElementNotFound {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "TUI element not found")
}
}
impl std::error::Error for TuiElementNotFound {}

View file

@ -1,108 +0,0 @@
use core::panic;
use dioxus_html::*;
use crate::element::TuiElement;
fn downcast(event: &PlatformEventData) -> plasmo::EventData {
event
.downcast::<plasmo::EventData>()
.expect("event should be of type EventData")
.clone()
}
pub(crate) struct SerializedHtmlEventConverter;
impl HtmlEventConverter for SerializedHtmlEventConverter {
fn convert_animation_data(&self, _: &PlatformEventData) -> AnimationData {
panic!("animation events not supported")
}
fn convert_clipboard_data(&self, _: &PlatformEventData) -> ClipboardData {
panic!("clipboard events not supported")
}
fn convert_composition_data(&self, _: &PlatformEventData) -> CompositionData {
panic!("composition events not supported")
}
fn convert_drag_data(&self, _: &PlatformEventData) -> DragData {
panic!("drag events not supported")
}
fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData {
if let plasmo::EventData::Focus(event) = downcast(event) {
FocusData::new(event)
} else {
panic!("event should be of type Focus")
}
}
fn convert_form_data(&self, event: &PlatformEventData) -> FormData {
if let plasmo::EventData::Form(event) = downcast(event) {
FormData::new(event)
} else {
panic!("event should be of type Form")
}
}
fn convert_image_data(&self, _: &PlatformEventData) -> ImageData {
panic!("image events not supported")
}
fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData {
if let plasmo::EventData::Keyboard(event) = downcast(event) {
KeyboardData::new(event)
} else {
panic!("event should be of type Keyboard")
}
}
fn convert_media_data(&self, _: &PlatformEventData) -> MediaData {
panic!("media events not supported")
}
fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData {
event.downcast::<TuiElement>().cloned().unwrap().into()
}
fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData {
if let plasmo::EventData::Mouse(event) = downcast(event) {
MouseData::new(event)
} else {
panic!("event should be of type Mouse")
}
}
fn convert_pointer_data(&self, _: &PlatformEventData) -> PointerData {
panic!("pointer events not supported")
}
fn convert_scroll_data(&self, _: &PlatformEventData) -> ScrollData {
panic!("scroll events not supported")
}
fn convert_selection_data(&self, _: &PlatformEventData) -> SelectionData {
panic!("selection events not supported")
}
fn convert_toggle_data(&self, _: &PlatformEventData) -> ToggleData {
panic!("toggle events not supported")
}
fn convert_touch_data(&self, _: &PlatformEventData) -> TouchData {
panic!("touch events not supported")
}
fn convert_transition_data(&self, _: &PlatformEventData) -> TransitionData {
panic!("transition events not supported")
}
fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData {
if let plasmo::EventData::Wheel(event) = downcast(event) {
WheelData::new(event)
} else {
panic!("event should be of type Wheel")
}
}
}

View file

@ -1,208 +0,0 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
mod element;
mod events;
use std::{
any::Any,
ops::Deref,
rc::Rc,
sync::{Arc, RwLock},
};
use dioxus_core::{Element, ElementId, ScopeId, VirtualDom};
use dioxus_html::PlatformEventData;
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
use dioxus_native_core::prelude::*;
use element::DioxusTUIMutationWriter;
pub use plasmo::{query::Query, Config, RenderingMode, Size, TuiContext};
use plasmo::{render, Driver};
pub mod launch {
use super::*;
pub type Config = super::Config;
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any>>>,
platform_config: Config,
) {
let mut virtual_dom = VirtualDom::new(root);
for context in contexts {
virtual_dom.insert_any_root_context(context());
}
launch_vdom_cfg(virtual_dom, platform_config)
}
}
pub fn launch(app: fn() -> Element) {
launch_cfg(app, Config::default())
}
pub fn launch_cfg(app: fn() -> Element, cfg: Config) {
launch_vdom_cfg(VirtualDom::new(app), cfg)
}
pub fn launch_cfg_with_props<P: Clone + 'static>(app: fn(P) -> Element, props: P, cfg: Config) {
launch_vdom_cfg(VirtualDom::new_with_props(app, props), cfg)
}
pub fn launch_vdom_cfg(vdom: VirtualDom, cfg: Config) {
dioxus_html::set_event_converter(Box::new(events::SerializedHtmlEventConverter));
render(cfg, |rdom, taffy, event_tx| {
let dioxus_state = {
let mut rdom = rdom.write().unwrap();
DioxusState::create(&mut rdom)
};
let dioxus_state = Rc::new(RwLock::new(dioxus_state));
let vdom = vdom
.with_root_context(TuiContext::new(event_tx))
.with_root_context(Query::new(rdom.clone(), taffy.clone()))
.with_root_context(DioxusElementToNodeId {
mapping: dioxus_state.clone(),
});
let queued_events = Vec::new();
let mut myself = DioxusRenderer {
vdom,
dioxus_state,
queued_events,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: {
let (hot_reload_tx, hot_reload_rx) =
tokio::sync::mpsc::unbounded_channel::<dioxus_hot_reload::HotReloadMsg>();
dioxus_hot_reload::connect(move |msg| {
let _ = hot_reload_tx.send(msg);
});
hot_reload_rx
},
};
{
let mut rdom = rdom.write().unwrap();
let mut dioxus_state = myself.dioxus_state.write().unwrap();
let mut writer = DioxusTUIMutationWriter {
query: myself
.vdom
.in_runtime(|| ScopeId::ROOT.consume_context().unwrap()),
events: &mut myself.queued_events,
native_core_writer: dioxus_state.create_mutation_writer(&mut rdom),
};
// Find any mount events
myself.vdom.rebuild(&mut writer);
}
myself
})
.unwrap();
}
struct DioxusRenderer {
vdom: VirtualDom,
dioxus_state: Rc<RwLock<DioxusState>>,
// Events that are queued up to be sent to the vdom next time the vdom is polled
queued_events: Vec<(ElementId, &'static str, Box<dyn Any>, bool)>,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
}
impl Driver for DioxusRenderer {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>) {
let mut rdom = rdom.write().unwrap();
let mut dioxus_state = self.dioxus_state.write().unwrap();
let mut writer = DioxusTUIMutationWriter {
query: self
.vdom
.in_runtime(|| ScopeId::ROOT.consume_context().unwrap()),
events: &mut self.queued_events,
native_core_writer: dioxus_state.create_mutation_writer(&mut rdom),
};
// Find any mount events
self.vdom.render_immediate(&mut writer);
}
fn handle_event(
&mut self,
rdom: &Arc<RwLock<RealDom>>,
id: NodeId,
event: &str,
value: Rc<plasmo::EventData>,
bubbles: bool,
) {
let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() };
if let Some(id) = id {
let inner_value = value.deref().clone();
let boxed_event = Box::new(inner_value);
let platform_event = PlatformEventData::new(boxed_event);
self.vdom
.handle_event(event, Rc::new(platform_event), id, bubbles);
}
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
// Add any queued events
for (id, event, value, bubbles) in self.queued_events.drain(..) {
let platform_event = PlatformEventData::new(value);
self.vdom
.handle_event(event, Rc::new(platform_event), id, bubbles);
}
#[cfg(all(feature = "hot-reload", debug_assertions))]
return Box::pin(async {
let hot_reload_wait = self.hot_reload_rx.recv();
let mut hot_reload_msg = None;
let wait_for_work = self.vdom.wait_for_work();
tokio::select! {
Some(msg) = hot_reload_wait => {
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
hot_reload_msg = Some(msg);
}
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
let () = msg;
}
_ = wait_for_work => {}
}
// if we have a new template, replace the old one
if let Some(msg) = hot_reload_msg {
match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
self.vdom.replace_template(template);
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
}
}
}
});
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
Box::pin(self.vdom.wait_for_work())
}
}
#[derive(Clone)]
pub struct DioxusElementToNodeId {
mapping: Rc<RwLock<DioxusState>>,
}
impl DioxusElementToNodeId {
pub fn get_node_id(&self, element_id: ElementId) -> Option<NodeId> {
self.mapping
.read()
.unwrap()
.try_element_to_node_id(element_id)
}
}

View file

@ -1,73 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style>
html,
body {
height: 100%;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: black;
/* justify-content: center;
align-items: center; */
/* margin: auto; */
}
.smaller {
height: 70%;
width: 70%;
background-color: green;
/* justify-content: center; */
/* align-items: center; */
}
.superinner {
height: 100%;
width: 100%;
/* display: flex; */
/* */
margin-top: 20px;
margin-bottom: 20px;
margin-left: 20px;
margin-right: 20px;
/* */
background-color: red;
justify-content: center;
align-items: center;
flex-direction: column;
/* margin: 20px; */
/* margin: 20px; */
}
</style>
</head>
<body>
<div class="container">
<div class="smaller">
<div class="superinner">
<h1>Hello World</h1>
<p>This is a test</p>
</div>
</div>
</div>
<!-- <div class="container">
<div class="smaller">
hello world
<div style="color: green; margin: 40px;">
goodbye
<div style="color:red;">
asdasdasd
</div>
</div>
</div>
</div> -->
</body>
</html>

View file

@ -1,381 +0,0 @@
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent};
use dioxus::prelude::*;
use dioxus_html::input_data::keyboard_types::Code;
use dioxus_tui::TuiContext;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
/// The tui renderer will look for any event that has occured or any future that has resolved in a loop.
/// It will resolve at most one event per loop.
/// This future will resolve after a certain number of polls. If the number of polls is greater than the number of events triggered, and the event has not been recieved there is an issue with the event system.
struct PollN(usize);
impl PollN {
fn new(n: usize) -> Self {
PollN(n)
}
}
impl Future for PollN {
type Output = ();
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
if self.0 == 0 {
Poll::Ready(())
} else {
self.0 -= 1;
Poll::Pending
}
}
}
#[test]
fn key_down() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let mut render_count_handle = render_count;
let tui_ctx: TuiContext = consume_context();
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
// focus the element
tui_ctx.inject_event(Event::Key(KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE,
}));
tui_ctx.inject_event(Event::Key(KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::NONE,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onkeydown: move |evt| {
assert_eq!(evt.data.code(), Code::KeyA);
tui_ctx.quit();
}
}
}
}
}
#[test]
fn mouse_down() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(2).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onmousedown: move |evt| {
assert!(
evt.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)
);
tui_ctx.quit();
}
}
}
}
}
#[test]
fn mouse_up() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onmouseup: move |_| {
tui_ctx.quit();
}
}
}
}
}
#[test]
fn mouse_enter() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let mut render_count_handle = render_count;
let tui_ctx: TuiContext = consume_context();
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 100,
row: 100,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "50%",
height: "50%",
onmouseenter: move |_| {
tui_ctx.quit();
}
}
}
}
}
#[test]
fn mouse_exit() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 100,
row: 100,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "50%",
height: "50%",
onmouseenter: move |_| {
tui_ctx.quit();
}
}
}
}
}
#[test]
fn mouse_move() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 40,
row: 40,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 60,
row: 60,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onmousemove: move |_| {
tui_ctx.quit();
}
}
}
}
}
#[test]
fn wheel() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::ScrollDown,
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onwheel: move |evt| {
assert!(evt.data.delta().strip_units().y > 0.0);
tui_ctx.quit();
}
}
}
}
}
#[test]
fn click() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onclick: move |_| {
tui_ctx.quit();
}
}
}
}
}
#[test]
fn context_menu() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Right),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Right),
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
oncontextmenu: move |_| {
tui_ctx.quit();
}
}
}
}
}

View file

@ -24,7 +24,6 @@ dioxus-desktop = { workspace = true, optional = true }
dioxus-fullstack = { workspace = true, optional = true }
dioxus-liveview = { workspace = true, optional = true }
dioxus-ssr ={ workspace = true, optional = true }
dioxus-tui = { workspace = true, optional = true }
serde = { version = "1.0.136", optional = true }
@ -50,7 +49,6 @@ web = ["dioxus-web", "dioxus-fullstack?/web", "dioxus-config-macro/web", "dioxus
ssr = ["dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"]
liveview = ["dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"]
axum = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "ssr", "dioxus-liveview?/axum"]
tui = ["dioxus-tui", "dioxus-config-macro/tui"]
# This feature just disables the no-renderer-enabled warning
third-party-renderer = []

View file

@ -7,7 +7,7 @@ fn main() {
let liveview_renderers = ["liveview", "axum"];
let fullstack_renderers = ["axum"];
let client_renderers = ["desktop", "mobile", "web", "tui"];
let client_renderers = ["desktop", "mobile", "web"];
let client_renderer_selected = client_renderers
.iter()
.any(|renderer| feature_enabled(renderer));

View file

@ -80,17 +80,6 @@ impl LaunchBuilder {
}
}
#[cfg(feature = "tui")]
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
/// Launch your tui application
pub fn tui() -> LaunchBuilder<dioxus_tui::Config, UnsendContext> {
LaunchBuilder {
launch_fn: dioxus_tui::launch::launch,
contexts: Vec::new(),
platform_config: None,
}
}
/// Provide a custom launch function for your application.
///
/// Useful for third party renderers to tap into the launch builder API without having to reimplement it.
@ -184,24 +173,11 @@ mod current_platform {
))]
pub use dioxus_liveview::launch::*;
#[cfg(all(
feature = "tui",
not(any(
feature = "liveview",
feature = "web",
feature = "desktop",
feature = "mobile",
feature = "fullstack"
))
))]
pub use dioxus_tui::launch::*;
#[cfg(not(any(
feature = "liveview",
feature = "desktop",
feature = "mobile",
feature = "web",
feature = "tui",
feature = "fullstack"
)))]
pub type Config = ();
@ -211,7 +187,6 @@ mod current_platform {
feature = "desktop",
feature = "mobile",
feature = "web",
feature = "tui",
feature = "fullstack"
)))]
pub fn launch(
@ -251,10 +226,3 @@ pub fn launch_desktop(app: fn() -> Element) {
pub fn launch_fullstack(app: fn() -> Element) {
LaunchBuilder::fullstack().launch(app)
}
#[cfg(feature = "tui")]
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
/// Launch your tui application without any additional configuration. See [`LaunchBuilder`] for more options.
pub fn launch_tui(app: fn() -> Element) {
LaunchBuilder::tui().launch(app)
}

View file

@ -106,10 +106,6 @@ pub use dioxus_desktop as mobile;
#[cfg_attr(docsrs, doc(cfg(feature = "liveview")))]
pub use dioxus_liveview as liveview;
#[cfg(feature = "tui")]
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
pub use dioxus_tui as tui;
#[cfg(feature = "ssr")]
#[cfg_attr(docsrs, doc(cfg(feature = "ssr")))]
pub use dioxus_ssr as ssr;

View file

@ -1,2 +0,0 @@
/target
Cargo.lock

View file

@ -1,24 +0,0 @@
[package]
name = "dioxus-native-core-macro"
version = { workspace = true }
edition = "2021"
description = "Build natively rendered apps with Dioxus"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react"]
authors = ["Jonathan Kelley", "Evan Almloff"]
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["extra-traits", "full"] }
quote = "1.0"
[dev-dependencies]
smallvec = "1.6"
rustc-hash = { workspace = true }
anymap = "0.12.1"
dioxus = { workspace = true }
dioxus-native-core = { workspace = true }

View file

@ -1,39 +0,0 @@
# Dioxus Native Core Macro
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
[![Discord chat][discord-badge]][discord-url]
[crates-badge]: https://img.shields.io/crates/v/dioxus-native-core-macro.svg
[crates-url]: https://crates.io/crates/dioxus-native-core-macro
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-native-core-macro/latest/dioxus_native_core_macro) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-native-core-macro` provides a handful of macros used by native-core for native renderers like TUI, Blitz, and Freya to derive their state.
## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.

View file

@ -1,390 +0,0 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
extern crate proc_macro;
use std::collections::HashSet;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, ItemImpl, Type, TypePath, TypeTuple};
/// A helper attribute for deriving `State` for a struct.
#[proc_macro_attribute]
pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream {
let impl_block: syn::ItemImpl = parse_macro_input!(input as syn::ItemImpl);
let has_create_fn = impl_block
.items
.iter()
.any(|item| matches!(item, syn::ImplItem::Fn(method) if method.sig.ident == "create"));
let parent_dependencies = impl_block
.items
.iter()
.find_map(|item| {
if let syn::ImplItem::Type(syn::ImplItemType { ident, ty, .. }) = item {
(ident == "ParentDependencies").then_some(ty)
} else {
None
}
})
.expect("ParentDependencies must be defined");
let child_dependencies = impl_block
.items
.iter()
.find_map(|item| {
if let syn::ImplItem::Type(syn::ImplItemType { ident, ty, .. }) = item {
(ident == "ChildDependencies").then_some(ty)
} else {
None
}
})
.expect("ChildDependencies must be defined");
let node_dependencies = impl_block
.items
.iter()
.find_map(|item| {
if let syn::ImplItem::Type(syn::ImplItemType { ident, ty, .. }) = item {
(ident == "NodeDependencies").then_some(ty)
} else {
None
}
})
.expect("NodeDependencies must be defined");
let this_type = &impl_block.self_ty;
let this_type = extract_type_path(this_type)
.unwrap_or_else(|| panic!("Self must be a type path, found {}", quote!(#this_type)));
let mut combined_dependencies = HashSet::new();
let self_path: TypePath = syn::parse_quote!(Self);
let parent_dependencies = match extract_tuple(parent_dependencies) {
Some(tuple) => {
let mut parent_dependencies = Vec::new();
for type_ in &tuple.elems {
let mut type_ = extract_type_path(type_).unwrap_or_else(|| {
panic!(
"ParentDependencies must be a tuple of type paths, found {}",
quote!(#type_)
)
});
if type_ == self_path {
type_ = this_type.clone();
}
combined_dependencies.insert(type_.clone());
parent_dependencies.push(type_);
}
parent_dependencies
}
_ => panic!(
"ParentDependencies must be a tuple, found {}",
quote!(#parent_dependencies)
),
};
let child_dependencies = match extract_tuple(child_dependencies) {
Some(tuple) => {
let mut child_dependencies = Vec::new();
for type_ in &tuple.elems {
let mut type_ = extract_type_path(type_).unwrap_or_else(|| {
panic!(
"ChildDependencies must be a tuple of type paths, found {}",
quote!(#type_)
)
});
if type_ == self_path {
type_ = this_type.clone();
}
combined_dependencies.insert(type_.clone());
child_dependencies.push(type_);
}
child_dependencies
}
_ => panic!(
"ChildDependencies must be a tuple, found {}",
quote!(#child_dependencies)
),
};
let node_dependencies = match extract_tuple(node_dependencies) {
Some(tuple) => {
let mut node_dependencies = Vec::new();
for type_ in &tuple.elems {
let mut type_ = extract_type_path(type_).unwrap_or_else(|| {
panic!(
"NodeDependencies must be a tuple of type paths, found {}",
quote!(#type_)
)
});
if type_ == self_path {
type_ = this_type.clone();
}
combined_dependencies.insert(type_.clone());
node_dependencies.push(type_);
}
node_dependencies
}
_ => panic!(
"NodeDependencies must be a tuple, found {}",
quote!(#node_dependencies)
),
};
combined_dependencies.insert(this_type.clone());
let combined_dependencies: Vec<_> = combined_dependencies.into_iter().collect();
let parent_dependancies_idxes: Vec<_> = parent_dependencies
.iter()
.filter_map(|ident| combined_dependencies.iter().position(|i| i == ident))
.collect();
let child_dependencies_idxes: Vec<_> = child_dependencies
.iter()
.filter_map(|ident| combined_dependencies.iter().position(|i| i == ident))
.collect();
let node_dependencies_idxes: Vec<_> = node_dependencies
.iter()
.filter_map(|ident| combined_dependencies.iter().position(|i| i == ident))
.collect();
let this_type_idx = combined_dependencies
.iter()
.enumerate()
.find_map(|(i, ident)| (this_type == *ident).then_some(i))
.unwrap();
let this_view = format_ident!("__data{}", this_type_idx);
let combined_dependencies_quote = combined_dependencies.iter().map(|ident| {
if ident == &this_type {
quote! {shipyard::ViewMut<#ident>}
} else {
quote! {shipyard::View<#ident>}
}
});
let combined_dependencies_quote = quote!((#(#combined_dependencies_quote,)*));
let ItemImpl {
attrs,
defaultness,
unsafety,
impl_token,
generics,
trait_,
self_ty,
items,
..
} = impl_block;
let for_ = trait_.as_ref().map(|t| t.2);
let trait_ = trait_.map(|t| t.1);
let split_views: Vec<_> = (0..combined_dependencies.len())
.map(|i| {
let ident = format_ident!("__data{}", i);
if i == this_type_idx {
quote! {mut #ident}
} else {
quote! {#ident}
}
})
.collect();
let node_view = node_dependencies_idxes
.iter()
.map(|i| format_ident!("__data{}", i))
.collect::<Vec<_>>();
let get_node_view = {
if node_dependencies.is_empty() {
quote! {
let raw_node = ();
}
} else {
let temps = (0..node_dependencies.len())
.map(|i| format_ident!("__temp{}", i))
.collect::<Vec<_>>();
quote! {
let raw_node: (#(*const #node_dependencies,)*) = {
let (#(#temps,)*) = (#(&#node_view,)*).get(id).unwrap_or_else(|err| panic!("Failed to get node view {:?}", err));
(#(#temps as *const _,)*)
};
}
}
};
let deref_node_view = {
if node_dependencies.is_empty() {
quote! {
let node = raw_node;
}
} else {
let indexes = (0..node_dependencies.len()).map(syn::Index::from);
quote! {
let node = unsafe { (#(dioxus_native_core::prelude::DependancyView::new(&*raw_node.#indexes),)*) };
}
}
};
let parent_view = parent_dependancies_idxes
.iter()
.map(|i| format_ident!("__data{}", i))
.collect::<Vec<_>>();
let get_parent_view = {
if parent_dependencies.is_empty() {
quote! {
let raw_parent = tree.parent_id_advanced(id, Self::TRAVERSE_SHADOW_DOM).map(|_| ());
}
} else {
let temps = (0..parent_dependencies.len())
.map(|i| format_ident!("__temp{}", i))
.collect::<Vec<_>>();
quote! {
let raw_parent = tree.parent_id_advanced(id, Self::TRAVERSE_SHADOW_DOM).and_then(|parent_id| {
let raw_parent: Option<(#(*const #parent_dependencies,)*)> = (#(&#parent_view,)*).get(parent_id).ok().map(|c| {
let (#(#temps,)*) = c;
(#(#temps as *const _,)*)
});
raw_parent
});
}
}
};
let deref_parent_view = {
if parent_dependencies.is_empty() {
quote! {
let parent = raw_parent;
}
} else {
let indexes = (0..parent_dependencies.len()).map(syn::Index::from);
quote! {
let parent = unsafe { raw_parent.map(|raw_parent| (#(dioxus_native_core::prelude::DependancyView::new(&*raw_parent.#indexes),)*)) };
}
}
};
let child_view = child_dependencies_idxes
.iter()
.map(|i| format_ident!("__data{}", i))
.collect::<Vec<_>>();
let get_child_view = {
if child_dependencies.is_empty() {
quote! {
let raw_children: Vec<_> = tree.children_ids_advanced(id, Self::TRAVERSE_SHADOW_DOM).into_iter().map(|_| ()).collect();
}
} else {
let temps = (0..child_dependencies.len())
.map(|i| format_ident!("__temp{}", i))
.collect::<Vec<_>>();
quote! {
let raw_children: Vec<_> = tree.children_ids_advanced(id, Self::TRAVERSE_SHADOW_DOM).into_iter().filter_map(|id| {
let raw_children: Option<(#(*const #child_dependencies,)*)> = (#(&#child_view,)*).get(id).ok().map(|c| {
let (#(#temps,)*) = c;
(#(#temps as *const _,)*)
});
raw_children
}).collect();
}
}
};
let deref_child_view = {
if child_dependencies.is_empty() {
quote! {
let children = raw_children;
}
} else {
let indexes = (0..child_dependencies.len()).map(syn::Index::from);
quote! {
let children = unsafe { raw_children.iter().map(|raw_children| (#(dioxus_native_core::prelude::DependancyView::new(&*raw_children.#indexes),)*)).collect::<Vec<_>>() };
}
}
};
let trait_generics = trait_
.as_ref()
.unwrap()
.segments
.last()
.unwrap()
.arguments
.clone();
// if a create function is defined, we don't generate one
// otherwise we generate a default one that uses the update function and the default constructor
let create_fn = (!has_create_fn).then(|| {
quote! {
fn create<'a>(
node_view: dioxus_native_core::prelude::NodeView # trait_generics,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &dioxus_native_core::prelude::SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
});
quote!(
#(#attrs)*
#defaultness #unsafety #impl_token #generics #trait_ #for_ #self_ty {
#create_fn
#(#items)*
fn workload_system(type_id: std::any::TypeId, dependants: std::sync::Arc<dioxus_native_core::prelude::Dependants>, pass_direction: dioxus_native_core::prelude::PassDirection) -> dioxus_native_core::exports::shipyard::WorkloadSystem {
use dioxus_native_core::exports::shipyard::{IntoWorkloadSystem, Get, AddComponent};
use dioxus_native_core::tree::TreeRef;
use dioxus_native_core::prelude::{NodeType, NodeView};
let node_mask = Self::NODE_MASK.build();
(move |data: #combined_dependencies_quote, run_view: dioxus_native_core::prelude::RunPassView #trait_generics| {
let (#(#split_views,)*) = data;
let tree = run_view.tree.clone();
let node_types = run_view.node_type.clone();
dioxus_native_core::prelude::run_pass(type_id, dependants.clone(), pass_direction, run_view, |id, context| {
let node_data: &NodeType<_> = node_types.get(id).unwrap_or_else(|err| panic!("Failed to get node type {:?}", err));
// get all of the states from the tree view
// Safety: No node has itself as a parent or child.
let raw_myself: Option<*mut Self> = (&mut #this_view).get(id).ok().map(|c| c as *mut _);
#get_node_view
#get_parent_view
#get_child_view
let myself: Option<&mut Self> = unsafe { raw_myself.map(|val| &mut *val) };
#deref_node_view
#deref_parent_view
#deref_child_view
let view = NodeView::new(id, node_data, &node_mask);
if let Some(myself) = myself {
myself
.update(view, node, parent, children, context)
}
else {
(&mut #this_view).add_component_unchecked(
id,
Self::create(view, node, parent, children, context));
true
}
})
}).into_workload_system().unwrap()
}
}
)
.into()
}
fn extract_tuple(ty: &Type) -> Option<TypeTuple> {
match ty {
Type::Tuple(tuple) => Some(tuple.clone()),
Type::Group(group) => extract_tuple(&group.elem),
_ => None,
}
}
fn extract_type_path(ty: &Type) -> Option<TypePath> {
match ty {
Type::Path(path) => Some(path.clone()),
Type::Group(group) => extract_type_path(&group.elem),
_ => None,
}
}

View file

@ -1,2 +0,0 @@
/target
Cargo.lock

View file

@ -1,39 +0,0 @@
[package]
name = "dioxus-native-core"
version = { workspace = true }
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
description = "Build natively rendered apps with Dioxus"
keywords = ["dom", "ui", "gui", "react"]
authors = ["Jonathan Kelley", "Evan Almloff"]
[dependencies]
dioxus-core = { workspace = true, optional = true }
keyboard-types = "0.7"
smallvec = "1.6"
rustc-hash = { workspace = true }
anymap = "1.0.0-beta.2"
parking_lot = { version = "0.12.1", features = ["send_guard"] }
dashmap = "5.4.0"
# for parsing attributes
taffy = { version = "0.3.12", optional = true }
lightningcss = { version = "1.0.0-alpha.39", optional = true }
shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false }
[dev-dependencies]
rand = "0.8.5"
dioxus = { workspace = true }
tokio = { workspace = true, features = ["full"] }
dioxus-native-core = { workspace = true, features = ["dioxus"] }
dioxus-native-core-macro = { workspace = true }
[features]
default = []
layout-attributes = ["dep:taffy", "dep:lightningcss"]
dioxus = ["dioxus-core"]
parallel = ["shipyard/parallel"]

View file

@ -1,45 +0,0 @@
# Dioxus Native Core
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
[![Discord chat][discord-badge]][discord-url]
[crates-badge]: https://img.shields.io/crates/v/dioxus-native-core.svg
[crates-url]: https://crates.io/crates/dioxus-native-core
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/guide/) |
[API Docs](https://docs.rs/dioxus-native-core/latest/dioxus_native_core) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-native-core` provides several helpful utilities for lazily resolving computed values of the Dioxus VirtualDom to be used in conjunction with a native rendering engine.
The main "value-add" of this crate over implementing your native tree is that this tree is incrementally recomputed using the Dioxus VirtualDom's edit stream. Only parts of the tree that rely on each other will be redrawn - all else will be ignored.
## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.

View file

@ -1,237 +0,0 @@
use dioxus::prelude::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
macro_rules! dep {
( child( $name:ty, $dep:ty ) ) => {
#[partial_derive_state]
impl State for $name {
type ParentDependencies = ();
type ChildDependencies = $dep;
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::ALL;
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
};
( parent( $name:ty, $dep:ty ) ) => {
#[partial_derive_state]
impl State for $name {
type ParentDependencies = $dep;
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::ALL;
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
};
( node( $name:ty, $dep:ty ) ) => {
#[partial_derive_state]
impl State for $name {
type ParentDependencies = $dep;
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::ALL;
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
};
}
macro_rules! test_state{
( state: ( $( $state:ty ),* ) ) => {
#[test]
fn state_reduce_initally_called_minimally() {
#[allow(non_snake_case)]
fn Base() -> Element {
rsx!{
div {
div{
div{
p{}
}
p{
"hello"
}
div{
h1{}
}
p{
"world"
}
}
}
}
}
let mut vdom = VirtualDom::new(Base);
let mutations = vdom.rebuild();
let mut dom: RealDom = RealDom::new([$( <$state>::to_type_erased() ),*]);
let mut dioxus_state = DioxusState::create(&mut dom);
dioxus_state.apply_mutations(&mut dom, mutations);
dom.update_state(SendAnyMap::new());
dom.traverse_depth_first_advanced(false, |n| {
$(
assert_eq!(n.get::<$state>().unwrap().0, 1);
)*
});
}
}
}
mod node_depends_on_child_and_parent {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, (Child, Parent)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
test_state!(state: (Child, Node, Parent));
}
mod child_depends_on_node_that_depends_on_parent {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, (Parent,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Node,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
test_state!(state: (Child, Node, Parent));
}
mod parent_depends_on_node_that_depends_on_child {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Node,)));
test_state!(state: (Child, Node, Parent));
}
mod node_depends_on_other_node_state {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node1(i32);
dep!(node(Node1, (Node2,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node2(i32);
dep!(node(Node2, ()));
test_state!(state: (Node1, Node2));
}
mod node_child_and_parent_state_depends_on_self {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, ()));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
test_state!(state: (Child, Node, Parent));
}

View file

@ -1,389 +0,0 @@
use dioxus::prelude::*;
use dioxus_native_core::{custom_element::CustomElement, prelude::*};
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)]
pub struct ColorState {
color: usize,
}
#[partial_derive_state]
impl State for ColorState {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// The color state should not be effected by the shadow dom
const TRAVERSE_SHADOW_DOM: bool = false;
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(&["color"]))
.with_element();
fn update<'a>(
&mut self,
view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some(size) = view
.attributes()
.into_iter()
.flatten()
.find(|attr| attr.attribute.name == "color")
{
self.color = size
.value
.as_float()
.or_else(|| size.value.as_int().map(|i| i as f64))
.or_else(|| size.value.as_text().and_then(|i| i.parse().ok()))
.unwrap_or(0.0) as usize;
} else if let Some((parent,)) = parent {
*self = *parent;
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)]
pub struct LayoutState {
size: usize,
}
#[partial_derive_state]
impl State for LayoutState {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// The layout state should be effected by the shadow dom
const TRAVERSE_SHADOW_DOM: bool = true;
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(&["size"]))
.with_element();
fn update<'a>(
&mut self,
view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some(size) = view
.attributes()
.into_iter()
.flatten()
.find(|attr| attr.attribute.name == "size")
{
self.size = size
.value
.as_float()
.or_else(|| size.value.as_int().map(|i| i as f64))
.or_else(|| size.value.as_text().and_then(|i| i.parse().ok()))
.unwrap_or(0.0) as usize;
} else if let Some((parent,)) = parent {
if parent.size > 0 {
self.size = parent.size - 1;
}
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
mod dioxus_elements {
macro_rules! builder_constructors {
(
$(
$(#[$attr:meta])*
$name:ident {
$(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident,
)*
};
)*
) => {
$(
#[allow(non_camel_case_types)]
$(#[$attr])*
pub struct $name;
#[allow(non_upper_case_globals, unused)]
impl $name {
pub const TAG_NAME: &'static str = stringify!($name);
pub const NAME_SPACE: Option<&'static str> = None;
$(
pub const $fil: (&'static str, Option<&'static str>, bool) = (stringify!($fil), None, false);
)*
}
impl GlobalAttributes for $name {}
)*
}
}
pub trait GlobalAttributes {}
pub trait SvgAttributes {}
builder_constructors! {
customelementslot {
size: attr,
color: attr,
};
customelementnoslot {
size: attr,
color: attr,
};
testing132 {
color: attr,
};
}
}
struct CustomElementWithSlot {
root: NodeId,
slot: NodeId,
}
impl CustomElement for CustomElementWithSlot {
const NAME: &'static str = "customelementslot";
fn create(mut node: NodeMut<()>) -> Self {
let dom = node.real_dom_mut();
let child = dom.create_node(ElementNode {
tag: "div".into(),
namespace: None,
attributes: Default::default(),
listeners: Default::default(),
});
let slot_id = child.id();
let mut root = dom.create_node(ElementNode {
tag: "div".into(),
namespace: None,
attributes: Default::default(),
listeners: Default::default(),
});
root.add_child(slot_id);
Self {
root: root.id(),
slot: slot_id,
}
}
fn slot(&self) -> Option<NodeId> {
Some(self.slot)
}
fn roots(&self) -> Vec<NodeId> {
vec![self.root]
}
fn attributes_changed(
&mut self,
node: NodeMut<()>,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
println!("attributes_changed");
println!("{:?}", attributes);
println!("{:?}: {:#?}", node.id(), &*node.node_type());
}
}
struct CustomElementWithNoSlot {
root: NodeId,
}
impl CustomElement for CustomElementWithNoSlot {
const NAME: &'static str = "customelementnoslot";
fn create(mut node: NodeMut<()>) -> Self {
let dom = node.real_dom_mut();
let root = dom.create_node(ElementNode {
tag: "div".into(),
namespace: None,
attributes: Default::default(),
listeners: Default::default(),
});
Self { root: root.id() }
}
fn roots(&self) -> Vec<NodeId> {
vec![self.root]
}
fn attributes_changed(
&mut self,
node: NodeMut<()>,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
println!("attributes_changed");
println!("{:?}", attributes);
println!("{:?}: {:#?}", node.id(), &*node.node_type());
}
}
#[test]
fn custom_elements_work() {
fn app() -> Element {
let count = use_signal(|| 0);
use_future(|count| async move {
count.with_mut(|count| *count += 1);
});
rsx! {
customelementslot {
size: "{count}",
color: "1",
customelementslot {
testing132 {}
}
}
}
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]);
rdom.register_custom_element::<CustomElementWithSlot>();
let mut dioxus_state = DioxusState::create(&mut rdom);
let mut dom = VirtualDom::new(app);
let mutations = dom.rebuild();
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
for i in 0..10usize {
dom.wait_for_work().await;
let mutations = dom.render_immediate();
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
// render...
rdom.traverse_depth_first_advanced(true, |node| {
let node_type = &*node.node_type();
let height = node.height() as usize;
let indent = " ".repeat(height);
let color = *node.get::<ColorState>().unwrap();
let size = *node.get::<LayoutState>().unwrap();
let id = node.id();
println!("{indent}{id:?} {color:?} {size:?} {node_type:?}");
if let NodeType::Element(el) = node_type {
match el.tag.as_str() {
// the color should bubble up from customelementslot
"testing132" | "customelementslot" => {
assert_eq!(color.color, 1);
}
// the color of the light dom should not effect the color of the shadow dom, so the color of divs in the shadow dom should be 0
"div" => {
assert_eq!(color.color, 0);
}
_ => {}
}
if el.tag != "Root" {
assert_eq!(size.size, (i + 2).saturating_sub(height));
}
}
});
}
});
}
#[test]
#[should_panic]
fn slotless_custom_element_cant_have_children() {
fn app() -> Element {
rsx! {
customelementnoslot {
testing132 {}
}
}
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]);
rdom.register_custom_element::<CustomElementWithNoSlot>();
let mut dioxus_state = DioxusState::create(&mut rdom);
let mut dom = VirtualDom::new(app);
let mutations = dom.rebuild();
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
});
}
#[test]
fn slotless_custom_element() {
fn app() -> Element {
rsx! {
customelementnoslot {
}
}
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]);
rdom.register_custom_element::<CustomElementWithNoSlot>();
let mut dioxus_state = DioxusState::create(&mut rdom);
let mut dom = VirtualDom::new(app);
let mutations = dom.rebuild();
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
});
}

View file

@ -1,386 +0,0 @@
use std::cell::Cell;
use dioxus::prelude::Props;
use dioxus_core::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
fn random_ns() -> Option<&'static str> {
let namespace = rand::random::<u8>() % 2;
match namespace {
0 => None,
1 => Some(Box::leak(
format!("ns{}", rand::random::<usize>()).into_boxed_str(),
)),
_ => unreachable!(),
}
}
fn create_random_attribute(attr_idx: &mut usize) -> TemplateAttribute<'static> {
match rand::random::<u8>() % 2 {
0 => TemplateAttribute::Static {
name: Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
value: Box::leak(format!("value{}", rand::random::<usize>()).into_boxed_str()),
namespace: random_ns(),
},
1 => TemplateAttribute::Dynamic {
id: {
let old_idx = *attr_idx;
*attr_idx += 1;
old_idx
},
},
_ => unreachable!(),
}
}
fn create_random_template_node(
dynamic_node_types: &mut Vec<DynamicNodeType>,
template_idx: &mut usize,
attr_idx: &mut usize,
depth: usize,
) -> TemplateNode {
match rand::random::<u8>() % 4 {
0 => {
let attrs = {
let attrs: Vec<_> = (0..(rand::random::<usize>() % 10))
.map(|_| create_random_attribute(attr_idx))
.collect();
Box::leak(attrs.into_boxed_slice())
};
TemplateNode::Element {
tag: Box::leak(format!("tag{}", rand::random::<usize>()).into_boxed_str()),
namespace: random_ns(),
attrs,
children: {
if depth > 4 {
&[]
} else {
let children: Vec<_> = (0..(rand::random::<usize>() % 3))
.map(|_| {
create_random_template_node(
dynamic_node_types,
template_idx,
attr_idx,
depth + 1,
)
})
.collect();
Box::leak(children.into_boxed_slice())
}
},
}
}
1 => TemplateNode::Text {
text: Box::leak(format!("{}", rand::random::<usize>()).into_boxed_str()),
},
2 => TemplateNode::DynamicText {
id: {
let old_idx = *template_idx;
*template_idx += 1;
dynamic_node_types.push(DynamicNodeType::Text);
old_idx
},
},
3 => TemplateNode::Dynamic {
id: {
let old_idx = *template_idx;
*template_idx += 1;
dynamic_node_types.push(DynamicNodeType::Other);
old_idx
},
},
_ => unreachable!(),
}
}
fn generate_paths(
node: &TemplateNode,
current_path: &[u8],
node_paths: &mut Vec<Vec<u8>>,
attr_paths: &mut Vec<Vec<u8>>,
) {
match node {
TemplateNode::Element {
children, attrs, ..
} => {
for attr in *attrs {
match attr {
TemplateAttribute::Static { .. } => {}
TemplateAttribute::Dynamic { .. } => {
attr_paths.push(current_path.to_vec());
}
}
}
for (i, child) in children.iter().enumerate() {
let mut current_path = current_path.to_vec();
current_path.push(i as u8);
generate_paths(child, &current_path, node_paths, attr_paths);
}
}
TemplateNode::Text { .. } => {}
TemplateNode::DynamicText { .. } => {
node_paths.push(current_path.to_vec());
}
TemplateNode::Dynamic { .. } => {
node_paths.push(current_path.to_vec());
}
}
}
enum DynamicNodeType {
Text,
Other,
}
fn create_random_template(name: &'static str) -> (Template, Vec<DynamicNodeType>) {
let mut dynamic_node_type = Vec::new();
let mut template_idx = 0;
let mut attr_idx = 0;
let roots = (0..(1 + rand::random::<usize>() % 5))
.map(|_| {
create_random_template_node(&mut dynamic_node_type, &mut template_idx, &mut attr_idx, 0)
})
.collect::<Vec<_>>();
assert!(!roots.is_empty());
let roots = Box::leak(roots.into_boxed_slice());
let mut node_paths = Vec::new();
let mut attr_paths = Vec::new();
for (i, root) in roots.iter().enumerate() {
generate_paths(root, &[i as u8], &mut node_paths, &mut attr_paths);
}
let node_paths = Box::leak(
node_paths
.into_iter()
.map(|v| &*Box::leak(v.into_boxed_slice()))
.collect::<Vec<_>>()
.into_boxed_slice(),
);
let attr_paths = Box::leak(
attr_paths
.into_iter()
.map(|v| &*Box::leak(v.into_boxed_slice()))
.collect::<Vec<_>>()
.into_boxed_slice(),
);
(
Template {
name,
roots,
node_paths,
attr_paths,
},
dynamic_node_type,
)
}
fn create_random_dynamic_node(depth: usize) -> DynamicNode {
let range = if depth > 3 { 1 } else { 3 };
match rand::random::<u8>() % range {
0 => DynamicNode::Placeholder(Default::default()),
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| {
cx.vnode(
None.into(),
Default::default(),
Cell::new(Template {
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
roots: &[TemplateNode::Dynamic { id: 0 }],
node_paths: &[&[0]],
attr_paths: &[],
}),
dioxus::dioxus_core::exports::bumpalo::collections::Vec::new_in(cx.bump()).into(),
cx.bump().alloc([cx.component(
create_random_element,
DepthProps { depth, root: false },
"create_random_element",
)]),
&[],
)
})),
2 => cx.component(
create_random_element,
DepthProps { depth, root: false },
"create_random_element",
),
_ => unreachable!(),
}
}
fn create_random_dynamic_attr() -> Attribute {
let value = match rand::random::<u8>() % 6 {
0 => AttributeValue::Text(Box::leak(
format!("{}", rand::random::<usize>()).into_boxed_str(),
)),
1 => AttributeValue::Float(rand::random()),
2 => AttributeValue::Int(rand::random()),
3 => AttributeValue::Bool(rand::random()),
4 => cx.any_value(rand::random::<usize>()),
5 => AttributeValue::None,
_ => unreachable!(),
};
Attribute::new(
Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
value,
random_ns(),
rand::random(),
)
}
static mut TEMPLATE_COUNT: usize = 0;
#[derive(PartialEq, Props, Component)]
struct DepthProps {
depth: usize,
root: bool,
}
fn create_random_element(cx: Scope<DepthProps>) -> Element {
cx.needs_update();
let range = if cx.props.root { 2 } else { 3 };
let node = match rand::random::<usize>() % range {
0 | 1 => {
let (template, dynamic_node_types) = create_random_template(Box::leak(
format!(
"{}{}",
concat!(file!(), ":", line!(), ":", column!(), ":"),
{
unsafe {
let old = TEMPLATE_COUNT;
TEMPLATE_COUNT += 1;
old
}
}
)
.into_boxed_str(),
));
println!("{template:#?}");
let node = cx.vnode(
None.into(),
None,
Cell::new(template),
dioxus::dioxus_core::exports::bumpalo::collections::Vec::new_in(cx.bump()).into(),
{
let dynamic_nodes: Vec<_> = dynamic_node_types
.iter()
.map(|ty| match ty {
DynamicNodeType::Text => DynamicNode::Text(VText::new(Box::leak(
format!("{}", rand::random::<usize>()).into_boxed_str(),
))),
DynamicNodeType::Other => {
create_random_dynamic_node(cx.props.depth + 1)
}
})
.collect();
cx.bump().alloc(dynamic_nodes)
},
cx.bump()
.alloc(
(0..template.attr_paths.len())
.map(|_| create_random_dynamic_attr(cx).into())
.collect::<Vec<_>>(),
)
.as_slice(),
);
Some(node)
}
_ => None,
};
println!("{node:#?}");
node
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Component)]
pub struct BlablaState {
count: usize,
}
#[partial_derive_state]
impl State for BlablaState {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(&["blabla"]))
.with_element();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some((parent,)) = parent {
if parent.count != 0 {
self.count += 1;
}
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
// test for panics when creating random nodes and templates
#[test]
fn create() {
for _ in 0..100 {
let mut vdom = VirtualDom::new_with_props(
create_random_element,
DepthProps {
depth: 0,
root: true,
},
);
let mutations = vdom.rebuild();
let mut rdom: RealDom = RealDom::new([BlablaState::to_type_erased()]);
let mut dioxus_state = DioxusState::create(&mut rdom);
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
}
}
// test for panics when diffing random nodes
// This test will change the template every render which is not very realistic, but it helps stress the system
#[test]
fn diff() {
for _ in 0..10 {
let mut vdom = VirtualDom::new_with_props(
create_random_element,
DepthProps {
depth: 0,
root: true,
},
);
let mutations = vdom.rebuild();
let mut rdom: RealDom = RealDom::new([BlablaState::to_type_erased()]);
let mut dioxus_state = DioxusState::create(&mut rdom);
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
for _ in 0..10 {
let mutations = vdom.render_immediate();
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
}
}
}

View file

@ -1,154 +0,0 @@
use dioxus::prelude::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
use tokio::time::sleep;
#[derive(Debug, Clone, PartialEq, Eq, Default, Component)]
pub struct BlablaState {
count: usize,
}
#[partial_derive_state]
impl State for BlablaState {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(&["blabla"]))
.with_element();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some((parent,)) = parent {
if parent.count != 0 {
self.count += 1;
}
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
mod dioxus_elements {
macro_rules! builder_constructors {
(
$(
$(#[$attr:meta])*
$name:ident {
$(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident,
)*
};
)*
) => {
$(
#[allow(non_camel_case_types)]
$(#[$attr])*
pub struct $name;
impl $name {
pub const TAG_NAME: &'static str = stringify!($name);
pub const NAME_SPACE: Option<&'static str> = None;
$(
pub const $fil: (&'static str, Option<&'static str>, bool) = (stringify!($fil), None, false);
)*
}
impl GlobalAttributes for $name {}
)*
}
}
pub trait GlobalAttributes {}
pub trait SvgAttributes {}
builder_constructors! {
blabla {
};
}
}
#[test]
fn native_core_is_okay() {
use std::sync::{Arc, Mutex};
use std::time::Duration;
fn app() -> Element {
let colors = use_signal(|| vec!["green", "blue", "red"]);
let padding = use_signal(|| 10);
use_effect(colors, |colors| async move {
sleep(Duration::from_millis(1000)).await;
colors.with_mut(|colors| colors.reverse());
});
use_effect(padding, |padding| async move {
sleep(Duration::from_millis(10)).await;
padding.with_mut(|padding| {
if *padding < 65 {
*padding += 1;
} else {
*padding = 5;
}
});
});
let _big = colors[0];
let _mid = colors[1];
let _small = colors[2];
rsx! {
blabla {}
}
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let rdom = Arc::new(Mutex::new(RealDom::new([BlablaState::to_type_erased()])));
let mut dioxus_state = DioxusState::create(&mut rdom.lock().unwrap());
let mut dom = VirtualDom::new(app);
let mutations = dom.rebuild();
dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations);
let ctx = SendAnyMap::new();
rdom.lock().unwrap().update_state(ctx);
for _ in 0..10 {
dom.wait_for_work().await;
let mutations = dom.render_immediate();
dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations);
let ctx = SendAnyMap::new();
rdom.lock().unwrap().update_state(ctx);
}
});
}

View file

@ -1,440 +0,0 @@
use dioxus_native_core::node::NodeType;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use rustc_hash::{FxHashMap, FxHashSet};
use shipyard::Component;
fn create_blank_element() -> NodeType {
NodeType::Element(ElementNode {
tag: "div".to_owned(),
namespace: None,
attributes: FxHashMap::default(),
listeners: FxHashSet::default(),
})
}
#[test]
fn node_pass() {
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct Number(i32);
#[partial_derive_state]
impl State for Number {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([Number::to_type_erased()]);
tree.update_state(SendAnyMap::new());
assert_eq!(
tree.get(tree.root_id()).unwrap().get().as_deref(),
Some(&Number(1))
);
// mark the node as dirty
tree.get_mut(tree.root_id()).unwrap().get_mut::<Number>();
tree.update_state(SendAnyMap::new());
assert_eq!(
tree.get(tree.root_id()).unwrap().get().as_deref(),
Some(&Number(2))
);
}
#[test]
fn dependant_node_pass() {
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct AddNumber(i32);
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = ();
type NodeDependencies = (SubtractNumber,);
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct SubtractNumber(i32);
#[partial_derive_state]
impl State for SubtractNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 -= 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([
AddNumber::to_type_erased(),
SubtractNumber::to_type_erased(),
]);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-1)));
// mark the subtract state as dirty, it should update the add state
tree.get_mut(tree.root_id())
.unwrap()
.get_mut::<SubtractNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(2)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
// mark the add state as dirty, it should ~not~ update the subtract state
tree.get_mut(tree.root_id()).unwrap().get_mut::<AddNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(3)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
}
#[test]
fn independant_node_pass() {
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct AddNumber(i32);
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct SubtractNumber(i32);
#[partial_derive_state]
impl State for SubtractNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 -= 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([
AddNumber::to_type_erased(),
SubtractNumber::to_type_erased(),
]);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-1)));
// mark the subtract state as dirty, it should ~not~ update the add state
tree.get_mut(tree.root_id())
.unwrap()
.get_mut::<SubtractNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
// mark the add state as dirty, it should ~not~ update the subtract state
tree.get_mut(tree.root_id()).unwrap().get_mut::<AddNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(2)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
}
#[test]
fn down_pass() {
#[derive(Debug, Clone, PartialEq, Component)]
struct AddNumber(i32);
impl Default for AddNumber {
fn default() -> Self {
Self(1)
}
}
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = (AddNumber,);
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some((parent,)) = parent {
self.0 += parent.0;
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([AddNumber::to_type_erased()]);
let grandchild1 = tree.create_node(create_blank_element());
let grandchild1 = grandchild1.id();
let mut child1 = tree.create_node(create_blank_element());
child1.add_child(grandchild1);
let child1 = child1.id();
let grandchild2 = tree.create_node(create_blank_element());
let grandchild2 = grandchild2.id();
let mut child2 = tree.create_node(create_blank_element());
child2.add_child(grandchild2);
let child2 = child2.id();
let mut parent = tree.get_mut(tree.root_id()).unwrap();
parent.add_child(child1);
parent.add_child(child2);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
dbg!(root.id());
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
let child1 = tree.get(child1).unwrap();
dbg!(child1.id());
assert_eq!(child1.get().as_deref(), Some(&AddNumber(2)));
let grandchild1 = tree.get(grandchild1).unwrap();
assert_eq!(grandchild1.get().as_deref(), Some(&AddNumber(3)));
let child2 = tree.get(child2).unwrap();
assert_eq!(child2.get().as_deref(), Some(&AddNumber(2)));
let grandchild2 = tree.get(grandchild2).unwrap();
assert_eq!(grandchild2.get().as_deref(), Some(&AddNumber(3)));
}
#[test]
fn up_pass() {
// Tree before:
// 1=\
// 1=\
// 1
// 1=\
// 1
// Tree after:
// 4=\
// 2=\
// 1
// 2=\
// 1
#[derive(Debug, Clone, PartialEq, Component)]
struct AddNumber(i32);
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = (AddNumber,);
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += children.iter().map(|(i,)| i.0).sum::<i32>();
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self(1);
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([AddNumber::to_type_erased()]);
let grandchild1 = tree.create_node(create_blank_element());
let grandchild1 = grandchild1.id();
let mut child1 = tree.create_node(create_blank_element());
child1.add_child(grandchild1);
let child1 = child1.id();
let grandchild2 = tree.create_node(create_blank_element());
let grandchild2 = grandchild2.id();
let mut child2 = tree.create_node(create_blank_element());
child2.add_child(grandchild2);
let child2 = child2.id();
let mut parent = tree.get_mut(tree.root_id()).unwrap();
parent.add_child(child1);
parent.add_child(child2);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(5)));
let child1 = tree.get(child1).unwrap();
assert_eq!(child1.get().as_deref(), Some(&AddNumber(2)));
let grandchild1 = tree.get(grandchild1).unwrap();
assert_eq!(grandchild1.get().as_deref(), Some(&AddNumber(1)));
let child2 = tree.get(child2).unwrap();
assert_eq!(child2.get().as_deref(), Some(&AddNumber(2)));
let grandchild2 = tree.get(grandchild2).unwrap();
assert_eq!(grandchild2.get().as_deref(), Some(&AddNumber(1)));
}

View file

@ -1,231 +0,0 @@
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core::real_dom::NodeTypeMut;
use dioxus_native_core_macro::partial_derive_state;
struct FontSize(f64);
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
#[derive(Default, Debug, Copy, Clone, Component)]
struct X;
impl FromAnyValue for X {
fn from_any_value(_: &dyn std::any::Any) -> Self {
X
}
}
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State<X> for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = ();
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<X>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool {
let font_size = context.get::<FontSize>().unwrap().0;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
#[partial_derive_state]
impl State<X> for TextColor {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
fn update<'a>(
&mut self,
node_view: NodeView<X>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node_view
.attributes()
.and_then(|mut attrs| attrs.next())
.and_then(|attr| attr.value.as_text())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(color) => panic!("unknown color {color}"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct Border(bool);
#[partial_derive_state]
impl State<X> for Border {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the border attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
fn update<'a>(
&mut self,
node_view: NodeView<X>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// check if the node contians a border attribute
let new = Self(
node_view
.attributes()
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rdom: RealDom<X> = RealDom::new([
Border::to_type_erased(),
TextColor::to_type_erased(),
Size::to_type_erased(),
]);
let mut count = 0;
// intial render
let text_id = rdom.create_node(format!("Count: {count}")).id();
let mut root = rdom.get_mut(rdom.root_id()).unwrap();
// set the color to red
if let NodeTypeMut::Element(mut element) = root.node_type_mut() {
element.set_attribute(("color", "style"), "red".to_string());
}
root.add_child(text_id);
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// update the count
count += 1;
let mut text = rdom.get_mut(text_id).unwrap();
if let NodeTypeMut::Text(mut text) = text.node_type_mut() {
*text = format!("Count: {count}");
}
let mut ctx = SendAnyMap::new();
ctx.insert(FontSize(3.3));
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first_advanced(true, |node| {
let indent = " ".repeat(node.height() as usize);
let color = *node.get::<TextColor>().unwrap();
let size = *node.get::<Size>().unwrap();
let border = *node.get::<Border>().unwrap();
let id = node.id();
println!("{indent}{id:?} {color:?} {size:?} {border:?}");
});
// wait 1 second
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
}

View file

@ -1,175 +0,0 @@
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core::real_dom::NodeTypeMut;
use dioxus_native_core_macro::partial_derive_state;
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = (FontSize,);
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<()>,
(font_size,): <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
let font_size = font_size.size;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Component)]
struct FontSize {
size: f64,
}
impl Default for FontSize {
fn default() -> Self {
Self { size: 16.0 }
}
}
#[partial_derive_state]
impl State for FontSize {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["font-size"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
let mut new = None;
for attr in node_view.attributes().into_iter().flatten() {
if attr.attribute.name == "font-size" {
new = Some(FontSize {
size: attr.value.as_float().unwrap(),
});
}
}
let new = new.unwrap_or(parent.map(|(p,)| *p).unwrap_or_default());
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rdom: RealDom = RealDom::new([FontSize::to_type_erased(), Size::to_type_erased()]);
let mut count = 0;
// intial render
let text_id = rdom.create_node(format!("Count: {count}")).id();
let mut root = rdom.get_mut(rdom.root_id()).unwrap();
// set the color to red
if let NodeTypeMut::Element(mut element) = root.node_type_mut() {
element.set_attribute(("color", "style"), "red".to_string());
element.set_attribute(("font-size", "style"), 1.);
}
root.add_child(text_id);
let ctx = SendAnyMap::new();
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// update the count and font size
count += 1;
let mut text = rdom.get_mut(text_id).unwrap();
if let NodeTypeMut::Text(mut text) = text.node_type_mut() {
*text = format!("Count: {count}");
}
if let NodeTypeMut::Element(mut element) =
rdom.get_mut(rdom.root_id()).unwrap().node_type_mut()
{
element.set_attribute(("font-size", "style"), count as f64);
}
let ctx = SendAnyMap::new();
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first_advanced(true, |node| {
let indent = " ".repeat(node.height() as usize);
let font_size = *node.get::<FontSize>().unwrap();
let size = *node.get::<Size>().unwrap();
let id = node.id();
println!("{indent}{id:?} {font_size:?} {size:?}");
});
// wait 1 second
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
}

View file

@ -1,222 +0,0 @@
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core::real_dom::NodeTypeMut;
use dioxus_native_core_macro::partial_derive_state;
struct FontSize(f64);
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = ();
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool {
let font_size = context.get::<FontSize>().unwrap().0;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
#[partial_derive_state]
impl State for TextColor {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node_view
.attributes()
.and_then(|mut attrs| attrs.next())
.and_then(|attr| attr.value.as_text())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(color) => panic!("unknown color {color}"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct Border(bool);
#[partial_derive_state]
impl State for Border {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the border attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// check if the node contians a border attribute
let new = Self(
node_view
.attributes()
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rdom: RealDom = RealDom::new([
Border::to_type_erased(),
TextColor::to_type_erased(),
Size::to_type_erased(),
]);
let mut count = 0;
// intial render
let text_id = rdom.create_node(format!("Count: {count}")).id();
let mut root = rdom.get_mut(rdom.root_id()).unwrap();
// set the color to red
if let NodeTypeMut::Element(mut element) = root.node_type_mut() {
element.set_attribute(("color", "style"), "red".to_string());
}
root.add_child(text_id);
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// update the count
count += 1;
let mut text = rdom.get_mut(text_id).unwrap();
if let NodeTypeMut::Text(mut text) = text.node_type_mut() {
*text = format!("Count: {count}");
}
let mut ctx = SendAnyMap::new();
ctx.insert(FontSize(3.3));
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first_advanced(true, |node| {
let indent = " ".repeat(node.height() as usize);
let color = *node.get::<TextColor>().unwrap();
let size = *node.get::<Size>().unwrap();
let border = *node.get::<Border>().unwrap();
let id = node.id();
println!("{indent}{id:?} {color:?} {size:?} {border:?}");
});
// wait 1 second
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
}

View file

@ -1,245 +0,0 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
struct FontSize(f64);
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = ();
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool {
let font_size = context.get::<FontSize>().unwrap().0;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
#[partial_derive_state]
impl State for TextColor {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node_view
.attributes()
.and_then(|mut attrs| attrs.next())
.and_then(|attr| attr.value.as_text())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(color) => panic!("unknown color {color}"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct Border(bool);
#[partial_derive_state]
impl State for Border {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the border attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// check if the node contians a border attribute
let new = Self(
node_view
.attributes()
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn app() -> Element {
let mut count = use_signal(|| 0);
use_future(move || async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
count += 1;
}
});
rsx! {
div { color: "red",
"{count}",
Comp {}
}
}
}
fn Comp() -> Element {
rsx! {
div { border: "", "hello world" }
}
}
// create the vdom, the real_dom, and the binding layer between them
let mut vdom = VirtualDom::new(app);
let mut rdom: RealDom = RealDom::new([
Border::to_type_erased(),
TextColor::to_type_erased(),
Size::to_type_erased(),
]);
let mut dioxus_intigration_state = DioxusState::create(&mut rdom);
// update the structure of the real_dom tree
let mut writer = dioxus_intigration_state.create_mutation_writer(&mut rdom);
vdom.rebuild(&mut writer);
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// wait for the vdom to update
vdom.wait_for_work().await;
// get the mutations from the vdom
// update the structure of the real_dom tree
let mut writer = dioxus_intigration_state.create_mutation_writer(&mut rdom);
vdom.rebuild(&mut writer);
// update the state of the real_dom tree
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first_advanced(true, |node| {
let indent = " ".repeat(node.height() as usize);
let color = *node.get::<TextColor>().unwrap();
let size = *node.get::<Size>().unwrap();
let border = *node.get::<Border>().unwrap();
let id = node.id();
let node = node.node_type();
let node_type = &*node;
println!("{indent}{id:?} {color:?} {size:?} {border:?} {node_type:?}");
});
}
})
}

View file

@ -1,180 +0,0 @@
//! A custom element is a controlled element that renders to a shadow DOM. This allows you to create elements that act like widgets without relying on a specific framework.
//!
//! Each custom element is registered with a element name and namespace with [`RealDom::register_custom_element`] or [`RealDom::register_custom_element_with_factory`]. Once registered, they will be created automatically when the element is added to the DOM.
// Used in doc links
#[allow(unused)]
use crate::real_dom::RealDom;
use std::sync::{Arc, RwLock};
use rustc_hash::FxHashMap;
use shipyard::Component;
use crate::{
node::{FromAnyValue, NodeType},
node_ref::AttributeMask,
prelude::{NodeImmutable, NodeMut},
tree::TreeMut,
NodeId,
};
pub(crate) struct CustomElementRegistry<V: FromAnyValue + Send + Sync> {
builders: FxHashMap<(&'static str, Option<&'static str>), CustomElementBuilder<V>>,
}
impl<V: FromAnyValue + Send + Sync> Default for CustomElementRegistry<V> {
fn default() -> Self {
Self {
builders: FxHashMap::default(),
}
}
}
impl<V: FromAnyValue + Send + Sync> CustomElementRegistry<V> {
pub fn register<F, U>(&mut self)
where
F: CustomElementFactory<U, V>,
U: CustomElementUpdater<V>,
{
self.builders.insert(
(F::NAME, F::NAMESPACE),
CustomElementBuilder {
create: |node| Box::new(F::create(node)),
},
);
}
pub fn add_shadow_dom(&self, mut node: NodeMut<V>) {
let element_tag = if let NodeType::Element(el) = &*node.node_type() {
Some((el.tag.clone(), el.namespace.clone()))
} else {
None
};
if let Some((tag, ns)) = element_tag {
if let Some(builder) = self.builders.get(&(tag.as_str(), ns.as_deref())) {
let boxed_custom_element = { (builder.create)(node.reborrow()) };
let shadow_roots = boxed_custom_element.roots();
let light_id = node.id();
node.real_dom_mut().tree_mut().create_subtree(
light_id,
shadow_roots,
boxed_custom_element.slot(),
);
let boxed_custom_element = CustomElementManager {
inner: Arc::new(RwLock::new(boxed_custom_element)),
};
node.insert(boxed_custom_element);
}
}
}
}
struct CustomElementBuilder<V: FromAnyValue + Send + Sync> {
create: fn(NodeMut<V>) -> Box<dyn CustomElementUpdater<V>>,
}
/// A controlled element that renders to a shadow DOM.
///
/// Register with [`RealDom::register_custom_element`]
///
/// This is a simplified custom element trait for elements that can create themselves. For more granular control, implement [`CustomElementFactory`] and [`CustomElementUpdater`] instead.
pub trait CustomElement<V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static {
/// The tag of the element
const NAME: &'static str;
/// The namespace of the element
const NAMESPACE: Option<&'static str> = None;
/// Create a new element *without mounting* it.
/// The node passed in is the light DOM node. The element should not modify the light DOM node, but it can get the [`NodeMut::real_dom_mut`] from the node to create new nodes.
fn create(light_root: NodeMut<V>) -> Self;
/// The root node of the custom element. These roots must be not change once the element is created.
fn roots(&self) -> Vec<NodeId>;
/// The slot to render children of the element into. The slot must be not change once the element is created.
fn slot(&self) -> Option<NodeId> {
None
}
/// Update the custom element's shadow tree with the new attributes.
/// Called when the attributes of the custom element are changed.
fn attributes_changed(&mut self, light_node: NodeMut<V>, attributes: &AttributeMask);
}
/// A factory for creating custom elements
///
/// Register with [`RealDom::register_custom_element_with_factory`]
pub trait CustomElementFactory<W: CustomElementUpdater<V>, V: FromAnyValue + Send + Sync = ()>:
Send + Sync + 'static
{
/// The tag of the element
const NAME: &'static str;
/// The namespace of the element
const NAMESPACE: Option<&'static str> = None;
/// Create a new element *without mounting* it.
/// The node passed in is the light DOM node. The element should not modify the light DOM node, but it can get the [`NodeMut::real_dom_mut`] from the node to create new nodes.
fn create(dom: NodeMut<V>) -> W;
}
impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> CustomElementFactory<W, V> for W {
const NAME: &'static str = W::NAME;
const NAMESPACE: Option<&'static str> = W::NAMESPACE;
fn create(node: NodeMut<V>) -> Self {
Self::create(node)
}
}
/// A trait for updating custom elements
pub trait CustomElementUpdater<V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static {
/// Update the custom element's shadow tree with the new attributes.
/// Called when the attributes of the custom element are changed.
fn attributes_changed(&mut self, light_root: NodeMut<V>, attributes: &AttributeMask);
/// The root node of the custom element. These roots must be not change once the element is created.
fn roots(&self) -> Vec<NodeId>;
/// The slot to render children of the element into. The slot must be not change once the element is created.
fn slot(&self) -> Option<NodeId> {
None
}
}
impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> CustomElementUpdater<V> for W {
fn attributes_changed(&mut self, light_root: NodeMut<V>, attributes: &AttributeMask) {
self.attributes_changed(light_root, attributes);
}
fn roots(&self) -> Vec<NodeId> {
self.roots()
}
fn slot(&self) -> Option<NodeId> {
self.slot()
}
}
/// A dynamic trait object wrapper for [`CustomElementUpdater`]
#[derive(Component, Clone)]
pub(crate) struct CustomElementManager<V: FromAnyValue = ()> {
inner: Arc<RwLock<Box<dyn CustomElementUpdater<V>>>>,
}
impl<V: FromAnyValue + Send + Sync> CustomElementManager<V> {
/// Update the custom element based on attributes changed.
pub fn on_attributes_changed(&self, light_root: NodeMut<V>, attributes: &AttributeMask) {
self.inner
.write()
.unwrap()
.attributes_changed(light_root, attributes);
}
}

View file

@ -1,326 +0,0 @@
//! Integration between Dioxus and the RealDom
use crate::tree::TreeMut;
use dioxus_core::{AttributeValue, ElementId, TemplateNode, WriteMutations};
use rustc_hash::{FxHashMap, FxHashSet};
use shipyard::Component;
use crate::{
node::{
ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue,
TextNode,
},
prelude::*,
real_dom::NodeTypeMut,
NodeId,
};
#[derive(Component)]
struct ElementIdComponent(ElementId);
/// The state of the Dioxus integration with the RealDom
pub struct DioxusState {
templates: FxHashMap<String, Vec<NodeId>>,
stack: Vec<NodeId>,
node_id_mapping: Vec<Option<NodeId>>,
}
impl DioxusState {
/// Initialize the DioxusState in the RealDom
pub fn create<V: FromAnyValue + Send + Sync>(rdom: &mut RealDom<V>) -> Self {
let root_id = rdom.root_id();
let mut root = rdom.get_mut(root_id).unwrap();
root.insert(ElementIdComponent(ElementId(0)));
Self {
templates: FxHashMap::default(),
stack: vec![root_id],
node_id_mapping: vec![Some(root_id)],
}
}
/// Convert an ElementId to a NodeId
pub fn element_to_node_id(&self, element_id: ElementId) -> NodeId {
self.try_element_to_node_id(element_id).unwrap()
}
/// Attempt to convert an ElementId to a NodeId. This will return None if the ElementId is not in the RealDom.
pub fn try_element_to_node_id(&self, element_id: ElementId) -> Option<NodeId> {
self.node_id_mapping.get(element_id.0).copied().flatten()
}
/// Create a mutation writer for the RealDom
pub fn create_mutation_writer<'a, V: FromAnyValue + Send + Sync>(
&'a mut self,
rdom: &'a mut RealDom<V>,
) -> DioxusNativeCoreMutationWriter<'a, V> {
DioxusNativeCoreMutationWriter { rdom, state: self }
}
fn set_element_id<V: FromAnyValue + Send + Sync>(
&mut self,
mut node: NodeMut<V>,
element_id: ElementId,
) {
let node_id = node.id();
node.insert(ElementIdComponent(element_id));
if self.node_id_mapping.len() <= element_id.0 {
self.node_id_mapping.resize(element_id.0 + 1, None);
} else if let Some(mut node) =
self.node_id_mapping[element_id.0].and_then(|id| node.real_dom_mut().get_mut(id))
{
node.remove();
}
self.node_id_mapping[element_id.0] = Some(node_id);
}
fn load_child<V: FromAnyValue + Send + Sync>(&self, rdom: &RealDom<V>, path: &[u8]) -> NodeId {
let mut current = rdom.get(*self.stack.last().unwrap()).unwrap();
for i in path {
let new_id = current.child_ids()[*i as usize];
current = rdom.get(new_id).unwrap();
}
current.id()
}
}
/// A writer for mutations that can be used with the RealDom.
pub struct DioxusNativeCoreMutationWriter<'a, V: FromAnyValue + Send + Sync = ()> {
/// The realdom associated with this writer
pub rdom: &'a mut RealDom<V>,
/// The state associated with this writer
pub state: &'a mut DioxusState,
}
impl<V: FromAnyValue + Send + Sync> WriteMutations for DioxusNativeCoreMutationWriter<'_, V> {
fn register_template(&mut self, template: dioxus_core::prelude::Template) {
let mut template_root_ids = Vec::new();
for root in template.roots {
let id = create_template_node(self.rdom, root);
template_root_ids.push(id);
}
self.state
.templates
.insert(template.name.to_string(), template_root_ids);
}
fn append_children(&mut self, id: ElementId, m: usize) {
let children = self.state.stack.split_off(self.state.stack.len() - m);
let parent = self.state.element_to_node_id(id);
for child in children {
self.rdom.get_mut(parent).unwrap().add_child(child);
}
}
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) {
let node_id = self.state.load_child(self.rdom, path);
self.state
.set_element_id(self.rdom.get_mut(node_id).unwrap(), id);
}
fn create_placeholder(&mut self, id: ElementId) {
let node = NodeType::Placeholder;
let node = self.rdom.create_node(node);
let node_id = node.id();
self.state.set_element_id(node, id);
self.state.stack.push(node_id);
}
fn create_text_node(&mut self, value: &str, id: ElementId) {
let node_data = NodeType::Text(TextNode {
listeners: FxHashSet::default(),
text: value.to_string(),
});
let node = self.rdom.create_node(node_data);
let node_id = node.id();
self.state.set_element_id(node, id);
self.state.stack.push(node_id);
}
fn hydrate_text_node(&mut self, path: &'static [u8], value: &str, id: ElementId) {
let node_id = self.state.load_child(self.rdom, path);
let node = self.rdom.get_mut(node_id).unwrap();
self.state.set_element_id(node, id);
let mut node = self.rdom.get_mut(node_id).unwrap();
let node_type_mut = node.node_type_mut();
if let NodeTypeMut::Text(mut text) = node_type_mut {
*text.text_mut() = value.to_string();
} else {
drop(node_type_mut);
node.set_type(NodeType::Text(TextNode {
text: value.to_string(),
listeners: FxHashSet::default(),
}));
}
}
fn load_template(&mut self, name: &'static str, index: usize, id: ElementId) {
let template_id = self.state.templates[name][index];
let clone_id = self.rdom.get_mut(template_id).unwrap().clone_node();
let clone = self.rdom.get_mut(clone_id).unwrap();
self.state.set_element_id(clone, id);
self.state.stack.push(clone_id);
}
fn replace_node_with(&mut self, id: ElementId, m: usize) {
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let old_node_id = self.state.element_to_node_id(id);
for new in new_nodes {
let mut node = self.rdom.get_mut(new).unwrap();
node.insert_before(old_node_id);
}
self.rdom.get_mut(old_node_id).unwrap().remove();
}
fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) {
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let old_node_id = self.state.load_child(self.rdom, path);
for new in new_nodes {
let mut node = self.rdom.get_mut(new).unwrap();
node.insert_before(old_node_id);
}
self.rdom.get_mut(old_node_id).unwrap().remove();
}
fn insert_nodes_after(&mut self, id: ElementId, m: usize) {
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let old_node_id = self.state.element_to_node_id(id);
for new in new_nodes.into_iter().rev() {
let mut node = self.rdom.get_mut(new).unwrap();
node.insert_after(old_node_id);
}
}
fn insert_nodes_before(&mut self, id: ElementId, m: usize) {
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let old_node_id = self.state.element_to_node_id(id);
for new in new_nodes {
self.rdom.tree_mut().insert_before(old_node_id, new);
}
}
fn set_attribute(
&mut self,
name: &'static str,
ns: Option<&'static str>,
value: &AttributeValue,
id: ElementId,
) {
let node_id = self.state.element_to_node_id(id);
let mut node = self.rdom.get_mut(node_id).unwrap();
let mut node_type_mut = node.node_type_mut();
if let NodeTypeMut::Element(element) = &mut node_type_mut {
if let AttributeValue::None = &value {
element.remove_attribute(&OwnedAttributeDiscription {
name: name.to_string(),
namespace: ns.map(|s| s.to_string()),
});
} else {
element.set_attribute(
OwnedAttributeDiscription {
name: name.to_string(),
namespace: ns.map(|s| s.to_string()),
},
OwnedAttributeValue::from(value),
);
}
}
}
fn set_node_text(&mut self, value: &str, id: ElementId) {
let node_id = self.state.element_to_node_id(id);
let mut node = self.rdom.get_mut(node_id).unwrap();
let node_type_mut = node.node_type_mut();
if let NodeTypeMut::Text(mut text) = node_type_mut {
*text.text_mut() = value.to_string();
}
}
fn create_event_listener(&mut self, name: &'static str, id: ElementId) {
let node_id = self.state.element_to_node_id(id);
let mut node = self.rdom.get_mut(node_id).unwrap();
node.add_event_listener(name);
}
fn remove_event_listener(&mut self, name: &'static str, id: ElementId) {
let node_id = self.state.element_to_node_id(id);
let mut node = self.rdom.get_mut(node_id).unwrap();
node.remove_event_listener(name);
}
fn remove_node(&mut self, id: ElementId) {
let node_id = self.state.element_to_node_id(id);
self.rdom.get_mut(node_id).unwrap().remove();
}
fn push_root(&mut self, id: ElementId) {
let node_id = self.state.element_to_node_id(id);
self.state.stack.push(node_id);
}
}
fn create_template_node<V: FromAnyValue + Send + Sync>(
rdom: &mut RealDom<V>,
node: &TemplateNode,
) -> NodeId {
match node {
TemplateNode::Element {
tag,
namespace,
attrs,
children,
} => {
let node = NodeType::Element(ElementNode {
tag: tag.to_string(),
namespace: namespace.map(|s| s.to_string()),
attributes: attrs
.iter()
.filter_map(|attr| match attr {
dioxus_core::TemplateAttribute::Static {
name,
value,
namespace,
} => Some((
OwnedAttributeDiscription {
namespace: namespace.map(|s| s.to_string()),
name: name.to_string(),
},
OwnedAttributeValue::Text(value.to_string()),
)),
dioxus_core::TemplateAttribute::Dynamic { .. } => None,
})
.collect(),
listeners: FxHashSet::default(),
});
let node_id = rdom.create_node(node).id();
for child in *children {
let child_id = create_template_node(rdom, child);
rdom.get_mut(node_id).unwrap().add_child(child_id);
}
node_id
}
TemplateNode::Text { text } => rdom
.create_node(NodeType::Text(TextNode {
text: text.to_string(),
..Default::default()
}))
.id(),
TemplateNode::Dynamic { .. } => rdom.create_node(NodeType::Placeholder).id(),
TemplateNode::DynamicText { .. } => {
rdom.create_node(NodeType::Text(TextNode::default())).id()
}
}
}
/// A trait that extends the `NodeImmutable` trait with methods that are useful for dioxus.
pub trait NodeImmutableDioxusExt<V: FromAnyValue + Send + Sync>: NodeImmutable<V> {
/// Returns the id of the element that this node is mounted to.
/// Not all nodes are mounted to an element, only nodes with dynamic content that have been renderered will have an id.
fn mounted_id(&self) -> Option<ElementId> {
let id = self.get::<ElementIdComponent>();
id.map(|id| id.0)
}
}
impl<T: NodeImmutable<V>, V: FromAnyValue + Send + Sync> NodeImmutableDioxusExt<V> for T {}

View file

@ -1,641 +0,0 @@
//! Utility functions for applying layout attributes to taffy layout
/*
- [ ] pub display: Display, ----> taffy doesnt support all display types
- [x] pub position: Position, --> taffy doesnt support everything
- [x] pub direction: Direction,
- [x] pub flex_direction: FlexDirection,
- [x] pub flex_wrap: FlexWrap,
- [x] pub flex_grow: f32,
- [x] pub flex_shrink: f32,
- [x] pub flex_basis: Dimension,
- [x]pub grid_auto_flow: GridAutoFlow,
- [x]pub grid_template_rows: GridTrackVec<TrackSizingFunction>,
- [x]pub grid_template_columns: GridTrackVec<TrackSizingFunction>,
- [x]pub grid_auto_rows: GridTrackVec<NonRepeatedTrackSizingFunction>,
- [x]pub grid_auto_columns: GridTrackVec<NonRepeatedTrackSizingFunction>,
- [x]pub grid_row: Line<GridPlacement>,
- [x]pub grid_column: Line<GridPlacement>,
- [x] pub overflow: Overflow, ---> taffy doesnt have support for directional overflow
- [x] pub align_items: AlignItems,
- [x] pub align_self: AlignSelf,
- [x] pub align_content: AlignContent,
- [x] pub margin: Rect<Dimension>,
- [x] pub padding: Rect<Dimension>,
- [x] pub justify_content: JustifyContent,
- [x] pub inset: Rect<Dimension>,
- [x] pub border: Rect<Dimension>,
- [ ] pub size: Size<Dimension>, ----> seems to only be relevant for input?
- [ ] pub min_size: Size<Dimension>,
- [ ] pub max_size: Size<Dimension>,
- [x] pub aspect_ratio: Number,
*/
use lightningcss::properties::border::LineStyle;
use lightningcss::properties::grid::{TrackBreadth, TrackSizing};
use lightningcss::properties::{align, border, display, flex, grid, position, size};
use lightningcss::values::percentage::Percentage;
use lightningcss::{
properties::{Property, PropertyId},
stylesheet::ParserOptions,
traits::Parse,
values::{
length::{Length, LengthPercentageOrAuto, LengthValue},
percentage::DimensionPercentage,
ratio::Ratio,
},
};
use taffy::{
prelude::*,
style::{FlexDirection, Position},
};
/// Default values for layout attributes
#[derive(Default)]
pub struct LayoutConfigeration {
/// the default border widths to use
pub border_widths: BorderWidths,
}
/// Default border widths
pub struct BorderWidths {
/// the default border width to use for thin borders
pub thin: f32,
/// the default border width to use for medium borders
pub medium: f32,
/// the default border width to use for thick borders
pub thick: f32,
}
impl Default for BorderWidths {
fn default() -> Self {
Self {
thin: 1.0,
medium: 3.0,
thick: 5.0,
}
}
}
/// applies the entire html namespace defined in dioxus-html
pub fn apply_layout_attributes(name: &str, value: &str, style: &mut Style) {
apply_layout_attributes_cfg(name, value, style, &LayoutConfigeration::default())
}
/// applies the entire html namespace defined in dioxus-html with the specified configeration
pub fn apply_layout_attributes_cfg(
name: &str,
value: &str,
style: &mut Style,
config: &LayoutConfigeration,
) {
if let Ok(property) =
Property::parse_string(PropertyId::from(name), value, ParserOptions::default())
{
match property {
Property::Display(display) => match display {
display::Display::Keyword(display::DisplayKeyword::None) => {
style.display = Display::None
}
display::Display::Pair(pair) => match pair.inside {
display::DisplayInside::Flex(_) => {
style.display = Display::Flex;
}
display::DisplayInside::Grid => {
style.display = Display::Grid;
}
_ => {}
},
_ => {}
},
Property::Position(position) => {
style.position = match position {
position::Position::Relative => Position::Relative,
position::Position::Absolute => Position::Absolute,
_ => return,
}
}
Property::Top(top) => style.inset.top = convert_length_percentage_or_auto(top),
Property::Bottom(bottom) => {
style.inset.bottom = convert_length_percentage_or_auto(bottom)
}
Property::Left(left) => style.inset.left = convert_length_percentage_or_auto(left),
Property::Right(right) => style.inset.right = convert_length_percentage_or_auto(right),
Property::Inset(inset) => {
style.inset.top = convert_length_percentage_or_auto(inset.top);
style.inset.bottom = convert_length_percentage_or_auto(inset.bottom);
style.inset.left = convert_length_percentage_or_auto(inset.left);
style.inset.right = convert_length_percentage_or_auto(inset.right);
}
Property::BorderTopWidth(width) => {
style.border.top = convert_border_side_width(width, &config.border_widths);
}
Property::BorderBottomWidth(width) => {
style.border.bottom = convert_border_side_width(width, &config.border_widths);
}
Property::BorderLeftWidth(width) => {
style.border.left = convert_border_side_width(width, &config.border_widths);
}
Property::BorderRightWidth(width) => {
style.border.right = convert_border_side_width(width, &config.border_widths);
}
Property::BorderWidth(width) => {
style.border.top = convert_border_side_width(width.top, &config.border_widths);
style.border.bottom =
convert_border_side_width(width.bottom, &config.border_widths);
style.border.left = convert_border_side_width(width.left, &config.border_widths);
style.border.right = convert_border_side_width(width.right, &config.border_widths);
}
Property::Border(border) => {
let width = convert_border_side_width(border.width, &config.border_widths);
style.border.top = width;
style.border.bottom = width;
style.border.left = width;
style.border.right = width;
}
Property::BorderTop(top) => {
style.border.top = convert_border_side_width(top.width, &config.border_widths);
}
Property::BorderBottom(bottom) => {
style.border.bottom =
convert_border_side_width(bottom.width, &config.border_widths);
}
Property::BorderLeft(left) => {
style.border.left = convert_border_side_width(left.width, &config.border_widths);
}
Property::BorderRight(right) => {
style.border.right = convert_border_side_width(right.width, &config.border_widths);
}
Property::BorderTopStyle(line_style) => {
if line_style != LineStyle::None {
style.border.top = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderBottomStyle(line_style) => {
if line_style != LineStyle::None {
style.border.bottom = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderLeftStyle(line_style) => {
if line_style != LineStyle::None {
style.border.left = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderRightStyle(line_style) => {
if line_style != LineStyle::None {
style.border.right = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderStyle(styles) => {
if styles.top != LineStyle::None {
style.border.top = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.bottom != LineStyle::None {
style.border.bottom = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.left != LineStyle::None {
style.border.left = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.right != LineStyle::None {
style.border.right = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
// Flexbox properties
Property::FlexDirection(flex_direction, _) => {
use FlexDirection::*;
style.flex_direction = match flex_direction {
flex::FlexDirection::Row => Row,
flex::FlexDirection::RowReverse => RowReverse,
flex::FlexDirection::Column => Column,
flex::FlexDirection::ColumnReverse => ColumnReverse,
}
}
Property::FlexWrap(wrap, _) => {
use FlexWrap::*;
style.flex_wrap = match wrap {
flex::FlexWrap::NoWrap => NoWrap,
flex::FlexWrap::Wrap => Wrap,
flex::FlexWrap::WrapReverse => WrapReverse,
}
}
Property::FlexGrow(grow, _) => {
style.flex_grow = grow;
}
Property::FlexShrink(shrink, _) => {
style.flex_shrink = shrink;
}
Property::FlexBasis(basis, _) => {
style.flex_basis = convert_length_percentage_or_auto(basis).into();
}
Property::Flex(flex, _) => {
style.flex_grow = flex.grow;
style.flex_shrink = flex.shrink;
style.flex_basis = convert_length_percentage_or_auto(flex.basis).into();
}
// Grid properties
Property::GridAutoFlow(grid_auto_flow) => {
let is_row = grid_auto_flow.contains(grid::GridAutoFlow::Row);
let is_dense = grid_auto_flow.contains(grid::GridAutoFlow::Dense);
style.grid_auto_flow = match (is_row, is_dense) {
(true, false) => GridAutoFlow::Row,
(false, false) => GridAutoFlow::Column,
(true, true) => GridAutoFlow::RowDense,
(false, true) => GridAutoFlow::ColumnDense,
};
}
Property::GridTemplateColumns(TrackSizing::TrackList(track_list)) => {
style.grid_template_columns = track_list
.items
.into_iter()
.map(convert_grid_track_item)
.collect();
}
Property::GridTemplateRows(TrackSizing::TrackList(track_list)) => {
style.grid_template_rows = track_list
.items
.into_iter()
.map(convert_grid_track_item)
.collect();
}
Property::GridAutoColumns(grid::TrackSizeList(track_size_list)) => {
style.grid_auto_columns = track_size_list
.into_iter()
.map(convert_grid_track_size)
.collect();
}
Property::GridAutoRows(grid::TrackSizeList(track_size_list)) => {
style.grid_auto_rows = track_size_list
.into_iter()
.map(convert_grid_track_size)
.collect();
}
Property::GridRow(grid_row) => {
style.grid_row = Line {
start: convert_grid_placement(grid_row.start),
end: convert_grid_placement(grid_row.end),
};
}
Property::GridColumn(grid_column) => {
style.grid_column = Line {
start: convert_grid_placement(grid_column.start),
end: convert_grid_placement(grid_column.end),
};
}
// Alignment properties
Property::AlignContent(align, _) => {
use AlignContent::*;
style.align_content = match align {
align::AlignContent::ContentDistribution(distribution) => match distribution {
align::ContentDistribution::SpaceBetween => Some(SpaceBetween),
align::ContentDistribution::SpaceAround => Some(SpaceAround),
align::ContentDistribution::SpaceEvenly => Some(SpaceEvenly),
align::ContentDistribution::Stretch => Some(Stretch),
},
align::AlignContent::ContentPosition {
value: position, ..
} => match position {
align::ContentPosition::Center => Some(Center),
align::ContentPosition::Start => Some(Start),
align::ContentPosition::FlexStart => Some(FlexStart),
align::ContentPosition::End => Some(End),
align::ContentPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::JustifyContent(justify, _) => {
use AlignContent::*;
style.justify_content = match justify {
align::JustifyContent::ContentDistribution(distribution) => {
match distribution {
align::ContentDistribution::SpaceBetween => Some(SpaceBetween),
align::ContentDistribution::SpaceAround => Some(SpaceAround),
align::ContentDistribution::SpaceEvenly => Some(SpaceEvenly),
_ => return,
}
}
align::JustifyContent::ContentPosition {
value: position, ..
} => match position {
align::ContentPosition::Center => Some(Center),
align::ContentPosition::Start => Some(Start),
align::ContentPosition::FlexStart => Some(FlexStart),
align::ContentPosition::End => Some(End),
align::ContentPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::AlignSelf(align, _) => {
use AlignItems::*;
style.align_self = match align {
align::AlignSelf::Auto => None,
align::AlignSelf::Stretch => Some(Stretch),
align::AlignSelf::BaselinePosition(_) => Some(Baseline),
align::AlignSelf::SelfPosition {
value: position, ..
} => match position {
align::SelfPosition::Center => Some(Center),
align::SelfPosition::Start | align::SelfPosition::SelfStart => Some(Start),
align::SelfPosition::FlexStart => Some(FlexStart),
align::SelfPosition::End | align::SelfPosition::SelfEnd => Some(End),
align::SelfPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::AlignItems(align, _) => {
use AlignItems::*;
style.align_items = match align {
align::AlignItems::BaselinePosition(_) => Some(Baseline),
align::AlignItems::Stretch => Some(Stretch),
align::AlignItems::SelfPosition {
value: position, ..
} => match position {
align::SelfPosition::Center => Some(Center),
align::SelfPosition::FlexStart => Some(FlexStart),
align::SelfPosition::FlexEnd => Some(FlexEnd),
align::SelfPosition::Start | align::SelfPosition::SelfStart => {
Some(FlexEnd)
}
align::SelfPosition::End | align::SelfPosition::SelfEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::RowGap(row_gap) => {
style.gap.width = convert_gap_value(row_gap);
}
Property::ColumnGap(column_gap) => {
style.gap.height = convert_gap_value(column_gap);
}
Property::Gap(gap) => {
style.gap = Size {
width: convert_gap_value(gap.row),
height: convert_gap_value(gap.column),
};
}
Property::MarginTop(margin) => {
style.margin.top = convert_length_percentage_or_auto(margin);
}
Property::MarginBottom(margin) => {
style.margin.bottom = convert_length_percentage_or_auto(margin);
}
Property::MarginLeft(margin) => {
style.margin.left = convert_length_percentage_or_auto(margin);
}
Property::MarginRight(margin) => {
style.margin.right = convert_length_percentage_or_auto(margin);
}
Property::Margin(margin) => {
style.margin = Rect {
top: convert_length_percentage_or_auto(margin.top),
bottom: convert_length_percentage_or_auto(margin.bottom),
left: convert_length_percentage_or_auto(margin.left),
right: convert_length_percentage_or_auto(margin.right),
};
}
Property::PaddingTop(padding) => {
style.padding.top = convert_padding(padding);
}
Property::PaddingBottom(padding) => {
style.padding.bottom = convert_padding(padding);
}
Property::PaddingLeft(padding) => {
style.padding.left = convert_padding(padding);
}
Property::PaddingRight(padding) => {
style.padding.right = convert_padding(padding);
}
Property::Padding(padding) => {
style.padding = Rect {
top: convert_padding(padding.top),
bottom: convert_padding(padding.bottom),
left: convert_padding(padding.left),
right: convert_padding(padding.right),
};
}
Property::Width(width) => {
style.size.width = convert_size(width);
}
Property::Height(height) => {
style.size.height = convert_size(height);
}
_ => (),
}
// currently not implemented in lightningcss
if name == "aspect-ratio" {
if let Ok(ratio) = Ratio::parse_string(value) {
style.aspect_ratio = Some(ratio.0 / ratio.1);
}
}
}
}
fn extract_px_value(length_value: LengthValue) -> f32 {
match length_value {
LengthValue::Px(value) => value,
_ => todo!("Only px values are supported"),
}
}
fn convert_length_percentage(
dimension_percentage: DimensionPercentage<LengthValue>,
) -> LengthPercentage {
match dimension_percentage {
DimensionPercentage::Dimension(value) => LengthPercentage::Points(extract_px_value(value)),
DimensionPercentage::Percentage(percentage) => LengthPercentage::Percent(percentage.0),
DimensionPercentage::Calc(_) => todo!("Calc is not supported yet"),
}
}
fn convert_padding(dimension_percentage: LengthPercentageOrAuto) -> LengthPercentage {
match dimension_percentage {
LengthPercentageOrAuto::Auto => unimplemented!(),
LengthPercentageOrAuto::LengthPercentage(lp) => match lp {
DimensionPercentage::Dimension(value) => {
LengthPercentage::Points(extract_px_value(value))
}
DimensionPercentage::Percentage(percentage) => LengthPercentage::Percent(percentage.0),
DimensionPercentage::Calc(_) => unimplemented!("Calc is not supported yet"),
},
}
}
fn convert_length_percentage_or_auto(
dimension_percentage: LengthPercentageOrAuto,
) -> LengthPercentageAuto {
match dimension_percentage {
LengthPercentageOrAuto::Auto => LengthPercentageAuto::Auto,
LengthPercentageOrAuto::LengthPercentage(lp) => match lp {
DimensionPercentage::Dimension(value) => {
LengthPercentageAuto::Points(extract_px_value(value))
}
DimensionPercentage::Percentage(percentage) => {
LengthPercentageAuto::Percent(percentage.0)
}
DimensionPercentage::Calc(_) => todo!("Calc is not supported yet"),
},
}
}
fn convert_dimension(dimension_percentage: DimensionPercentage<LengthValue>) -> Dimension {
match dimension_percentage {
DimensionPercentage::Dimension(value) => Dimension::Points(extract_px_value(value)),
DimensionPercentage::Percentage(percentage) => Dimension::Percent(percentage.0),
DimensionPercentage::Calc(_) => todo!("Calc is not supported yet"),
}
}
fn convert_border_side_width(
border_side_width: border::BorderSideWidth,
border_width_config: &BorderWidths,
) -> LengthPercentage {
match border_side_width {
border::BorderSideWidth::Length(Length::Value(value)) => {
LengthPercentage::Points(extract_px_value(value))
}
border::BorderSideWidth::Thick => LengthPercentage::Points(border_width_config.thick),
border::BorderSideWidth::Medium => LengthPercentage::Points(border_width_config.medium),
border::BorderSideWidth::Thin => LengthPercentage::Points(border_width_config.thin),
border::BorderSideWidth::Length(_) => todo!("Only Length::Value is supported"),
}
}
fn convert_gap_value(gap_value: align::GapValue) -> LengthPercentage {
match gap_value {
align::GapValue::LengthPercentage(dim) => convert_length_percentage(dim),
align::GapValue::Normal => LengthPercentage::Points(0.0),
}
}
fn convert_size(size: size::Size) -> Dimension {
match size {
size::Size::Auto => Dimension::Auto,
size::Size::LengthPercentage(length) => convert_dimension(length),
size::Size::MinContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::MaxContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::FitContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::FitContentFunction(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::Stretch(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::Contain => Dimension::Auto, // Unimplemented, so default auto
}
}
fn convert_grid_placement(input: grid::GridLine) -> GridPlacement {
match input {
grid::GridLine::Auto => GridPlacement::Auto,
grid::GridLine::Line { index, .. } => line(index as i16),
grid::GridLine::Span { index, .. } => span(index as u16),
grid::GridLine::Area { .. } => unimplemented!(),
}
}
fn convert_grid_track_item(input: grid::TrackListItem) -> TrackSizingFunction {
match input {
grid::TrackListItem::TrackSize(size) => {
TrackSizingFunction::Single(convert_grid_track_size(size))
}
grid::TrackListItem::TrackRepeat(_) => todo!("requires TrackRepeat fields to be public!"),
}
}
fn convert_grid_track_size(input: grid::TrackSize) -> NonRepeatedTrackSizingFunction {
match input {
grid::TrackSize::TrackBreadth(breadth) => minmax(
convert_track_breadth_min(&breadth),
convert_track_breadth_max(&breadth),
),
grid::TrackSize::MinMax { min, max } => minmax(
convert_track_breadth_min(&min),
convert_track_breadth_max(&max),
),
grid::TrackSize::FitContent(limit) => match limit {
DimensionPercentage::Dimension(LengthValue::Px(len)) => minmax(auto(), points(len)),
DimensionPercentage::Percentage(Percentage(pct)) => minmax(auto(), percent(pct)),
_ => unimplemented!(),
},
}
}
fn convert_track_breadth_max(breadth: &TrackBreadth) -> MaxTrackSizingFunction {
match breadth {
grid::TrackBreadth::Length(length_percentage) => match length_percentage {
DimensionPercentage::Dimension(LengthValue::Px(len)) => points(*len),
DimensionPercentage::Percentage(Percentage(pct)) => percent(*pct),
_ => unimplemented!(),
},
grid::TrackBreadth::Flex(fraction) => fr(*fraction),
grid::TrackBreadth::MinContent => MaxTrackSizingFunction::MinContent,
grid::TrackBreadth::MaxContent => MaxTrackSizingFunction::MaxContent,
grid::TrackBreadth::Auto => MaxTrackSizingFunction::Auto,
}
}
fn convert_track_breadth_min(breadth: &TrackBreadth) -> MinTrackSizingFunction {
match breadth {
grid::TrackBreadth::Length(length_percentage) => match length_percentage {
DimensionPercentage::Dimension(LengthValue::Px(len)) => points(*len),
DimensionPercentage::Percentage(Percentage(pct)) => percent(*pct),
_ => unimplemented!(),
},
grid::TrackBreadth::MinContent => MinTrackSizingFunction::MinContent,
grid::TrackBreadth::MaxContent => MinTrackSizingFunction::MaxContent,
grid::TrackBreadth::Auto => MinTrackSizingFunction::Auto,
grid::TrackBreadth::Flex(_) => MinTrackSizingFunction::Auto,
}
}
/// parse relative or absolute value
pub fn parse_value(value: &str) -> Option<Dimension> {
if value.ends_with("px") {
if let Ok(px) = value.trim_end_matches("px").parse::<f32>() {
Some(Dimension::Points(px))
} else {
None
}
} else if value.ends_with('%') {
if let Ok(pct) = value.trim_end_matches('%').parse::<f32>() {
Some(Dimension::Percent(pct / 100.0))
} else {
None
}
} else {
None
}
}

View file

@ -1,54 +0,0 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![warn(missing_docs)]
use std::any::Any;
use std::hash::BuildHasherDefault;
use node_ref::NodeMask;
use rustc_hash::FxHasher;
pub mod custom_element;
#[cfg(feature = "dioxus")]
pub mod dioxus;
#[cfg(feature = "layout-attributes")]
pub mod layout_attributes;
pub mod node;
pub mod node_ref;
pub mod node_watcher;
mod passes;
pub mod real_dom;
pub mod tree;
pub mod utils;
pub use shipyard::EntityId as NodeId;
pub mod exports {
//! Important dependencies that are used by the rest of the library
//! Feel free to just add the dependencies in your own Crates.toml
// exported for the macro
#[doc(hidden)]
pub use rustc_hash::FxHashSet;
pub use shipyard;
}
/// A prelude of commonly used items
pub mod prelude {
#[cfg(feature = "dioxus")]
pub use crate::dioxus::*;
pub use crate::node::{ElementNode, FromAnyValue, NodeType, OwnedAttributeView, TextNode};
pub use crate::node_ref::{AttributeMaskBuilder, NodeMaskBuilder, NodeView};
pub use crate::passes::{run_pass, PassDirection, RunPassView, TypeErasedState};
pub use crate::passes::{Dependancy, DependancyView, Dependants, State};
pub use crate::real_dom::{NodeImmutable, NodeMut, NodeRef, RealDom};
pub use crate::NodeId;
pub use crate::SendAnyMap;
}
/// A map that can be sent between threads
pub type FxDashMap<K, V> = dashmap::DashMap<K, V, BuildHasherDefault<FxHasher>>;
/// A set that can be sent between threads
pub type FxDashSet<K> = dashmap::DashSet<K, BuildHasherDefault<FxHasher>>;
/// A map of types that can be sent between threads
pub type SendAnyMap = anymap::Map<dyn Any + Send + Sync + 'static>;

View file

@ -1,260 +0,0 @@
//! Items related to Nodes in the RealDom
use rustc_hash::{FxHashMap, FxHashSet};
use shipyard::Component;
use std::{
any::Any,
fmt::{Debug, Display},
};
/// A element node in the RealDom
#[derive(Debug, Clone, Default)]
pub struct ElementNode<V: FromAnyValue = ()> {
/// The [tag](https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName) of the element
pub tag: String,
/// The [namespace](https://developer.mozilla.org/en-US/docs/Web/API/Element/namespaceURI) of the element
pub namespace: Option<String>,
/// The attributes of the element
pub attributes: FxHashMap<OwnedAttributeDiscription, OwnedAttributeValue<V>>,
/// The events the element is listening for
pub listeners: FxHashSet<String>,
}
impl ElementNode {
/// Create a new element node
pub fn new(tag: impl Into<String>, namespace: impl Into<Option<String>>) -> Self {
Self {
tag: tag.into(),
namespace: namespace.into(),
attributes: Default::default(),
listeners: Default::default(),
}
}
}
/// A text node in the RealDom
#[derive(Debug, Clone, Default)]
pub struct TextNode {
/// The text of the node
pub text: String,
/// The events the node is listening for
pub listeners: FxHashSet<String>,
}
impl TextNode {
/// Create a new text node
pub fn new(text: String) -> Self {
Self {
text,
listeners: Default::default(),
}
}
}
/// A type of node with data specific to the node type.
#[derive(Debug, Clone, Component)]
pub enum NodeType<V: FromAnyValue = ()> {
/// A text node
Text(TextNode),
/// An element node
Element(ElementNode<V>),
/// A placeholder node. This can be used as a cheaper placeholder for a node that will be created later
Placeholder,
}
impl<V: FromAnyValue, S: Into<String>> From<S> for NodeType<V> {
fn from(text: S) -> Self {
Self::Text(TextNode::new(text.into()))
}
}
impl<V: FromAnyValue> From<TextNode> for NodeType<V> {
fn from(text: TextNode) -> Self {
Self::Text(text)
}
}
impl<V: FromAnyValue> From<ElementNode<V>> for NodeType<V> {
fn from(element: ElementNode<V>) -> Self {
Self::Element(element)
}
}
/// A discription of an attribute on a DOM node, such as `id` or `href`.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct OwnedAttributeDiscription {
/// The name of the attribute.
pub name: String,
/// The namespace of the attribute used to identify what kind of attribute it is.
///
/// For renderers that use HTML, this can be used to identify if the attribute is a style attribute.
/// Instead of parsing the style attribute every time a style is changed, you can set an attribute with the `style` namespace.
pub namespace: Option<String>,
}
impl From<String> for OwnedAttributeDiscription {
fn from(name: String) -> Self {
Self {
name,
namespace: None,
}
}
}
impl<S: Into<String>, N: Into<String>> From<(S, N)> for OwnedAttributeDiscription {
fn from(name: (S, N)) -> Self {
Self {
name: name.0.into(),
namespace: Some(name.1.into()),
}
}
}
/// An attribute on a DOM node, such as `id="my-thing"` or
/// `href="https://example.com"`.
#[derive(Clone, Copy, Debug)]
pub struct OwnedAttributeView<'a, V: FromAnyValue = ()> {
/// The discription of the attribute.
pub attribute: &'a OwnedAttributeDiscription,
/// The value of the attribute.
pub value: &'a OwnedAttributeValue<V>,
}
/// The value of an attribute on a DOM node. This contains non-text values to allow users to skip parsing attribute values in some cases.
#[derive(Clone)]
pub enum OwnedAttributeValue<V: FromAnyValue = ()> {
/// A string value. This is the most common type of attribute.
Text(String),
/// A floating point value.
Float(f64),
/// An integer value.
Int(i64),
/// A boolean value.
Bool(bool),
/// A custom value specific to the renderer
Custom(V),
}
impl<V: FromAnyValue> From<String> for OwnedAttributeValue<V> {
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl<V: FromAnyValue> From<f64> for OwnedAttributeValue<V> {
fn from(value: f64) -> Self {
Self::Float(value)
}
}
impl<V: FromAnyValue> From<i64> for OwnedAttributeValue<V> {
fn from(value: i64) -> Self {
Self::Int(value)
}
}
impl<V: FromAnyValue> From<bool> for OwnedAttributeValue<V> {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl<V: FromAnyValue> From<V> for OwnedAttributeValue<V> {
fn from(value: V) -> Self {
Self::Custom(value)
}
}
/// Something that can be converted from a borrowed [Any] value.
pub trait FromAnyValue: Clone + 'static {
/// Convert from an [Any] value.
fn from_any_value(value: &dyn Any) -> Self;
}
impl FromAnyValue for () {
fn from_any_value(_: &dyn Any) -> Self {}
}
impl<V: FromAnyValue> Debug for OwnedAttributeValue<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text(arg0) => f.debug_tuple("Text").field(arg0).finish(),
Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(),
Self::Int(arg0) => f.debug_tuple("Int").field(arg0).finish(),
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
Self::Custom(_) => f.debug_tuple("Any").finish(),
}
}
}
impl<V: FromAnyValue> Display for OwnedAttributeValue<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text(arg0) => f.write_str(arg0),
Self::Float(arg0) => f.write_str(&arg0.to_string()),
Self::Int(arg0) => f.write_str(&arg0.to_string()),
Self::Bool(arg0) => f.write_str(&arg0.to_string()),
Self::Custom(_) => f.write_str("custom"),
}
}
}
#[cfg(feature = "dioxus")]
impl<V: FromAnyValue> From<&dioxus_core::AttributeValue> for OwnedAttributeValue<V> {
fn from(value: &dioxus_core::AttributeValue) -> Self {
match value {
dioxus_core::AttributeValue::Text(text) => Self::Text(text.clone()),
dioxus_core::AttributeValue::Float(float) => Self::Float(*float),
dioxus_core::AttributeValue::Int(int) => Self::Int(*int),
dioxus_core::AttributeValue::Bool(bool) => Self::Bool(*bool),
dioxus_core::AttributeValue::Any(any) => Self::Custom(V::from_any_value(any.as_any())),
dioxus_core::AttributeValue::None => panic!("None attribute values result in removing the attribute, not converting it to a None value."),
_ => panic!("Unsupported attribute value type"),
}
}
}
impl<V: FromAnyValue> OwnedAttributeValue<V> {
/// Attempt to convert the attribute value to a string.
pub fn as_text(&self) -> Option<&str> {
match self {
OwnedAttributeValue::Text(text) => Some(text),
_ => None,
}
}
/// Attempt to convert the attribute value to a float.
pub fn as_float(&self) -> Option<f64> {
match self {
OwnedAttributeValue::Float(float) => Some(*float),
OwnedAttributeValue::Int(int) => Some(*int as f64),
_ => None,
}
}
/// Attempt to convert the attribute value to an integer.
pub fn as_int(&self) -> Option<i64> {
match self {
OwnedAttributeValue::Float(float) => Some(*float as i64),
OwnedAttributeValue::Int(int) => Some(*int),
_ => None,
}
}
/// Attempt to convert the attribute value to a boolean.
pub fn as_bool(&self) -> Option<bool> {
match self {
OwnedAttributeValue::Bool(bool) => Some(*bool),
_ => None,
}
}
/// Attempt to convert the attribute value to a custom value.
pub fn as_custom(&self) -> Option<&V> {
match self {
OwnedAttributeValue::Custom(custom) => Some(custom),
_ => None,
}
}
}

View file

@ -1,332 +0,0 @@
//! Utilities that provide limited access to nodes
use rustc_hash::FxHashSet;
use crate::{
node::{ElementNode, FromAnyValue, NodeType, OwnedAttributeView},
NodeId,
};
/// A view into a [NodeType] with a mask that determines what is visible.
#[derive(Debug)]
pub struct NodeView<'a, V: FromAnyValue = ()> {
id: NodeId,
inner: &'a NodeType<V>,
mask: &'a NodeMask,
}
impl<'a, V: FromAnyValue> NodeView<'a, V> {
/// Create a new NodeView from a VNode, and mask.
pub fn new(id: NodeId, node: &'a NodeType<V>, view: &'a NodeMask) -> Self {
Self {
inner: node,
mask: view,
id,
}
}
/// Get the node id of the node
pub fn node_id(&self) -> NodeId {
self.id
}
/// Get the tag of the node if the tag is enabled in the mask
pub fn tag(&self) -> Option<&'a str> {
self.mask
.tag
.then_some(match &self.inner {
NodeType::Element(ElementNode { tag, .. }) => Some(&**tag),
_ => None,
})
.flatten()
}
/// Get the tag of the node if the namespace is enabled in the mask
pub fn namespace(&self) -> Option<&'a str> {
self.mask
.namespace
.then_some(match &self.inner {
NodeType::Element(ElementNode { namespace, .. }) => namespace.as_deref(),
_ => None,
})
.flatten()
}
/// Get any attributes that are enabled in the mask
pub fn attributes<'b>(
&'b self,
) -> Option<impl Iterator<Item = OwnedAttributeView<'a, V>> + 'b> {
match &self.inner {
NodeType::Element(ElementNode { attributes, .. }) => Some(
attributes
.iter()
.filter(move |(attr, _)| self.mask.attritutes.contains(&attr.name))
.map(|(attr, val)| OwnedAttributeView {
attribute: attr,
value: val,
}),
),
_ => None,
}
}
/// Get the text if it is enabled in the mask
pub fn text(&self) -> Option<&str> {
self.mask
.text
.then_some(match &self.inner {
NodeType::Text(text) => Some(&text.text),
_ => None,
})
.flatten()
.map(|x| &**x)
}
/// Get the listeners if it is enabled in the mask
pub fn listeners(&self) -> Option<impl Iterator<Item = &'a str> + '_> {
if self.mask.listeners {
match &self.inner {
NodeType::Element(ElementNode { listeners, .. }) => {
Some(listeners.iter().map(|l| &**l))
}
_ => None,
}
} else {
None
}
}
}
/// A mask that contains a list of attributes that are visible.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum AttributeMask {
/// All attributes are visible
All,
/// Only the given attributes are visible
Some(FxHashSet<Box<str>>),
}
impl AttributeMask {
/// Check if the mask contains the given attribute
pub fn contains(&self, attr: &str) -> bool {
match self {
AttributeMask::All => true,
AttributeMask::Some(attrs) => attrs.contains(attr),
}
}
/// Create a new dynamic attribute mask with a single attribute
pub fn single(new: &str) -> Self {
let mut set = FxHashSet::default();
set.insert(new.into());
Self::Some(set)
}
/// Combine two attribute masks
pub fn union(&self, other: &Self) -> Self {
match (self, other) {
(AttributeMask::Some(s), AttributeMask::Some(o)) => {
AttributeMask::Some(s.union(o).cloned().collect())
}
_ => AttributeMask::All,
}
}
/// Check if two attribute masks overlap
fn overlaps(&self, other: &Self) -> bool {
match (self, other) {
(AttributeMask::All, AttributeMask::Some(attrs)) => !attrs.is_empty(),
(AttributeMask::Some(attrs), AttributeMask::All) => !attrs.is_empty(),
(AttributeMask::Some(attrs1), AttributeMask::Some(attrs2)) => {
!attrs1.is_disjoint(attrs2)
}
_ => true,
}
}
}
impl Default for AttributeMask {
fn default() -> Self {
AttributeMask::Some(FxHashSet::default())
}
}
/// A mask that limits what parts of a node a dependency can see.
#[derive(Default, PartialEq, Eq, Clone, Debug)]
pub struct NodeMask {
attritutes: AttributeMask,
tag: bool,
namespace: bool,
text: bool,
listeners: bool,
}
impl NodeMask {
/// Check if two masks overlap
pub fn overlaps(&self, other: &Self) -> bool {
(self.tag && other.tag)
|| (self.namespace && other.namespace)
|| self.attritutes.overlaps(&other.attritutes)
|| (self.text && other.text)
|| (self.listeners && other.listeners)
}
/// Combine two node masks
pub fn union(&self, other: &Self) -> Self {
Self {
attritutes: self.attritutes.union(&other.attritutes),
tag: self.tag | other.tag,
namespace: self.namespace | other.namespace,
text: self.text | other.text,
listeners: self.listeners | other.listeners,
}
}
/// Allow the mask to view the given attributes
pub fn add_attributes(&mut self, attributes: AttributeMask) {
self.attritutes = self.attritutes.union(&attributes);
}
/// Get the mask for the attributes
pub fn attributes(&self) -> &AttributeMask {
&self.attritutes
}
/// Set the mask to view the tag
pub fn set_tag(&mut self) {
self.tag = true;
}
/// Get the mask for the tag
pub fn tag(&self) -> bool {
self.tag
}
/// Set the mask to view the namespace
pub fn set_namespace(&mut self) {
self.namespace = true;
}
/// Get the mask for the namespace
pub fn namespace(&self) -> bool {
self.namespace
}
/// Set the mask to view the text
pub fn set_text(&mut self) {
self.text = true;
}
/// Get the mask for the text
pub fn text(&self) -> bool {
self.text
}
/// Set the mask to view the listeners
pub fn set_listeners(&mut self) {
self.listeners = true;
}
/// Get the mask for the listeners
pub fn listeners(&self) -> bool {
self.listeners
}
}
/// A builder for a mask that controls what attributes are visible.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum AttributeMaskBuilder<'a> {
/// All attributes are visible
All,
/// Only the given attributes are visible
Some(&'a [&'a str]),
}
impl Default for AttributeMaskBuilder<'_> {
fn default() -> Self {
AttributeMaskBuilder::Some(&[])
}
}
/// A mask that limits what parts of a node a dependency can see.
#[derive(Default, PartialEq, Eq, Clone, Debug)]
pub struct NodeMaskBuilder<'a> {
attritutes: AttributeMaskBuilder<'a>,
tag: bool,
namespace: bool,
text: bool,
listeners: bool,
}
impl<'a> NodeMaskBuilder<'a> {
/// A node mask with no parts visible.
pub const NONE: Self = Self::new();
/// A node mask with every part visible.
pub const ALL: Self = Self::new()
.with_attrs(AttributeMaskBuilder::All)
.with_text()
.with_element()
.with_listeners();
/// Create a empty node mask
pub const fn new() -> Self {
Self {
attritutes: AttributeMaskBuilder::Some(&[]),
tag: false,
namespace: false,
text: false,
listeners: false,
}
}
/// Allow the mask to view the given attributes
pub const fn with_attrs(mut self, attritutes: AttributeMaskBuilder<'a>) -> Self {
self.attritutes = attritutes;
self
}
/// Allow the mask to view the tag
pub const fn with_tag(mut self) -> Self {
self.tag = true;
self
}
/// Allow the mask to view the namespace
pub const fn with_namespace(mut self) -> Self {
self.namespace = true;
self
}
/// Allow the mask to view the namespace and tag
pub const fn with_element(self) -> Self {
self.with_namespace().with_tag()
}
/// Allow the mask to view the text
pub const fn with_text(mut self) -> Self {
self.text = true;
self
}
/// Allow the mask to view the listeners
pub const fn with_listeners(mut self) -> Self {
self.listeners = true;
self
}
/// Build the mask
pub fn build(self) -> NodeMask {
NodeMask {
attritutes: match self.attritutes {
AttributeMaskBuilder::All => AttributeMask::All,
AttributeMaskBuilder::Some(attrs) => {
AttributeMask::Some(attrs.iter().map(|s| (*s).into()).collect())
}
},
tag: self.tag,
namespace: self.namespace,
text: self.text,
listeners: self.listeners,
}
}
}

View file

@ -1,19 +0,0 @@
//! Helpers for watching for changes in the DOM tree.
use crate::{node::FromAnyValue, node_ref::AttributeMask, prelude::*};
/// A trait for watching for changes in the DOM tree.
pub trait NodeWatcher<V: FromAnyValue + Send + Sync> {
/// Called after a node is added to the tree.
fn on_node_added(&mut self, _node: NodeMut<V>) {}
/// Called before a node is removed from the tree.
fn on_node_removed(&mut self, _node: NodeMut<V>) {}
/// Called after a node is moved to a new parent.
fn on_node_moved(&mut self, _node: NodeMut<V>) {}
}
/// A trait for watching for changes to attributes of an element.
pub trait AttributeWatcher<V: FromAnyValue + Send + Sync> {
/// Called before update_state is called on the RealDom
fn on_attributes_changed(&self, _node: NodeMut<V>, _attributes: &AttributeMask) {}
}

View file

@ -1,379 +0,0 @@
use parking_lot::RwLock;
use rustc_hash::{FxHashMap, FxHashSet};
use shipyard::{Borrow, BorrowInfo, Component, Unique, UniqueView, View, WorkloadSystem};
use std::any::{Any, TypeId};
use std::collections::BTreeMap;
use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::Arc;
use crate::node::{FromAnyValue, NodeType};
use crate::node_ref::{NodeMaskBuilder, NodeView};
use crate::real_dom::{DirtyNodesResult, SendAnyMapWrapper};
use crate::tree::{TreeRef, TreeRefView};
use crate::SendAnyMap;
use crate::{NodeId, NodeMask};
#[derive(Default)]
struct DirtyNodes {
nodes_dirty: FxHashSet<NodeId>,
}
impl DirtyNodes {
pub fn add_node(&mut self, node_id: NodeId) {
self.nodes_dirty.insert(node_id);
}
pub fn is_empty(&self) -> bool {
self.nodes_dirty.is_empty()
}
pub fn pop(&mut self) -> Option<NodeId> {
self.nodes_dirty.iter().next().copied().map(|id| {
self.nodes_dirty.remove(&id);
id
})
}
}
/// Tracks the dirty nodes sorted by height for each pass. We resolve passes based on the height of the node in order to avoid resolving any node twice in a pass.
#[derive(Clone, Unique)]
pub struct DirtyNodeStates {
dirty: Arc<FxHashMap<TypeId, RwLock<BTreeMap<u16, DirtyNodes>>>>,
}
impl DirtyNodeStates {
pub fn with_passes(passes: impl Iterator<Item = TypeId>) -> Self {
Self {
dirty: Arc::new(
passes
.map(|pass| (pass, RwLock::new(BTreeMap::new())))
.collect(),
),
}
}
pub fn insert(&self, pass_id: TypeId, node_id: NodeId, height: u16) {
if let Some(btree) = self.dirty.get(&pass_id) {
let mut write = btree.write();
if let Some(entry) = write.get_mut(&height) {
entry.add_node(node_id);
} else {
let mut entry = DirtyNodes::default();
entry.add_node(node_id);
write.insert(height, entry);
}
}
}
fn pop_front(&self, pass_id: TypeId) -> Option<(u16, NodeId)> {
let mut values = self.dirty.get(&pass_id)?.write();
let mut value = values.first_entry()?;
let height = *value.key();
let ids = value.get_mut();
let id = ids.pop()?;
if ids.is_empty() {
value.remove_entry();
}
Some((height, id))
}
fn pop_back(&self, pass_id: TypeId) -> Option<(u16, NodeId)> {
let mut values = self.dirty.get(&pass_id)?.write();
let mut value = values.last_entry()?;
let height = *value.key();
let ids = value.get_mut();
let id = ids.pop()?;
if ids.is_empty() {
value.remove_entry();
}
Some((height, id))
}
}
/// A state that is automatically inserted in a node with dependencies.
pub trait State<V: FromAnyValue + Send + Sync = ()>: Any + Send + Sync {
/// This is a tuple of (T: State, ..) of states read from the parent required to update this state
type ParentDependencies: Dependancy;
/// This is a tuple of (T: State, ..) of states read from the children required to update this state
type ChildDependencies: Dependancy;
/// This is a tuple of (T: State, ..) of states read from the node required to update this state
type NodeDependencies: Dependancy;
/// This is a mask of what aspects of the node are required to update this state
const NODE_MASK: NodeMaskBuilder<'static>;
/// Does the state traverse into the shadow dom or pass over it. This should be true for layout and false for styles
const TRAVERSE_SHADOW_DOM: bool = false;
/// Update this state in a node, returns if the state was updated
fn update<'a>(
&mut self,
node_view: NodeView<V>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool;
/// Create a new instance of this state
fn create<'a>(
node_view: NodeView<V>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self;
/// Create a workload system for this state
fn workload_system(
type_id: TypeId,
dependants: Arc<Dependants>,
pass_direction: PassDirection,
) -> WorkloadSystem;
/// Converts to a type erased version of the trait
fn to_type_erased() -> TypeErasedState<V>
where
Self: Sized,
{
let node_mask = Self::NODE_MASK.build();
TypeErasedState {
this_type_id: TypeId::of::<Self>(),
parent_dependancies_ids: Self::ParentDependencies::type_ids()
.iter()
.copied()
.collect(),
child_dependancies_ids: Self::ChildDependencies::type_ids()
.iter()
.copied()
.collect(),
node_dependancies_ids: Self::NodeDependencies::type_ids().iter().copied().collect(),
dependants: Default::default(),
mask: node_mask,
pass_direction: pass_direction::<V, Self>(),
enter_shadow_dom: Self::TRAVERSE_SHADOW_DOM,
workload: Self::workload_system,
phantom: PhantomData,
}
}
}
fn pass_direction<V: FromAnyValue + Send + Sync, S: State<V>>() -> PassDirection {
if S::ChildDependencies::type_ids()
.iter()
.any(|type_id| *type_id == TypeId::of::<S>())
{
PassDirection::ChildToParent
} else if S::ParentDependencies::type_ids()
.iter()
.any(|type_id| *type_id == TypeId::of::<S>())
{
PassDirection::ParentToChild
} else {
PassDirection::AnyOrder
}
}
#[doc(hidden)]
#[derive(Borrow, BorrowInfo)]
pub struct RunPassView<'a, V: FromAnyValue + Send + Sync = ()> {
pub tree: TreeRefView<'a>,
pub node_type: View<'a, NodeType<V>>,
dirty_nodes_result: UniqueView<'a, DirtyNodesResult>,
node_states: UniqueView<'a, DirtyNodeStates>,
any_map: UniqueView<'a, SendAnyMapWrapper>,
}
// This is used by the macro
/// Updates the given pass, marking any nodes that were changed
#[doc(hidden)]
pub fn run_pass<V: FromAnyValue + Send + Sync>(
type_id: TypeId,
dependants: Arc<Dependants>,
pass_direction: PassDirection,
view: RunPassView<V>,
mut update_node: impl FnMut(NodeId, &SendAnyMap) -> bool,
) {
let RunPassView {
tree,
dirty_nodes_result: nodes_updated,
node_states: dirty,
any_map: ctx,
..
} = view;
let ctx = ctx.as_ref();
match pass_direction {
PassDirection::ParentToChild => {
while let Some((height, id)) = dirty.pop_front(type_id) {
if (update_node)(id, ctx) {
nodes_updated.insert(id);
dependants.mark_dirty(&dirty, id, &tree, height);
}
}
}
PassDirection::ChildToParent => {
while let Some((height, id)) = dirty.pop_back(type_id) {
if (update_node)(id, ctx) {
nodes_updated.insert(id);
dependants.mark_dirty(&dirty, id, &tree, height);
}
}
}
PassDirection::AnyOrder => {
while let Some((height, id)) = dirty.pop_back(type_id) {
if (update_node)(id, ctx) {
nodes_updated.insert(id);
dependants.mark_dirty(&dirty, id, &tree, height);
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Dependant {
pub(crate) type_id: TypeId,
pub(crate) enter_shadow_dom: bool,
}
/// The states that depend on this state
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct Dependants {
/// The states in the parent direction that should be invalidated when this state is invalidated
pub(crate) parent: Vec<Dependant>,
/// The states in the child direction that should be invalidated when this state is invalidated
pub(crate) child: Vec<Dependant>,
/// The states in the node direction that should be invalidated when this state is invalidated
pub(crate) node: Vec<TypeId>,
}
impl Dependants {
fn mark_dirty(&self, dirty: &DirtyNodeStates, id: NodeId, tree: &impl TreeRef, height: u16) {
for &Dependant {
type_id,
enter_shadow_dom,
} in &self.child
{
for id in tree.children_ids_advanced(id, enter_shadow_dom) {
dirty.insert(type_id, id, height + 1);
}
}
for &Dependant {
type_id,
enter_shadow_dom,
} in &self.parent
{
if let Some(id) = tree.parent_id_advanced(id, enter_shadow_dom) {
dirty.insert(type_id, id, height - 1);
}
}
for dependant in &self.node {
dirty.insert(*dependant, id, height);
}
}
}
/// A type erased version of [`State`] that can be added to the [`crate::prelude::RealDom`] with [`crate::prelude::RealDom::new`]
pub struct TypeErasedState<V: FromAnyValue + Send = ()> {
pub(crate) this_type_id: TypeId,
pub(crate) parent_dependancies_ids: FxHashSet<TypeId>,
pub(crate) child_dependancies_ids: FxHashSet<TypeId>,
pub(crate) node_dependancies_ids: FxHashSet<TypeId>,
pub(crate) dependants: Arc<Dependants>,
pub(crate) mask: NodeMask,
pub(crate) workload: fn(TypeId, Arc<Dependants>, PassDirection) -> WorkloadSystem,
pub(crate) pass_direction: PassDirection,
pub(crate) enter_shadow_dom: bool,
phantom: PhantomData<V>,
}
impl<V: FromAnyValue + Send> TypeErasedState<V> {
pub(crate) fn create_workload(&self) -> WorkloadSystem {
(self.workload)(
self.this_type_id,
self.dependants.clone(),
self.pass_direction,
)
}
pub(crate) fn combined_dependancy_type_ids(&self) -> impl Iterator<Item = TypeId> + '_ {
self.parent_dependancies_ids
.iter()
.chain(self.child_dependancies_ids.iter())
.chain(self.node_dependancies_ids.iter())
.copied()
}
}
/// The direction that a pass should be run in
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum PassDirection {
/// The pass should be run from the root to the leaves
ParentToChild,
/// The pass should be run from the leaves to the root
ChildToParent,
/// The pass can be run in any order
AnyOrder,
}
/// A trait that is implemented for all the dependancies of a [`State`]
pub trait Dependancy {
/// A tuple with all the elements of the dependancy as [`DependancyView`]
type ElementBorrowed<'a>;
/// Returns a list of all the [`TypeId`]s of the elements in the dependancy
fn type_ids() -> Box<[TypeId]> {
Box::new([])
}
}
macro_rules! impl_dependancy {
($($t:ident),*) => {
impl< $($t: Send + Sync + Component),* > Dependancy for ($($t,)*) {
type ElementBorrowed<'a> = ($(DependancyView<'a, $t>,)*);
fn type_ids() -> Box<[TypeId]> {
Box::new([$(TypeId::of::<$t>()),*])
}
}
};
}
// TODO: track what components are actually read to update subscriptions
// making this a wrapper makes it possible to implement that optimization without a breaking change
/// A immutable view of a [`State`]
pub struct DependancyView<'a, T> {
inner: &'a T,
}
impl<'a, T> DependancyView<'a, T> {
// This should only be used in the macro. This is not a public API or stable
#[doc(hidden)]
pub fn new(inner: &'a T) -> Self {
Self { inner }
}
}
impl<'a, T> Deref for DependancyView<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.inner
}
}
impl_dependancy!();
impl_dependancy!(A);
impl_dependancy!(A, B);
impl_dependancy!(A, B, C);
impl_dependancy!(A, B, C, D);
impl_dependancy!(A, B, C, D, E);
impl_dependancy!(A, B, C, D, E, F);
impl_dependancy!(A, B, C, D, E, F, G);
impl_dependancy!(A, B, C, D, E, F, G, H);
impl_dependancy!(A, B, C, D, E, F, G, H, I);
impl_dependancy!(A, B, C, D, E, F, G, H, I, J);

File diff suppressed because it is too large Load diff

View file

@ -1,599 +0,0 @@
//! A tree of nodes intigated with shipyard
use crate::NodeId;
use shipyard::{Component, EntitiesViewMut, Get, View, ViewMut};
use std::fmt::Debug;
/// A shadow tree reference inside of a tree. This tree is isolated from the main tree.
#[derive(PartialEq, Eq, Clone, Debug, Component)]
pub struct ShadowTree {
/// The root of the shadow tree
pub shadow_roots: Vec<NodeId>,
/// The node that children of the super tree should be inserted under.
pub slot: Option<NodeId>,
}
/// A node in a tree.
#[derive(PartialEq, Eq, Clone, Debug, Component)]
pub struct Node {
parent: Option<NodeId>,
children: Vec<NodeId>,
child_subtree: Option<ShadowTree>,
/// If this node is a slot in a shadow_tree, this is node whose child_subtree is that shadow_tree.
slot_for_light_tree: Option<NodeId>,
/// If this node is a root of a shadow_tree, this is the node whose child_subtree is that shadow_tree.
root_for_light_tree: Option<NodeId>,
height: u16,
}
/// A view of a tree.
pub type TreeRefView<'a> = View<'a, Node>;
/// A mutable view of a tree.
pub type TreeMutView<'a> = (EntitiesViewMut<'a>, ViewMut<'a, Node>);
/// A immutable view of a tree.
pub trait TreeRef {
/// Get the id of the parent of the current node, if enter_shadow_dom is true and the current node is a shadow root, the node the shadow root is attached to will be returned
#[inline]
fn parent_id_advanced(&self, id: NodeId, enter_shadow_dom: bool) -> Option<NodeId> {
// If this node is the root of a shadow_tree, return the node the shadow_tree is attached
let root_for_light_tree = self.root_for_light_tree(id);
match (root_for_light_tree, enter_shadow_dom) {
(Some(id), true) => Some(id),
_ => {
let parent_id = self.parent_id(id);
if enter_shadow_dom {
// If this node is attached via a slot, return the slot as the parent instead of the light tree parent
parent_id.map(|id| {
self.shadow_tree(id)
.and_then(|tree| tree.slot)
.unwrap_or(id)
})
} else {
parent_id
}
}
}
}
/// The parent id of the node.
fn parent_id(&self, id: NodeId) -> Option<NodeId>;
/// Get the ids of the children of the current node, if enter_shadow_dom is true and the current node is a shadow slot, the ids of the nodes under the node the shadow slot is attached to will be returned
#[inline]
fn children_ids_advanced(&self, id: NodeId, enter_shadow_dom: bool) -> Vec<NodeId> {
let shadow_tree = self.shadow_tree(id);
let slot_of_light_tree = self.slot_for_light_tree(id);
match (shadow_tree, slot_of_light_tree, enter_shadow_dom) {
// If this node is a shadow root, return the shadow roots
(Some(tree), _, true) => tree.shadow_roots.clone(),
// If this node is a slot, return the children of the node the slot is attached to
(None, Some(id), true) => self.children_ids(id),
_ => self.children_ids(id),
}
}
/// The children ids of the node.
fn children_ids(&self, id: NodeId) -> Vec<NodeId>;
/// The shadow tree tree under the node.
fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree>;
/// The node that contains the shadow tree this node is a slot for
fn slot_for_light_tree(&self, id: NodeId) -> Option<NodeId>;
/// The node that contains the shadow tree this node is a root of
fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId>;
/// The height of the node.
fn height(&self, id: NodeId) -> Option<u16>;
/// Returns true if the node exists.
fn contains(&self, id: NodeId) -> bool;
}
/// A mutable view of a tree.
pub trait TreeMut: TreeRef {
/// Removes the node and its children from the tree but do not delete the entities.
fn remove(&mut self, id: NodeId);
/// Adds a new node to the tree.
fn create_node(&mut self, id: NodeId);
/// Adds a child to the node.
fn add_child(&mut self, parent: NodeId, new: NodeId);
/// Replaces the node with a new node.
fn replace(&mut self, old_id: NodeId, new_id: NodeId);
/// Inserts a node before another node.
fn insert_before(&mut self, old_id: NodeId, new_id: NodeId);
/// Inserts a node after another node.
fn insert_after(&mut self, old_id: NodeId, new_id: NodeId);
/// Creates a new shadow tree.
fn create_subtree(&mut self, id: NodeId, shadow_roots: Vec<NodeId>, slot: Option<NodeId>);
/// Remove any shadow tree.
fn remove_subtree(&mut self, id: NodeId);
}
impl<'a> TreeRef for TreeRefView<'a> {
fn parent_id(&self, id: NodeId) -> Option<NodeId> {
self.get(id).ok()?.parent
}
fn children_ids(&self, id: NodeId) -> Vec<NodeId> {
self.get(id)
.map(|node| node.children.clone())
.unwrap_or_default()
}
fn height(&self, id: NodeId) -> Option<u16> {
Some(self.get(id).ok()?.height)
}
fn contains(&self, id: NodeId) -> bool {
self.get(id).is_ok()
}
fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree> {
self.get(id).ok()?.child_subtree.as_ref()
}
fn slot_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
self.get(id).ok()?.slot_for_light_tree
}
fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
self.get(id).ok()?.root_for_light_tree
}
}
impl<'a> TreeMut for TreeMutView<'a> {
fn remove(&mut self, id: NodeId) {
fn recurse(tree: &mut TreeMutView<'_>, id: NodeId) {
let (light_tree, children) = {
let node = (&mut tree.1).get(id).unwrap();
(node.slot_for_light_tree, std::mem::take(&mut node.children))
};
for child in children {
recurse(tree, child);
}
// If this node is a slot in a shadow_tree, remove it from the shadow_tree.
if let Some(light_tree) = light_tree {
let root_for_light_tree = (&mut tree.1).get(light_tree).unwrap();
if let Some(shadow_tree) = &mut root_for_light_tree.child_subtree {
shadow_tree.slot = None;
}
debug_assert!(
root_for_light_tree.children.is_empty(),
"ShadowTree root should have no children when slot is removed."
);
}
}
{
let mut node_data_mut = &mut self.1;
if let Some(parent) = node_data_mut.get(id).unwrap().parent {
let parent = (&mut node_data_mut).get(parent).unwrap();
parent.children.retain(|&child| child != id);
}
}
recurse(self, id);
}
fn create_node(&mut self, id: NodeId) {
let (entities, node_data_mut) = self;
entities.add_component(
id,
node_data_mut,
Node {
parent: None,
children: Vec::new(),
height: 0,
child_subtree: None,
slot_for_light_tree: None,
root_for_light_tree: None,
},
);
}
fn add_child(&mut self, parent: NodeId, new: NodeId) {
{
let mut node_state = &mut self.1;
(&mut node_state).get(new).unwrap().parent = Some(parent);
let parent = (&mut node_state).get(parent).unwrap();
parent.children.push(new);
}
let height = child_height((&self.1).get(parent).unwrap(), self);
set_height(self, new, height);
}
fn replace(&mut self, old_id: NodeId, new_id: NodeId) {
{
let mut node_state = &mut self.1;
// update the parent's link to the child
if let Some(parent_id) = node_state.get(old_id).unwrap().parent {
let parent = (&mut node_state).get(parent_id).unwrap();
for id in &mut parent.children {
if *id == old_id {
*id = new_id;
break;
}
}
let height = child_height((&self.1).get(parent_id).unwrap(), self);
set_height(self, new_id, height);
}
}
self.remove(old_id);
}
fn insert_before(&mut self, old_id: NodeId, new_id: NodeId) {
let parent_id = {
let old_node = self.1.get(old_id).unwrap();
old_node.parent.expect("tried to insert before root")
};
{
(&mut self.1).get(new_id).unwrap().parent = Some(parent_id);
}
let parent = (&mut self.1).get(parent_id).unwrap();
let index = parent
.children
.iter()
.position(|child| *child == old_id)
.unwrap();
parent.children.insert(index, new_id);
let height = child_height((&self.1).get(parent_id).unwrap(), self);
set_height(self, new_id, height);
}
fn insert_after(&mut self, old_id: NodeId, new_id: NodeId) {
let mut node_state = &mut self.1;
let old_node = node_state.get(old_id).unwrap();
let parent_id = old_node.parent.expect("tried to insert before root");
(&mut node_state).get(new_id).unwrap().parent = Some(parent_id);
let parent = (&mut node_state).get(parent_id).unwrap();
let index = parent
.children
.iter()
.position(|child| *child == old_id)
.unwrap();
parent.children.insert(index + 1, new_id);
let height = child_height((&self.1).get(parent_id).unwrap(), self);
set_height(self, new_id, height);
}
fn create_subtree(&mut self, id: NodeId, shadow_roots: Vec<NodeId>, slot: Option<NodeId>) {
let (_, node_data_mut) = self;
let light_root_height;
{
let shadow_tree = ShadowTree {
shadow_roots: shadow_roots.clone(),
slot,
};
let light_root = node_data_mut
.get(id)
.expect("tried to create shadow_tree with non-existent id");
light_root.child_subtree = Some(shadow_tree);
light_root_height = light_root.height;
if let Some(slot) = slot {
let slot = node_data_mut
.get(slot)
.expect("tried to create shadow_tree with non-existent slot");
slot.slot_for_light_tree = Some(id);
}
}
// Now that we have created the shadow_tree, we need to update the height of the shadow_tree roots
for root in shadow_roots {
(&mut self.1).get(root).unwrap().root_for_light_tree = Some(id);
set_height(self, root, light_root_height + 1);
}
}
fn remove_subtree(&mut self, id: NodeId) {
let (_, node_data_mut) = self;
if let Ok(node) = node_data_mut.get(id) {
if let Some(shadow_tree) = node.child_subtree.take() {
// Remove the slot's link to the shadow_tree
if let Some(slot) = shadow_tree.slot {
let slot = node_data_mut
.get(slot)
.expect("tried to remove shadow_tree with non-existent slot");
slot.slot_for_light_tree = None;
}
let node = node_data_mut.get(id).unwrap();
// Reset the height of the light root's children
let height = node.height;
for child in node.children.clone() {
println!("child: {:?}", child);
set_height(self, child, height + 1);
}
// Reset the height of the shadow roots
for root in &shadow_tree.shadow_roots {
set_height(self, *root, 0);
}
}
}
}
}
fn child_height(parent: &Node, tree: &impl TreeRef) -> u16 {
match &parent.child_subtree {
Some(shadow_tree) => {
if let Some(slot) = shadow_tree.slot {
tree.height(slot)
.expect("Attempted to read a slot that does not exist")
+ 1
} else {
panic!("Attempted to read the height of a child of a node with a shadow tree, but the shadow tree does not have a slot. Every shadow tree attached to a node with children must have a slot.")
}
}
None => parent.height + 1,
}
}
/// Sets the height of a node and updates the height of all its children
fn set_height(tree: &mut TreeMutView<'_>, node: NodeId, height: u16) {
let (shadow_tree, light_tree, children) = {
let mut node_data_mut = &mut tree.1;
let node = (&mut node_data_mut).get(node).unwrap();
node.height = height;
(
node.child_subtree.clone(),
node.slot_for_light_tree,
node.children.clone(),
)
};
// If the children are actually part of a shadow_tree, there height is determined by the height of the shadow_tree
if let Some(shadow_tree) = shadow_tree {
// Set the height of the shadow_tree roots
for &shadow_root in &shadow_tree.shadow_roots {
set_height(tree, shadow_root, height + 1);
}
} else {
// Otherwise, we just set the height of the children to be one more than the height of the parent
for child in children {
set_height(tree, child, height + 1);
}
}
// If this nodes is a slot for a shadow_tree, we need to go to the super tree and update the height of its children
if let Some(light_tree) = light_tree {
let children = (&tree.1).get(light_tree).unwrap().children.clone();
for child in children {
set_height(tree, child, height + 1);
}
}
}
impl<'a> TreeRef for TreeMutView<'a> {
fn parent_id(&self, id: NodeId) -> Option<NodeId> {
let node_data = &self.1;
node_data.get(id).unwrap().parent
}
fn children_ids(&self, id: NodeId) -> Vec<NodeId> {
let node_data = &self.1;
node_data
.get(id)
.map(|node| node.children.clone())
.unwrap_or_default()
}
fn height(&self, id: NodeId) -> Option<u16> {
let node_data = &self.1;
node_data.get(id).map(|node| node.height).ok()
}
fn contains(&self, id: NodeId) -> bool {
self.1.get(id).is_ok()
}
fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree> {
let node_data = &self.1;
node_data.get(id).ok()?.child_subtree.as_ref()
}
fn slot_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
let node_data = &self.1;
node_data.get(id).ok()?.slot_for_light_tree
}
fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
let node_data = &self.1;
node_data.get(id).ok()?.root_for_light_tree
}
}
#[test]
fn creation() {
use shipyard::World;
#[derive(Component)]
struct Num(i32);
let mut world = World::new();
let parent_id = world.add_entity(Num(1i32));
let child_id = world.add_entity(Num(0i32));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent_id);
tree.create_node(child_id);
tree.add_child(parent_id, child_id);
assert_eq!(tree.height(parent_id), Some(0));
assert_eq!(tree.height(child_id), Some(1));
assert_eq!(tree.parent_id(parent_id), None);
assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
assert_eq!(tree.children_ids(parent_id), &[child_id]);
}
#[test]
fn shadow_tree() {
use shipyard::World;
#[derive(Component)]
struct Num(i32);
let mut world = World::new();
// Create main tree
let parent_id = world.add_entity(Num(1i32));
let child_id = world.add_entity(Num(0i32));
// Create shadow tree
let shadow_parent_id = world.add_entity(Num(2i32));
let shadow_child_id = world.add_entity(Num(3i32));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent_id);
tree.create_node(child_id);
tree.add_child(parent_id, child_id);
tree.create_node(shadow_parent_id);
tree.create_node(shadow_child_id);
tree.add_child(shadow_parent_id, shadow_child_id);
// Check that both trees are correct individually
assert_eq!(tree.height(parent_id), Some(0));
assert_eq!(tree.height(child_id), Some(1));
assert_eq!(tree.parent_id(parent_id), None);
assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
assert_eq!(tree.children_ids(parent_id), &[child_id]);
assert_eq!(tree.height(shadow_parent_id), Some(0));
assert_eq!(tree.height(shadow_child_id), Some(1));
assert_eq!(tree.parent_id(shadow_parent_id), None);
assert_eq!(tree.parent_id(shadow_child_id).unwrap(), shadow_parent_id);
assert_eq!(tree.children_ids(shadow_parent_id), &[shadow_child_id]);
// Add shadow tree to main tree
tree.create_subtree(parent_id, vec![shadow_parent_id], Some(shadow_child_id));
assert_eq!(tree.height(parent_id), Some(0));
assert_eq!(tree.height(shadow_parent_id), Some(1));
assert_eq!(tree.height(shadow_child_id), Some(2));
assert_eq!(tree.height(child_id), Some(3));
assert_eq!(
tree.1
.get(parent_id)
.unwrap()
.child_subtree
.as_ref()
.unwrap()
.shadow_roots,
&[shadow_parent_id]
);
assert_eq!(
tree.1.get(shadow_child_id).unwrap().slot_for_light_tree,
Some(parent_id)
);
// Remove shadow tree from main tree
tree.remove_subtree(parent_id);
// Check that both trees are correct individually
assert_eq!(tree.height(parent_id), Some(0));
assert_eq!(tree.height(child_id), Some(1));
assert_eq!(tree.parent_id(parent_id), None);
assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
assert_eq!(tree.children_ids(parent_id), &[child_id]);
assert_eq!(tree.height(shadow_parent_id), Some(0));
assert_eq!(tree.height(shadow_child_id), Some(1));
assert_eq!(tree.parent_id(shadow_parent_id), None);
assert_eq!(tree.parent_id(shadow_child_id).unwrap(), shadow_parent_id);
assert_eq!(tree.children_ids(shadow_parent_id), &[shadow_child_id]);
}
#[test]
fn insertion() {
use shipyard::World;
#[derive(Component)]
struct Num(i32);
let mut world = World::new();
let parent = world.add_entity(Num(0));
let child = world.add_entity(Num(2));
let before = world.add_entity(Num(1));
let after = world.add_entity(Num(3));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent);
tree.create_node(child);
tree.create_node(before);
tree.create_node(after);
tree.add_child(parent, child);
tree.insert_before(child, before);
tree.insert_after(child, after);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(child), Some(1));
assert_eq!(tree.height(before), Some(1));
assert_eq!(tree.height(after), Some(1));
assert_eq!(tree.parent_id(before).unwrap(), parent);
assert_eq!(tree.parent_id(child).unwrap(), parent);
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent), &[before, child, after]);
}
#[test]
fn deletion() {
use shipyard::World;
#[derive(Component)]
struct Num(i32);
let mut world = World::new();
let parent = world.add_entity(Num(0));
let child = world.add_entity(Num(2));
let before = world.add_entity(Num(1));
let after = world.add_entity(Num(3));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent);
tree.create_node(child);
tree.create_node(before);
tree.create_node(after);
tree.add_child(parent, child);
tree.insert_before(child, before);
tree.insert_after(child, after);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(child), Some(1));
assert_eq!(tree.height(before), Some(1));
assert_eq!(tree.height(after), Some(1));
assert_eq!(tree.parent_id(before).unwrap(), parent);
assert_eq!(tree.parent_id(child).unwrap(), parent);
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent), &[before, child, after]);
tree.remove(child);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(before), Some(1));
assert_eq!(tree.height(after), Some(1));
assert_eq!(tree.parent_id(before).unwrap(), parent);
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent), &[before, after]);
tree.remove(before);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(after), Some(1));
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent), &[after]);
tree.remove(after);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.children_ids(parent), &[]);
}

View file

@ -1,541 +0,0 @@
//! A cursor implementation that can be used to navigate and edit text.
use std::{cmp::Ordering, ops::Range};
use keyboard_types::{Code, Key, Modifiers};
/// This contains the information about the text that is used by the cursor to handle navigation.
pub trait Text {
/// Returns the line at the given index.
fn line(&self, number: usize) -> Option<&Self>;
/// Returns the length of the text in characters.
fn length(&self) -> usize;
/// Returns the number of lines in the text.
fn line_count(&self) -> usize;
/// Returns the character at the given character index.
fn character(&self, idx: usize) -> Option<char>;
/// Returns the length of the text before the given line in characters.
fn len_before_line(&self, line: usize) -> usize;
}
impl Text for str {
fn line(&self, number: usize) -> Option<&str> {
self.lines().nth(number)
}
fn length(&self) -> usize {
self.chars().count()
}
fn line_count(&self) -> usize {
self.lines().count()
}
fn character(&self, idx: usize) -> Option<char> {
self.chars().nth(idx)
}
fn len_before_line(&self, line: usize) -> usize {
self.lines()
.take(line)
.map(|l| l.chars().count())
.sum::<usize>()
}
}
/// This contains the information about the text that is used by the cursor to handle editing text.
pub trait TextEditable<T: Text + ?Sized>: AsRef<T> {
/// Inserts a character at the given character index.
fn insert_character(&mut self, idx: usize, text: char);
/// Deletes the given character range.
fn delete_range(&mut self, range: Range<usize>);
}
impl TextEditable<str> for String {
fn insert_character(&mut self, idx: usize, text: char) {
self.insert(idx, text);
}
fn delete_range(&mut self, range: Range<usize>) {
self.replace_range(range, "");
}
}
/// A cursor position
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pos {
/// The virtual column of the cursor. This can be more than the line length. To get the realized column, use [`Pos::col()`].
pub col: usize,
/// The row of the cursor.
pub row: usize,
}
impl Pos {
/// Creates a new cursor position.
pub fn new(col: usize, row: usize) -> Self {
Self { row, col }
}
/// Moves the position up by one line.
pub fn up(&mut self, text: &(impl Text + ?Sized)) {
self.move_row(-1, text);
}
/// Moves the position down by one line.
pub fn down(&mut self, text: &(impl Text + ?Sized)) {
self.move_row(1, text);
}
/// Moves the position right by one character.
pub fn right(&mut self, text: &(impl Text + ?Sized)) {
self.move_col(1, text);
}
/// Moves the position left by one character.
pub fn left(&mut self, text: &(impl Text + ?Sized)) {
self.move_col(-1, text);
}
/// Move the position's row by the given amount. (positive is down, negative is up)
pub fn move_row(&mut self, change: i32, text: &(impl Text + ?Sized)) {
let new = self.row as i32 + change;
if new >= 0 && new < text.line_count() as i32 {
self.row = new as usize;
}
}
/// Move the position's column by the given amount. (positive is right, negative is left)
pub fn move_col(&mut self, change: i32, text: &(impl Text + ?Sized)) {
self.realize_col(text);
let idx = self.idx(text) as i32;
if idx + change >= 0 && idx + change <= text.length() as i32 {
let len_line = self.len_line(text) as i32;
let new_col = self.col as i32 + change;
let diff = new_col - len_line;
if diff > 0 {
self.down(text);
self.col = 0;
self.move_col(diff - 1, text);
} else if new_col < 0 {
self.up(text);
self.col = self.len_line(text);
self.move_col(new_col + 1, text);
} else {
self.col = new_col as usize;
}
}
}
/// Get the realized column of the position. This is the column, but capped at the line length.
pub fn col(&self, text: &(impl Text + ?Sized)) -> usize {
self.col.min(self.len_line(text))
}
/// Get the row of the position.
pub fn row(&self) -> usize {
self.row
}
fn len_line(&self, text: &(impl Text + ?Sized)) -> usize {
if let Some(line) = text.line(self.row) {
let len = line.length();
if len > 0 && line.character(len - 1) == Some('\n') {
len - 1
} else {
len
}
} else {
0
}
}
/// Get the character index of the position.
pub fn idx(&self, text: &(impl Text + ?Sized)) -> usize {
text.len_before_line(self.row) + self.col(text)
}
/// If the column is more than the line length, cap it to the line length.
pub fn realize_col(&mut self, text: &(impl Text + ?Sized)) {
self.col = self.col(text);
}
}
impl Ord for Pos {
fn cmp(&self, other: &Self) -> Ordering {
self.row.cmp(&other.row).then(self.col.cmp(&other.col))
}
}
impl PartialOrd for Pos {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// A cursor is a selection of text. It has a start and end position of the selection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cursor {
/// The start position of the selection. The start position is the origin of the selection, not necessarily the first position.
pub start: Pos,
/// The end position of the selection. If the end position is None, the cursor is a caret.
pub end: Option<Pos>,
}
impl Cursor {
/// Create a new cursor with the given start position.
pub fn from_start(pos: Pos) -> Self {
Self {
start: pos,
end: None,
}
}
/// Create a new cursor with the given start and end position.
pub fn new(start: Pos, end: Pos) -> Self {
Self {
start,
end: Some(end),
}
}
/// Move the cursor position. If shift is true, the end position will be moved instead of the start position.
pub fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) {
if shift {
self.with_end(f);
} else {
f(&mut self.start);
self.end = None;
}
}
/// Delete the currently selected text and update the cursor position.
pub fn delete_selection<T: Text + ?Sized>(&mut self, text: &mut impl TextEditable<T>) {
let first = self.first();
let last = self.last();
text.delete_range(first.idx(text.as_ref())..last.idx(text.as_ref()));
if let Some(end) = self.end.take() {
if self.start > end {
self.start = end;
}
}
}
/// Handle moving the cursor with the given key.
pub fn handle_input<T: Text + ?Sized>(
&mut self,
code: &Code,
key: &Key,
modifiers: &Modifiers,
text: &mut impl TextEditable<T>,
max_text_length: usize,
) {
use Code::*;
match code {
ArrowUp => {
self.move_cursor(
|c| c.up(text.as_ref()),
modifiers.contains(Modifiers::SHIFT),
);
}
ArrowDown => {
self.move_cursor(
|c| c.down(text.as_ref()),
modifiers.contains(Modifiers::SHIFT),
);
}
ArrowRight => {
if modifiers.contains(Modifiers::CONTROL) {
self.move_cursor(
|c| {
let mut change = 1;
let idx = c.idx(text.as_ref());
let length = text.as_ref().length();
while idx + change < length {
let chr = text.as_ref().character(idx + change).unwrap();
if chr.is_whitespace() {
break;
}
change += 1;
}
c.move_col(change as i32, text.as_ref());
},
modifiers.contains(Modifiers::SHIFT),
);
} else {
self.move_cursor(
|c| c.right(text.as_ref()),
modifiers.contains(Modifiers::SHIFT),
);
}
}
ArrowLeft => {
if modifiers.contains(Modifiers::CONTROL) {
self.move_cursor(
|c| {
let mut change = -1;
let idx = c.idx(text.as_ref()) as i32;
while idx + change > 0 {
let chr = text.as_ref().character((idx + change) as usize).unwrap();
if chr == ' ' {
break;
}
change -= 1;
}
c.move_col(change, text.as_ref());
},
modifiers.contains(Modifiers::SHIFT),
);
} else {
self.move_cursor(
|c| c.left(text.as_ref()),
modifiers.contains(Modifiers::SHIFT),
);
}
}
End => {
self.move_cursor(
|c| c.col = c.len_line(text.as_ref()),
modifiers.contains(Modifiers::SHIFT),
);
}
Home => {
self.move_cursor(|c| c.col = 0, modifiers.contains(Modifiers::SHIFT));
}
Backspace => {
self.start.realize_col(text.as_ref());
let mut start_idx = self.start.idx(text.as_ref());
if self.end.is_some() {
self.delete_selection(text);
} else if start_idx > 0 {
self.start.left(text.as_ref());
text.delete_range(start_idx - 1..start_idx);
if modifiers.contains(Modifiers::CONTROL) {
start_idx = self.start.idx(text.as_ref());
while start_idx > 0
&& text
.as_ref()
.character(start_idx - 1)
.filter(|c| *c != ' ')
.is_some()
{
self.start.left(text.as_ref());
text.delete_range(start_idx - 1..start_idx);
start_idx = self.start.idx(text.as_ref());
}
}
}
}
Enter => {
if text.as_ref().length() + 1 - self.selection_len(text.as_ref()) <= max_text_length
{
text.insert_character(self.start.idx(text.as_ref()), '\n');
self.start.col = 0;
self.start.down(text.as_ref());
}
}
Tab => {
if text.as_ref().length() + 1 - self.selection_len(text.as_ref()) <= max_text_length
{
self.start.realize_col(text.as_ref());
self.delete_selection(text);
text.insert_character(self.start.idx(text.as_ref()), '\t');
self.start.right(text.as_ref());
}
}
_ => {
self.start.realize_col(text.as_ref());
if let Key::Character(character) = key {
if text.as_ref().length() + 1 - self.selection_len(text.as_ref())
<= max_text_length
{
self.delete_selection(text);
let character = character.chars().next().unwrap();
text.insert_character(self.start.idx(text.as_ref()), character);
self.start.right(text.as_ref());
}
}
}
}
}
/// Modify the end selection position
pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) {
let mut new = self.end.take().unwrap_or_else(|| self.start.clone());
f(&mut new);
self.end.replace(new);
}
/// Returns first position of the selection (this could be the start or the end depending on the position)
pub fn first(&self) -> &Pos {
if let Some(e) = &self.end {
e.min(&self.start)
} else {
&self.start
}
}
/// Returns last position of the selection (this could be the start or the end depending on the position)
pub fn last(&self) -> &Pos {
if let Some(e) = &self.end {
e.max(&self.start)
} else {
&self.start
}
}
/// Returns the length of the selection
pub fn selection_len(&self, text: &(impl Text + ?Sized)) -> usize {
self.last().idx(text) - self.first().idx(text)
}
}
impl Default for Cursor {
fn default() -> Self {
Self {
start: Pos::new(0, 0),
end: None,
}
}
}
#[test]
fn pos_direction_movement() {
let mut pos = Pos::new(100, 0);
let text = "hello world\nhi";
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
pos.down(text);
assert_eq!(pos.col(text), text.lines().nth(1).unwrap_or_default().len());
pos.up(text);
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
pos.left(text);
assert_eq!(
pos.col(text),
text.lines().next().unwrap_or_default().len() - 1
);
pos.right(text);
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
}
#[test]
fn pos_col_movement() {
let mut pos = Pos::new(100, 0);
let text = "hello world\nhi";
// move inside a row
pos.move_col(-5, text);
assert_eq!(
pos.col(text),
text.lines().next().unwrap_or_default().len() - 5
);
pos.move_col(5, text);
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
// move between rows
pos.move_col(3, text);
assert_eq!(pos.col(text), 2);
pos.move_col(-3, text);
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
// don't panic if moving out of range
pos.move_col(-100, text);
pos.move_col(1000, text);
}
#[test]
fn cursor_row_movement() {
let mut pos = Pos::new(100, 0);
let text = "hello world\nhi";
pos.move_row(1, text);
assert_eq!(pos.row(), 1);
pos.move_row(-1, text);
assert_eq!(pos.row(), 0);
// don't panic if moving out of range
pos.move_row(-100, text);
pos.move_row(1000, text);
}
#[test]
fn cursor_input() {
let mut cursor = Cursor::from_start(Pos::new(0, 0));
let mut text = "hello world\nhi".to_string();
for _ in 0..5 {
cursor.handle_input(
&keyboard_types::Code::ArrowRight,
&keyboard_types::Key::ArrowRight,
&Modifiers::empty(),
&mut text,
10,
);
}
for _ in 0..5 {
cursor.handle_input(
&keyboard_types::Code::Backspace,
&keyboard_types::Key::Backspace,
&Modifiers::empty(),
&mut text,
10,
);
}
assert_eq!(text, " world\nhi");
let goal_text = "hello world\nhi";
let max_width = goal_text.len();
cursor.handle_input(
&keyboard_types::Code::KeyH,
&keyboard_types::Key::Character("h".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
cursor.handle_input(
&keyboard_types::Code::KeyE,
&keyboard_types::Key::Character("e".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
cursor.handle_input(
&keyboard_types::Code::KeyL,
&keyboard_types::Key::Character("l".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
cursor.handle_input(
&keyboard_types::Code::KeyL,
&keyboard_types::Key::Character("l".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
cursor.handle_input(
&keyboard_types::Code::KeyO,
&keyboard_types::Key::Character("o".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
// these should be ignored
for _ in 0..10 {
cursor.handle_input(
&keyboard_types::Code::KeyO,
&keyboard_types::Key::Character("o".to_string()),
&Modifiers::empty(),
&mut text,
max_width,
);
}
assert_eq!(text.to_string(), goal_text);
}

View file

@ -1,7 +0,0 @@
//! # Utilities for renders using the RealDOM
//!
//! This includes an iterator that can be used to iterate over the children of a node that persists changes in the struture of the DOM, and a cursor for text editing.
mod persistant_iterator;
pub use persistant_iterator::*;
pub mod cursor;

View file

@ -1,474 +0,0 @@
use smallvec::SmallVec;
use crate::{
node::FromAnyValue,
node_watcher::NodeWatcher,
prelude::{NodeMut, NodeRef},
real_dom::{NodeImmutable, RealDom},
NodeId,
};
use std::{
fmt::Debug,
sync::{Arc, Mutex},
};
/// The element produced by the iterator
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct ElementProduced {
id: NodeId,
movement: IteratorMovement,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
/// The method by which the iterator produced an element
pub enum IteratorMovement {
/// The iterator produced an element by progressing to the next node
Progressed,
/// The iterator reached the end of the tree and looped back to the root
Looped,
}
impl ElementProduced {
/// Get the id of the element produced
pub fn id(&self) -> NodeId {
self.id
}
/// The movement the iterator made to produce the element
pub fn movement(&self) -> &IteratorMovement {
&self.movement
}
fn looped(id: NodeId) -> Self {
Self {
id,
movement: IteratorMovement::Looped,
}
}
fn progressed(id: NodeId) -> Self {
Self {
id,
movement: IteratorMovement::Progressed,
}
}
}
struct PersistantElementIterUpdater<V> {
stack: Arc<Mutex<smallvec::SmallVec<[NodeId; 5]>>>,
phantom: std::marker::PhantomData<V>,
}
impl<V: FromAnyValue + Sync + Send> NodeWatcher<V> for PersistantElementIterUpdater<V> {
fn on_node_moved(&mut self, node: NodeMut<V>) {
// if any element is moved, update its parents in the stack
let mut stack = self.stack.lock().unwrap();
let moved = node.id();
let rdom = node.real_dom();
if let Some(r) = stack.iter().position(|el_id| *el_id == moved) {
let back = &stack[r..];
let mut new = SmallVec::new();
let mut parent = node.parent_id();
while let Some(p) = parent.and_then(|id| rdom.get(id)) {
new.push(p.id());
parent = p.parent_id();
}
new.extend(back.iter().copied());
*stack = new;
}
}
fn on_node_removed(&mut self, node: NodeMut<V>) {
// if any element is removed in the chain, remove it and its children from the stack
let mut stack = self.stack.lock().unwrap();
let removed = node.id();
if let Some(r) = stack.iter().position(|el_id| *el_id == removed) {
stack.truncate(r);
}
}
}
/// Focus systems need a iterator that can persist through changes in the [crate::prelude::RealDom]
/// This iterator traverses the tree depth first.
/// You can iterate through it with [PersistantElementIter::next] and [PersistantElementIter::prev].
/// The iterator loops around when it reaches the end or the beginning.
pub struct PersistantElementIter {
// stack of elements and fragments, the last element is the last element that was yielded
stack: Arc<Mutex<smallvec::SmallVec<[NodeId; 5]>>>,
}
impl PersistantElementIter {
/// Create a new iterator in the RealDom
pub fn create<V: FromAnyValue + Send + Sync>(rdom: &mut RealDom<V>) -> Self {
let inner = Arc::new(Mutex::new(smallvec::smallvec![rdom.root_id()]));
rdom.add_node_watcher(PersistantElementIterUpdater {
stack: inner.clone(),
phantom: std::marker::PhantomData,
});
PersistantElementIter { stack: inner }
}
/// get the next element
pub fn next<V: FromAnyValue + Send + Sync>(&mut self, rdom: &RealDom<V>) -> ElementProduced {
let mut stack = self.stack.lock().unwrap();
if stack.is_empty() {
let id = rdom.root_id();
let new = id;
stack.push(new);
ElementProduced::looped(id)
} else {
let mut look_in_children = true;
loop {
if let Some(current) = stack.last().and_then(|last| rdom.get(*last)) {
// if the current element has children, add the first child to the stack and return it
if look_in_children {
if let Some(first) = current.children().first() {
let new = first.id();
stack.push(new);
return ElementProduced::progressed(new);
}
}
stack.pop();
if let Some(new) = current.next() {
// the next element exists, add it to the stack and return it
let new = new.id();
stack.push(new);
return ElementProduced::progressed(new);
}
// otherwise, continue the loop and go to the parent
} else {
// if there is no parent, loop back to the root
let new = rdom.root_id();
stack.clear();
stack.push(new);
return ElementProduced::looped(new);
}
look_in_children = false;
}
}
}
/// get the previous element
pub fn prev<V: FromAnyValue + Send + Sync>(&mut self, rdom: &RealDom<V>) -> ElementProduced {
// recursively add the last child element to the stack
fn push_back<V: FromAnyValue + Send + Sync>(
stack: &mut smallvec::SmallVec<[NodeId; 5]>,
node: NodeRef<V>,
) -> NodeId {
stack.push(node.id());
if let Some(last) = node.children().last() {
push_back(stack, *last)
} else {
node.id()
}
}
let mut stack = self.stack.lock().unwrap();
if stack.is_empty() {
let id = rdom.root_id();
let last = push_back(&mut stack, rdom.get(id).unwrap());
ElementProduced::looped(last)
} else if let Some(current) = stack.pop().and_then(|last| rdom.get(last)) {
if let Some(new) = current.prev() {
// the next element exists, add it to the stack and return it
let new = push_back(&mut stack, new);
ElementProduced::progressed(new)
}
// otherwise, yeild the parent
else if let Some(parent) = stack.last() {
// if there is a parent, return it
ElementProduced::progressed(*parent)
} else {
// if there is no parent, loop back to the root
let id = rdom.root_id();
let last = push_back(&mut stack, rdom.get(id).unwrap());
ElementProduced::looped(last)
}
} else {
// if there is no parent, loop back to the root
let id = rdom.root_id();
let last = push_back(&mut stack, rdom.get(id).unwrap());
ElementProduced::looped(last)
}
}
}
#[test]
#[allow(unused_variables)]
fn traverse() {
use crate::dioxus::DioxusState;
use crate::prelude::*;
use dioxus::prelude::*;
#[allow(non_snake_case)]
fn Base() -> Element {
rsx!(
div{
div{
"hello"
p{
"world"
}
"hello world"
}
}
)
}
let mut vdom = VirtualDom::new(Base);
let mut rdom: RealDom = RealDom::new([]);
let mut iter = PersistantElementIter::create(&mut rdom);
let mut dioxus_state = DioxusState::create(&mut rdom);
vdom.rebuild(&mut dioxus_state.create_mutation_writer(&mut rdom));
let div_tag = "div".to_string();
assert!(matches!(
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
assert!(matches!(
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
let text1 = "hello".to_string();
assert!(matches!(
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Text(text1)
));
let p_tag = "p".to_string();
assert!(matches!(
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: p_tag, .. })
));
let text2 = "world".to_string();
assert!(matches!(
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Text(text2)
));
let text3 = "hello world".to_string();
assert!(matches!(
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Text(text3)
));
assert!(matches!(
&*rdom.get(iter.next(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
assert!(matches!(
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Text(text3)
));
assert!(matches!(
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Text(text2)
));
assert!(matches!(
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: p_tag, .. })
));
assert!(matches!(
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Text(text1)
));
assert!(matches!(
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
assert!(matches!(
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
assert!(matches!(
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Element(ElementNode { tag: div_tag, .. })
));
assert!(matches!(
&*rdom.get(iter.prev(&rdom).id()).unwrap().node_type(),
NodeType::Text(text3)
));
}
#[test]
#[allow(unused_variables)]
fn persist_removes() {
use crate::dioxus::DioxusState;
use crate::prelude::*;
use dioxus::prelude::*;
#[allow(non_snake_case)]
fn Base() -> Element {
let children = match generation() % 2 {
0 => 3,
1 => 2,
_ => unreachable!(),
};
rsx!(
div {
for i in 0..children {
p { key: "{i}", "{i}" }
}
}
)
}
let mut vdom = VirtualDom::new(Base);
let mut rdom: RealDom = RealDom::new([]);
// this will end on the node that is removed
let mut iter1 = PersistantElementIter::create(&mut rdom);
// this will end on the after node that is removed
let mut iter2 = PersistantElementIter::create(&mut rdom);
let mut dioxus_state = DioxusState::create(&mut rdom);
vdom.rebuild(&mut dioxus_state.create_mutation_writer(&mut rdom));
// root
iter1.next(&rdom).id();
iter2.next(&rdom).id();
// div
iter1.next(&rdom).id();
iter2.next(&rdom).id();
// p
iter1.next(&rdom).id();
iter2.next(&rdom).id();
// "1"
iter1.next(&rdom).id();
iter2.next(&rdom).id();
// p
iter1.next(&rdom).id();
iter2.next(&rdom).id();
// "2"
iter1.next(&rdom).id();
iter2.next(&rdom).id();
// p
iter2.next(&rdom).id();
// "3"
iter2.next(&rdom).id();
vdom.mark_dirty(ScopeId::ROOT);
vdom.render_immediate(&mut dioxus_state.create_mutation_writer(&mut rdom));
let root_tag = "Root".to_string();
let idx = iter1.next(&rdom).id();
assert!(matches!(
&*rdom.get(idx).unwrap().node_type(),
NodeType::Element(ElementNode { tag: root_tag, .. })
));
let idx = iter2.next(&rdom).id();
assert!(matches!(
&*rdom.get(idx).unwrap().node_type(),
NodeType::Element(ElementNode { tag: root_tag, .. })
));
}
#[test]
#[allow(unused_variables)]
fn persist_instertions_before() {
use crate::dioxus::DioxusState;
use crate::prelude::*;
use dioxus::prelude::*;
#[allow(non_snake_case)]
fn Base() -> Element {
let children = match generation() % 2 {
0 => 3,
1 => 2,
_ => unreachable!(),
};
rsx!(
div {
for i in 0..children {
p { key: "{i}", "{i}" }
}
}
)
}
let mut vdom = VirtualDom::new(Base);
let mut rdom: RealDom = RealDom::new([]);
let mut dioxus_state = DioxusState::create(&mut rdom);
vdom.rebuild(&mut dioxus_state.create_mutation_writer(&mut rdom));
let mut iter = PersistantElementIter::create(&mut rdom);
// div
iter.next(&rdom).id();
// p
iter.next(&rdom).id();
// "1"
iter.next(&rdom).id();
// p
iter.next(&rdom).id();
// "2"
iter.next(&rdom).id();
vdom.mark_dirty(ScopeId::ROOT);
vdom.render_immediate(&mut dioxus_state.create_mutation_writer(&mut rdom));
let p_tag = "div".to_string();
let idx = iter.next(&rdom).id();
assert!(matches!(
&*rdom.get(idx).unwrap().node_type(),
NodeType::Element(ElementNode { tag: p_tag, .. })
));
}
#[test]
#[allow(unused_variables)]
fn persist_instertions_after() {
use crate::dioxus::DioxusState;
use crate::prelude::*;
use dioxus::prelude::*;
#[allow(non_snake_case)]
fn Base() -> Element {
let children = match generation() % 2 {
0 => 3,
1 => 2,
_ => unreachable!(),
};
rsx!(
div{
for i in 0..children {
p { key: "{i}", "{i}" }
}
}
)
}
let mut vdom = VirtualDom::new(Base);
let mut rdom: RealDom = RealDom::new([]);
let mut iter = PersistantElementIter::create(&mut rdom);
let mut dioxus_state = DioxusState::create(&mut rdom);
let mut writer = dioxus_state.create_mutation_writer(&mut rdom);
vdom.rebuild(&mut writer);
// div
iter.next(&rdom).id();
// p
iter.next(&rdom).id();
// "hello"
iter.next(&rdom).id();
// p
iter.next(&rdom).id();
// "world"
iter.next(&rdom).id();
let mut writer = dioxus_state.create_mutation_writer(&mut rdom);
vdom.rebuild(&mut writer);
let p_tag = "p".to_string();
let idx = iter.next(&rdom).id();
assert!(matches!(
&*rdom.get(idx).unwrap().node_type(),
NodeType::Element(ElementNode { tag: p_tag, .. })
));
let text = "hello world".to_string();
let idx = iter.next(&rdom).id();
assert!(matches!(
&*rdom.get(idx).unwrap().node_type(),
NodeType::Text(text)
));
}

View file

@ -1,2 +0,0 @@
/target
Cargo.lock

View file

@ -1,2 +0,0 @@
esque
Tui

View file

@ -1,36 +0,0 @@
[package]
name = "plasmo"
version = { workspace = true }
authors = ["Jonathan Kelley, Evan Almloff"]
edition = "2021"
description = "TUI-based renderer for Dioxus"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "terminal"]
license = "MIT OR Apache-2.0"
[dependencies]
dioxus-html = { workspace = true, features = ["serialize", "mounted"] }
dioxus-native-core = { workspace = true, features = ["layout-attributes"] }
dioxus-native-core-macro = { workspace = true }
ratatui = "0.24.0"
crossterm = "0.26.1"
anyhow = "1.0.42"
tokio = { workspace = true, features = ["full"] }
futures = "0.3.19"
taffy = "0.3.12"
smallvec = "1.6"
rustc-hash = { workspace = true }
anymap = "1.0.0-beta.2"
futures-channel = { workspace = true }
shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false }
once_cell = "1.17.1"
[dev-dependencies]
tokio = { version = "1" }
criterion = "0.3.5"
[features]
default = []
parallel = ["shipyard/parallel"]

View file

@ -1,74 +0,0 @@
<div align="center">
<h1>Plasmo</h1>
<p>
<strong>A beautiful terminal user interfaces library in Rust.</strong>
</p>
</div>
<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/plasmo">
<img src="https://img.shields.io/crates/v/plasmo.svg?style=flat-square"
alt="Crates.io version" />
</a>
<!-- Downloads -->
<a href="https://crates.io/crates/plasmo">
<img src="https://img.shields.io/crates/d/plasmo.svg?style=flat-square"
alt="Download" />
</a>
<!-- docs -->
<a href="https://docs.rs/plasmo">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
<!-- CI -->
<a href="https://github.com/jkelleyrtp/plasmo/actions">
<img src="https://github.com/dioxuslabs/plasmo/actions/workflows/main.yml/badge.svg"
alt="CI status" />
</a>
<!-- Discord -->
<a href="https://discord.gg/XgGxMSkvUM">
<img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
</a>
</div>
<br/>
Leverage CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces. Plasmo is the cross-framework library that powers [`Dioxus-TUI`](https://github.com/DioxusLabs/dioxus/tree/master/packages/dioxus-tui)
![demo app](examples/example.png)
## Background
You can use Html-like semantics with inline styles, tree hierarchy, components, and more in your [`text-based user interface (TUI)`](https://en.wikipedia.org/wiki/Text-based_user_interface) application.
Plasmo is essentially a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/). Plasmo doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful.
## Limitations
- **Subset of Html**
Terminals can only render a subset of HTML. We support as much as we can.
- **Particular frontend design**
Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers.
## Status
**WARNING: Plasmo is currently under construction!**
Rendering a Dom works fine, but the ecosystem of widgets is not ready yet. Additionally, some bugs in the flexbox implementation might be quirky at times.
## Features
Plasmo features:
- [x] Flexbox-based layout system
- [ ] CSS selectors
- [x] inline CSS support
- [x] Built-in focusing system
* [ ] Widgets
* [ ] Support for events, hooks, and callbacks<sup>1</sup>
* [ ] Html tags<sup>2</sup>
<sup>1</sup> Basic keyboard, mouse, and focus events are implemented.
<sup>2</sup> Currently, most HTML tags don't translate into any meaning inside of Plasmo. So an `input` _element_ won't mean anything nor does it have any additional functionality.

View file

@ -1,103 +0,0 @@
use dioxus_native_core::{
node::TextNode,
prelude::*,
real_dom::{NodeImmutable, NodeTypeMut},
NodeId,
};
use plasmo::{render, Config, Driver, EventData};
use std::rc::Rc;
use std::sync::{Arc, RwLock};
#[derive(Default)]
struct Counter {
count: usize,
counter_id: NodeId,
button_id: NodeId,
}
impl Counter {
fn create(mut root: NodeMut) -> Self {
let mut myself = Self::default();
let root_id = root.id();
let rdom = root.real_dom_mut();
// create the counter
let count = myself.count;
myself.counter_id = rdom
.create_node(NodeType::Text(TextNode::new(count.to_string())))
.id();
let mut button = rdom.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [
("display".to_string().into(), "flex".to_string().into()),
(
("background-color", "style").into(),
format!("rgb({}, {}, {})", count * 10, 0, 0,).into(),
),
(("width", "style").into(), "100%".to_string().into()),
(("height", "style").into(), "100%".to_string().into()),
(("flex-direction", "style").into(), "row".to_string().into()),
(
("justify-content", "style").into(),
"center".to_string().into(),
),
(("align-items", "style").into(), "center".to_string().into()),
]
.into_iter()
.collect(),
..Default::default()
}));
button.add_event_listener("click");
button.add_event_listener("wheel");
button.add_child(myself.counter_id);
myself.button_id = button.id();
rdom.get_mut(root_id).unwrap().add_child(myself.button_id);
myself
}
}
impl Driver for Counter {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>) {
// update the counter
let mut rdom = rdom.write().unwrap();
let mut node = rdom.get_mut(self.button_id).unwrap();
if let NodeTypeMut::Element(mut el) = node.node_type_mut() {
el.set_attribute(
("background-color", "style"),
format!("rgb({}, {}, {})", self.count * 10, 0, 0,),
);
}
let mut text = rdom.get_mut(self.counter_id).unwrap();
let type_mut = text.node_type_mut();
if let NodeTypeMut::Text(mut text) = type_mut {
*text = self.count.to_string();
}
}
fn handle_event(
&mut self,
_: &Arc<RwLock<RealDom>>,
_: NodeId,
_: &str,
_: Rc<EventData>,
_: bool,
) {
// when a click or wheel event is fired, increment the counter
self.count += 1;
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
Box::pin(async move { tokio::time::sleep(std::time::Duration::from_millis(1000)).await })
}
}
fn main() {
render(Config::new(), |rdom, _, _| {
let mut rdom = rdom.write().unwrap();
let root = rdom.root_id();
Counter::create(rdom.get_mut(root).unwrap())
})
.unwrap();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

View file

@ -1,172 +0,0 @@
use dioxus_native_core::{
node::TextNode,
prelude::*,
real_dom::{NodeImmutable, NodeTypeMut},
NodeId,
};
use plasmo::{render, Config, Driver, EventData};
use rustc_hash::FxHashSet;
use std::rc::Rc;
use std::sync::{Arc, RwLock};
const SIZE: usize = 20;
#[derive(Default, Clone, Copy)]
struct Node {
container_id: Option<NodeId>,
text_id: Option<NodeId>,
count: usize,
}
struct Test {
node_states: [[Node; SIZE]; SIZE],
dirty: FxHashSet<(usize, usize)>,
}
impl Default for Test {
fn default() -> Self {
Self {
node_states: [[Node {
container_id: None,
text_id: None,
count: 0,
}; SIZE]; SIZE],
dirty: FxHashSet::default(),
}
}
}
impl Test {
fn create(mut root: NodeMut) -> Self {
let mut myself = Self::default();
// Set the root node to be a flexbox with a column direction.
if let NodeTypeMut::Element(mut el) = root.node_type_mut() {
el.set_attribute("display".to_string(), "flex".to_string());
el.set_attribute(("flex-direction", "style"), "column".to_string());
el.set_attribute(("width", "style"), "100%".to_string());
el.set_attribute(("height", "style"), "100%".to_string());
}
let root_id = root.id();
let rdom = root.real_dom_mut();
// create the grid
for (x, row) in myself.node_states.iter_mut().enumerate() {
let row_node = rdom
.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [
("display".to_string().into(), "flex".to_string().into()),
(("flex-direction", "style").into(), "row".to_string().into()),
(("width", "style").into(), "100%".to_string().into()),
(("height", "style").into(), "100%".to_string().into()),
]
.into_iter()
.collect(),
..Default::default()
}))
.id();
for (y, node) in row.iter_mut().enumerate() {
let count = node.count;
let id = rdom
.create_node(NodeType::Text(TextNode::new(count.to_string())))
.id();
let mut button = rdom.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [
("display".to_string().into(), "flex".to_string().into()),
(
("background-color", "style").into(),
format!("rgb({}, {}, {})", count * 10, 0, (x + y),).into(),
),
(("width", "style").into(), "100%".to_string().into()),
(("height", "style").into(), "100%".to_string().into()),
(("flex-direction", "style").into(), "row".to_string().into()),
(
("justify-content", "style").into(),
"center".to_string().into(),
),
(("align-items", "style").into(), "center".to_string().into()),
]
.into_iter()
.collect(),
..Default::default()
}));
button.add_event_listener("click");
button.add_event_listener("wheel");
button.add_child(id);
let button_id = button.id();
rdom.get_mut(row_node).unwrap().add_child(button_id);
node.container_id = Some(button_id);
node.text_id = Some(id);
}
rdom.get_mut(root_id).unwrap().add_child(row_node);
}
myself
}
}
impl Driver for Test {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>) {
let mut rdom = rdom.write().unwrap();
for (x, y) in self.dirty.drain() {
let node = self.node_states[x][y];
let node_id = node.container_id.unwrap();
let mut container = rdom.get_mut(node_id).unwrap();
if let NodeTypeMut::Element(mut el) = container.node_type_mut() {
el.set_attribute(
("background-color", "style"),
format!("rgb({}, {}, {})", node.count * 10, 0, (x + y),),
);
}
let text_id = node.text_id.unwrap();
let mut text = rdom.get_mut(text_id).unwrap();
let type_mut = text.node_type_mut();
if let NodeTypeMut::Text(mut text) = type_mut {
*text = node.count.to_string();
}
}
}
fn handle_event(
&mut self,
rdom: &Arc<RwLock<RealDom>>,
id: NodeId,
_: &str,
_: Rc<EventData>,
_: bool,
) {
let rdom = rdom.read().unwrap();
let node = rdom.get(id).unwrap();
if let Some(parent) = node.parent() {
let child_number = parent
.child_ids()
.iter()
.position(|id| *id == node.id())
.unwrap();
if let Some(parents_parent) = parent.parent() {
let parents_child_number = parents_parent
.child_ids()
.iter()
.position(|id| *id == parent.id())
.unwrap();
self.node_states[parents_child_number][child_number].count += 1;
self.dirty.insert((parents_child_number, child_number));
}
}
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
Box::pin(async move { tokio::time::sleep(std::time::Duration::from_millis(1000)).await })
}
}
fn main() {
render(Config::new(), |rdom, _, _| {
let mut rdom = rdom.write().unwrap();
let root = rdom.root_id();
Test::create(rdom.get_mut(root).unwrap())
})
.unwrap();
}

View file

@ -1,102 +0,0 @@
use dioxus_html::HasFormData;
use dioxus_native_core::{
prelude::*,
real_dom::{NodeImmutable, NodeTypeMut},
NodeId,
};
use plasmo::{render, Config, Driver, EventData};
use std::rc::Rc;
use std::sync::{Arc, RwLock};
#[derive(Default)]
struct Counter {
count: f64,
button_id: NodeId,
}
impl Counter {
fn create(mut root: NodeMut) -> Self {
let mut myself = Self::default();
let root_id = root.id();
let rdom = root.real_dom_mut();
// create the counter
let count = myself.count;
let mut button = rdom.create_node(NodeType::Element(ElementNode {
tag: "input".to_string(),
attributes: [
// supported types: button, checkbox, textbox, password, number, range
("type".to_string().into(), "range".to_string().into()),
("display".to_string().into(), "flex".to_string().into()),
(("flex-direction", "style").into(), "row".to_string().into()),
(
("justify-content", "style").into(),
"center".to_string().into(),
),
(("align-items", "style").into(), "center".to_string().into()),
(
"value".to_string().into(),
format!("click me {count}").into(),
),
(("width", "style").into(), "50%".to_string().into()),
(("height", "style").into(), "10%".to_string().into()),
("min".to_string().into(), "20".to_string().into()),
("max".to_string().into(), "80".to_string().into()),
]
.into_iter()
.collect(),
..Default::default()
}));
button.add_event_listener("input");
myself.button_id = button.id();
rdom.get_mut(root_id).unwrap().add_child(myself.button_id);
myself
}
}
impl Driver for Counter {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>) {
// update the counter
let mut rdom = rdom.write().unwrap();
let mut node = rdom.get_mut(self.button_id).unwrap();
if let NodeTypeMut::Element(mut el) = node.node_type_mut() {
el.set_attribute(
("background-color", "style"),
format!("rgb({}, {}, {})", 255.0 - self.count * 2.0, 0, 0,),
);
};
}
fn handle_event(
&mut self,
_: &Arc<RwLock<RealDom>>,
_: NodeId,
event_type: &str,
event: Rc<EventData>,
_: bool,
) {
if event_type == "input" {
// when the button is clicked, increment the counter
if let EventData::Form(input_event) = &*event {
if let Ok(value) = input_event.value().parse::<f64>() {
self.count = value;
}
}
}
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
Box::pin(async move { tokio::time::sleep(std::time::Duration::from_millis(1000)).await })
}
}
fn main() {
render(Config::new(), |rdom, _, _| {
let mut rdom = rdom.write().unwrap();
let root = rdom.root_id();
Counter::create(rdom.get_mut(root).unwrap())
})
.unwrap();
}

View file

@ -1,58 +0,0 @@
#[derive(Clone, Copy)]
#[non_exhaustive]
pub struct Config {
pub(crate) rendering_mode: RenderingMode,
/// Controls if the terminal quit when the user presses `ctrl+c`?
/// To handle quiting on your own, use the [crate::TuiContext] root context.
pub(crate) ctrl_c_quit: bool,
/// Controls if the terminal should dislay anything, usefull for testing.
pub(crate) headless: bool,
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn with_rendering_mode(self, rendering_mode: RenderingMode) -> Self {
Self {
rendering_mode,
..self
}
}
pub fn without_ctrl_c_quit(self) -> Self {
Self {
ctrl_c_quit: false,
..self
}
}
pub fn with_headless(self) -> Self {
Self {
headless: true,
..self
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
rendering_mode: Default::default(),
ctrl_c_quit: true,
headless: false,
}
}
}
#[derive(Clone, Copy, Default)]
pub enum RenderingMode {
/// only 16 colors by accessed by name, no alpha support
BaseColors,
/// 8 bit colors, will be downsampled from rgb colors
Ansi,
/// 24 bit colors, most terminals support this
#[default]
Rgb,
}

View file

@ -1,292 +0,0 @@
use crate::prevent_default::PreventDefault;
use dioxus_native_core::{
node_ref::{AttributeMaskBuilder, NodeMaskBuilder},
prelude::*,
real_dom::NodeImmutable,
utils::{IteratorMovement, PersistantElementIter},
};
use dioxus_native_core_macro::partial_derive_state;
use once_cell::sync::Lazy;
use rustc_hash::FxHashSet;
use shipyard::Component;
use shipyard::{Get, ViewMut};
use std::{cmp::Ordering, num::NonZeroU16};
use dioxus_native_core::node_ref::NodeView;
#[derive(Component)]
pub struct Focused(pub bool);
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub(crate) enum FocusLevel {
#[default]
Unfocusable,
Focusable,
Ordered(std::num::NonZeroU16),
}
impl FocusLevel {
pub fn focusable(&self) -> bool {
match self {
FocusLevel::Unfocusable => false,
FocusLevel::Focusable => true,
FocusLevel::Ordered(_) => true,
}
}
}
impl PartialOrd for FocusLevel {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FocusLevel {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(FocusLevel::Unfocusable, FocusLevel::Unfocusable) => std::cmp::Ordering::Equal,
(FocusLevel::Unfocusable, FocusLevel::Focusable) => std::cmp::Ordering::Less,
(FocusLevel::Unfocusable, FocusLevel::Ordered(_)) => std::cmp::Ordering::Less,
(FocusLevel::Focusable, FocusLevel::Unfocusable) => std::cmp::Ordering::Greater,
(FocusLevel::Focusable, FocusLevel::Focusable) => std::cmp::Ordering::Equal,
(FocusLevel::Focusable, FocusLevel::Ordered(_)) => std::cmp::Ordering::Greater,
(FocusLevel::Ordered(_), FocusLevel::Unfocusable) => std::cmp::Ordering::Greater,
(FocusLevel::Ordered(_), FocusLevel::Focusable) => std::cmp::Ordering::Less,
(FocusLevel::Ordered(a), FocusLevel::Ordered(b)) => a.cmp(b),
}
}
}
#[derive(Clone, PartialEq, Debug, Default, Component)]
pub(crate) struct Focus {
pub level: FocusLevel,
}
#[partial_derive_state]
impl State for Focus {
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(FOCUS_ATTRIBUTES))
.with_listeners();
type ParentDependencies = ();
type ChildDependencies = ();
type NodeDependencies = ();
fn update<'a>(
&mut self,
node_view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
let new = Focus {
level: if let Some(a) = node_view
.attributes()
.and_then(|mut a| a.find(|a| a.attribute.name == "tabindex"))
{
if let Some(index) = a
.value
.as_int()
.or_else(|| a.value.as_text().and_then(|v| v.parse::<i64>().ok()))
{
match index.cmp(&0) {
Ordering::Less => FocusLevel::Unfocusable,
Ordering::Equal => FocusLevel::Focusable,
Ordering::Greater => {
FocusLevel::Ordered(NonZeroU16::new(index as u16).unwrap())
}
}
} else {
FocusLevel::Unfocusable
}
} else if node_view
.listeners()
.and_then(|mut listeners| {
listeners.any(|l| FOCUS_EVENTS.contains(&l)).then_some(())
})
.is_some()
{
FocusLevel::Focusable
} else {
FocusLevel::Unfocusable
},
};
if *self != new {
*self = new;
true
} else {
false
}
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
static FOCUS_EVENTS: Lazy<FxHashSet<&str>> =
Lazy::new(|| ["keydown", "keypress", "keyup"].into_iter().collect());
const FOCUS_ATTRIBUTES: &[&str] = &["tabindex"];
pub(crate) struct FocusState {
pub(crate) focus_iter: PersistantElementIter,
pub(crate) last_focused_id: Option<NodeId>,
pub(crate) focus_level: FocusLevel,
pub(crate) dirty: bool,
}
impl FocusState {
pub fn create(rdom: &mut RealDom) -> Self {
let focus_iter = PersistantElementIter::create(rdom);
Self {
focus_iter,
last_focused_id: Default::default(),
focus_level: Default::default(),
dirty: Default::default(),
}
}
/// Returns true if the focus has changed.
pub fn progress(&mut self, rdom: &mut RealDom, forward: bool) -> bool {
if let Some(last) = self.last_focused_id {
if rdom.get(last).unwrap().get::<PreventDefault>().map(|p| *p)
== Some(PreventDefault::KeyDown)
{
return false;
}
}
// the id that started focused to track when a loop has happened
let mut loop_marker_id = self.last_focused_id;
let focus_level = &mut self.focus_level;
let mut next_focus = None;
loop {
let new = if forward {
self.focus_iter.next(rdom)
} else {
self.focus_iter.prev(rdom)
};
let new_id = new.id();
if let IteratorMovement::Looped = new.movement() {
let mut closest_level = None;
if forward {
// find the closest focusable element after the current level
rdom.traverse_depth_first(|n| {
let node_level = n.get::<Focus>().unwrap().level;
if node_level != *focus_level
&& node_level.focusable()
&& node_level > *focus_level
{
if let Some(level) = &mut closest_level {
if node_level < *level {
*level = node_level;
}
} else {
closest_level = Some(node_level);
}
}
});
} else {
// find the closest focusable element before the current level
rdom.traverse_depth_first(|n| {
let node_level = n.get::<Focus>().unwrap().level;
if node_level != *focus_level
&& node_level.focusable()
&& node_level < *focus_level
{
if let Some(level) = &mut closest_level {
if node_level > *level {
*level = node_level;
}
} else {
closest_level = Some(node_level);
}
}
});
}
// extend the loop_marker_id to allow for another pass
loop_marker_id = None;
if let Some(level) = closest_level {
*focus_level = level;
} else if forward {
*focus_level = FocusLevel::Unfocusable;
} else {
*focus_level = FocusLevel::Focusable;
}
}
// once we have looked at all the elements exit the loop
if let Some(last) = loop_marker_id {
if new_id == last {
break;
}
} else {
loop_marker_id = Some(new_id);
}
let current_level = rdom.get(new_id).unwrap().get::<Focus>().unwrap().level;
let after_previous_focused = if forward {
current_level >= *focus_level
} else {
current_level <= *focus_level
};
if after_previous_focused && current_level.focusable() && current_level == *focus_level
{
next_focus = Some(new_id);
break;
}
}
if let Some(id) = next_focus {
let mut node = rdom.get_mut(id).unwrap();
if !node.get::<Focus>().unwrap().level.focusable() {
panic!()
}
node.insert(Focused(true));
if let Some(old) = self.last_focused_id.replace(id) {
let mut focused_borrow: ViewMut<Focused> = rdom.raw_world().borrow().unwrap();
let focused = (&mut focused_borrow).get(old).unwrap();
focused.0 = false;
}
// reset the position to the currently focused element
while self.focus_iter.next(rdom).id() != id {}
self.dirty = true;
return true;
}
false
}
pub(crate) fn set_focus(&mut self, rdom: &mut RealDom, id: NodeId) {
if let Some(old) = self.last_focused_id.replace(id) {
let mut node = rdom.get_mut(old).unwrap();
node.insert(Focused(false));
}
let mut node = rdom.get_mut(id).unwrap();
node.insert(Focused(true));
self.focus_level = node.get::<Focus>().unwrap().level;
// reset the position to the currently focused element
while self.focus_iter.next(rdom).id() != id {}
self.dirty = true;
}
pub(crate) fn clean(&mut self) -> bool {
let old = self.dirty;
self.dirty = false;
old
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,380 +0,0 @@
use std::sync::{Arc, Mutex};
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::layout_attributes::{
apply_layout_attributes_cfg, BorderWidths, LayoutConfigeration,
};
use dioxus_native_core::node::OwnedAttributeView;
use dioxus_native_core::node_ref::{AttributeMaskBuilder, NodeMaskBuilder, NodeView};
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use taffy::prelude::*;
use crate::{screen_to_layout_space, unit_to_layout_space};
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum PossiblyUninitalized<T> {
Uninitalized,
Initialized(T),
}
impl<T> PossiblyUninitalized<T> {
pub fn unwrap(self) -> T {
match self {
Self::Initialized(i) => i,
_ => panic!("uninitalized"),
}
}
}
impl<T> Default for PossiblyUninitalized<T> {
fn default() -> Self {
Self::Uninitalized
}
}
#[derive(Clone, PartialEq, Default, Debug, Component)]
pub(crate) struct TaffyLayout {
pub style: Style,
pub node: PossiblyUninitalized<Node>,
}
#[partial_derive_state]
impl State for TaffyLayout {
type ChildDependencies = (Self,);
type ParentDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(SORTED_LAYOUT_ATTRS))
.with_text();
// The layout state should be effected by the shadow dom
const TRAVERSE_SHADOW_DOM: bool = true;
fn update<'a>(
&mut self,
node_view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
ctx: &SendAnyMap,
) -> bool {
let mut changed = false;
let taffy: &Arc<Mutex<Taffy>> = ctx.get().unwrap();
let mut taffy = taffy.lock().expect("poisoned taffy");
let mut style = Style::default();
if let Some(text) = node_view.text() {
let char_len = text.chars().count();
style = Style {
size: Size {
// characters are 1 point tall
height: Dimension::Points(screen_to_layout_space(1)),
// text is as long as it is declared
width: Dimension::Points(screen_to_layout_space(char_len as u16)),
},
..Default::default()
};
if let PossiblyUninitalized::Initialized(n) = self.node {
if self.style != style {
taffy.set_style(n, style.clone()).unwrap();
}
} else {
self.node =
PossiblyUninitalized::Initialized(taffy.new_leaf(style.clone()).unwrap());
changed = true;
}
} else {
// gather up all the styles from the attribute list
if let Some(attributes) = node_view.attributes() {
for OwnedAttributeView {
attribute, value, ..
} in attributes
{
if value.as_custom().is_none() {
apply_layout_attributes_cfg(
&attribute.name,
&value.to_string(),
&mut style,
&LayoutConfigeration {
border_widths: BorderWidths {
thin: 1.0,
medium: 1.0,
thick: 1.0,
},
},
);
}
}
}
// Set all direct nodes as our children
let mut child_layout = vec![];
for (l,) in children {
child_layout.push(l.node.unwrap());
}
fn scale_dimension(d: Dimension) -> Dimension {
match d {
Dimension::Points(p) => Dimension::Points(unit_to_layout_space(p)),
Dimension::Percent(p) => Dimension::Percent(p),
Dimension::Auto => Dimension::Auto,
}
}
fn scale_length_percentage_auto(d: LengthPercentageAuto) -> LengthPercentageAuto {
match d {
LengthPercentageAuto::Points(p) => {
LengthPercentageAuto::Points(unit_to_layout_space(p))
}
LengthPercentageAuto::Percent(p) => LengthPercentageAuto::Percent(p),
LengthPercentageAuto::Auto => LengthPercentageAuto::Auto,
}
}
fn scale_length_percentage(d: LengthPercentage) -> LengthPercentage {
match d {
LengthPercentage::Points(p) => {
LengthPercentage::Points(unit_to_layout_space(p))
}
LengthPercentage::Percent(p) => LengthPercentage::Percent(p),
}
}
let scaled_style = Style {
inset: Rect {
left: scale_length_percentage_auto(style.inset.left),
right: scale_length_percentage_auto(style.inset.right),
top: scale_length_percentage_auto(style.inset.top),
bottom: scale_length_percentage_auto(style.inset.bottom),
},
margin: Rect {
left: scale_length_percentage_auto(style.margin.left),
right: scale_length_percentage_auto(style.margin.right),
top: scale_length_percentage_auto(style.margin.top),
bottom: scale_length_percentage_auto(style.margin.bottom),
},
padding: Rect {
left: scale_length_percentage(style.padding.left),
right: scale_length_percentage(style.padding.right),
top: scale_length_percentage(style.padding.top),
bottom: scale_length_percentage(style.padding.bottom),
},
border: Rect {
left: scale_length_percentage(style.border.left),
right: scale_length_percentage(style.border.right),
top: scale_length_percentage(style.border.top),
bottom: scale_length_percentage(style.border.bottom),
},
gap: Size {
width: scale_length_percentage(style.gap.width),
height: scale_length_percentage(style.gap.height),
},
flex_basis: scale_dimension(style.flex_basis),
size: Size {
width: scale_dimension(style.size.width),
height: scale_dimension(style.size.height),
},
min_size: Size {
width: scale_dimension(style.min_size.width),
height: scale_dimension(style.min_size.height),
},
max_size: Size {
width: scale_dimension(style.max_size.width),
height: scale_dimension(style.max_size.height),
},
..style.clone()
};
if let PossiblyUninitalized::Initialized(n) = self.node {
if self.style != style {
taffy.set_style(n, scaled_style).unwrap();
}
if taffy.children(n).unwrap() != child_layout {
taffy.set_children(n, &child_layout).unwrap();
}
} else {
self.node = PossiblyUninitalized::Initialized(
taffy
.new_with_children(scaled_style, &child_layout)
.unwrap(),
);
changed = true;
}
}
if self.style != style {
changed = true;
self.style = style;
}
changed
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
// these are the attributes in layout_attiributes in native-core
const SORTED_LAYOUT_ATTRS: &[&str] = &[
"align-content",
"align-items",
"align-self",
"animation",
"animation-delay",
"animation-direction",
"animation-duration",
"animation-fill-mode",
"animation-iteration-count",
"animation-name",
"animation-play-state",
"animation-timing-function",
"backface-visibility",
"border",
"border-bottom",
"border-bottom-color",
"border-bottom-left-radius",
"border-bottom-right-radius",
"border-bottom-style",
"border-bottom-width",
"border-collapse",
"border-color",
"border-image",
"border-image-outset",
"border-image-repeat",
"border-image-slice",
"border-image-source",
"border-image-width",
"border-left",
"border-left-color",
"border-left-style",
"border-left-width",
"border-radius",
"border-right",
"border-right-color",
"border-right-style",
"border-right-width",
"border-spacing",
"border-style",
"border-top",
"border-top-color",
"border-top-left-radius",
"border-top-right-radius",
"border-top-style",
"border-top-width",
"border-width",
"bottom",
"box-shadow",
"box-sizing",
"caption-side",
"clear",
"clip",
"column-count",
"column-fill",
"column-gap",
"column-rule",
"column-rule-color",
"column-rule-style",
"column-rule-width",
"column-span",
"column-width",
"columns",
"content",
"counter-increment",
"counter-reset",
"cursor",
"direction",
"ltr",
"rtl",
"display",
"empty-cells",
"flex",
"flex-basis",
"flex-direction",
"flex-flow",
"flex-grow",
"flex-shrink",
"flex-wrap",
"float",
"height",
"justify-content",
"flex-start",
"flex-end",
"center",
"space-between",
"space-around",
"space-evenly",
"left",
"letter-spacing",
"line-height",
"list-style",
"list-style-image",
"list-style-position",
"list-style-type",
"margin",
"margin-bottom",
"margin-left",
"margin-right",
"margin-top",
"max-height",
"max-width",
"min-height",
"min-width",
"opacity",
"order",
"outline",
"outline-color",
"outline-offset",
"outline-style",
"outline-width",
"overflow",
"overflow-x",
"overflow-y",
"padding",
"padding-bottom",
"padding-left",
"padding-right",
"padding-top",
"page-break-after",
"page-break-before",
"page-break-inside",
"perspective",
"perspective-origin",
"position",
"static",
"relative",
"fixed",
"absolute",
"sticky",
"pointer-events",
"quotes",
"resize",
"right",
"tab-size",
"table-layout",
"top",
"transform",
"transform-origin",
"transform-style",
"transition",
"transition-delay",
"transition-duration",
"transition-property",
"transition-timing-function",
"vertical-align",
"visibility",
"white-space",
"width",
"word-break",
"word-spacing",
"word-wrap",
"z-index",
];

View file

@ -1,396 +0,0 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
use crate::focus::Focus;
use anyhow::Result;
use crossterm::{
cursor::{MoveTo, RestorePosition, SavePosition, Show},
event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use dioxus_native_core::{prelude::*, tree::TreeRef};
use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, SendAnyMap};
use focus::FocusState;
use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt};
use futures_channel::mpsc::unbounded;
use layout::TaffyLayout;
use prevent_default::PreventDefault;
use ratatui::{backend::CrosstermBackend, Terminal};
use std::{io, time::Duration};
use std::{
pin::Pin,
sync::{Arc, Mutex},
};
use std::{rc::Rc, sync::RwLock};
use style_attributes::StyleModifier;
pub use taffy::{geometry::Point, prelude::*};
use tokio::select;
use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject};
mod config;
mod focus;
mod hooks;
mod layout;
mod prevent_default;
pub mod query;
mod render;
mod style;
mod style_attributes;
mod widget;
mod widgets;
pub use config::*;
pub use hooks::*;
pub use query::Query;
// the layout space has a multiplier of 10 to minimize rounding errors
pub(crate) fn screen_to_layout_space(screen: u16) -> f32 {
screen as f32 * 10.0
}
pub(crate) fn unit_to_layout_space(screen: f32) -> f32 {
screen * 10.0
}
pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
layout / 10.0
}
#[derive(Clone)]
pub struct TuiContext {
tx: UnboundedSender<InputEvent>,
}
impl TuiContext {
pub fn new(tx: UnboundedSender<InputEvent>) -> Self {
Self { tx }
}
pub fn quit(&self) {
// panic!("ack")
self.tx.unbounded_send(InputEvent::Close).unwrap();
}
pub fn inject_event(&self, event: crossterm::event::Event) {
self.tx
.unbounded_send(InputEvent::UserInput(event))
.unwrap();
}
}
pub fn render<R: Driver>(
cfg: Config,
create_renderer: impl FnOnce(
&Arc<RwLock<RealDom>>,
&Arc<Mutex<Taffy>>,
UnboundedSender<InputEvent>,
) -> R,
) -> Result<()> {
let mut rdom = RealDom::new([
TaffyLayout::to_type_erased(),
Focus::to_type_erased(),
StyleModifier::to_type_erased(),
PreventDefault::to_type_erased(),
]);
// Setup input handling
// The event channel for fully resolved events
let (event_tx, mut event_reciever) = unbounded();
// The event channel for raw terminal events
let (raw_event_tx, mut raw_event_reciever) = unbounded();
let event_tx_clone = raw_event_tx.clone();
if !cfg.headless {
std::thread::spawn(move || {
// Timeout after 10ms when waiting for events
let tick_rate = Duration::from_millis(10);
loop {
if crossterm::event::poll(tick_rate).unwrap() {
let evt = crossterm::event::read().unwrap();
if raw_event_tx
.unbounded_send(InputEvent::UserInput(evt))
.is_err()
{
break;
}
}
}
});
}
register_widgets(&mut rdom, event_tx);
let (handler, mut register_event) = RinkInputHandler::create(&mut rdom);
let rdom = Arc::new(RwLock::new(rdom));
let taffy = Arc::new(Mutex::new(Taffy::new()));
let mut renderer = create_renderer(&rdom, &taffy, event_tx_clone);
// insert the query engine into the rdom
let query_engine = Query::new(rdom.clone(), taffy.clone());
{
let mut rdom = rdom.write().unwrap();
rdom.raw_world_mut().add_unique(query_engine);
}
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
{
renderer.update(&rdom);
let mut any_map = SendAnyMap::new();
any_map.insert(taffy.clone());
let mut rdom = rdom.write().unwrap();
let _ = rdom.update_state(any_map);
}
let mut terminal = (!cfg.headless).then(|| {
enable_raw_mode().unwrap();
let mut stdout = std::io::stdout();
execute!(
stdout,
EnterAlternateScreen,
EnableMouseCapture,
MoveTo(0, 1000)
)
.unwrap();
let backend = CrosstermBackend::new(io::stdout());
Terminal::new(backend).unwrap()
});
if let Some(terminal) = &mut terminal {
terminal.clear().unwrap();
}
let mut to_rerender = FxDashSet::default();
to_rerender.insert(rdom.read().unwrap().root_id());
let mut updated = true;
loop {
/*
-> render the nodes in the right place with tui/crossterm
-> wait for changes
-> resolve events
-> lazily update the layout and style based on nodes changed
use simd to compare lines for diffing?
todo: lazy re-rendering
*/
if !to_rerender.is_empty() || updated {
updated = false;
fn resize(dims: ratatui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) {
let width = screen_to_layout_space(dims.width);
let height = screen_to_layout_space(dims.height);
let root_node = rdom
.get(rdom.root_id())
.unwrap()
.get::<TaffyLayout>()
.unwrap()
.node
.unwrap();
// the root node fills the entire area
let mut style = taffy.style(root_node).unwrap().clone();
let new_size = Size {
width: Dimension::Points(width),
height: Dimension::Points(height),
};
if style.size != new_size {
style.size = new_size;
taffy.set_style(root_node, style).unwrap();
}
let size = Size {
width: AvailableSpace::Definite(width),
height: AvailableSpace::Definite(height),
};
taffy.compute_layout(root_node, size).unwrap();
}
if let Some(terminal) = &mut terminal {
execute!(terminal.backend_mut(), SavePosition).unwrap();
terminal.draw(|frame| {
let rdom = rdom.write().unwrap();
let mut taffy = taffy.lock().expect("taffy lock poisoned");
// size is guaranteed to not change when rendering
resize(frame.size(), &mut taffy, &rdom);
let root = rdom.get(rdom.root_id()).unwrap();
render::render_vnode(frame, &taffy, root, cfg, Point::ZERO);
})?;
execute!(terminal.backend_mut(), RestorePosition, Show).unwrap();
} else {
let rdom = rdom.read().unwrap();
resize(
ratatui::layout::Rect {
x: 0,
y: 0,
width: 1000,
height: 1000,
},
&mut taffy.lock().expect("taffy lock poisoned"),
&rdom,
);
}
}
let mut event_recieved = None;
{
let wait = renderer.poll_async();
pin_mut!(wait);
select! {
_ = wait => {
},
evt = raw_event_reciever.next() => {
match evt.as_ref().unwrap() {
InputEvent::UserInput(event) => match event {
TermEvent::Key(key) => {
if matches!(key.code, KeyCode::Char('C' | 'c'))
&& key.modifiers.contains(KeyModifiers::CONTROL)
&& cfg.ctrl_c_quit
{
break;
}
}
TermEvent::Resize(_, _) => updated = true,
_ => {}
},
InputEvent::Close => break,
};
if let InputEvent::UserInput(evt) = evt.unwrap() {
register_event(evt);
}
},
Some(evt) = event_reciever.next() => {
event_recieved = Some(evt);
}
}
}
{
if let Some(evt) = event_recieved {
renderer.handle_event(
&rdom,
evt.id,
evt.name,
Rc::new(evt.data),
evt.bubbles,
);
}
{
let evts = handler.get_events(
&taffy.lock().expect("taffy lock poisoned"),
&mut rdom.write().unwrap(),
);
updated |= handler.state().focus_state.clean();
for e in evts {
bubble_event_to_widgets(&mut rdom.write().unwrap(), &e);
renderer.handle_event(&rdom, e.id, e.name, Rc::new(e.data), e.bubbles);
}
}
// updates the dom's nodes
renderer.update(&rdom);
// update the style and layout
let mut rdom = rdom.write().unwrap();
let mut any_map = SendAnyMap::new();
any_map.insert(taffy.clone());
let (new_to_rerender, dirty) = rdom.update_state(any_map);
to_rerender = new_to_rerender;
let text_mask = NodeMaskBuilder::new().with_text().build();
for (id, mask) in dirty {
if mask.overlaps(&text_mask) {
to_rerender.insert(id);
}
}
}
}
if let Some(terminal) = &mut terminal {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
}
Ok(())
})
}
#[derive(Debug)]
pub enum InputEvent {
UserInput(TermEvent),
Close,
}
pub trait Driver {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>);
fn handle_event(
&mut self,
rdom: &Arc<RwLock<RealDom>>,
id: NodeId,
event: &str,
value: Rc<EventData>,
bubbles: bool,
);
fn poll_async(&mut self) -> Pin<Box<dyn Future<Output = ()> + '_>>;
}
/// Before sending the event to drivers, we need to bubble it up the tree to any widgets that are listening
fn bubble_event_to_widgets(rdom: &mut RealDom, event: &Event) {
let id = event.id;
let mut node = Some(id);
while let Some(node_id) = node {
let parent_id = {
let tree = rdom.tree_ref();
tree.parent_id_advanced(node_id, true)
};
{
// println!("@ bubbling event to node {:?}", node_id);
let mut node_mut = rdom.get_mut(node_id).unwrap();
if let Some(mut widget) = node_mut
.get_mut::<RinkWidgetTraitObject>()
.map(|w| w.clone())
{
widget.handle_event(event, node_mut)
}
}
if !event.bubbles {
// println!("event does not bubble");
break;
}
node = parent_id;
}
}
pub(crate) fn get_abs_layout(node: NodeRef, taffy: &Taffy) -> Layout {
let mut node_layout = *taffy
.layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
.unwrap();
let mut current = node;
let dom = node.real_dom();
let tree = dom.tree_ref();
while let Some(parent) = tree.parent_id_advanced(current.id(), true) {
let parent = dom.get(parent).unwrap();
current = parent;
let parent_layout = taffy
.layout(parent.get::<TaffyLayout>().unwrap().node.unwrap())
.unwrap();
node_layout.location.x += parent_layout.location.x;
node_layout.location.y += parent_layout.location.y;
}
node_layout
}

View file

@ -1,86 +0,0 @@
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
#[derive(PartialEq, Debug, Clone, Copy, Component, Default)]
pub(crate) enum PreventDefault {
Focus,
KeyPress,
KeyRelease,
KeyDown,
KeyUp,
MouseDown,
Click,
MouseEnter,
MouseLeave,
MouseOut,
#[default]
Unknown,
MouseOver,
ContextMenu,
Wheel,
MouseUp,
}
#[partial_derive_state]
impl State for PreventDefault {
type ParentDependencies = ();
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: dioxus_native_core::node_ref::NodeMaskBuilder<'static> =
dioxus_native_core::node_ref::NodeMaskBuilder::new()
.with_attrs(dioxus_native_core::node_ref::AttributeMaskBuilder::Some(&[
"dioxus-prevent-default",
]))
.with_listeners();
fn update<'a>(
&mut self,
node_view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
let new = match node_view.attributes().and_then(|mut attrs| {
attrs
.find(|a| a.attribute.name == "dioxus-prevent-default")
.and_then(|a| a.value.as_text())
}) {
Some("onfocus") => PreventDefault::Focus,
Some("onkeypress") => PreventDefault::KeyPress,
Some("onkeyrelease") => PreventDefault::KeyRelease,
Some("onkeydown") => PreventDefault::KeyDown,
Some("onkeyup") => PreventDefault::KeyUp,
Some("onclick") => PreventDefault::Click,
Some("onmousedown") => PreventDefault::MouseDown,
Some("onmouseup") => PreventDefault::MouseUp,
Some("onmouseenter") => PreventDefault::MouseEnter,
Some("onmouseover") => PreventDefault::MouseOver,
Some("onmouseleave") => PreventDefault::MouseLeave,
Some("onmouseout") => PreventDefault::MouseOut,
Some("onwheel") => PreventDefault::Wheel,
Some("oncontextmenu") => PreventDefault::ContextMenu,
_ => return false,
};
if new == *self {
false
} else {
*self = new;
true
}
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}

View file

@ -1,109 +0,0 @@
use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard};
use dioxus_native_core::prelude::*;
use shipyard::Unique;
use taffy::{
geometry::Point,
prelude::{Layout, Size},
Taffy,
};
use crate::{get_abs_layout, layout_to_screen_space};
/// Allows querying the layout of nodes after rendering. It will only provide a correct value after a node is rendered.
/// Provided as a root context for all tui applictions.
/// # Example
/// ```rust, ignore
/// use dioxus::prelude::*;
/// use dioxus_tui::query::Query;
/// use dioxus_tui::Size;
///
/// fn main() {
/// dioxus_tui::launch(app);
/// }
///
/// fn app() -> Element {
/// let hue = use_signal(|| 0.0);
/// let brightness = use_signal(|| 0.0);
/// let tui_query: Query = cx.consume_context().unwrap();
/// rsx! {
/// div{
/// width: "100%",
/// background_color: "hsl({hue}, 70%, {brightness}%)",
/// onmousemove: move |evt| {
/// let node = tui_query.get(cx.root_node().mounted_id());
/// let Size{width, height} = node.size().unwrap();
/// hue.set((evt.data.offset_x as f32/width as f32)*255.0);
/// brightness.set((evt.data.offset_y as f32/height as f32)*100.0);
/// },
/// "hsl({hue}, 70%, {brightness}%)",
/// }
/// })
/// }
/// ```
#[derive(Clone, Unique)]
pub struct Query {
pub(crate) rdom: Arc<RwLock<RealDom>>,
pub(crate) stretch: Arc<Mutex<Taffy>>,
}
impl Query {
pub fn new(rdom: Arc<RwLock<RealDom>>, stretch: Arc<Mutex<Taffy>>) -> Self {
Self { rdom, stretch }
}
pub fn get(&self, id: NodeId) -> ElementRef {
let rdom = self.rdom.read();
let stretch = self.stretch.lock();
ElementRef::new(
rdom.expect("rdom lock poisoned"),
stretch.expect("taffy lock poisoned"),
id,
)
}
}
pub struct ElementRef<'a> {
inner: RwLockReadGuard<'a, RealDom>,
stretch: MutexGuard<'a, Taffy>,
id: NodeId,
}
impl<'a> ElementRef<'a> {
pub(crate) fn new(
inner: RwLockReadGuard<'a, RealDom>,
stretch: MutexGuard<'a, Taffy>,
id: NodeId,
) -> Self {
Self { inner, stretch, id }
}
pub fn size(&self) -> Option<Size<u32>> {
self.layout().map(|l| l.size.map(|v| v.round() as u32))
}
pub fn pos(&self) -> Option<Point<u32>> {
self.layout().map(|l| Point {
x: l.location.x.round() as u32,
y: l.location.y.round() as u32,
})
}
pub fn layout(&self) -> Option<Layout> {
get_layout(self.inner.get(self.id).unwrap(), &self.stretch)
}
}
pub(crate) fn get_layout(node: NodeRef, stretch: &Taffy) -> Option<Layout> {
let layout = get_abs_layout(node, stretch);
let pos = layout.location;
Some(Layout {
order: layout.order,
size: layout.size.map(layout_to_screen_space),
location: Point {
x: layout_to_screen_space(pos.x).round(),
y: layout_to_screen_space(pos.y).round(),
},
})
}

View file

@ -1,429 +0,0 @@
use dioxus_native_core::{prelude::*, tree::TreeRef};
use ratatui::{layout::Rect, style::Color};
use taffy::{
geometry::Point,
prelude::{Dimension, Layout, Size},
Taffy,
};
use crate::{
focus::Focused,
layout::TaffyLayout,
layout_to_screen_space,
style::{RinkColor, RinkStyle},
style_attributes::{BorderEdge, BorderStyle, StyleModifier},
widget::{RinkBuffer, RinkCell, RinkWidget, WidgetWithContext},
Config,
};
const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
pub(crate) fn render_vnode(
frame: &mut ratatui::Frame,
layout: &Taffy,
node: NodeRef,
cfg: Config,
parent_location: Point<f32>,
) {
if let NodeType::Placeholder = &*node.node_type() {
return;
}
let Layout {
mut location, size, ..
} = layout
.layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
.unwrap();
location.x += parent_location.x;
location.y += parent_location.y;
let Point { x: fx, y: fy } = location;
let x = layout_to_screen_space(fx).round() as u16;
let y = layout_to_screen_space(fy).round() as u16;
let Size { width, height } = *size;
let width = layout_to_screen_space(fx + width).round() as u16 - x;
let height = layout_to_screen_space(fy + height).round() as u16 - y;
match &*node.node_type() {
NodeType::Text(text) => {
#[derive(Default)]
struct Label<'a> {
text: &'a str,
style: RinkStyle,
}
impl<'a> RinkWidget for Label<'a> {
fn render(self, area: Rect, mut buf: RinkBuffer) {
for (i, c) in self.text.char_indices() {
let mut new_cell = RinkCell::default();
new_cell.set_style(self.style);
new_cell.symbol = c.to_string();
buf.set(area.left() + i as u16, area.top(), new_cell);
}
}
}
let label = Label {
text: &text.text,
style: node.get::<StyleModifier>().unwrap().core,
};
let area = Rect::new(x, y, width, height);
// the renderer will panic if a node is rendered out of range even if the size is zero
if area.width > 0 && area.height > 0 {
frame.render_widget(WidgetWithContext::new(label, cfg), area);
}
}
NodeType::Element { .. } => {
let area = Rect::new(x, y, width, height);
// the renderer will panic if a node is rendered out of range even if the size is zero
if area.width > 0 && area.height > 0 {
frame.render_widget(WidgetWithContext::new(node, cfg), area);
}
let node_id = node.id();
let rdom = node.real_dom();
for child_id in rdom.tree_ref().children_ids_advanced(node_id, true) {
let c = rdom.get(child_id).unwrap();
render_vnode(frame, layout, c, cfg, location);
}
}
NodeType::Placeholder => unreachable!(),
}
}
impl RinkWidget for NodeRef<'_> {
fn render(self, area: Rect, mut buf: RinkBuffer<'_>) {
use ratatui::symbols::line::*;
enum Direction {
Left,
Right,
Up,
Down,
}
fn draw(
buf: &mut RinkBuffer,
points_history: [[i32; 2]; 3],
symbols: &Set,
pos: [u16; 2],
color: &Option<RinkColor>,
) {
let [before, current, after] = points_history;
let start_dir = match [before[0] - current[0], before[1] - current[1]] {
[1, 0] => Direction::Right,
[-1, 0] => Direction::Left,
[0, 1] => Direction::Down,
[0, -1] => Direction::Up,
[a, b] => {
panic!("draw({before:?} {current:?} {after:?}) {a}, {b} no cell adjacent")
}
};
let end_dir = match [after[0] - current[0], after[1] - current[1]] {
[1, 0] => Direction::Right,
[-1, 0] => Direction::Left,
[0, 1] => Direction::Down,
[0, -1] => Direction::Up,
[a, b] => {
panic!("draw({before:?} {current:?} {after:?}) {a}, {b} no cell adjacent")
}
};
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = *c;
}
new_cell.symbol = match [start_dir, end_dir] {
[Direction::Down, Direction::Up] => symbols.vertical,
[Direction::Down, Direction::Right] => symbols.top_left,
[Direction::Down, Direction::Left] => symbols.top_right,
[Direction::Up, Direction::Down] => symbols.vertical,
[Direction::Up, Direction::Right] => symbols.bottom_left,
[Direction::Up, Direction::Left] => symbols.bottom_right,
[Direction::Right, Direction::Left] => symbols.horizontal,
[Direction::Right, Direction::Up] => symbols.bottom_left,
[Direction::Right, Direction::Down] => symbols.top_left,
[Direction::Left, Direction::Up] => symbols.bottom_right,
[Direction::Left, Direction::Right] => symbols.horizontal,
[Direction::Left, Direction::Down] => symbols.top_right,
_ => panic!("{before:?} {current:?} {after:?} cannont connect cell to itself"),
}
.to_string();
buf.set(
(current[0] + pos[0] as i32) as u16,
(current[1] + pos[1] as i32) as u16,
new_cell,
);
}
fn draw_arc(
pos: [u16; 2],
starting_angle: f32,
arc_angle: f32,
radius: f32,
symbols: &Set,
buf: &mut RinkBuffer,
color: &Option<RinkColor>,
) {
if radius < 0.0 {
return;
}
let num_points = (radius * arc_angle) as i32;
let starting_point = [
(starting_angle.cos() * (radius * RADIUS_MULTIPLIER[0])) as i32,
(starting_angle.sin() * (radius * RADIUS_MULTIPLIER[1])) as i32,
];
// keep track of the last 3 point to allow filling diagonals
let mut points_history = [
[0, 0],
{
// change the x or y value based on which one is changing quicker
let ddx = -starting_angle.sin();
let ddy = starting_angle.cos();
if ddx.abs() > ddy.abs() {
[starting_point[0] - ddx.signum() as i32, starting_point[1]]
} else {
[starting_point[0], starting_point[1] - ddy.signum() as i32]
}
},
starting_point,
];
for i in 1..=num_points {
let angle = (i as f32 / num_points as f32) * arc_angle + starting_angle;
let x = angle.cos() * radius * RADIUS_MULTIPLIER[0];
let y = angle.sin() * radius * RADIUS_MULTIPLIER[1];
let new = [x as i32, y as i32];
if new != points_history[2] {
points_history = [points_history[1], points_history[2], new];
let dx = points_history[2][0] - points_history[1][0];
let dy = points_history[2][1] - points_history[1][1];
// fill diagonals
if dx != 0 && dy != 0 {
let connecting_point = match [dx, dy] {
[1, 1] => [points_history[1][0] + 1, points_history[1][1]],
[1, -1] => [points_history[1][0], points_history[1][1] - 1],
[-1, 1] => [points_history[1][0], points_history[1][1] + 1],
[-1, -1] => [points_history[1][0] - 1, points_history[1][1]],
_ => unreachable!(),
};
draw(
buf,
[points_history[0], points_history[1], connecting_point],
symbols,
pos,
color,
);
points_history = [points_history[1], connecting_point, points_history[2]];
}
draw(buf, points_history, symbols, pos, color);
}
}
points_history = [points_history[1], points_history[2], {
// change the x or y value based on which one is changing quicker
let ddx = -(starting_angle + arc_angle).sin();
let ddy = (starting_angle + arc_angle).cos();
if ddx.abs() > ddy.abs() {
[
points_history[2][0] + ddx.signum() as i32,
points_history[2][1],
]
} else {
[
points_history[2][0],
points_history[2][1] + ddy.signum() as i32,
]
}
}];
draw(buf, points_history, symbols, pos, color);
}
fn get_radius(border: &BorderEdge, area: Rect) -> f32 {
match border.style {
BorderStyle::Hidden => 0.0,
BorderStyle::None => 0.0,
_ => match border.radius {
Dimension::Percent(p) => p * area.width as f32 / 100.0,
Dimension::Points(p) => p,
_ => unreachable!(),
}
.abs()
.min((area.width as f32 / RADIUS_MULTIPLIER[0]) / 2.0)
.min((area.height as f32 / RADIUS_MULTIPLIER[1]) / 2.0),
}
}
if area.area() == 0 {
return;
}
// todo: only render inside borders
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
let mut new_cell = RinkCell::default();
if let Some(c) = self.get::<StyleModifier>().unwrap().core.bg {
new_cell.bg = c;
}
if let Some(focused) = self.get::<Focused>() {
if focused.0 {
new_cell.bg.alpha = 100;
new_cell.bg.color = new_cell.bg.blend(Color::White);
}
}
buf.set(x, y, new_cell);
}
}
let style = self.get::<StyleModifier>().unwrap();
let borders = &style.modifier.borders;
let last_edge = &borders.left;
let current_edge = &borders.top;
if let Some(symbols) = current_edge.style.symbol_set() {
// the radius for the curve between this line and the next
let r = get_radius(current_edge, area);
let radius = [
(r * RADIUS_MULTIPLIER[0]) as u16,
(r * RADIUS_MULTIPLIER[1]) as u16,
];
// the radius for the curve between this line and the last
let last_r = get_radius(last_edge, area);
let last_radius = [
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(style.core.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
}
for x in (area.left() + last_radius[0] + 1)..(area.right() - radius[0]) {
new_cell.symbol = symbols.horizontal.to_string();
buf.set(x, area.top(), new_cell.clone());
}
draw_arc(
[area.right() - radius[0] - 1, area.top() + radius[1]],
std::f32::consts::FRAC_PI_2 * 3.0,
std::f32::consts::FRAC_PI_2,
r,
&symbols,
&mut buf,
&color,
);
}
let last_edge = &borders.top;
let current_edge = &borders.right;
if let Some(symbols) = current_edge.style.symbol_set() {
// the radius for the curve between this line and the next
let r = get_radius(current_edge, area);
let radius = [
(r * RADIUS_MULTIPLIER[0]) as u16,
(r * RADIUS_MULTIPLIER[1]) as u16,
];
// the radius for the curve between this line and the last
let last_r = get_radius(last_edge, area);
let last_radius = [
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(style.core.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
}
for y in (area.top() + last_radius[1] + 1)..(area.bottom() - radius[1]) {
new_cell.symbol = symbols.vertical.to_string();
buf.set(area.right() - 1, y, new_cell.clone());
}
draw_arc(
[area.right() - radius[0] - 1, area.bottom() - radius[1] - 1],
0.0,
std::f32::consts::FRAC_PI_2,
r,
&symbols,
&mut buf,
&color,
);
}
let last_edge = &borders.right;
let current_edge = &borders.bottom;
if let Some(symbols) = current_edge.style.symbol_set() {
// the radius for the curve between this line and the next
let r = get_radius(current_edge, area);
let radius = [
(r * RADIUS_MULTIPLIER[0]) as u16,
(r * RADIUS_MULTIPLIER[1]) as u16,
];
// the radius for the curve between this line and the last
let last_r = get_radius(last_edge, area);
let last_radius = [
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(style.core.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
}
for x in (area.left() + radius[0])..(area.right() - last_radius[0] - 1) {
new_cell.symbol = symbols.horizontal.to_string();
buf.set(x, area.bottom() - 1, new_cell.clone());
}
draw_arc(
[area.left() + radius[0], area.bottom() - radius[1] - 1],
std::f32::consts::FRAC_PI_2,
std::f32::consts::FRAC_PI_2,
r,
&symbols,
&mut buf,
&color,
);
}
let last_edge = &borders.bottom;
let current_edge = &borders.left;
if let Some(symbols) = current_edge.style.symbol_set() {
// the radius for the curve between this line and the next
let r = get_radius(current_edge, area);
let radius = [
(r * RADIUS_MULTIPLIER[0]) as u16,
(r * RADIUS_MULTIPLIER[1]) as u16,
];
// the radius for the curve between this line and the last
let last_r = get_radius(last_edge, area);
let last_radius = [
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(style.core.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
}
for y in (area.top() + radius[1])..(area.bottom() - last_radius[1] - 1) {
new_cell.symbol = symbols.vertical.to_string();
buf.set(area.left(), y, new_cell.clone());
}
draw_arc(
[area.left() + radius[0], area.top() + radius[1]],
std::f32::consts::PI,
std::f32::consts::FRAC_PI_2,
r,
&symbols,
&mut buf,
&color,
);
}
}
}

View file

@ -1,452 +0,0 @@
use std::{num::ParseFloatError, str::FromStr};
use ratatui::style::{Color, Modifier, Style};
use crate::RenderingMode;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RinkColor {
pub color: Color,
pub alpha: u8,
}
impl Default for RinkColor {
fn default() -> Self {
Self {
color: Color::Black,
alpha: 0,
}
}
}
impl RinkColor {
pub fn blend(self, other: Color) -> Color {
if self.color == Color::Reset {
Color::Reset
} else if self.alpha == 0 {
other
} else {
let [sr, sg, sb] = to_rgb(self.color).map(|e| e as u16);
let [or, og, ob] = to_rgb(other).map(|e| e as u16);
let sa = self.alpha as u16;
let rsa = 255 - sa;
Color::Rgb(
((sr * sa + or * rsa) / 255) as u8,
((sg * sa + og * rsa) / 255) as u8,
((sb * sa + ob * rsa) / 255) as u8,
)
}
}
}
fn parse_value(
v: &str,
current_max_output: f32,
required_max_output: f32,
) -> Result<f32, ParseFloatError> {
if let Some(stripped) = v.strip_suffix('%') {
Ok((stripped.trim().parse::<f32>()? / 100.0) * required_max_output)
} else {
Ok((v.trim().parse::<f32>()? / current_max_output) * required_max_output)
}
}
pub struct ParseColorError;
fn parse_hex(color: &str) -> Result<Color, ParseColorError> {
let mut values = [0, 0, 0];
let mut color_ok = true;
for i in 0..values.len() {
if let Ok(v) = u8::from_str_radix(&color[(1 + 2 * i)..(1 + 2 * (i + 1))], 16) {
values[i] = v;
} else {
color_ok = false;
}
}
if color_ok {
Ok(Color::Rgb(values[0], values[1], values[2]))
} else {
Err(ParseColorError)
}
}
fn parse_rgb(color: &str) -> Result<Color, ParseColorError> {
let mut values = [0, 0, 0];
let mut color_ok = true;
for (v, i) in color.split(',').zip(0..values.len()) {
if let Ok(v) = parse_value(v.trim(), 255.0, 255.0) {
values[i] = v as u8;
} else {
color_ok = false;
}
}
if color_ok {
Ok(Color::Rgb(values[0], values[1], values[2]))
} else {
Err(ParseColorError)
}
}
fn parse_hsl(color: &str) -> Result<Color, ParseColorError> {
let mut values = [0.0, 0.0, 0.0];
let mut color_ok = true;
for (v, i) in color.split(',').zip(0..values.len()) {
if let Ok(v) = parse_value(v.trim(), if i == 0 { 360.0 } else { 100.0 }, 1.0) {
values[i] = v;
} else {
color_ok = false;
}
}
if color_ok {
let [h, s, l] = values;
let rgb = if s == 0.0 {
[l as u8; 3]
} else {
fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
if t < 1.0 / 6.0 {
p + (q - p) * 6.0 * t
} else if t < 1.0 / 2.0 {
q
} else if t < 2.0 / 3.0 {
p + (q - p) * (2.0 / 3.0 - t) * 6.0
} else {
p
}
}
let q = if l < 0.5 {
l * (1.0 + s)
} else {
l + s - l * s
};
let p = 2.0 * l - q;
[
(hue_to_rgb(p, q, h + 1.0 / 3.0) * 255.0) as u8,
(hue_to_rgb(p, q, h) * 255.0) as u8,
(hue_to_rgb(p, q, h - 1.0 / 3.0) * 255.0) as u8,
]
};
Ok(Color::Rgb(rgb[0], rgb[1], rgb[2]))
} else {
Err(ParseColorError)
}
}
impl FromStr for RinkColor {
type Err = ParseColorError;
fn from_str(color: &str) -> Result<Self, Self::Err> {
match color {
"red" => Ok(RinkColor {
color: Color::Red,
alpha: 255,
}),
"black" => Ok(RinkColor {
color: Color::Black,
alpha: 255,
}),
"green" => Ok(RinkColor {
color: Color::Green,
alpha: 255,
}),
"yellow" => Ok(RinkColor {
color: Color::Yellow,
alpha: 255,
}),
"blue" => Ok(RinkColor {
color: Color::Blue,
alpha: 255,
}),
"magenta" => Ok(RinkColor {
color: Color::Magenta,
alpha: 255,
}),
"cyan" => Ok(RinkColor {
color: Color::Cyan,
alpha: 255,
}),
"gray" => Ok(RinkColor {
color: Color::Gray,
alpha: 255,
}),
"darkgray" => Ok(RinkColor {
color: Color::DarkGray,
alpha: 255,
}),
// light red does not exist
"orangered" => Ok(RinkColor {
color: Color::LightRed,
alpha: 255,
}),
"lightgreen" => Ok(RinkColor {
color: Color::LightGreen,
alpha: 255,
}),
"lightyellow" => Ok(RinkColor {
color: Color::LightYellow,
alpha: 255,
}),
"lightblue" => Ok(RinkColor {
color: Color::LightBlue,
alpha: 255,
}),
// light magenta does not exist
"orchid" => Ok(RinkColor {
color: Color::LightMagenta,
alpha: 255,
}),
"lightcyan" => Ok(RinkColor {
color: Color::LightCyan,
alpha: 255,
}),
"white" => Ok(RinkColor {
color: Color::White,
alpha: 255,
}),
_ => {
if color.len() == 7 && color.starts_with('#') {
parse_hex(color).map(|c| RinkColor {
color: c,
alpha: 255,
})
} else if let Some(stripped) = color.strip_prefix("rgb(") {
let color_values = stripped.trim_end_matches(')');
if color.matches(',').count() == 3 {
let (alpha, rgb_values) =
color_values.rsplit_once(',').ok_or(ParseColorError)?;
if let Ok(a) = alpha.parse() {
parse_rgb(rgb_values).map(|c| RinkColor { color: c, alpha: a })
} else {
Err(ParseColorError)
}
} else {
parse_rgb(color_values).map(|c| RinkColor {
color: c,
alpha: 255,
})
}
} else if let Some(stripped) = color.strip_prefix("rgba(") {
let color_values = stripped.trim_end_matches(')');
if color.matches(',').count() == 3 {
let (rgb_values, alpha) =
color_values.rsplit_once(',').ok_or(ParseColorError)?;
if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
parse_rgb(rgb_values).map(|c| RinkColor {
color: c,
alpha: (a * 255.0) as u8,
})
} else {
Err(ParseColorError)
}
} else {
parse_rgb(color_values).map(|c| RinkColor {
color: c,
alpha: 255,
})
}
} else if let Some(stripped) = color.strip_prefix("hsl(") {
let color_values = stripped.trim_end_matches(')');
if color.matches(',').count() == 3 {
let (rgb_values, alpha) =
color_values.rsplit_once(',').ok_or(ParseColorError)?;
if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
parse_hsl(rgb_values).map(|c| RinkColor {
color: c,
alpha: (a * 255.0) as u8,
})
} else {
Err(ParseColorError)
}
} else {
parse_hsl(color_values).map(|c| RinkColor {
color: c,
alpha: 255,
})
}
} else if let Some(stripped) = color.strip_prefix("hsla(") {
let color_values = stripped.trim_end_matches(')');
if color.matches(',').count() == 3 {
let (rgb_values, alpha) =
color_values.rsplit_once(',').ok_or(ParseColorError)?;
if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
parse_hsl(rgb_values).map(|c| RinkColor {
color: c,
alpha: (a * 255.0) as u8,
})
} else {
Err(ParseColorError)
}
} else {
parse_hsl(color_values).map(|c| RinkColor {
color: c,
alpha: 255,
})
}
} else {
Err(ParseColorError)
}
}
}
}
}
fn to_rgb(c: Color) -> [u8; 3] {
match c {
Color::Black => [0, 0, 0],
Color::Red => [255, 0, 0],
Color::Green => [0, 128, 0],
Color::Yellow => [255, 255, 0],
Color::Blue => [0, 0, 255],
Color::Magenta => [255, 0, 255],
Color::Cyan => [0, 255, 255],
Color::Gray => [128, 128, 128],
Color::DarkGray => [169, 169, 169],
Color::LightRed => [255, 69, 0],
Color::LightGreen => [144, 238, 144],
Color::LightYellow => [255, 255, 224],
Color::LightBlue => [173, 216, 230],
Color::LightMagenta => [218, 112, 214],
Color::LightCyan => [224, 255, 255],
Color::White => [255, 255, 255],
Color::Rgb(r, g, b) => [r, g, b],
Color::Indexed(idx) => match idx {
16..=231 => {
let v = idx - 16;
// add 3 to round up
let r = ((v as u16 / 36) * 255 + 3) / 5;
let g = (((v as u16 % 36) / 6) * 255 + 3) / 5;
let b = ((v as u16 % 6) * 255 + 3) / 5;
[r as u8, g as u8, b as u8]
}
232..=255 => {
let l = (idx - 232) / 24;
[l; 3]
}
// plasmo will never generate these colors, but they might be on the screen from another program
_ => [0, 0, 0],
},
Color::Reset => [0, 0, 0],
}
}
pub fn convert(mode: RenderingMode, c: Color) -> Color {
if let Color::Reset = c {
c
} else {
match mode {
crate::RenderingMode::BaseColors => match c {
Color::Rgb(_, _, _) => panic!("cannot convert rgb color to base color"),
Color::Indexed(_) => panic!("cannot convert Ansi color to base color"),
_ => c,
},
crate::RenderingMode::Rgb => {
let rgb = to_rgb(c);
Color::Rgb(rgb[0], rgb[1], rgb[2])
}
crate::RenderingMode::Ansi => match c {
Color::Indexed(_) => c,
_ => {
let rgb = to_rgb(c);
// 16-231: 6 × 6 × 6 color cube
// 232-255: 23 step grayscale
if rgb[0] == rgb[1] && rgb[1] == rgb[2] {
let idx = 232 + (rgb[0] as u16 * 23 / 255) as u8;
Color::Indexed(idx)
} else {
let r = (rgb[0] as u16 * 5) / 255;
let g = (rgb[1] as u16 * 5) / 255;
let b = (rgb[2] as u16 * 5) / 255;
let idx = 16 + r * 36 + g * 6 + b;
Color::Indexed(idx as u8)
}
}
},
}
}
}
#[test]
fn rgb_to_ansi() {
for idx in 17..=231 {
let idxed = Color::Indexed(idx);
let rgb = to_rgb(idxed);
// gray scale colors have two equivelent repersentations
let color = Color::Rgb(rgb[0], rgb[1], rgb[2]);
let converted = convert(RenderingMode::Ansi, color);
if let Color::Indexed(i) = converted {
if rgb[0] != rgb[1] || rgb[1] != rgb[2] {
assert_eq!(idxed, converted);
} else {
assert!(i >= 232);
}
} else {
panic!("color is not indexed")
}
}
for idx in 232..=255 {
let idxed = Color::Indexed(idx);
let rgb = to_rgb(idxed);
assert!(rgb[0] == rgb[1] && rgb[1] == rgb[2]);
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct RinkStyle {
pub fg: Option<RinkColor>,
pub bg: Option<RinkColor>,
pub add_modifier: Modifier,
pub sub_modifier: Modifier,
}
impl Default for RinkStyle {
fn default() -> Self {
Self {
fg: Some(RinkColor {
color: Color::White,
alpha: 255,
}),
bg: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
}
}
impl RinkStyle {
pub fn add_modifier(mut self, m: Modifier) -> Self {
self.sub_modifier.remove(m);
self.add_modifier.insert(m);
self
}
pub fn remove_modifier(mut self, m: Modifier) -> Self {
self.add_modifier.remove(m);
self.sub_modifier.insert(m);
self
}
pub fn merge(mut self, other: RinkStyle) -> Self {
self.fg = self.fg.or(other.fg);
self.add_modifier(other.add_modifier)
.remove_modifier(other.sub_modifier)
}
}
impl From<RinkStyle> for Style {
fn from(val: RinkStyle) -> Self {
Style {
underline_color: None,
fg: val.fg.map(|c| c.color),
bg: val.bg.map(|c| c.color),
add_modifier: val.add_modifier,
sub_modifier: val.sub_modifier,
}
}
}

View file

@ -1,827 +0,0 @@
/*
- [ ] pub display: Display,
- [x] pub position_type: PositionType, --> kinda, taffy doesnt support everything
- [ ] pub direction: Direction,
- [x] pub flex_direction: FlexDirection,
- [x] pub flex_wrap: FlexWrap,
- [x] pub flex_grow: f32,
- [x] pub flex_shrink: f32,
- [x] pub flex_basis: Dimension,
- [x] pub overflow: Overflow, ---> kinda implemented... taffy doesnt have support for directional overflow
- [x] pub align_items: AlignItems,
- [x] pub align_self: AlignSelf,
- [x] pub align_content: AlignContent,
- [x] pub margin: Rect<Dimension>,
- [x] pub padding: Rect<Dimension>,
- [x] pub justify_content: JustifyContent,
- [ ] pub position: Rect<Dimension>,
- [x] pub border: Rect<Dimension>,
- [ ] pub size: Size<Dimension>, ----> ??? seems to only be relevant for input?
- [ ] pub min_size: Size<Dimension>,
- [ ] pub max_size: Size<Dimension>,
- [ ] pub aspect_ratio: Number,
*/
use dioxus_native_core::{
layout_attributes::parse_value,
node::OwnedAttributeView,
node_ref::{AttributeMaskBuilder, NodeMaskBuilder, NodeView},
prelude::*,
};
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
use taffy::prelude::*;
use crate::style::{RinkColor, RinkStyle};
#[derive(Default, Clone, PartialEq, Debug, Component)]
pub struct StyleModifier {
pub core: RinkStyle,
pub modifier: TuiModifier,
}
#[partial_derive_state]
impl State for StyleModifier {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// todo: seperate each attribute into it's own class
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(SORTED_STYLE_ATTRS))
.with_element();
fn update<'a>(
&mut self,
node_view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
let mut new = StyleModifier::default();
if parent.is_some() {
new.core.fg = None;
}
// handle text modifier elements
if node_view.namespace().is_none() {
if let Some(tag) = node_view.tag() {
match tag {
"b" => apply_style_attributes("font-weight", "bold", &mut new),
"strong" => apply_style_attributes("font-weight", "bold", &mut new),
"u" => apply_style_attributes("text-decoration", "underline", &mut new),
"ins" => apply_style_attributes("text-decoration", "underline", &mut new),
"del" => apply_style_attributes("text-decoration", "line-through", &mut new),
"i" => apply_style_attributes("font-style", "italic", &mut new),
"em" => apply_style_attributes("font-style", "italic", &mut new),
"mark" => {
apply_style_attributes("background-color", "rgba(241, 231, 64, 50%)", self)
}
_ => (),
}
}
}
// gather up all the styles from the attribute list
if let Some(attrs) = node_view.attributes() {
for OwnedAttributeView {
attribute, value, ..
} in attrs
{
if let Some(text) = value.as_text() {
apply_style_attributes(&attribute.name, text, &mut new);
}
}
}
// keep the text styling from the parent element
if let Some((parent,)) = parent {
let mut new_style = new.core.merge(parent.core);
new_style.bg = new.core.bg;
new.core = new_style;
}
if &mut new != self {
*self = new;
true
} else {
false
}
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
#[derive(Default, Clone, PartialEq, Debug)]
pub struct TuiModifier {
pub borders: Borders,
}
#[derive(Default, Clone, PartialEq, Debug)]
pub struct Borders {
pub top: BorderEdge,
pub right: BorderEdge,
pub bottom: BorderEdge,
pub left: BorderEdge,
}
impl Borders {
fn slice(&mut self) -> [&mut BorderEdge; 4] {
[
&mut self.top,
&mut self.right,
&mut self.bottom,
&mut self.left,
]
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct BorderEdge {
pub color: Option<RinkColor>,
pub style: BorderStyle,
pub width: Dimension,
pub radius: Dimension,
}
impl Default for BorderEdge {
fn default() -> Self {
Self {
color: None,
style: BorderStyle::None,
width: Dimension::Points(0.0),
radius: Dimension::Points(0.0),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BorderStyle {
Dotted,
Dashed,
Solid,
Double,
Groove,
Ridge,
Inset,
Outset,
Hidden,
None,
}
impl BorderStyle {
pub fn symbol_set(&self) -> Option<ratatui::symbols::line::Set> {
use ratatui::symbols::line::*;
const DASHED: Set = Set {
horizontal: "",
vertical: "",
..NORMAL
};
const DOTTED: Set = Set {
horizontal: "",
vertical: "",
..NORMAL
};
match self {
BorderStyle::Dotted => Some(DOTTED),
BorderStyle::Dashed => Some(DASHED),
BorderStyle::Solid => Some(NORMAL),
BorderStyle::Double => Some(DOUBLE),
BorderStyle::Groove => Some(NORMAL),
BorderStyle::Ridge => Some(NORMAL),
BorderStyle::Inset => Some(NORMAL),
BorderStyle::Outset => Some(NORMAL),
BorderStyle::Hidden => None,
BorderStyle::None => None,
}
}
}
/// applies the entire html namespace defined in dioxus-html
pub fn apply_style_attributes(
//
name: &str,
value: &str,
style: &mut StyleModifier,
) {
match name {
"animation"
| "animation-delay"
| "animation-direction"
| "animation-duration"
| "animation-fill-mode"
| "animation-iteration-count"
| "animation-name"
| "animation-play-state"
| "animation-timing-function" => apply_animation(name, value, style),
"backface-visibility" => {}
"background"
| "background-attachment"
| "background-clip"
| "background-color"
| "background-image"
| "background-origin"
| "background-position"
| "background-repeat"
| "background-size" => apply_background(name, value, style),
"border"
| "border-bottom"
| "border-bottom-color"
| "border-bottom-left-radius"
| "border-bottom-right-radius"
| "border-bottom-style"
| "border-bottom-width"
| "border-collapse"
| "border-color"
| "border-image"
| "border-image-outset"
| "border-image-repeat"
| "border-image-slice"
| "border-image-source"
| "border-image-width"
| "border-left"
| "border-left-color"
| "border-left-style"
| "border-left-width"
| "border-radius"
| "border-right"
| "border-right-color"
| "border-right-style"
| "border-right-width"
| "border-spacing"
| "border-style"
| "border-top"
| "border-top-color"
| "border-top-left-radius"
| "border-top-right-radius"
| "border-top-style"
| "border-top-width"
| "border-width" => apply_border(name, value, style),
"bottom" => {}
"box-shadow" => {}
"box-sizing" => {}
"caption-side" => {}
"clear" => {}
"clip" => {}
"color" => {
if let Ok(c) = value.parse() {
style.core.fg.replace(c);
}
}
"columns" => {}
"content" => {}
"counter-increment" => {}
"counter-reset" => {}
"cursor" => {}
"empty-cells" => {}
"float" => {}
"font" | "font-family" | "font-size" | "font-size-adjust" | "font-stretch"
| "font-style" | "font-variant" | "font-weight" => apply_font(name, value, style),
"letter-spacing" => {}
"line-height" => {}
"list-style" | "list-style-image" | "list-style-position" | "list-style-type" => {}
"opacity" => {}
"order" => {}
"outline" => {}
"outline-color" | "outline-offset" | "outline-style" | "outline-width" => {}
"page-break-after" | "page-break-before" | "page-break-inside" => {}
"perspective" | "perspective-origin" => {}
"pointer-events" => {}
"quotes" => {}
"resize" => {}
"tab-size" => {}
"table-layout" => {}
"text-align"
| "text-align-last"
| "text-decoration"
| "text-decoration-color"
| "text-decoration-line"
| "text-decoration-style"
| "text-indent"
| "text-justify"
| "text-overflow"
| "text-shadow"
| "text-transform" => apply_text(name, value, style),
"transition"
| "transition-delay"
| "transition-duration"
| "transition-property"
| "transition-timing-function" => apply_transition(name, value, style),
"visibility" => {}
"white-space" => {}
_ => {}
}
}
fn apply_background(name: &str, value: &str, style: &mut StyleModifier) {
match name {
"background-color" => {
if let Ok(c) = value.parse() {
style.core.bg.replace(c);
}
}
"background" => {}
"background-attachment" => {}
"background-clip" => {}
"background-image" => {}
"background-origin" => {}
"background-position" => {}
"background-repeat" => {}
"background-size" => {}
_ => {}
}
}
fn apply_border(name: &str, value: &str, style: &mut StyleModifier) {
fn parse_border_style(v: &str) -> BorderStyle {
match v {
"dotted" => BorderStyle::Dotted,
"dashed" => BorderStyle::Dashed,
"solid" => BorderStyle::Solid,
"double" => BorderStyle::Double,
"groove" => BorderStyle::Groove,
"ridge" => BorderStyle::Ridge,
"inset" => BorderStyle::Inset,
"outset" => BorderStyle::Outset,
"none" => BorderStyle::None,
"hidden" => BorderStyle::Hidden,
_ => todo!("Implement other border styles"),
}
}
match name {
"border" => {}
"border-bottom" => {}
"border-bottom-color" => {
if let Ok(c) = value.parse() {
style.modifier.borders.bottom.color = Some(c);
}
}
"border-bottom-left-radius" => {
if let Some(v) = parse_value(value) {
style.modifier.borders.left.radius = v;
}
}
"border-bottom-right-radius" => {
if let Some(v) = parse_value(value) {
style.modifier.borders.right.radius = v;
}
}
"border-bottom-style" => style.modifier.borders.bottom.style = parse_border_style(value),
"border-bottom-width" => {
if let Some(v) = parse_value(value) {
style.modifier.borders.bottom.width = v;
}
}
"border-collapse" => {}
"border-color" => {
let values: Vec<_> = value.split(' ').collect();
if values.len() == 1 {
if let Ok(c) = values[0].parse() {
style
.modifier
.borders
.slice()
.iter_mut()
.for_each(|b| b.color = Some(c));
}
} else {
for (v, b) in values
.into_iter()
.zip(style.modifier.borders.slice().iter_mut())
{
if let Ok(c) = v.parse() {
b.color = Some(c);
}
}
}
}
"border-image" => {}
"border-image-outset" => {}
"border-image-repeat" => {}
"border-image-slice" => {}
"border-image-source" => {}
"border-image-width" => {}
"border-left" => {}
"border-left-color" => {
if let Ok(c) = value.parse() {
style.modifier.borders.left.color = Some(c);
}
}
"border-left-style" => style.modifier.borders.left.style = parse_border_style(value),
"border-left-width" => {
if let Some(v) = parse_value(value) {
style.modifier.borders.left.width = v;
}
}
"border-radius" => {
let values: Vec<_> = value.split(' ').collect();
if values.len() == 1 {
if let Some(r) = parse_value(values[0]) {
style
.modifier
.borders
.slice()
.iter_mut()
.for_each(|b| b.radius = r);
}
} else {
for (v, b) in values
.into_iter()
.zip(style.modifier.borders.slice().iter_mut())
{
if let Some(r) = parse_value(v) {
b.radius = r;
}
}
}
}
"border-right" => {}
"border-right-color" => {
if let Ok(c) = value.parse() {
style.modifier.borders.right.color = Some(c);
}
}
"border-right-style" => style.modifier.borders.right.style = parse_border_style(value),
"border-right-width" => {
if let Some(v) = parse_value(value) {
style.modifier.borders.right.width = v;
}
}
"border-spacing" => {}
"border-style" => {
let values: Vec<_> = value.split(' ').collect();
if values.len() == 1 {
let border_style = parse_border_style(values[0]);
style
.modifier
.borders
.slice()
.iter_mut()
.for_each(|b| b.style = border_style);
} else {
for (v, b) in values
.into_iter()
.zip(style.modifier.borders.slice().iter_mut())
{
b.style = parse_border_style(v);
}
}
}
"border-top" => {}
"border-top-color" => {
if let Ok(c) = value.parse() {
style.modifier.borders.top.color = Some(c);
}
}
"border-top-left-radius" => {
if let Some(v) = parse_value(value) {
style.modifier.borders.left.radius = v;
}
}
"border-top-right-radius" => {
if let Some(v) = parse_value(value) {
style.modifier.borders.right.radius = v;
}
}
"border-top-style" => style.modifier.borders.top.style = parse_border_style(value),
"border-top-width" => {
if let Some(v) = parse_value(value) {
style.modifier.borders.top.width = v;
}
}
"border-width" => {
let values: Vec<_> = value.split(' ').collect();
if values.len() == 1 {
if let Some(w) = parse_value(values[0]) {
style
.modifier
.borders
.slice()
.iter_mut()
.for_each(|b| b.width = w);
}
} else {
for (v, width) in values
.into_iter()
.zip(style.modifier.borders.slice().iter_mut())
{
if let Some(w) = parse_value(v) {
width.width = w;
}
}
}
}
_ => (),
}
}
fn apply_animation(name: &str, _value: &str, _style: &mut StyleModifier) {
match name {
"animation" => {}
"animation-delay" => {}
"animation-direction =>{}" => {}
"animation-duration" => {}
"animation-fill-mode" => {}
"animation-itera =>{}tion-count" => {}
"animation-name" => {}
"animation-play-state" => {}
"animation-timing-function" => {}
_ => {}
}
}
fn apply_font(name: &str, value: &str, style: &mut StyleModifier) {
use ratatui::style::Modifier;
match name {
"font" => (),
"font-family" => (),
"font-size" => (),
"font-size-adjust" => (),
"font-stretch" => (),
"font-style" => match value {
"italic" => style.core = style.core.add_modifier(Modifier::ITALIC),
"oblique" => style.core = style.core.add_modifier(Modifier::ITALIC),
_ => (),
},
"font-variant" => todo!("Implement font-variant"),
"font-weight" => match value {
"bold" => style.core = style.core.add_modifier(Modifier::BOLD),
"normal" => style.core = style.core.remove_modifier(Modifier::BOLD),
_ => (),
},
_ => (),
}
}
fn apply_text(name: &str, value: &str, style: &mut StyleModifier) {
use ratatui::style::Modifier;
match name {
"text-align" => todo!("Implement text-align"),
"text-align-last" => todo!("text-Implement align-last"),
"text-decoration" | "text-decoration-line" => {
for v in value.split(' ') {
match v {
"line-through" => style.core = style.core.add_modifier(Modifier::CROSSED_OUT),
"underline" => style.core = style.core.add_modifier(Modifier::UNDERLINED),
_ => (),
}
}
}
"text-decoration-color" => todo!("text-Implement decoration-color"),
"text-decoration-style" => todo!("text-Implement decoration-style"),
"text-indent" => todo!("Implement text-indent"),
"text-justify" => todo!("Implement text-justify"),
"text-overflow" => todo!("Implement text-overflow"),
"text-shadow" => todo!("Implement text-shadow"),
"text-transform" => todo!("Implement text-transform"),
_ => todo!("Implement other text attributes"),
}
}
fn apply_transition(_name: &str, _value: &str, _style: &mut StyleModifier) {
todo!("Implement transitions")
}
const SORTED_STYLE_ATTRS: &[&str] = &[
"animation",
"animation-delay",
"animation-direction",
"animation-duration",
"animation-fill-mode",
"animation-iteration-count",
"animation-name",
"animation-play-state",
"animation-timing-function",
"backface-visibility",
"background",
"background-attachment",
"background-clip",
"background-color",
"background-image",
"background-origin",
"background-position",
"background-repeat",
"background-size",
"border",
"border-bottom",
"border-bottom-color",
"border-bottom-left-radius",
"border-bottom-right-radius",
"border-bottom-style",
"border-bottom-width",
"border-collapse",
"border-color",
"border-image",
"border-image-outset",
"border-image-repeat",
"border-image-slice",
"border-image-source",
"border-image-width",
"border-left",
"border-left-color",
"border-left-style",
"border-left-width",
"border-radius",
"border-right",
"border-right-color",
"border-right-style",
"border-right-width",
"border-spacing",
"border-style",
"border-top",
"border-top-color",
"border-top-left-radius",
"border-top-right-radius",
"border-top-style",
"border-top-width",
"border-width",
"bottom",
"box-shadow",
"box-sizing",
"caption-side",
"clear",
"clip",
"color",
"columns",
"content",
"counter-increment",
"counter-reset",
"cursor",
"empty-cells",
"float",
"font",
"font-family",
"font-size",
"font-size-adjust",
"font-stretch",
"font-style",
"font-variant",
"font-weight",
"letter-spacing",
"line-height",
"list-style",
"list-style-image",
"list-style-position",
"list-style-type",
"opacity",
"order",
"outline",
"outline-color",
"outline-offset",
"outline-style",
"outline-width",
"page-break-after",
"page-break-before",
"page-break-inside",
"perspective",
"perspective-origin",
"pointer-events",
"quotes",
"resize",
"tab-size",
"table-layout",
"text-align",
"text-align-last",
"text-decoration",
"text-decoration-color",
"text-decoration-line",
"text-decoration-style",
"text-indent",
"text-justify",
"text-overflow",
"text-shadow",
"text-transform",
"transition",
"transition-delay",
"transition-duration",
"transition-property",
"transition-timing-function",
"visibility",
"white-space",
"background-color",
"background",
"background-attachment",
"background-clip",
"background-image",
"background-origin",
"background-position",
"background-repeat",
"background-size",
"dotted",
"dashed",
"solid",
"double",
"groove",
"ridge",
"inset",
"outset",
"none",
"hidden",
"border",
"border-bottom",
"border-bottom-color",
"border-bottom-left-radius",
"border-bottom-right-radius",
"border-bottom-style",
"border-bottom-width",
"border-collapse",
"border-color",
"border-image",
"border-image-outset",
"border-image-repeat",
"border-image-slice",
"border-image-source",
"border-image-width",
"border-left",
"border-left-color",
"border-left-style",
"border-left-width",
"border-radius",
"border-right",
"border-right-color",
"border-right-style",
"border-right-width",
"border-spacing",
"border-style",
"border-top",
"border-top-color",
"border-top-left-radius",
"border-top-right-radius",
"border-top-style",
"border-top-width",
"border-width",
"animation",
"animation-delay",
"animation-direction",
"animation-duration",
"animation-fill-mode",
"animation-itera ",
"animation-name",
"animation-play-state",
"animation-timing-function",
"font",
"font-family",
"font-size",
"font-size-adjust",
"font-stretch",
"font-style",
"italic",
"oblique",
"font-variant",
"font-weight",
"bold",
"normal",
"text-align",
"text-align-last",
"text-decoration",
"text-decoration-line",
"line-through",
"underline",
"text-decoration-color",
"text-decoration-style",
"text-indent",
"text-justify",
"text-overflow",
"text-shadow",
"text-transform",
];

View file

@ -1,101 +0,0 @@
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier},
widgets::Widget,
};
use crate::{
style::{convert, RinkColor, RinkStyle},
Config,
};
pub struct RinkBuffer<'a> {
buf: &'a mut Buffer,
cfg: Config,
}
impl<'a> RinkBuffer<'a> {
fn new(buf: &'a mut Buffer, cfg: Config) -> RinkBuffer<'a> {
Self { buf, cfg }
}
pub fn set(&mut self, x: u16, y: u16, new: RinkCell) {
let area = self.buf.area();
if x < area.x || x >= area.width + area.x || y < area.y || y >= area.height + area.y {
// panic!("({x}, {y}) is not in {area:?}");
return;
}
let cell = self.buf.get_mut(x, y);
cell.bg = convert(self.cfg.rendering_mode, new.bg.blend(cell.bg));
if new.symbol.is_empty() {
if !cell.symbol.is_empty() {
// allows text to "shine through" transparent backgrounds
cell.fg = convert(self.cfg.rendering_mode, new.bg.blend(cell.fg));
}
} else {
cell.modifier = new.modifier;
cell.symbol = new.symbol;
cell.fg = convert(self.cfg.rendering_mode, new.fg.blend(cell.bg));
}
}
}
pub trait RinkWidget {
fn render(self, area: Rect, buf: RinkBuffer);
}
pub struct WidgetWithContext<T: RinkWidget> {
widget: T,
config: Config,
}
impl<T: RinkWidget> WidgetWithContext<T> {
pub fn new(widget: T, config: Config) -> WidgetWithContext<T> {
WidgetWithContext { widget, config }
}
}
impl<T: RinkWidget> Widget for WidgetWithContext<T> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.widget.render(area, RinkBuffer::new(buf, self.config));
}
}
#[derive(Clone)]
pub struct RinkCell {
pub symbol: String,
pub bg: RinkColor,
pub fg: RinkColor,
pub modifier: Modifier,
}
impl Default for RinkCell {
fn default() -> Self {
Self {
symbol: "".to_string(),
fg: RinkColor {
color: Color::Rgb(0, 0, 0),
alpha: 0,
},
bg: RinkColor {
color: Color::Rgb(0, 0, 0),
alpha: 0,
},
modifier: Modifier::empty(),
}
}
}
impl RinkCell {
pub fn set_style(&mut self, style: RinkStyle) {
if let Some(c) = style.fg {
self.fg = c;
}
if let Some(c) = style.bg {
self.bg = c;
}
self.modifier = style.add_modifier;
self.modifier.remove(style.sub_modifier);
}
}

View file

@ -1,214 +0,0 @@
use std::collections::HashMap;
use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData};
use dioxus_native_core::{
custom_element::CustomElement,
node::OwnedAttributeDiscription,
node_ref::AttributeMask,
prelude::NodeType,
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
NodeId,
};
use shipyard::UniqueView;
use crate::hooks::FormData;
use super::{RinkWidget, WidgetContext};
#[derive(Debug, Default)]
pub(crate) struct Button {
text_id: NodeId,
value: String,
}
impl Button {
fn width(el: &ElementNodeMut) -> String {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "width".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
value
} else {
"1px".to_string()
}
}
fn height(el: &ElementNodeMut) -> String {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "height".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
value
} else {
"1px".to_string()
}
}
fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
let width = Self::width(el);
let height = Self::height(el);
let single_char = width == "1px" || height == "1px";
let border_style = if single_char { "none" } else { "solid" };
el.set_attribute(
OwnedAttributeDiscription {
name: "border-style".to_string(),
namespace: Some("style".to_string()),
},
border_style.to_string(),
);
}
fn update_value_attr(&mut self, el: &ElementNodeMut) {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "value".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
self.value = value;
}
}
fn write_value(&self, rdom: &mut RealDom) {
if let Some(mut text) = rdom.get_mut(self.text_id) {
let node_type = text.node_type_mut();
let NodeTypeMut::Text(mut text) = node_type else {
panic!("input must be an element")
};
*text.text_mut() = self.value.clone();
}
}
fn switch(&mut self, ctx: &WidgetContext, node: NodeMut) {
let data = FormData {
value: self.value.to_string(),
values: HashMap::new(),
files: None,
};
ctx.send(crate::Event {
id: node.id(),
name: "input",
data: crate::EventData::Form(data),
bubbles: true,
});
}
}
impl CustomElement for Button {
const NAME: &'static str = "input";
fn roots(&self) -> Vec<NodeId> {
vec![self.text_id]
}
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
let node_type = root.node_type();
let NodeType::Element(el) = &*node_type else {
panic!("input must be an element")
};
let value = el
.attributes
.get(&OwnedAttributeDiscription {
name: "value".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string());
drop(node_type);
let rdom = root.real_dom_mut();
let text = rdom.create_node(value.clone().unwrap_or_default());
let text_id = text.id();
root.add_event_listener("keydown");
root.add_event_listener("click");
Self {
text_id,
value: value.unwrap_or_default(),
}
}
fn attributes_changed(
&mut self,
mut root: dioxus_native_core::real_dom::NodeMut,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
match attributes {
AttributeMask::All => {
{
let node_type = root.node_type_mut();
let NodeTypeMut::Element(mut el) = node_type else {
panic!("input must be an element")
};
self.update_value_attr(&el);
self.update_size_attr(&mut el);
}
self.write_value(root.real_dom_mut());
}
AttributeMask::Some(attrs) => {
{
let node_type = root.node_type_mut();
let NodeTypeMut::Element(mut el) = node_type else {
panic!("input must be an element")
};
if attrs.contains("width") || attrs.contains("height") {
self.update_size_attr(&mut el);
}
if attrs.contains("value") {
self.update_value_attr(&el);
}
}
if attrs.contains("value") {
self.write_value(root.real_dom_mut());
}
}
}
}
}
impl RinkWidget for Button {
fn handle_event(
&mut self,
event: &crate::Event,
mut node: dioxus_native_core::real_dom::NodeMut,
) {
let ctx: WidgetContext = {
node.real_dom_mut()
.raw_world_mut()
.borrow::<UniqueView<WidgetContext>>()
.expect("expected widget context")
.clone()
};
match event.name {
"click" => self.switch(&ctx, node),
"keydown" => {
if let crate::EventData::Keyboard(data) = &event.data {
if !data.is_auto_repeating()
&& match data.key() {
Key::Character(c) if c == " " => true,
Key::Enter => true,
_ => false,
}
{
self.switch(&ctx, node);
}
}
}
_ => {}
}
}
}

View file

@ -1,259 +0,0 @@
use std::collections::HashMap;
use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData};
use dioxus_native_core::{
custom_element::CustomElement,
node::OwnedAttributeDiscription,
node_ref::AttributeMask,
prelude::NodeType,
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut},
NodeId,
};
use shipyard::UniqueView;
use crate::hooks::FormData;
use super::{RinkWidget, WidgetContext};
#[derive(Debug, Default)]
pub(crate) struct CheckBox {
div_id: NodeId,
text_id: NodeId,
value: String,
checked: bool,
}
impl CheckBox {
fn width(el: &ElementNodeMut) -> String {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "width".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
value
} else {
"1px".to_string()
}
}
fn height(el: &ElementNodeMut) -> String {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "height".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
value
} else {
"1px".to_string()
}
}
fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
let width = Self::width(el);
let height = Self::height(el);
let single_char = width == "1px" || height == "1px";
let border_style = if single_char { "none" } else { "solid" };
el.set_attribute(
OwnedAttributeDiscription {
name: "border-style".to_string(),
namespace: Some("style".to_string()),
},
border_style.to_string(),
);
}
fn update_value_attr(&mut self, el: &ElementNodeMut) {
self.value = el
.get_attribute(&OwnedAttributeDiscription {
name: "value".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
.unwrap_or_else(|| "on".to_string());
}
fn update_checked_attr(&mut self, el: &ElementNodeMut) {
self.checked = el
.get_attribute(&OwnedAttributeDiscription {
name: "checked".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
.unwrap_or_else(|| "false".to_string())
== "true";
}
fn write_value(&self, mut root: NodeMut) {
let single_char = {
let node_type = root.node_type_mut();
let NodeTypeMut::Element(el) = node_type else {
panic!("input must be an element")
};
Self::width(&el) == "1px" || Self::height(&el) == "1px"
};
let rdom = root.real_dom_mut();
if let Some(mut text) = rdom.get_mut(self.text_id) {
let node_type = text.node_type_mut();
let NodeTypeMut::Text(mut text) = node_type else {
panic!("input must be an element")
};
let value = if single_char {
if self.checked {
""
} else {
""
}
} else if self.checked {
""
} else {
" "
};
*text.text_mut() = value.to_string();
}
}
fn switch(&mut self, mut node: NodeMut) {
let new_state = !self.checked;
let data = FormData {
value: new_state
.then(|| self.value.to_string())
.unwrap_or_default(),
values: HashMap::new(),
files: None,
};
{
let ctx: UniqueView<WidgetContext> = node
.real_dom_mut()
.raw_world_mut()
.borrow()
.expect("expected widget context");
ctx.send(crate::Event {
id: self.div_id,
name: "input",
data: crate::EventData::Form(data),
bubbles: true,
});
}
self.checked = new_state;
self.write_value(node);
}
}
impl CustomElement for CheckBox {
const NAME: &'static str = "input";
fn roots(&self) -> Vec<NodeId> {
vec![self.text_id]
}
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
let node_type = root.node_type();
let NodeType::Element(el) = &*node_type else {
panic!("input must be an element")
};
let value = el
.attributes
.get(&OwnedAttributeDiscription {
name: "value".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string());
drop(node_type);
let rdom = root.real_dom_mut();
let text = rdom.create_node(String::new());
let text_id = text.id();
root.add_event_listener("click");
root.add_event_listener("keydown");
let div_id = root.id();
let myself = Self {
div_id,
text_id,
value: value.unwrap_or_default(),
checked: false,
};
myself.write_value(root);
myself
}
fn attributes_changed(
&mut self,
mut root: dioxus_native_core::real_dom::NodeMut,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
match attributes {
AttributeMask::All => {
{
let node_type = root.node_type_mut();
let NodeTypeMut::Element(mut el) = node_type else {
panic!("input must be an element")
};
self.update_value_attr(&el);
self.update_size_attr(&mut el);
self.update_checked_attr(&el);
}
self.write_value(root);
}
AttributeMask::Some(attrs) => {
{
let node_type = root.node_type_mut();
let NodeTypeMut::Element(mut el) = node_type else {
panic!("input must be an element")
};
if attrs.contains("width") || attrs.contains("height") {
self.update_size_attr(&mut el);
}
if attrs.contains("value") {
self.update_value_attr(&el);
}
if attrs.contains("checked") {
self.update_checked_attr(&el);
}
}
if attrs.contains("checked") {
self.write_value(root);
}
}
}
}
}
impl RinkWidget for CheckBox {
fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) {
match event.name {
"click" => self.switch(node),
"keydown" => {
if let crate::EventData::Keyboard(data) = &event.data {
if !data.is_auto_repeating()
&& match data.key() {
Key::Character(c) if c == " " => true,
Key::Enter => true,
_ => false,
}
{
self.switch(node);
}
}
}
_ => {}
}
}
}

View file

@ -1,155 +0,0 @@
use dioxus_native_core::{
custom_element::CustomElement, node::OwnedAttributeDiscription, prelude::NodeType,
real_dom::NodeImmutable,
};
use super::{
checkbox::CheckBox, number::Number, password::Password, slider::Slider, textbox::TextBox,
RinkWidget,
};
use crate::widgets::button::Button;
pub(crate) enum Input {
Button(Button),
CheckBox(CheckBox),
TextBox(TextBox),
Password(Password),
Number(Number),
Slider(Slider),
}
impl CustomElement for Input {
const NAME: &'static str = "input";
fn roots(&self) -> Vec<dioxus_native_core::NodeId> {
match self {
Input::Button(button) => button.roots(),
Input::CheckBox(checkbox) => checkbox.roots(),
Input::TextBox(textbox) => textbox.roots(),
Input::Password(password) => password.roots(),
Input::Number(number) => number.roots(),
Input::Slider(slider) => slider.roots(),
}
}
fn slot(&self) -> Option<dioxus_native_core::NodeId> {
match self {
Input::Button(button) => button.slot(),
Input::CheckBox(checkbox) => checkbox.slot(),
Input::TextBox(textbox) => textbox.slot(),
Input::Password(password) => password.slot(),
Input::Number(number) => number.slot(),
Input::Slider(slider) => slider.slot(),
}
}
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
{
// currently widgets are not allowed to have children
let children = root.child_ids();
let rdom = root.real_dom_mut();
for child in children {
if let Some(mut child) = rdom.get_mut(child) {
child.remove();
}
}
}
let node_type = root.node_type();
let NodeType::Element(el) = &*node_type else {
panic!("input must be an element")
};
let input_type = el
.attributes
.get(&OwnedAttributeDiscription {
name: "type".to_string(),
namespace: None,
})
.and_then(|value| value.as_text());
match input_type
.map(|type_| type_.trim().to_lowercase())
.as_deref()
{
Some("button") => {
drop(node_type);
Input::Button(Button::create(root))
}
Some("checkbox") => {
drop(node_type);
Input::CheckBox(CheckBox::create(root))
}
Some("textbox") => {
drop(node_type);
Input::TextBox(TextBox::create(root))
}
Some("password") => {
drop(node_type);
Input::Password(Password::create(root))
}
Some("number") => {
drop(node_type);
Input::Number(Number::create(root))
}
Some("range") => {
drop(node_type);
Input::Slider(Slider::create(root))
}
_ => {
drop(node_type);
Input::TextBox(TextBox::create(root))
}
}
}
fn attributes_changed(
&mut self,
root: dioxus_native_core::real_dom::NodeMut,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
match self {
Input::Button(button) => {
button.attributes_changed(root, attributes);
}
Input::CheckBox(checkbox) => {
checkbox.attributes_changed(root, attributes);
}
Input::TextBox(textbox) => {
textbox.attributes_changed(root, attributes);
}
Input::Password(password) => {
password.attributes_changed(root, attributes);
}
Input::Number(number) => {
number.attributes_changed(root, attributes);
}
Input::Slider(slider) => {
slider.attributes_changed(root, attributes);
}
}
}
}
impl RinkWidget for Input {
fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) {
match self {
Input::Button(button) => {
button.handle_event(event, node);
}
Input::CheckBox(checkbox) => {
checkbox.handle_event(event, node);
}
Input::TextBox(textbox) => {
textbox.handle_event(event, node);
}
Input::Password(password) => {
password.handle_event(event, node);
}
Input::Number(number) => {
number.handle_event(event, node);
}
Input::Slider(slider) => {
slider.handle_event(event, node);
}
}
}
}

View file

@ -1,128 +0,0 @@
mod button;
mod checkbox;
mod input;
mod number;
mod password;
mod slider;
mod text_like;
mod textbox;
use std::sync::{Arc, RwLock};
use dioxus_native_core::{
custom_element::{CustomElement, CustomElementUpdater},
real_dom::{NodeMut, RealDom},
};
use futures_channel::mpsc::UnboundedSender;
use shipyard::{Component, Unique};
use crate::Event;
pub(crate) fn register_widgets(rdom: &mut RealDom, sender: UnboundedSender<Event>) {
// inject the widget context
rdom.raw_world().add_unique(WidgetContext { sender });
rdom.register_custom_element::<RinkWidgetWrapper<input::Input>>();
}
trait RinkWidget: Sync + Send + CustomElement + 'static {
fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut);
}
pub trait RinkWidgetResponder: CustomElementUpdater {
fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut);
}
impl<W: RinkWidget> RinkWidgetResponder for W {
fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut) {
RinkWidget::handle_event(self, event, node)
}
}
struct RinkWidgetWrapper<W: RinkWidget> {
inner: RinkWidgetTraitObject,
_marker: std::marker::PhantomData<W>,
}
impl<W: RinkWidget> CustomElement for RinkWidgetWrapper<W> {
const NAME: &'static str = W::NAME;
const NAMESPACE: Option<&'static str> = W::NAMESPACE;
fn create(mut node: NodeMut) -> Self {
let myself = RinkWidgetTraitObject {
widget: Arc::new(RwLock::new(W::create(node.reborrow()))),
};
// Insert the widget as an arbitrary data node so that it can be recognized when bubbling events
node.insert(myself.clone());
RinkWidgetWrapper {
inner: myself,
_marker: std::marker::PhantomData,
}
}
fn attributes_changed(
&mut self,
root: dioxus_native_core::real_dom::NodeMut,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
let mut widget = self.inner.widget.write().unwrap();
widget.attributes_changed(root, attributes);
}
fn roots(&self) -> Vec<dioxus_native_core::NodeId> {
let widget = self.inner.widget.read().unwrap();
widget.roots()
}
fn slot(&self) -> Option<dioxus_native_core::NodeId> {
let widget = self.inner.widget.read().unwrap();
widget.slot()
}
}
#[derive(Clone, Component)]
pub(crate) struct RinkWidgetTraitObject {
widget: Arc<RwLock<dyn RinkWidgetResponder + Send + Sync>>,
}
impl CustomElementUpdater for RinkWidgetTraitObject {
fn attributes_changed(
&mut self,
light_root: dioxus_native_core::real_dom::NodeMut,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
let mut widget = self.widget.write().unwrap();
widget.attributes_changed(light_root, attributes);
}
fn roots(&self) -> Vec<dioxus_native_core::NodeId> {
let widget = self.widget.read().unwrap();
widget.roots()
}
fn slot(&self) -> Option<dioxus_native_core::NodeId> {
let widget = self.widget.read().unwrap();
widget.slot()
}
}
impl RinkWidgetResponder for RinkWidgetTraitObject {
fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut) {
let mut widget = self.widget.write().unwrap();
widget.handle_event(event, node);
}
}
#[derive(Unique, Clone)]
pub(crate) struct WidgetContext {
sender: UnboundedSender<Event>,
}
impl WidgetContext {
pub(crate) fn send(&self, event: Event) {
self.sender.unbounded_send(event).unwrap();
}
}

View file

@ -1,91 +0,0 @@
use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData};
use dioxus_native_core::{
custom_element::CustomElement,
real_dom::{NodeImmutable, RealDom},
NodeId,
};
use crate::EventData;
use super::{text_like::TextLike, RinkWidget};
#[derive(Debug, Default)]
pub(crate) struct Number {
text: TextLike,
}
impl Number {
fn increase(&mut self, rdom: &mut RealDom, id: NodeId) {
let num = self.text.text().parse::<f64>().unwrap_or(0.0);
self.text.set_text((num + 1.0).to_string(), rdom, id);
}
fn decrease(&mut self, rdom: &mut RealDom, id: NodeId) {
let num = self.text.text().parse::<f64>().unwrap_or(0.0);
self.text.set_text((num - 1.0).to_string(), rdom, id);
}
}
impl CustomElement for Number {
const NAME: &'static str = "input";
fn roots(&self) -> Vec<NodeId> {
self.text.roots()
}
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
Number {
text: TextLike::create(root.reborrow()),
}
}
fn attributes_changed(
&mut self,
root: dioxus_native_core::real_dom::NodeMut,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
self.text.attributes_changed(root, attributes)
}
}
impl RinkWidget for Number {
fn handle_event(
&mut self,
event: &crate::Event,
mut node: dioxus_native_core::real_dom::NodeMut,
) {
if event.name == "keydown" {
if let EventData::Keyboard(data) = &event.data {
let key = data.key();
let is_num_like = match key.clone() {
Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true,
Key::Character(c)
if c == "." || c == "-" || c.chars().all(|c| c.is_numeric()) =>
{
true
}
_ => false,
};
if is_num_like {
self.text.handle_event(event, node)
} else {
let id = node.id();
let rdom = node.real_dom_mut();
match key {
Key::ArrowUp => {
self.increase(rdom, id);
}
Key::ArrowDown => {
self.decrease(rdom, id);
}
_ => (),
}
}
return;
}
}
self.text.handle_event(event, node)
}
}

View file

@ -1,12 +0,0 @@
use super::text_like::{TextLike, TextLikeController};
pub(crate) type Password = TextLike<PasswordController>;
#[derive(Debug, Default)]
pub(crate) struct PasswordController;
impl TextLikeController for PasswordController {
fn display_text(&self, text: &str) -> String {
text.chars().map(|_| '.').collect()
}
}

View file

@ -1,470 +0,0 @@
use std::collections::HashMap;
use dioxus_html::{
input_data::keyboard_types::Key, prelude::*, HasKeyboardData, SerializedKeyboardData,
SerializedMouseData,
};
use dioxus_native_core::{
custom_element::CustomElement,
node::{OwnedAttributeDiscription, OwnedAttributeValue},
node_ref::AttributeMask,
prelude::{ElementNode, NodeType},
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
NodeId,
};
use shipyard::UniqueView;
use super::{RinkWidget, WidgetContext};
use crate::hooks::FormData;
use crate::{query::get_layout, Event, EventData, Query};
#[derive(Debug)]
pub(crate) struct Slider {
div_wrapper: NodeId,
pre_cursor_div: NodeId,
post_cursor_div: NodeId,
min: f64,
max: f64,
step: Option<f64>,
value: f64,
border: bool,
}
impl Default for Slider {
fn default() -> Self {
Self {
div_wrapper: Default::default(),
pre_cursor_div: Default::default(),
post_cursor_div: Default::default(),
min: 0.0,
max: 100.0,
step: None,
value: 0.0,
border: false,
}
}
}
impl Slider {
fn size(&self) -> f64 {
self.max - self.min
}
fn step(&self) -> f64 {
self.step.unwrap_or(self.size() / 10.0)
}
fn width(el: &ElementNodeMut) -> String {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "width".to_string(),
namespace: Some("style".to_string()),
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
value
} else {
"1px".to_string()
}
}
fn height(el: &ElementNodeMut) -> String {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "height".to_string(),
namespace: Some("style".to_string()),
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
value
} else {
"1px".to_string()
}
}
fn update_min_attr(&mut self, el: &ElementNodeMut) {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "min".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
self.min = value.parse().ok().unwrap_or(0.0);
}
}
fn update_max_attr(&mut self, el: &ElementNodeMut) {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "max".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
self.max = value.parse().ok().unwrap_or(100.0);
}
}
fn update_step_attr(&mut self, el: &ElementNodeMut) {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "step".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
self.step = value.parse().ok();
}
}
fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
let width = Self::width(el);
let height = Self::height(el);
let single_char = width
.strip_prefix("px")
.and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
.is_some()
|| height
.strip_prefix("px")
.and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
.is_some();
self.border = !single_char;
let border_style = if self.border { "solid" } else { "none" };
el.set_attribute(
OwnedAttributeDiscription {
name: "border-style".to_string(),
namespace: Some("style".to_string()),
},
border_style.to_string(),
);
}
fn update_value(&mut self, new: f64) {
self.value = new.clamp(self.min, self.max);
}
fn update_value_attr(&mut self, el: &ElementNodeMut) {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "value".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
self.update_value(value.parse().ok().unwrap_or(0.0));
}
}
fn write_value(&self, rdom: &mut RealDom, id: NodeId) {
let value_percent = (self.value - self.min) / self.size() * 100.0;
if let Some(mut div) = rdom.get_mut(self.pre_cursor_div) {
let node_type = div.node_type_mut();
let NodeTypeMut::Element(mut element) = node_type else {
panic!("input must be an element")
};
element.set_attribute(
OwnedAttributeDiscription {
name: "width".to_string(),
namespace: Some("style".to_string()),
},
format!("{}%", value_percent),
);
}
if let Some(mut div) = rdom.get_mut(self.post_cursor_div) {
let node_type = div.node_type_mut();
let NodeTypeMut::Element(mut element) = node_type else {
panic!("input must be an element")
};
element.set_attribute(
OwnedAttributeDiscription {
name: "width".to_string(),
namespace: Some("style".to_string()),
},
format!("{}%", 100.0 - value_percent),
);
}
// send the event
let world = rdom.raw_world_mut();
{
let ctx: UniqueView<WidgetContext> = world.borrow().expect("expected widget context");
let data = FormData {
value: self.value.to_string(),
values: HashMap::new(),
files: None,
};
ctx.send(Event {
id,
name: "input",
data: EventData::Form(data),
bubbles: true,
});
}
}
fn handle_keydown(&mut self, mut root: NodeMut, data: &SerializedKeyboardData) {
let key = data.key();
let step = self.step();
match key {
Key::ArrowDown | Key::ArrowLeft => {
self.update_value(self.value - step);
}
Key::ArrowUp | Key::ArrowRight => {
self.update_value(self.value + step);
}
_ => {
return;
}
}
let id = root.id();
let rdom = root.real_dom_mut();
self.write_value(rdom, id);
}
fn handle_mousemove(&mut self, mut root: NodeMut, data: &SerializedMouseData) {
if !data.held_buttons().is_empty() {
let id = root.id();
let rdom = root.real_dom_mut();
let world = rdom.raw_world_mut();
let taffy = {
let query: UniqueView<Query> = world.borrow().unwrap();
query.stretch.clone()
};
let taffy = taffy.lock().unwrap();
let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
let width = layout.size.width as f64;
let offset = data.element_coordinates();
self.update_value(self.min + self.size() * offset.x / width);
self.write_value(rdom, id);
}
}
}
impl CustomElement for Slider {
const NAME: &'static str = "input";
fn roots(&self) -> Vec<NodeId> {
vec![self.div_wrapper]
}
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
let node_type = root.node_type();
let NodeType::Element(el) = &*node_type else {
panic!("input must be an element")
};
let value = el.attributes.get(&OwnedAttributeDiscription {
name: "value".to_string(),
namespace: None,
});
let value = value
.and_then(|value| match value {
OwnedAttributeValue::Text(text) => text.as_str().parse().ok(),
OwnedAttributeValue::Float(float) => Some(*float),
OwnedAttributeValue::Int(int) => Some(*int as f64),
_ => None,
})
.unwrap_or(0.0);
drop(node_type);
let rdom = root.real_dom_mut();
let pre_cursor_div = rdom.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [(
OwnedAttributeDiscription {
name: "background-color".to_string(),
namespace: Some("style".to_string()),
},
"rgba(10,10,10,0.5)".to_string().into(),
)]
.into_iter()
.collect(),
..Default::default()
}));
let pre_cursor_div_id = pre_cursor_div.id();
let cursor_text = rdom.create_node("|".to_string());
let cursor_text_id = cursor_text.id();
let mut cursor_span = rdom.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [].into_iter().collect(),
..Default::default()
}));
cursor_span.add_child(cursor_text_id);
let cursor_span_id = cursor_span.id();
let post_cursor_div = rdom.create_node(NodeType::Element(ElementNode {
tag: "span".to_string(),
attributes: [
(
OwnedAttributeDiscription {
name: "width".to_string(),
namespace: Some("style".to_string()),
},
"100%".to_string().into(),
),
(
OwnedAttributeDiscription {
name: "background-color".to_string(),
namespace: Some("style".to_string()),
},
"rgba(10,10,10,0.5)".to_string().into(),
),
]
.into_iter()
.collect(),
..Default::default()
}));
let post_cursor_div_id = post_cursor_div.id();
let mut div_wrapper = rdom.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [
(
OwnedAttributeDiscription {
name: "display".to_string(),
namespace: Some("style".to_string()),
},
"flex".to_string().into(),
),
(
OwnedAttributeDiscription {
name: "flex-direction".to_string(),
namespace: Some("style".to_string()),
},
"row".to_string().into(),
),
(
OwnedAttributeDiscription {
name: "width".to_string(),
namespace: Some("style".to_string()),
},
"100%".to_string().into(),
),
(
OwnedAttributeDiscription {
name: "height".to_string(),
namespace: Some("style".to_string()),
},
"100%".to_string().into(),
),
]
.into_iter()
.collect(),
..Default::default()
}));
let div_wrapper_id = div_wrapper.id();
div_wrapper.add_child(pre_cursor_div_id);
div_wrapper.add_child(cursor_span_id);
div_wrapper.add_child(post_cursor_div_id);
root.add_event_listener("mousemove");
root.add_event_listener("mousedown");
root.add_event_listener("keydown");
Self {
pre_cursor_div: pre_cursor_div_id,
post_cursor_div: post_cursor_div_id,
div_wrapper: div_wrapper_id,
value,
..Default::default()
}
}
fn attributes_changed(
&mut self,
mut root: dioxus_native_core::real_dom::NodeMut,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
match attributes {
AttributeMask::All => {
{
let node_type = root.node_type_mut();
let NodeTypeMut::Element(mut el) = node_type else {
panic!("input must be an element")
};
self.update_value_attr(&el);
self.update_size_attr(&mut el);
self.update_max_attr(&el);
self.update_min_attr(&el);
self.update_step_attr(&el);
}
let id = root.id();
self.write_value(root.real_dom_mut(), id);
}
AttributeMask::Some(attrs) => {
{
let node_type = root.node_type_mut();
let NodeTypeMut::Element(mut el) = node_type else {
panic!("input must be an element")
};
if attrs.contains("width") || attrs.contains("height") {
self.update_size_attr(&mut el);
}
if attrs.contains("max") {
self.update_max_attr(&el);
}
if attrs.contains("min") {
self.update_min_attr(&el);
}
if attrs.contains("step") {
self.update_step_attr(&el);
}
if attrs.contains("value") {
self.update_value_attr(&el);
}
}
if attrs.contains("value") {
let id = root.id();
self.write_value(root.real_dom_mut(), id);
}
}
}
}
}
impl RinkWidget for Slider {
fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) {
match event.name {
"keydown" => {
if let EventData::Keyboard(data) = &event.data {
self.handle_keydown(node, data);
}
}
"mousemove" => {
if let EventData::Mouse(data) = &event.data {
self.handle_mousemove(node, data);
}
}
"mousedown" => {
if let EventData::Mouse(data) = &event.data {
self.handle_mousemove(node, data);
}
}
_ => {}
}
}
}

View file

@ -1,460 +0,0 @@
use std::{collections::HashMap, io::stdout};
use crossterm::{cursor::MoveTo, execute};
use dioxus_html::{
input_data::keyboard_types::Key, prelude::*, HasKeyboardData, SerializedKeyboardData,
SerializedMouseData,
};
use dioxus_native_core::{
custom_element::CustomElement,
node::OwnedAttributeDiscription,
node_ref::AttributeMask,
prelude::{ElementNode, NodeType},
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
utils::cursor::{Cursor, Pos},
NodeId,
};
use shipyard::UniqueView;
use taffy::geometry::Point;
use crate::hooks::FormData;
use crate::{query::get_layout, Event, EventData, Query};
use super::{RinkWidget, WidgetContext};
pub(crate) trait TextLikeController {
fn display_text(&self, text: &str) -> String {
text.to_string()
}
}
#[derive(Debug, Default)]
pub(crate) struct EmptyController;
impl TextLikeController for EmptyController {}
#[derive(Debug, Default)]
pub(crate) struct TextLike<C: TextLikeController = EmptyController> {
text: String,
div_wrapper: NodeId,
pre_cursor_text: NodeId,
highlighted_text: NodeId,
post_cursor_text: NodeId,
cursor: Cursor,
dragging: bool,
border: bool,
max_len: Option<usize>,
controller: C,
}
impl<C: TextLikeController> TextLike<C> {
fn width(el: &ElementNodeMut) -> String {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "width".to_string(),
namespace: Some("style".to_string()),
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
value
} else {
"1px".to_string()
}
}
fn height(el: &ElementNodeMut) -> String {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "height".to_string(),
namespace: Some("style".to_string()),
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
value
} else {
"1px".to_string()
}
}
fn update_max_width_attr(&mut self, el: &ElementNodeMut) {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "maxlength".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
if let Ok(max_len) = value.parse::<usize>() {
self.max_len = Some(max_len);
}
}
}
fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
let width = Self::width(el);
let height = Self::height(el);
let single_char = width
.strip_prefix("px")
.and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
.is_some()
|| height
.strip_prefix("px")
.and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
.is_some();
self.border = !single_char;
let border_style = if self.border { "solid" } else { "none" };
el.set_attribute(
OwnedAttributeDiscription {
name: "border-style".to_string(),
namespace: Some("style".to_string()),
},
border_style.to_string(),
);
}
fn update_value_attr(&mut self, el: &ElementNodeMut) {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "value".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
self.text = value;
}
}
pub(crate) fn set_text(&mut self, text: String, rdom: &mut RealDom, id: NodeId) {
self.text = text;
self.write_value(rdom, id);
}
pub(crate) fn text(&self) -> &str {
self.text.as_str()
}
fn write_value(&self, rdom: &mut RealDom, id: NodeId) {
let start_highlight = self.cursor.first().idx(self.text.as_str());
let end_highlight = self.cursor.last().idx(self.text.as_str());
let (text_before_first_cursor, text_after_first_cursor) =
self.text.split_at(start_highlight);
let (text_highlighted, text_after_second_cursor) =
text_after_first_cursor.split_at(end_highlight - start_highlight);
if let Some(mut text) = rdom.get_mut(self.pre_cursor_text) {
let node_type = text.node_type_mut();
let NodeTypeMut::Text(mut text) = node_type else {
panic!("input must be an element")
};
*text.text_mut() = self.controller.display_text(text_before_first_cursor);
}
if let Some(mut text) = rdom.get_mut(self.highlighted_text) {
let node_type = text.node_type_mut();
let NodeTypeMut::Text(mut text) = node_type else {
panic!("input must be an element")
};
*text.text_mut() = self.controller.display_text(text_highlighted);
}
if let Some(mut text) = rdom.get_mut(self.post_cursor_text) {
let node_type = text.node_type_mut();
let NodeTypeMut::Text(mut text) = node_type else {
panic!("input must be an element")
};
*text.text_mut() = self.controller.display_text(text_after_second_cursor);
}
// send the event
{
let world = rdom.raw_world_mut();
let data: FormData = FormData {
value: self.text.clone(),
values: HashMap::new(),
files: None,
};
let ctx: UniqueView<WidgetContext> = world.borrow().expect("expected widget context");
ctx.send(Event {
id,
name: "input",
data: EventData::Form(data),
bubbles: true,
});
}
}
fn handle_keydown(&mut self, mut root: NodeMut, data: &SerializedKeyboardData) {
let key = data.key();
let modifiers = data.modifiers();
let code = data.code();
if key == Key::Enter {
return;
}
self.cursor.handle_input(
&code,
&key,
&modifiers,
&mut self.text,
self.max_len.unwrap_or(1000),
);
let id = root.id();
let rdom = root.real_dom_mut();
self.write_value(rdom, id);
let world = rdom.raw_world_mut();
// move cursor to new position
let taffy = {
let query: UniqueView<Query> = world.borrow().unwrap();
query.stretch.clone()
};
let taffy = taffy.lock().unwrap();
let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
let Point { x, y } = layout.location;
let Pos { col, row } = self.cursor.start;
let (x, y) = (col as u16 + x as u16, row as u16 + y as u16);
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y) {
execute!(stdout(), MoveTo(x, y)).unwrap();
}
} else {
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
fn handle_mousemove(&mut self, mut root: NodeMut, data: &SerializedMouseData) {
if self.dragging {
let id = root.id();
let offset = data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
// textboxs are only one line tall
new.row = 0;
if new != self.cursor.start {
self.cursor.end = Some(new);
}
let rdom = root.real_dom_mut();
self.write_value(rdom, id);
}
}
fn handle_mousedown(&mut self, mut root: NodeMut, data: &SerializedMouseData) {
let offset = data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
// textboxs are only one line tall
new.row = 0;
new.realize_col(self.text.as_str());
self.cursor = Cursor::from_start(new);
self.dragging = true;
let id = root.id();
// move cursor to new position
let rdom = root.real_dom_mut();
let world = rdom.raw_world_mut();
let taffy = {
let query: UniqueView<Query> = world.borrow().unwrap();
query.stretch.clone()
};
let taffy = taffy.lock().unwrap();
let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
let Point { x, y } = layout.location;
let Pos { col, row } = self.cursor.start;
let (x, y) = (col as u16 + x as u16, row as u16 + y as u16);
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y) {
execute!(stdout(), MoveTo(x, y)).unwrap();
}
} else {
execute!(stdout(), MoveTo(x, y)).unwrap();
}
self.write_value(rdom, id)
}
}
impl<C: TextLikeController + Send + Sync + Default + 'static> CustomElement for TextLike<C> {
const NAME: &'static str = "input";
fn roots(&self) -> Vec<NodeId> {
vec![self.div_wrapper]
}
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
let node_type = root.node_type();
let NodeType::Element(el) = &*node_type else {
panic!("input must be an element")
};
let value = el
.attributes
.get(&OwnedAttributeDiscription {
name: "value".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string());
drop(node_type);
let rdom = root.real_dom_mut();
let pre_text = rdom.create_node(String::new());
let pre_text_id = pre_text.id();
let highlighted_text = rdom.create_node(String::new());
let highlighted_text_id = highlighted_text.id();
let mut highlighted_text_span = rdom.create_node(NodeType::Element(ElementNode {
tag: "span".to_string(),
attributes: [(
OwnedAttributeDiscription {
name: "background-color".to_string(),
namespace: Some("style".to_string()),
},
"rgba(255, 255, 255, 50%)".to_string().into(),
)]
.into_iter()
.collect(),
..Default::default()
}));
highlighted_text_span.add_child(highlighted_text_id);
let highlighted_text_span_id = highlighted_text_span.id();
let post_text = rdom.create_node(value.clone().unwrap_or_default());
let post_text_id = post_text.id();
let mut div_wrapper = rdom.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [(
OwnedAttributeDiscription {
name: "display".to_string(),
namespace: Some("style".to_string()),
},
"flex".to_string().into(),
)]
.into_iter()
.collect(),
..Default::default()
}));
let div_wrapper_id = div_wrapper.id();
div_wrapper.add_child(pre_text_id);
div_wrapper.add_child(highlighted_text_span_id);
div_wrapper.add_child(post_text_id);
div_wrapper.add_event_listener("mousemove");
div_wrapper.add_event_listener("mousedown");
div_wrapper.add_event_listener("mouseup");
div_wrapper.add_event_listener("mouseleave");
div_wrapper.add_event_listener("mouseenter");
root.add_event_listener("keydown");
root.add_event_listener("focusout");
Self {
pre_cursor_text: pre_text_id,
highlighted_text: highlighted_text_id,
post_cursor_text: post_text_id,
div_wrapper: div_wrapper_id,
cursor: Cursor::default(),
text: value.unwrap_or_default(),
..Default::default()
}
}
fn attributes_changed(
&mut self,
mut root: dioxus_native_core::real_dom::NodeMut,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
match attributes {
AttributeMask::All => {
{
let node_type = root.node_type_mut();
let NodeTypeMut::Element(mut el) = node_type else {
panic!("input must be an element")
};
self.update_value_attr(&el);
self.update_size_attr(&mut el);
self.update_max_width_attr(&el);
}
let id = root.id();
self.write_value(root.real_dom_mut(), id);
}
AttributeMask::Some(attrs) => {
{
let node_type = root.node_type_mut();
let NodeTypeMut::Element(mut el) = node_type else {
panic!("input must be an element")
};
if attrs.contains("width") || attrs.contains("height") {
self.update_size_attr(&mut el);
}
if attrs.contains("maxlength") {
self.update_max_width_attr(&el);
}
if attrs.contains("value") {
self.update_value_attr(&el);
}
}
if attrs.contains("value") {
let id = root.id();
self.write_value(root.real_dom_mut(), id);
}
}
}
}
}
impl<C: TextLikeController + Send + Sync + Default + 'static> RinkWidget for TextLike<C> {
fn handle_event(&mut self, event: &crate::Event, node: NodeMut) {
match event.name {
"keydown" => {
if let EventData::Keyboard(data) = &event.data {
self.handle_keydown(node, data);
}
}
"mousemove" => {
if let EventData::Mouse(data) = &event.data {
self.handle_mousemove(node, data);
}
}
"mousedown" => {
if let EventData::Mouse(data) = &event.data {
self.handle_mousedown(node, data);
}
}
"mouseup" => {
self.dragging = false;
}
"mouseleave" => {
self.dragging = false;
}
"mouseenter" => {
self.dragging = false;
}
"focusout" => {
execute!(stdout(), MoveTo(0, 1000)).unwrap();
}
_ => {}
}
}
}

View file

@ -1,3 +0,0 @@
use super::text_like::TextLike;
pub(crate) type TextBox = TextLike;

View file

@ -1,73 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style>
html,
body {
height: 100%;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: black;
/* justify-content: center;
align-items: center; */
/* margin: auto; */
}
.smaller {
height: 70%;
width: 70%;
background-color: green;
/* justify-content: center; */
/* align-items: center; */
}
.superinner {
height: 100%;
width: 100%;
/* display: flex; */
/* */
margin-top: 20px;
margin-bottom: 20px;
margin-left: 20px;
margin-right: 20px;
/* */
background-color: red;
justify-content: center;
align-items: center;
flex-direction: column;
/* margin: 20px; */
/* margin: 20px; */
}
</style>
</head>
<body>
<div class="container">
<div class="smaller">
<div class="superinner">
<h1>Hello World</h1>
<p>This is a test</p>
</div>
</div>
</div>
<!-- <div class="container">
<div class="smaller">
hello world
<div style="color: green; margin: 40px;">
goodbye
<div style="color:red;">
asdasdasd
</div>
</div>
</div>
</div> -->
</body>
</html>