mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Merge pull request #2084 from DioxusLabs/jk/split-out-tui
Move TUI renderer into blitz repo
This commit is contained in:
commit
a9a8619489
91 changed files with 37 additions and 17047 deletions
303
Cargo.lock
generated
303
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -24,4 +24,3 @@ mobile = []
|
|||
web = []
|
||||
ssr = []
|
||||
liveview = []
|
||||
tui = []
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
2
packages/dioxus-tui/.gitignore
vendored
2
packages/dioxus-tui/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
/target
|
||||
Cargo.lock
|
2
packages/dioxus-tui/.vscode/spellright.dict
vendored
2
packages/dioxus-tui/.vscode/spellright.dict
vendored
|
@ -1,2 +0,0 @@
|
|||
esque
|
||||
Tui
|
|
@ -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"]
|
|
@ -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.
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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%",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = []
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
2
packages/native-core-macro/.gitignore
vendored
2
packages/native-core-macro/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
/target
|
||||
Cargo.lock
|
|
@ -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 }
|
|
@ -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.
|
|
@ -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,
|
||||
}
|
||||
}
|
2
packages/native-core/.gitignore
vendored
2
packages/native-core/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
/target
|
||||
Cargo.lock
|
|
@ -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"]
|
|
@ -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.
|
|
@ -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));
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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, ¤t_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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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)));
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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:?}");
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>;
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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
|
@ -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), &[]);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -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)
|
||||
));
|
||||
}
|
2
packages/plasmo/.gitignore
vendored
2
packages/plasmo/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
/target
|
||||
Cargo.lock
|
2
packages/plasmo/.vscode/spellright.dict
vendored
2
packages/plasmo/.vscode/spellright.dict
vendored
|
@ -1,2 +0,0 @@
|
|||
esque
|
||||
Tui
|
|
@ -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"]
|
|
@ -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.
|
|
@ -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 |
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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
|
@ -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",
|
||||
];
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
];
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
use super::text_like::TextLike;
|
||||
|
||||
pub(crate) type TextBox = TextLike;
|
|
@ -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>
|
Loading…
Reference in a new issue