mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2025-05-05 07:50:15 +02:00
tabula rasa
This commit is contained in:
commit
47b3413d7d
.gitignoreCargo.lockCargo.tomlJustfileREADME.md
edn
input
output
shell.nixtui
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
target
|
||||
cov
|
||||
*.profraw
|
1281
Cargo.lock
generated
Normal file
1281
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"./input",
|
||||
"./output",
|
||||
"./tui",
|
||||
"./edn"
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
[profile.coverage]
|
||||
inherits = "test"
|
||||
lto = false
|
10
Justfile
Normal file
10
Justfile
Normal file
|
@ -0,0 +1,10 @@
|
|||
covfig := "CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' RUSTDOCFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cov/cargo-test-%p-%m.profraw'"
|
||||
grcov-binary := "--binary-path ./target/coverage/deps/"
|
||||
grcov-ignore := "--ignore-not-existing --ignore '../*' --ignore \"/*\" --ignore 'target/*'"
|
||||
cov:
|
||||
{{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage
|
||||
rm -rf target/coverage/html || true
|
||||
{{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t html -o target/coverage/html
|
||||
cov-md:
|
||||
{{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage
|
||||
{{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t markdown | sort
|
980
edn/Cargo.lock
generated
Normal file
980
edn/Cargo.lock
generated
Normal file
|
@ -0,0 +1,980 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "better-panic"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fa9e1d11a268684cbd90ed36370d7577afb6c62d912ddff5c15fc34343e5036"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"console",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "by_address"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clojure-reader"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edf141eea627c101a97509266bc9f6ba8cd408618f5e2ac4a0cb6b64b1d4ea8"
|
||||
dependencies = [
|
||||
"ordered-float",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const_panic"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53857514f72ee4a2b583de67401e3ff63a5472ca4acf289d09a9ea7636dfec17"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"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 = "darling"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fast-srgb8"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "894813a444908c0c8c0e221b041771d107c4a21de1d317dc49bcc66e3c9e5b3f"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "konst"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9"
|
||||
dependencies = [
|
||||
"const_panic",
|
||||
"konst_kernel",
|
||||
"konst_proc_macros",
|
||||
"typewit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "konst_kernel"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c"
|
||||
dependencies = [
|
||||
"typewit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "konst_proc_macros"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.169"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"fast-srgb8",
|
||||
"palette_derive",
|
||||
"phf",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
|
||||
dependencies = [
|
||||
"by_address",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.13.0",
|
||||
"lru",
|
||||
"paste",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[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.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tek_edn"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clojure-reader",
|
||||
"itertools 0.14.0",
|
||||
"konst",
|
||||
"tek_tui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tek_input"
|
||||
version = "0.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "tek_output"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"tek_edn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tek_tui"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"better-panic",
|
||||
"crossterm",
|
||||
"palette",
|
||||
"rand",
|
||||
"ratatui",
|
||||
"tek_edn",
|
||||
"tek_input",
|
||||
"tek_output",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typewit"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb77c29baba9e4d3a6182d51fa75e3215c7fd1dab8f4ea9d107c716878e55fc0"
|
||||
dependencies = [
|
||||
"typewit_proc_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typewit_proc_macros"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
17
edn/Cargo.toml
Normal file
17
edn/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "tek_edn"
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
clojure-reader = "0.3.0"
|
||||
konst = { version = "0.3.16", features = [ "rust_1_83" ] }
|
||||
itertools = "0.14.0"
|
||||
thiserror = "2.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dev-dependencies]
|
||||
tek_tui = { path = "../tui" }
|
||||
proptest = "^1.6.0"
|
94
edn/README.md
Normal file
94
edn/README.md
Normal file
|
@ -0,0 +1,94 @@
|
|||
# `ket`
|
||||
|
||||
**ket** is the configuration language of **tek**.
|
||||
it's based on [edn](https://en.wikipedia.org/wiki/Clojure#Extensible_Data_Notation)
|
||||
but without all the features.
|
||||
|
||||
## usage
|
||||
|
||||
### with `tek_output`
|
||||
|
||||
this is a `tek_output` view layout defined using ket:
|
||||
|
||||
```edn
|
||||
(bsp/s (fixed/y 2 :toolbar)
|
||||
(fill/x (align/c (bsp/w :pool
|
||||
(bsp/s :outputs (bsp/s :inputs (bsp/s :tracks :scenes)))))))
|
||||
```
|
||||
|
||||
### with `tek_input`
|
||||
|
||||
this is a `tek_input` keymap defined using ket:
|
||||
|
||||
```edn
|
||||
(@u undo 1)
|
||||
(@shift-u redo 1)
|
||||
(@e editor show :pool-clip)
|
||||
(@ctrl-a scene add)
|
||||
(@ctrl-t track add)
|
||||
(@tab pool toggle)
|
||||
```
|
||||
|
||||
## tokens
|
||||
|
||||
ket has 4 "types", represented by variants of the `Value` enum:
|
||||
|
||||
* `Num` - numeric literal
|
||||
* `Sym` - textual symbol
|
||||
* `Key` - textual key
|
||||
* `Exp` - parenthesized group of tokens
|
||||
|
||||
### numeric literal
|
||||
|
||||
numbers are passed through as is. only non-negative integers are supported.
|
||||
|
||||
```edn
|
||||
0
|
||||
123456
|
||||
```
|
||||
|
||||
### keys
|
||||
|
||||
keys are the names of available operations. they look like this:
|
||||
|
||||
```edn
|
||||
simple-key
|
||||
multi-part/key
|
||||
```
|
||||
|
||||
keys are implemented by the underlying subsystem:
|
||||
|
||||
* in `tek_output`, keys are names of layout primitives
|
||||
* in `tek_input`, keys are names of commands
|
||||
|
||||
### symbols
|
||||
|
||||
symbols that start with `:` represent the names of variables
|
||||
provided by the `Context` trait - such as command parameters,
|
||||
or entire layout components:
|
||||
|
||||
```edn
|
||||
:symbol-name
|
||||
```
|
||||
|
||||
symbols that start with `@` represent keybindings.
|
||||
they are parsed in `tek_tui` and look like this:
|
||||
|
||||
```edn
|
||||
@ctrl-alt-shift-space
|
||||
```
|
||||
|
||||
### parenthesized groups
|
||||
|
||||
parenthesized groups represent things like expressions
|
||||
or configuration statements, and look like this:
|
||||
|
||||
```edn
|
||||
(some-key :symbol (some/other-key @another-symbol 123) 456)
|
||||
```
|
||||
|
||||
## goals
|
||||
|
||||
* [ ] const parse
|
||||
* [ ] live reload
|
||||
* [ ] serialize modified code back to original indentation
|
7
edn/proptest-regressions/iter.txt
Normal file
7
edn/proptest-regressions/iter.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc bbb90b16e6106f17dbb5a4f57594f451360a2ea7e3e20c28adeb8babc98d39df # shrinks to source = "(𰀀"
|
7
edn/proptest-regressions/token.txt
Normal file
7
edn/proptest-regressions/token.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 5fb814b74ae035bdecb536817090cfb473f0a874e9acf9aaa136a4794cdb367f # shrinks to source = "", start = 10336420442936153584, length = 8110323630773398032
|
130
edn/src/context.rs
Normal file
130
edn/src/context.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
use crate::*;
|
||||
pub trait TryFromAtom<'a, T>: Sized {
|
||||
fn try_from_expr (_state: &'a T, _iter: TokenIter<'a>) -> Option<Self> { None }
|
||||
fn try_from_atom (state: &'a T, value: Value<'a>) -> Option<Self> {
|
||||
if let Exp(0, iter) = value { return Self::try_from_expr(state, iter) }
|
||||
None
|
||||
}
|
||||
}
|
||||
pub trait TryIntoAtom<T>: Sized {
|
||||
fn try_into_atom (&self) -> Option<Value>;
|
||||
}
|
||||
/// Map EDN tokens to parameters of a given type for a given context
|
||||
pub trait Context<U>: Sized {
|
||||
fn get (&self, _atom: &Value) -> Option<U> {
|
||||
None
|
||||
}
|
||||
fn get_or_fail (&self, atom: &Value) -> U {
|
||||
self.get(atom).expect("no value")
|
||||
}
|
||||
}
|
||||
impl<T: Context<U>, U> Context<U> for &T {
|
||||
fn get (&self, atom: &Value) -> Option<U> {
|
||||
(*self).get(atom)
|
||||
}
|
||||
fn get_or_fail (&self, atom: &Value) -> U {
|
||||
(*self).get_or_fail(atom)
|
||||
}
|
||||
}
|
||||
impl<T: Context<U>, U> Context<U> for Option<T> {
|
||||
fn get (&self, atom: &Value) -> Option<U> {
|
||||
self.as_ref().map(|s|s.get(atom)).flatten()
|
||||
}
|
||||
fn get_or_fail (&self, atom: &Value) -> U {
|
||||
self.as_ref().map(|s|s.get_or_fail(atom)).expect("no provider")
|
||||
}
|
||||
}
|
||||
/// Implement `Context` for a context and type.
|
||||
#[macro_export] macro_rules! provide {
|
||||
// Provide a value to the EDN template
|
||||
($type:ty:|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
impl Context<$type> for $State {
|
||||
#[allow(unreachable_code)]
|
||||
fn get (&$self, atom: &Value) -> Option<$type> {
|
||||
use Value::*;
|
||||
Some(match atom { $(Sym($pat) => $expr,)* _ => return None })
|
||||
}
|
||||
}
|
||||
};
|
||||
// Provide a value more generically
|
||||
($lt:lifetime: $type:ty:|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
impl<$lt> Context<$lt, $type> for $State {
|
||||
#[allow(unreachable_code)]
|
||||
fn get (&$lt $self, atom: &Value) -> Option<$type> {
|
||||
use Value::*;
|
||||
Some(match atom { $(Sym($pat) => $expr,)* _ => return None })
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
/// Implement `Context` for a context and numeric type.
|
||||
///
|
||||
/// This enables support for numeric literals.
|
||||
#[macro_export] macro_rules! provide_num {
|
||||
// Provide a value that may also be a numeric literal in the EDN, to a generic implementation.
|
||||
($type:ty:|$self:ident:<$T:ident:$Trait:path>|{ $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
impl<$T: $Trait> Context<$type> for $T {
|
||||
fn get (&$self, atom: &Value) -> Option<$type> {
|
||||
use Value::*;
|
||||
Some(match atom { $(Sym($pat) => $expr,)* Num(n) => *n as $type, _ => return None })
|
||||
}
|
||||
}
|
||||
};
|
||||
// Provide a value that may also be a numeric literal in the EDN, to a concrete implementation.
|
||||
($type:ty:|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
impl Context<$type> for $State {
|
||||
fn get (&$self, atom: &Value) -> Option<$type> {
|
||||
use Value::*;
|
||||
Some(match atom { $(Sym($pat) => $expr,)* Num(n) => *n as $type, _ => return None })
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
/// Implement `Context` for a context and the boolean type.
|
||||
///
|
||||
/// This enables support for boolean literals.
|
||||
#[macro_export] macro_rules! provide_bool {
|
||||
// Provide a value that may also be a numeric literal in the EDN, to a generic implementation.
|
||||
($type:ty:|$self:ident:<$T:ident:$Trait:path>|{ $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
impl<$T: $Trait> Context<$type> for $T {
|
||||
fn get (&$self, atom: &Value) -> Option<$type> {
|
||||
use Value::*;
|
||||
Some(match atom {
|
||||
Num(n) => match *n { 0 => false, _ => true },
|
||||
Sym(":false") | Sym(":f") => false,
|
||||
Sym(":true") | Sym(":t") => true,
|
||||
$(Sym($pat) => $expr,)*
|
||||
_ => return Context::get(self, atom)
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
// Provide a value that may also be a numeric literal in the EDN, to a concrete implementation.
|
||||
($type:ty:|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
impl Context<$type> for $State {
|
||||
fn get (&$self, atom: &Value) -> Option<$type> {
|
||||
use Value::*;
|
||||
Some(match atom {
|
||||
Num(n) => match *n { 0 => false, _ => true },
|
||||
Sym(":false") | Sym(":f") => false,
|
||||
Sym(":true") | Sym(":t") => true,
|
||||
$(Sym($pat) => $expr,)*
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
#[cfg(test)] #[test] fn test_edn_context () {
|
||||
struct Test;
|
||||
provide_bool!(bool: |self: Test|{
|
||||
":provide-bool" => true
|
||||
});
|
||||
let test = Test;
|
||||
assert_eq!(test.get(&Value::Sym(":false")), Some(false));
|
||||
assert_eq!(test.get(&Value::Sym(":true")), Some(true));
|
||||
assert_eq!(test.get(&Value::Sym(":provide-bool")), Some(true));
|
||||
assert_eq!(test.get(&Value::Sym(":missing-bool")), None);
|
||||
assert_eq!(test.get(&Value::Num(0)), Some(false));
|
||||
assert_eq!(test.get(&Value::Num(1)), Some(true));
|
||||
}
|
15
edn/src/error.rs
Normal file
15
edn/src/error.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use crate::*;
|
||||
use thiserror::Error;
|
||||
pub type ParseResult<T> = Result<T, ParseError>;
|
||||
#[derive(Error, Debug, Copy, Clone, PartialEq)] pub enum ParseError {
|
||||
#[error("parse failed: not implemented")]
|
||||
Unimplemented,
|
||||
#[error("parse failed: empty")]
|
||||
Empty,
|
||||
#[error("parse failed: incomplete")]
|
||||
Incomplete,
|
||||
#[error("parse failed: unexpected character '{0}'")]
|
||||
Unexpected(char),
|
||||
#[error("parse failed: error #{0}")]
|
||||
Code(u8),
|
||||
}
|
144
edn/src/iter.rs
Normal file
144
edn/src/iter.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
//! The token iterator [TokenIter] allows you to get the
|
||||
//! general-purpose syntactic [Token]s represented by the source text.
|
||||
//!
|
||||
//! Both iterators are `peek`able:
|
||||
//!
|
||||
//! ```
|
||||
//! let src = include_str!("../test.edn");
|
||||
//! let mut view = tek_edn::TokenIter::new(src);
|
||||
//! assert_eq!(view.0.0, src);
|
||||
//! assert_eq!(view.peek(), view.0.peek())
|
||||
//! ```
|
||||
use crate::*;
|
||||
/// Provides a native [Iterator] API over the [ConstIntoIter] [SourceIter]
|
||||
/// [TokenIter::next] returns just the [Token] and mutates `self`,
|
||||
/// instead of returning an updated version of the struct as [SourceIter::next] does.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct TokenIter<'a>(pub SourceIter<'a>);
|
||||
impl<'a> TokenIter<'a> {
|
||||
pub const fn new (source: &'a str) -> Self { Self(SourceIter::new(source)) }
|
||||
pub const fn peek (&self) -> Option<Token<'a>> { self.0.peek() }
|
||||
}
|
||||
impl<'a> Iterator for TokenIter<'a> {
|
||||
type Item = Token<'a>;
|
||||
fn next (&mut self) -> Option<Token<'a>> {
|
||||
self.0.next().map(|(item, rest)|{self.0 = rest; item})
|
||||
}
|
||||
}
|
||||
/// Owns a reference to the source text.
|
||||
/// [SourceIter::next] emits subsequent pairs of:
|
||||
/// * a [Token] and
|
||||
/// * the source text remaining
|
||||
/// * [ ] TODO: maybe [SourceIter::next] should wrap the remaining source in `Self` ?
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct SourceIter<'a>(pub &'a str);
|
||||
const_iter!(<'a>|self: SourceIter<'a>| => Token<'a> => self.next_mut().map(|(result, _)|result));
|
||||
impl<'a> From<&'a str> for SourceIter<'a> {fn from (source: &'a str) -> Self{Self::new(source)}}
|
||||
impl<'a> SourceIter<'a> {
|
||||
pub const fn new (source: &'a str) -> Self { Self(source) }
|
||||
pub const fn chomp (&self, index: usize) -> Self { Self(split_at(self.0, index).1) }
|
||||
pub const fn next (mut self) -> Option<(Token<'a>, Self)> { Self::next_mut(&mut self) }
|
||||
pub const fn peek (&self) -> Option<Token<'a>> { peek_src(self.0) }
|
||||
pub const fn next_mut (&mut self) -> Option<(Token<'a>, Self)> {
|
||||
match self.peek() {
|
||||
Some(token) => Some((token, self.chomp(token.end()))),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
}
|
||||
pub const fn peek_src <'a> (source: &'a str) -> Option<Token<'a>> {
|
||||
let mut token: Token<'a> = Token::new(source, 0, 0, Nil);
|
||||
iterate!(char_indices(source) => (start, c) => token = match token.value() {
|
||||
Err(_) => return Some(token),
|
||||
Nil => match c {
|
||||
' '|'\n'|'\r'|'\t' =>
|
||||
token.grow(),
|
||||
'(' =>
|
||||
Token::new(source, start, 1, Exp(1, TokenIter::new(str_range(source, start, start + 1)))),
|
||||
':'|'@' =>
|
||||
Token::new(source, start, 1, Sym(str_range(source, start, start + 1))),
|
||||
'/'|'a'..='z' =>
|
||||
Token::new(source, start, 1, Key(str_range(source, start, start + 1))),
|
||||
'0'..='9' =>
|
||||
Token::new(source, start, 1, match to_digit(c) {
|
||||
Ok(c) => Value::Num(c),
|
||||
Result::Err(e) => Value::Err(e)
|
||||
}),
|
||||
_ => token.error(Unexpected(c))
|
||||
},
|
||||
Num(n) => match c {
|
||||
'0'..='9' => token.grow_num(n, c),
|
||||
' '|'\n'|'\r'|'\t'|')' => return Some(token),
|
||||
_ => token.error(Unexpected(c))
|
||||
},
|
||||
Sym(_) => match c {
|
||||
'a'..='z'|'A'..='Z'|'0'..='9'|'-' => token.grow_sym(),
|
||||
' '|'\n'|'\r'|'\t'|')' => return Some(token),
|
||||
_ => token.error(Unexpected(c))
|
||||
},
|
||||
Key(_) => match c {
|
||||
'a'..='z'|'0'..='9'|'-'|'/' => token.grow_key(),
|
||||
' '|'\n'|'\r'|'\t'|')' => return Some(token),
|
||||
_ => token.error(Unexpected(c))
|
||||
},
|
||||
Exp(depth, _) => match depth {
|
||||
0 => return Some(token.grow_exp()),
|
||||
_ => match c {
|
||||
')' => token.grow_out(),
|
||||
'(' => token.grow_in(),
|
||||
_ => token.grow_exp(),
|
||||
}
|
||||
},
|
||||
});
|
||||
match token.value() {
|
||||
Nil => None,
|
||||
_ => Some(token),
|
||||
}
|
||||
}
|
||||
pub const fn to_number (digits: &str) -> Result<usize, ParseError> {
|
||||
let mut value = 0;
|
||||
iterate!(char_indices(digits) => (_, c) => match to_digit(c) {
|
||||
Ok(digit) => value = 10 * value + digit,
|
||||
Result::Err(e) => return Result::Err(e)
|
||||
});
|
||||
Ok(value)
|
||||
}
|
||||
pub const fn to_digit (c: char) -> Result<usize, ParseError> {
|
||||
Ok(match c {
|
||||
'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4,
|
||||
'5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
|
||||
_ => return Result::Err(Unexpected(c))
|
||||
})
|
||||
}
|
||||
#[cfg(test)] mod test_token_iter {
|
||||
use super::*;
|
||||
//use proptest::prelude::*;
|
||||
#[test] fn test_iters () {
|
||||
let mut iter = crate::SourceIter::new(&":foo :bar");
|
||||
let _ = iter.next();
|
||||
let mut iter = crate::TokenIter::new(&":foo :bar");
|
||||
let _ = iter.next();
|
||||
}
|
||||
#[test] const fn test_const_iters () {
|
||||
let mut iter = crate::SourceIter::new(&":foo :bar");
|
||||
let _ = iter.next();
|
||||
}
|
||||
#[test] fn test_num () {
|
||||
let digit = to_digit('0');
|
||||
let digit = to_digit('x');
|
||||
let number = to_number(&"123");
|
||||
let number = to_number(&"12asdf3");
|
||||
}
|
||||
//proptest! {
|
||||
//#[test] fn proptest_source_iter (
|
||||
//source in "\\PC*"
|
||||
//) {
|
||||
//let mut iter = crate::SourceIter::new(&source);
|
||||
////let _ = iter.next();
|
||||
//}
|
||||
//#[test] fn proptest_token_iter (
|
||||
//source in "\\PC*"
|
||||
//) {
|
||||
//let mut iter = crate::TokenIter::new(&source);
|
||||
////let _ = iter.next();
|
||||
//}
|
||||
//}
|
||||
}
|
61
edn/src/lib.rs
Normal file
61
edn/src/lib.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
#![feature(adt_const_params)]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(impl_trait_in_fn_trait_return)]
|
||||
mod error; pub use self::error::*;
|
||||
mod token; pub use self::token::*;
|
||||
mod iter; pub use self::iter::*;
|
||||
mod context; pub use self::context::*;
|
||||
pub(crate) use self::Value::*;
|
||||
pub(crate) use self::ParseError::*;
|
||||
pub(crate) use konst::iter::{ConstIntoIter, IsIteratorKind};
|
||||
pub(crate) use konst::string::{split_at, str_range, char_indices};
|
||||
pub(crate) use std::error::Error;
|
||||
pub(crate) use std::fmt::{Debug, Display, Formatter, Result as FormatResult};
|
||||
/// Static iteration helper.
|
||||
#[macro_export] macro_rules! iterate {
|
||||
($expr:expr => $arg: pat => $body:expr) => {
|
||||
let mut iter = $expr;
|
||||
while let Some(($arg, next)) = iter.next() {
|
||||
$body;
|
||||
iter = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Implement the const iterator pattern.
|
||||
#[macro_export] macro_rules! const_iter {
|
||||
($(<$l:lifetime>)?|$self:ident: $Struct:ty| => $Item:ty => $expr:expr) => {
|
||||
impl$(<$l>)? Iterator for $Struct {
|
||||
type Item = $Item;
|
||||
fn next (&mut $self) -> Option<$Item> { $expr }
|
||||
}
|
||||
impl$(<$l>)? ConstIntoIter for $Struct {
|
||||
type Kind = IsIteratorKind;
|
||||
type Item = $Item;
|
||||
type IntoIter = Self;
|
||||
}
|
||||
}
|
||||
}
|
||||
//#[cfg(test)] #[test] fn test_examples () -> Result<(), ParseError> {
|
||||
//// Let's pretend to render some view.
|
||||
//let source = include_str!("../../tek/src/view_arranger.edn");
|
||||
//// The token iterator allows you to get the tokens represented by the source text.
|
||||
//let mut view = TokenIter(source);
|
||||
//// The token iterator wraps a const token+source iterator.
|
||||
//assert_eq!(view.0.0, source);
|
||||
//let mut expr = view.peek();
|
||||
//assert_eq!(view.0.0, source);
|
||||
//assert_eq!(expr, Some(Token {
|
||||
//source, start: 0, length: source.len() - 1, value: Exp(0, SourceIter(&source[1..]))
|
||||
//}));
|
||||
////panic!("{view:?}");
|
||||
////panic!("{:#?}", expr);
|
||||
////for example in [
|
||||
////include_str!("../../tui/examples/edn01.edn"),
|
||||
////include_str!("../../tui/examples/edn02.edn"),
|
||||
////] {
|
||||
//////let items = Atom::read_all(example)?;
|
||||
//////panic!("{layout:?}");
|
||||
//////let content = <dyn ViewContext<::tek_engine::tui::Tui>>::from(&layout);
|
||||
////}
|
||||
//Ok(())
|
||||
//}
|
169
edn/src/token.rs
Normal file
169
edn/src/token.rs
Normal file
|
@ -0,0 +1,169 @@
|
|||
//! [Token]s are parsed substrings with an associated [Value].
|
||||
//!
|
||||
//! * [ ] FIXME: Value may be [Err] which may shadow [Result::Err]
|
||||
//! * [Value::Exp] wraps an expression depth and a [SourceIter]
|
||||
//! with the remaining part of the expression.
|
||||
//! * expression depth other that 0 mean unclosed parenthesis.
|
||||
//! * closing and unopened parenthesis panics during reading.
|
||||
//! * [ ] TODO: signed depth might be interesting
|
||||
//! * [Value::Sym] and [Value::Key] are stringish literals
|
||||
//! with slightly different parsing rules.
|
||||
//! * [Value::Num] is an unsigned integer literal.
|
||||
//!```
|
||||
//! use tek_edn::{*, Value::*};
|
||||
//! let source = include_str!("../test.edn");
|
||||
//! let mut view = TokenIter::new(source);
|
||||
//! assert_eq!(view.peek(), Some(Token {
|
||||
//! source,
|
||||
//! start: 0,
|
||||
//! length: source.len(),
|
||||
//! value: Exp(0, TokenIter::new(&source[1..]))
|
||||
//! }));
|
||||
//!```
|
||||
use crate::*;
|
||||
use self::Value::*;
|
||||
#[derive(Debug, Copy, Clone, Default, PartialEq)] pub struct Token<'a> {
|
||||
pub source: &'a str,
|
||||
pub start: usize,
|
||||
pub length: usize,
|
||||
pub value: Value<'a>,
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Default, PartialEq)] pub enum Value<'a> {
|
||||
#[default] Nil,
|
||||
Err(ParseError),
|
||||
Num(usize),
|
||||
Sym(&'a str),
|
||||
Key(&'a str),
|
||||
Exp(usize, TokenIter<'a>),
|
||||
}
|
||||
impl<'a> Token<'a> {
|
||||
pub const fn new (source: &'a str, start: usize, length: usize, value: Value<'a>) -> Self {
|
||||
Self { source, start, length, value }
|
||||
}
|
||||
pub const fn end (&self) -> usize {
|
||||
self.start.saturating_add(self.length)
|
||||
}
|
||||
pub const fn slice (&'a self) -> &'a str {
|
||||
self.slice_source(self.source)
|
||||
//str_range(self.source, self.start, self.end())
|
||||
}
|
||||
pub const fn slice_source <'b> (&'a self, source: &'b str) -> &'b str {
|
||||
str_range(source, self.start, self.end())
|
||||
}
|
||||
pub const fn slice_source_exp <'b> (&'a self, source: &'b str) -> &'b str {
|
||||
str_range(source, self.start.saturating_add(1), self.end())
|
||||
}
|
||||
pub const fn value (&self) -> Value {
|
||||
self.value
|
||||
}
|
||||
pub const fn error (self, error: ParseError) -> Self {
|
||||
Self { value: Value::Err(error), ..self }
|
||||
}
|
||||
pub const fn grow (self) -> Self {
|
||||
Self { length: self.length.saturating_add(1), ..self }
|
||||
}
|
||||
pub const fn grow_num (self, m: usize, c: char) -> Self {
|
||||
match to_digit(c) {
|
||||
Ok(n) => Self { value: Num(10*m+n), ..self.grow() },
|
||||
Result::Err(e) => Self { value: Err(e), ..self.grow() },
|
||||
}
|
||||
}
|
||||
pub const fn grow_key (self) -> Self {
|
||||
let mut token = self.grow();
|
||||
token.value = Key(token.slice_source(self.source));
|
||||
token
|
||||
}
|
||||
pub const fn grow_sym (self) -> Self {
|
||||
let mut token = self.grow();
|
||||
token.value = Sym(token.slice_source(self.source));
|
||||
token
|
||||
}
|
||||
pub const fn grow_exp (self) -> Self {
|
||||
let mut token = self.grow();
|
||||
if let Exp(depth, _) = token.value {
|
||||
token.value = Exp(depth, TokenIter::new(token.slice_source_exp(self.source)));
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
token
|
||||
}
|
||||
pub const fn grow_in (self) -> Self {
|
||||
let mut token = self.grow_exp();
|
||||
if let Value::Exp(depth, source) = token.value {
|
||||
token.value = Value::Exp(depth.saturating_add(1), source)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
token
|
||||
}
|
||||
pub const fn grow_out (self) -> Self {
|
||||
let mut token = self.grow_exp();
|
||||
if let Value::Exp(depth, source) = token.value {
|
||||
if depth > 0 {
|
||||
token.value = Value::Exp(depth - 1, source)
|
||||
} else {
|
||||
return self.error(Unexpected(')'))
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
#[cfg(test)] mod test_token_prop {
|
||||
use proptest::prelude::*;
|
||||
proptest! {
|
||||
#[test] fn test_token_prop (
|
||||
source in "\\PC*",
|
||||
start in usize::MIN..usize::MAX,
|
||||
length in usize::MIN..usize::MAX,
|
||||
) {
|
||||
let token = crate::Token {
|
||||
source: &source,
|
||||
start,
|
||||
length,
|
||||
value: crate::Value::Nil
|
||||
};
|
||||
let _ = token.slice();
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)] #[test] fn test_token () -> Result<(), Box<dyn std::error::Error>> {
|
||||
let source = ":f00";
|
||||
let mut token = Token { source, start: 0, length: 1, value: Sym(":") };
|
||||
token = token.grow_sym();
|
||||
assert_eq!(token, Token { source, start: 0, length: 2, value: Sym(":f") });
|
||||
token = token.grow_sym();
|
||||
assert_eq!(token, Token { source, start: 0, length: 3, value: Sym(":f0") });
|
||||
token = token.grow_sym();
|
||||
assert_eq!(token, Token { source, start: 0, length: 4, value: Sym(":f00") });
|
||||
|
||||
let src = "";
|
||||
assert_eq!(None, SourceIter(src).next());
|
||||
|
||||
let src = " \n \r \t ";
|
||||
assert_eq!(None, SourceIter(src).next());
|
||||
|
||||
let src = "7";
|
||||
assert_eq!(Num(7), SourceIter(src).next().unwrap().0.value);
|
||||
|
||||
let src = " 100 ";
|
||||
assert_eq!(Num(100), SourceIter(src).next().unwrap().0.value);
|
||||
|
||||
let src = " 9a ";
|
||||
assert_eq!(Err(Unexpected('a')), SourceIter(src).next().unwrap().0.value);
|
||||
|
||||
let src = " :123foo ";
|
||||
assert_eq!(Sym(":123foo"), SourceIter(src).next().unwrap().0.value);
|
||||
|
||||
let src = " \r\r\r\n\n\n@bar456\t\t\t\t\t\t";
|
||||
assert_eq!(Sym("@bar456"), SourceIter(src).next().unwrap().0.value);
|
||||
|
||||
let src = "foo123";
|
||||
assert_eq!(Key("foo123"), SourceIter(src).next().unwrap().0.value);
|
||||
|
||||
let src = "foo/bar";
|
||||
assert_eq!(Key("foo/bar"), SourceIter(src).next().unwrap().0.value);
|
||||
|
||||
Ok(())
|
||||
}
|
1
edn/test.edn
Normal file
1
edn/test.edn
Normal file
|
@ -0,0 +1 @@
|
|||
(bsp/n (fixed/y 2 :transport) (bsp/s (fixed/y 2 :status) (fill/xy (bsp/a (fill/xy (align/e :pool)) :arranger))))
|
7
input/Cargo.lock
generated
Normal file
7
input/Cargo.lock
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "tek_engine"
|
||||
version = "0.2.0"
|
10
input/Cargo.toml
Normal file
10
input/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "tek_input"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
tek_edn = { path = "../edn" }
|
||||
|
||||
[dev-dependencies]
|
||||
tek_tui = { path = "../tui" }
|
16
input/README.md
Normal file
16
input/README.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# `tek_engine`
|
||||
|
||||
## rendering
|
||||
|
||||
## input handling
|
||||
|
||||
the **input thread** polls for keyboard events
|
||||
and passes them onto the application's `Handle::handle` method.
|
||||
|
||||
thus, for a type to be a valid application for engine `E`,
|
||||
it must implement the trait `Handle<E>`, which allows it
|
||||
to respond to user input.
|
||||
|
||||
this thread has write access to the application state,
|
||||
and is responsible for mutating it in response to
|
||||
user activity.
|
28
input/src/command.rs
Normal file
28
input/src/command.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use crate::*;
|
||||
#[macro_export] macro_rules! command {
|
||||
($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
|
||||
impl$(<$($l),+>)? Command<$State> for $Command {
|
||||
fn execute ($self, $state: &mut $State) -> Perhaps<Self> {
|
||||
Ok($handler)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
pub trait Command<S>: Send + Sync + Sized {
|
||||
fn execute (self, state: &mut S) -> Perhaps<Self>;
|
||||
fn delegate <T> (self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T>
|
||||
where Self: Sized
|
||||
{
|
||||
Ok(self.execute(state)?.map(wrap))
|
||||
}
|
||||
}
|
||||
impl<S, T: Command<S>> Command<S> for Option<T> {
|
||||
fn execute (self, _: &mut S) -> Perhaps<Self> {
|
||||
Ok(None)
|
||||
}
|
||||
fn delegate <U> (self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps<U>
|
||||
where Self: Sized
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
}
|
73
input/src/event_map.rs
Normal file
73
input/src/event_map.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct EventMap<'a, S, I: PartialEq, C> {
|
||||
pub bindings: &'a [(I, &'a dyn Fn(&S) -> Option<C>)],
|
||||
pub fallback: Option<&'a dyn Fn(&S, &I) -> Option<C>>
|
||||
}
|
||||
|
||||
impl<'a, S, I: PartialEq, C> EventMap<'a, S, I, C> {
|
||||
pub fn handle (&self, state: &S, input: &I) -> Option<C> {
|
||||
for (binding, handler) in self.bindings.iter() {
|
||||
if input == binding {
|
||||
return handler(state)
|
||||
}
|
||||
}
|
||||
if let Some(fallback) = self.fallback {
|
||||
fallback(state, input)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! keymap {
|
||||
(
|
||||
$(<$lt:lifetime>)? $KEYS:ident = |$state:ident: $State:ty, $input:ident: $Input:ty| $Command:ty
|
||||
{ $($key:expr => $handler:expr),* $(,)? } $(,)?
|
||||
) => {
|
||||
pub const $KEYS: EventMap<'static, $State, $Input, $Command> = EventMap {
|
||||
fallback: None,
|
||||
bindings: &[ $(($key, &|$state|Some($handler)),)* ]
|
||||
};
|
||||
input_to_command!($(<$lt>)? $Command: |$state: $State, input: $Input|$KEYS.handle($state, input)?);
|
||||
};
|
||||
(
|
||||
$(<$lt:lifetime>)? $KEYS:ident = |$state:ident: $State:ty, $input:ident: $Input:ty| $Command:ty
|
||||
{ $($key:expr => $handler:expr),* $(,)? }, $default:expr
|
||||
) => {
|
||||
pub const $KEYS: EventMap<'static, $State, $Input, $Command> = EventMap {
|
||||
fallback: Some(&|$state, $input|Some($default)),
|
||||
bindings: &[ $(($key, &|$state|Some($handler)),)* ]
|
||||
};
|
||||
input_to_command!($(<$lt>)? $Command: |$state: $State, input: $Input|$KEYS.handle($state, input)?);
|
||||
};
|
||||
}
|
||||
#[macro_export] macro_rules! input_to_command {
|
||||
(<$($l:lifetime),+> $Command:ty: |$state:ident:$State:ty, $input:ident:$Input:ty| $handler:expr) => {
|
||||
impl<$($l),+> InputToCommand<$Input, $State> for $Command {
|
||||
fn input_to_command ($state: &$State, $input: &$Input) -> Option<Self> {
|
||||
Some($handler)
|
||||
}
|
||||
}
|
||||
};
|
||||
($Command:ty: |$state:ident:$State:ty, $input:ident:$Input:ty| $handler:expr) => {
|
||||
impl InputToCommand<$Input, $State> for $Command {
|
||||
fn input_to_command ($state: &$State, $input: &$Input) -> Option<Self> {
|
||||
Some($handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait InputToCommand<I, S>: Command<S> + Sized {
|
||||
fn input_to_command (state: &S, input: &I) -> Option<Self>;
|
||||
fn execute_with_state (state: &mut S, input: &I) -> Perhaps<bool> {
|
||||
Ok(if let Some(command) = Self::input_to_command(state, input) {
|
||||
let _undo = command.execute(state)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
75
input/src/input.rs
Normal file
75
input/src/input.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use crate::*;
|
||||
use std::sync::{Mutex, Arc, RwLock};
|
||||
|
||||
/// Event source
|
||||
pub trait Input: Send + Sync + Sized {
|
||||
/// Type of input event
|
||||
type Event;
|
||||
/// Result of handling input
|
||||
type Handled; // TODO: make this an Option<Box dyn Command<Self>> containing the undo
|
||||
/// Currently handled event
|
||||
fn event (&self) -> &Self::Event;
|
||||
/// Whether component should exit
|
||||
fn is_done (&self) -> bool;
|
||||
/// Mark component as done
|
||||
fn done (&self);
|
||||
}
|
||||
|
||||
/// Implement the [Handle] trait.
|
||||
#[macro_export] macro_rules! handle {
|
||||
(|$self:ident:$Struct:ty,$input:ident|$handler:expr) => {
|
||||
impl<E: Engine> Handle<E> for $Struct {
|
||||
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
|
||||
$handler
|
||||
}
|
||||
}
|
||||
};
|
||||
($E:ty: |$self:ident:$Struct:ty,$input:ident|$handler:expr) => {
|
||||
impl Handle<$E> for $Struct {
|
||||
fn handle (&mut $self, $input: &$E) -> Perhaps<<$E as Input>::Handled> {
|
||||
$handler
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle input
|
||||
pub trait Handle<E: Input>: Send + Sync {
|
||||
fn handle (&mut self, _input: &E) -> Perhaps<E::Handled> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
impl<E: Input, H: Handle<E>> Handle<E> for &mut H {
|
||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||
(*self).handle(context)
|
||||
}
|
||||
}
|
||||
impl<E: Input, H: Handle<E>> Handle<E> for Option<H> {
|
||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||
if let Some(ref mut handle) = self {
|
||||
handle.handle(context)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H, E: Input> Handle<E> for Mutex<H> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||
self.get_mut().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
impl<H, E: Input> Handle<E> for Arc<Mutex<H>> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||
self.lock().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
impl<H, E: Input> Handle<E> for RwLock<H> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||
self.write().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
impl<H, E: Input> Handle<E> for Arc<RwLock<H>> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||
self.write().unwrap().handle(context)
|
||||
}
|
||||
}
|
227
input/src/keymap.rs
Normal file
227
input/src/keymap.rs
Normal file
|
@ -0,0 +1,227 @@
|
|||
use crate::*;
|
||||
/// [Input] state that can be matched against a [Value].
|
||||
pub trait AtomInput: Input {
|
||||
fn matches_atom (&self, token: &str) -> bool;
|
||||
}
|
||||
pub trait KeyMap<'a> {
|
||||
/// Try to find a command that matches the current input event.
|
||||
fn command <S, C: AtomCommand<'a, S>, I: AtomInput> (&'a self, state: &'a S, input: &'a I)
|
||||
-> Option<C>;
|
||||
}
|
||||
impl<'a> KeyMap<'a> for SourceIter<'a> {
|
||||
fn command <S, C: AtomCommand<'a, S>, I: AtomInput> (&'a self, state: &'a S, input: &'a I)
|
||||
-> Option<C>
|
||||
{
|
||||
let mut iter = self.clone();
|
||||
while let Some((token, rest)) = iter.next() {
|
||||
iter = rest;
|
||||
match token {
|
||||
Token { value: Value::Exp(0, exp_iter), .. } => {
|
||||
let mut exp_iter = exp_iter.clone();
|
||||
match exp_iter.next() {
|
||||
Some(Token { value: Value::Sym(binding), .. }) => {
|
||||
if input.matches_atom(binding) {
|
||||
if let Some(command) = C::try_from_expr(state, exp_iter.clone()) {
|
||||
return Some(command)
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => panic!("invalid config (expected symbol)")
|
||||
}
|
||||
},
|
||||
_ => panic!("invalid config (expected expression)")
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
impl<'a> KeyMap<'a> for TokenIter<'a> {
|
||||
fn command <S, C: AtomCommand<'a, S>, I: AtomInput> (&'a self, state: &'a S, input: &'a I)
|
||||
-> Option<C>
|
||||
{
|
||||
let mut iter = self.clone();
|
||||
while let Some(next) = iter.next() {
|
||||
match next {
|
||||
Token { value: Value::Exp(0, exp_iter), .. } => {
|
||||
let mut exp_iter = exp_iter.clone();
|
||||
match exp_iter.next() {
|
||||
Some(Token { value: Value::Sym(binding), .. }) => {
|
||||
if input.matches_atom(binding) {
|
||||
if let Some(command) = C::try_from_expr(state, exp_iter.clone()) {
|
||||
return Some(command)
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => panic!("invalid config (expected symbol)")
|
||||
}
|
||||
},
|
||||
_ => panic!("invalid config (expected expression)")
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
/// A [Command] that can be constructed from a [Token].
|
||||
pub trait AtomCommand<'a, C>: TryFromAtom<'a, C> + Command<C> {}
|
||||
impl<'a, C, T: TryFromAtom<'a, C> + Command<C>> AtomCommand<'a, C> for T {}
|
||||
/** Implement `AtomCommand` for given `State` and `Command` */
|
||||
#[macro_export] macro_rules! atom_command {
|
||||
($Command:ty : |$state:ident:<$State:ident: $Trait:path>| { $((
|
||||
// identifier
|
||||
$key:literal [
|
||||
// named parameters
|
||||
$(
|
||||
// argument name
|
||||
$arg:ident
|
||||
// if type is not provided defaults to Atom
|
||||
$(
|
||||
// type:name separator
|
||||
:
|
||||
// argument type
|
||||
$type:ty
|
||||
)?
|
||||
),*
|
||||
// rest of parameters
|
||||
$(, ..$rest:ident)?
|
||||
]
|
||||
// bound command:
|
||||
$command:expr
|
||||
))* }) => {
|
||||
impl<'a, $State: $Trait> TryFromAtom<'a, $State> for $Command {
|
||||
fn try_from_expr ($state: &$State, iter: TokenIter) -> Option<Self> {
|
||||
let iter = iter.clone();
|
||||
match iter.next() {
|
||||
$(Some(Token { value: Value::Key($key), .. }) => {
|
||||
let iter = iter.clone();
|
||||
$(
|
||||
let next = iter.next();
|
||||
if next.is_none() { panic!("no argument: {}", stringify!($arg)); }
|
||||
let $arg = next.unwrap();
|
||||
$(let $arg: Option<$type> = Context::<$type>::get($state, &$arg.value);)?
|
||||
)*
|
||||
$(let $rest = iter.clone();)?
|
||||
return $command
|
||||
},)*
|
||||
_ => None
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
($Command:ty : |$state:ident:$State:ty| { $((
|
||||
// identifier
|
||||
$key:literal [
|
||||
// named parameters
|
||||
$(
|
||||
// argument name
|
||||
$arg:ident
|
||||
// if type is not provided defaults to Atom
|
||||
$(
|
||||
// type:name separator
|
||||
:
|
||||
// argument type
|
||||
$type:ty
|
||||
)?
|
||||
),*
|
||||
// rest of parameters
|
||||
$(, ..$rest:ident)?
|
||||
]
|
||||
// bound command:
|
||||
$command:expr
|
||||
))* }) => {
|
||||
impl<'a> TryFromAtom<'a, $State> for $Command {
|
||||
fn try_from_expr ($state: &$State, iter: TokenIter) -> Option<Self> {
|
||||
let mut iter = iter.clone();
|
||||
match iter.next() {
|
||||
$(Some(Token { value: Value::Key($key), .. }) => {
|
||||
let mut iter = iter.clone();
|
||||
$(
|
||||
let next = iter.next();
|
||||
if next.is_none() { panic!("no argument: {}", stringify!($arg)); }
|
||||
let $arg = next.unwrap();
|
||||
$(let $arg: Option<$type> = Context::<$type>::get($state, &$arg.value);)?
|
||||
)*
|
||||
$(let $rest = iter.clone();)?
|
||||
return $command
|
||||
}),*
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
(@bind $state:ident =>$arg:ident ? : $type:ty) => {
|
||||
let $arg: Option<$type> = Context::<$type>::get($state, $arg);
|
||||
};
|
||||
(@bind $state:ident => $arg:ident : $type:ty) => {
|
||||
let $arg: $type = Context::<$type>::get_or_fail($state, $arg);
|
||||
};
|
||||
}
|
||||
//pub struct SourceKeyMap<'a>(&'a str);
|
||||
//impl<'a> KeyMap for SourceKeyMap<'a> {
|
||||
//fn command <S, C: AtomCommand<S>> (&self, state: &S, input: &AtomInput) -> Option<C> {
|
||||
//todo!();
|
||||
//None
|
||||
//}
|
||||
//}
|
||||
//pub struct ParsedKeyMap<'a>(TokensIterator<'a>);
|
||||
//impl<'a> KeyMap for ParsedKeyMap<'a> {
|
||||
//fn command <S, C: AtomCommand<S>> (&self, state: &S, input: &AtomInput) -> Option<C> {
|
||||
//todo!();
|
||||
//None
|
||||
//}
|
||||
//}
|
||||
//pub struct RefKeyMap<'a>(TokensIterator<'a>);
|
||||
//impl<'a> KeyMap for RefKeyMap<'a> {
|
||||
//fn command <S, C: AtomCommand<S>> (&self, state: &S, input: &AtomInput) -> Option<C> {
|
||||
//todo!();
|
||||
////for token in self.0 {
|
||||
////match token?.kind() {
|
||||
////TokenKind::Exp => match atoms.as_slice() {
|
||||
////[key, command, args @ ..] => match (key.kind(), key.text()) {
|
||||
////(TokenKind::Sym, key) => {
|
||||
////if input.matches_atom(key) {
|
||||
////let command = C::from_atom(state, command, args);
|
||||
////if command.is_some() {
|
||||
////return command
|
||||
////}
|
||||
////}
|
||||
////},
|
||||
////_ => panic!("invalid config: {item}")
|
||||
////},
|
||||
////_ => panic!("invalid config: {item}")
|
||||
////}
|
||||
////_ => panic!("invalid config: {item}")
|
||||
////}
|
||||
////}
|
||||
//None
|
||||
//}
|
||||
//}
|
||||
//pub struct ArcKeyMap(Vec<ArcAtom>);
|
||||
//impl KeyMap for ArcKeyMap {
|
||||
//fn command <S, C: AtomCommand<S>> (&self, state: &S, input: &AtomInput) -> Option<C> {
|
||||
//for atom in self.0.iter() {
|
||||
//match atom {
|
||||
//ArcAtom::Exp(atoms) => match atoms.as_slice() {
|
||||
//[key, command, args @ ..] => match (key.kind(), key.text()) {
|
||||
//(TokenKind::Sym, key) => {
|
||||
//if input.matches_atom(key) {
|
||||
//let command = C::from_atom(state, command, args);
|
||||
//if command.is_some() {
|
||||
//return command
|
||||
//}
|
||||
//}
|
||||
//},
|
||||
//_ => panic!("invalid config: {atom}")
|
||||
//},
|
||||
//_ => panic!("invalid config: {atom}")
|
||||
//}
|
||||
//_ => panic!("invalid config: {atom}")
|
||||
//}
|
||||
//}
|
||||
//None
|
||||
//}
|
||||
//}
|
||||
#[cfg(test)] #[test] fn test_atom_keymap () -> Usually<()> {
|
||||
let keymap = SourceIter::new("");
|
||||
Ok(())
|
||||
}
|
32
input/src/lib.rs
Normal file
32
input/src/lib.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
#![feature(associated_type_defaults)]
|
||||
mod input; pub use self::input::*;
|
||||
mod command; pub use self::command::*;
|
||||
mod keymap; pub use self::keymap::*;
|
||||
//mod event_map; pub use self::event_map::*;
|
||||
pub(crate) use ::tek_edn::*;
|
||||
/// Standard error trait.
|
||||
pub(crate) use std::error::Error;
|
||||
/// Standard result type.
|
||||
#[cfg(test)] pub(crate) type Usually<T> = Result<T, Box<dyn Error>>;
|
||||
/// Standard optional result type.
|
||||
pub(crate) type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
||||
#[cfg(test)] #[test] fn test_stub_input () -> Usually<()> {
|
||||
use crate::*;
|
||||
struct TestInput(bool);
|
||||
enum TestEvent { Test1 }
|
||||
impl Input for TestInput {
|
||||
type Event = TestEvent;
|
||||
type Handled = ();
|
||||
fn event (&self) -> &Self::Event {
|
||||
&TestEvent::Test1
|
||||
}
|
||||
fn is_done (&self) -> bool {
|
||||
self.0
|
||||
}
|
||||
fn done (&self) {}
|
||||
}
|
||||
let _ = TestInput(true).event();
|
||||
assert!(TestInput(true).is_done());
|
||||
assert!(!TestInput(false).is_done());
|
||||
Ok(())
|
||||
}
|
14
output/Cargo.lock
generated
Normal file
14
output/Cargo.lock
generated
Normal file
|
@ -0,0 +1,14 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "tek_engine"
|
||||
version = "0.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "tek_layout"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"tek_engine",
|
||||
]
|
12
output/Cargo.toml
Normal file
12
output/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "tek_output"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
tek_edn = { path = "../edn" }
|
||||
|
||||
[dev-dependencies]
|
||||
tek_tui = { path = "../tui" }
|
||||
proptest = "^1"
|
||||
proptest-derive = "^0.5.1"
|
76
output/README.md
Normal file
76
output/README.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
# `tek_output`
|
||||
|
||||
## free floating layout primitives
|
||||
|
||||
this crate exposes several layout operators
|
||||
which work entirely in unsigned coordinates
|
||||
and are generic over `tek_engine::Engine`
|
||||
and `tek_engine::Content`. chiefly, they
|
||||
are not dependent on rendering framework.
|
||||
|
||||
|operator|description|
|
||||
|-|-|
|
||||
|**`When(x, a)`**|render `a` only when `x == true`|
|
||||
|**`Either(x, a, b)`**|render `a` when `x == true`, otherwise render `b`|
|
||||
|**`Map(get_iterator, callback)`**|transform items in uniform way|
|
||||
|**`Bsp`**|concatenative layout|
|
||||
|...|...|
|
||||
|**`Align`**|pin content along axis|
|
||||
|...|...|
|
||||
|**`Fill`**|**make content's dimension equal to container's:**|
|
||||
|`Fill::x(a)`|use container's width for content|
|
||||
|`Fill::y(a)`|use container's height for content|
|
||||
|`Fill::xy(a)`|use container's width and height for content|
|
||||
|**`Fixed`**|**assign fixed dimension to content:**|
|
||||
|`Fixed::x(w, a)`|use width `w` for content|
|
||||
|`Fixed::y(w, a)`|use height `w` for content|
|
||||
|`Fixed::xy(w, h, a)`|use width `w` and height `h` for content|
|
||||
|**`Expand`/`Shrink`**|**change dimension of content:**|
|
||||
|`Expand::x(n, a)`/`Shrink::x(n, a)`|increment/decrement width of content area by `n`|
|
||||
|`Expand::y(n, a)`/`Shrink::y(n, a)`|increment/decrement height of content area by `m`|
|
||||
|`Expand::xy(n, m, a)`/`Shrink::xy(n, m, a)`|increment/decrement width of content area by `n`, height by `m`|
|
||||
|**`Min`/`Max`**|**constrain dimension of content:**|
|
||||
|`Min::x(w, a)`/`Max::x(w, a)`|enforce minimum/maximum width `w` for content|
|
||||
|`Min::y(h, a)`/`Max::y(h, a)`|enforce minimum/maximum height `h` for content|
|
||||
|`Min::xy(w, h, a)`/`Max::xy(w, h, a)`|enforce minimum/maximum width `w` and height `h` for content|
|
||||
|**`Push`/`Pull`**|**move content along axis:**|
|
||||
|`Push::x(n, a)`/`Pull::x(n, a)`|increment/decrement `x` of content area|
|
||||
|`Push::y(n, a)`/`Pull::y(n, a)`|increment/decrement `y` of content area|
|
||||
|`Push::xy(n, m, a)`/`Pull::xy(n, m, a)`|increment/decrement `x` and `y` of content area|
|
||||
|
||||
**todo:**
|
||||
* sensible `Margin`/`Padding`
|
||||
* `Reduce`
|
||||
|
||||
## example rendering loop
|
||||
|
||||
the **render thread** continually invokes the
|
||||
`Content::render` method of the application
|
||||
to redraw the display. it does this efficiently
|
||||
by using ratatui's double buffering.
|
||||
|
||||
thus, for a type to be a valid application for engine `E`,
|
||||
it must implement the trait `Content<E>`, which allows
|
||||
it to display content to the engine's output.
|
||||
|
||||
the most important thing about the `Content` trait is that
|
||||
it composes:
|
||||
* you can implement `Content::content` to build
|
||||
`Content`s out of other `Content`s
|
||||
* and/or `Content::area` for custom positioning and sizing,
|
||||
* and/or `Content::render` for custom rendering
|
||||
within the given `Content`'s area.
|
||||
|
||||
the manner of output is determined by the
|
||||
`Engine::Output` type, a mutable pointer to which
|
||||
is passed to the render method, e.g. in the case of
|
||||
the `Tui` engine: `fn render(&self, output: &mut TuiOut)`
|
||||
|
||||
you can use `TuiOut::blit` and `TuiOut::place`
|
||||
to draw at specified coordinates of the display, and/or
|
||||
directly modify the underlying `ratatui::Buffer` at
|
||||
`output.buffer`
|
||||
|
||||
rendering is intended to work with read-only access
|
||||
to the application state. if you really need to update
|
||||
values during rendering, use interior mutability.
|
7
output/proptest-regressions/area.txt
Normal file
7
output/proptest-regressions/area.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc d2cd65ec39a1bf43c14bb2d3196c7e84ba854411360e570f06dd7ede62b0fd61 # shrinks to x = 0, y = 43998, w = 0, h = 43076, a = 0, b = 0
|
7
output/proptest-regressions/direction.txt
Normal file
7
output/proptest-regressions/direction.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 5b236150b286e479089d5bf6accc8ffbc3c0b0a1f955682af1987f342930d31e # shrinks to x = 0, y = 0, w = 0, h = 0, a = 1
|
10
output/proptest-regressions/op_transform.txt
Normal file
10
output/proptest-regressions/op_transform.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc b05b448ca4eb29304cae506927639494cae99a9e1ab40c58ac9dcb70d1ea1298 # shrinks to op_x = Some(0), op_y = None, content = "", x = 0, y = 46377, w = 0, h = 38318
|
||||
cc efdb7136c68396fa7c632cc6d3b304545ada1ba134269278f890639559a17575 # shrinks to op_x = Some(0), op_y = Some(32768), content = "", x = 0, y = 0, w = 0, h = 0
|
||||
cc f6d43c39db04f4c0112fe998ef68cff0a4454cd9791775a3014cc81997fbadf4 # shrinks to op_x = Some(10076), op_y = None, content = "", x = 60498, y = 0, w = 0, h = 0
|
||||
cc 3cabc97f3fa3a83fd5f8cf2c619ed213c2be5e9b1cb13e5178bde87dd838e2f4 # shrinks to op_x = Some(3924), op_y = None, content = "", x = 63574, y = 0, w = 0, h = 0
|
137
output/src/area.rs
Normal file
137
output/src/area.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
use crate::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub trait Area<N: Coordinate>: From<[N;4]> + Debug + Copy {
|
||||
fn x (&self) -> N;
|
||||
fn y (&self) -> N;
|
||||
fn w (&self) -> N;
|
||||
fn h (&self) -> N;
|
||||
#[inline] fn zero () -> [N;4] {
|
||||
[N::zero(), N::zero(), N::zero(), N::zero()]
|
||||
}
|
||||
#[inline] fn from_position (pos: impl Size<N>) -> [N;4] {
|
||||
let [x, y] = pos.wh();
|
||||
[x, y, 0.into(), 0.into()]
|
||||
}
|
||||
#[inline] fn from_size (size: impl Size<N>) -> [N;4] {
|
||||
let [w, h] = size.wh();
|
||||
[0.into(), 0.into(), w, h]
|
||||
}
|
||||
#[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
|
||||
if self.w() < w || self.h() < h {
|
||||
Err(format!("min {w}x{h}").into())
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
#[inline] fn xy (&self) -> [N;2] {
|
||||
[self.x(), self.y()]
|
||||
}
|
||||
#[inline] fn wh (&self) -> [N;2] {
|
||||
[self.w(), self.h()]
|
||||
}
|
||||
#[inline] fn xywh (&self) -> [N;4] {
|
||||
[self.x(), self.y(), self.w(), self.h()]
|
||||
}
|
||||
#[inline] fn clip_h (&self, h: N) -> [N;4] {
|
||||
[self.x(), self.y(), self.w(), self.h().min(h)]
|
||||
}
|
||||
#[inline] fn clip_w (&self, w: N) -> [N;4] {
|
||||
[self.x(), self.y(), self.w().min(w), self.h()]
|
||||
}
|
||||
#[inline] fn clip (&self, wh: impl Size<N>) -> [N;4] {
|
||||
[self.x(), self.y(), wh.w(), wh.h()]
|
||||
}
|
||||
#[inline] fn set_w (&self, w: N) -> [N;4] {
|
||||
[self.x(), self.y(), w, self.h()]
|
||||
}
|
||||
#[inline] fn set_h (&self, h: N) -> [N;4] {
|
||||
[self.x(), self.y(), self.w(), h]
|
||||
}
|
||||
#[inline] fn x2 (&self) -> N {
|
||||
self.x().plus(self.w())
|
||||
}
|
||||
#[inline] fn y2 (&self) -> N {
|
||||
self.y().plus(self.h())
|
||||
}
|
||||
#[inline] fn lrtb (&self) -> [N;4] {
|
||||
[self.x(), self.x2(), self.y(), self.y2()]
|
||||
}
|
||||
#[inline] fn center (&self) -> [N;2] {
|
||||
[self.x().plus(self.w()/2.into()), self.y().plus(self.h()/2.into())]
|
||||
}
|
||||
#[inline] fn center_x (&self, n: N) -> [N;4] {
|
||||
let [x, y, w, h] = self.xywh();
|
||||
[(x.plus(w / 2.into())).minus(n / 2.into()), y.plus(h / 2.into()), n, 1.into()]
|
||||
}
|
||||
#[inline] fn center_y (&self, n: N) -> [N;4] {
|
||||
let [x, y, w, h] = self.xywh();
|
||||
[x.plus(w / 2.into()), (y.plus(h / 2.into())).minus(n / 2.into()), 1.into(), n]
|
||||
}
|
||||
#[inline] fn center_xy (&self, [n, m]: [N;2]) -> [N;4] {
|
||||
let [x, y, w, h] = self.xywh();
|
||||
[(x.plus(w / 2.into())).minus(n / 2.into()), (y.plus(h / 2.into())).minus(m / 2.into()), n, m]
|
||||
}
|
||||
|
||||
#[inline] fn centered (&self) -> [N;2] {
|
||||
[self.x().minus(self.w()/2.into()), self.y().minus(self.h()/2.into())]
|
||||
}
|
||||
|
||||
fn iter_x (&self) -> impl Iterator<Item = N> where N: std::iter::Step {
|
||||
self.x()..(self.x()+self.w())
|
||||
}
|
||||
fn iter_y (&self) -> impl Iterator<Item = N> where N: std::iter::Step {
|
||||
self.y()..(self.y()+self.h())
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: Coordinate> Area<N> for (N, N, N, N) {
|
||||
#[inline] fn x (&self) -> N { self.0 }
|
||||
#[inline] fn y (&self) -> N { self.1 }
|
||||
#[inline] fn w (&self) -> N { self.2 }
|
||||
#[inline] fn h (&self) -> N { self.3 }
|
||||
}
|
||||
|
||||
impl<N: Coordinate> Area<N> for [N;4] {
|
||||
#[inline] fn x (&self) -> N { self[0] }
|
||||
#[inline] fn y (&self) -> N { self[1] }
|
||||
#[inline] fn w (&self) -> N { self[2] }
|
||||
#[inline] fn h (&self) -> N { self[3] }
|
||||
}
|
||||
|
||||
#[cfg(test)] mod test_area {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
proptest! {
|
||||
#[test] fn test_area_prop (
|
||||
x in u16::MIN..u16::MAX,
|
||||
y in u16::MIN..u16::MAX,
|
||||
w in u16::MIN..u16::MAX,
|
||||
h in u16::MIN..u16::MAX,
|
||||
a in u16::MIN..u16::MAX,
|
||||
b in u16::MIN..u16::MAX,
|
||||
) {
|
||||
let _: [u16;4] = <[u16;4] as Area<u16>>::zero();
|
||||
let _: [u16;4] = <[u16;4] as Area<u16>>::from_position([a, b]);
|
||||
let _: [u16;4] = <[u16;4] as Area<u16>>::from_size([a, b]);
|
||||
let area: [u16;4] = [x, y, w, h];
|
||||
let _ = area.expect_min(a, b);
|
||||
let _ = area.xy();
|
||||
let _ = area.wh();
|
||||
let _ = area.xywh();
|
||||
let _ = area.clip_h(a);
|
||||
let _ = area.clip_w(b);
|
||||
let _ = area.clip([a, b]);
|
||||
let _ = area.set_w(a);
|
||||
let _ = area.set_h(b);
|
||||
let _ = area.x2();
|
||||
let _ = area.y2();
|
||||
let _ = area.lrtb();
|
||||
let _ = area.center();
|
||||
let _ = area.center_x(a);
|
||||
let _ = area.center_y(b);
|
||||
let _ = area.center_xy([a, b]);
|
||||
let _ = area.centered();
|
||||
}
|
||||
}
|
||||
}
|
23
output/src/collection.rs
Normal file
23
output/src/collection.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
//! Groupings of elements.
|
||||
use crate::*;
|
||||
|
||||
/// A function or closure that emits renderables.
|
||||
pub trait Collector<E: Engine>: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)) {}
|
||||
|
||||
/// Any function or closure that emits renderables for the given engine matches [CollectCallback].
|
||||
impl<E, F> Collector<E> for F
|
||||
where E: Engine, F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)) {}
|
||||
|
||||
pub trait Render<E: Engine> {
|
||||
fn area (&self, to: E::Area) -> E::Area;
|
||||
fn render (&self, to: &mut E::Output);
|
||||
}
|
||||
|
||||
impl<E: Engine, C: Content<E>> Render<E> for C {
|
||||
fn area (&self, to: E::Area) -> E::Area {
|
||||
Content::area(self, to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) {
|
||||
Content::render(self, to)
|
||||
}
|
||||
}
|
31
output/src/coordinate.rs
Normal file
31
output/src/coordinate.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use std::fmt::{Debug, Display};
|
||||
use std::ops::{Add, Sub, Mul, Div};
|
||||
|
||||
impl Coordinate for u16 {
|
||||
#[inline] fn plus (self, other: Self) -> Self {
|
||||
self.saturating_add(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// A linear coordinate.
|
||||
pub trait Coordinate: Send + Sync + Copy
|
||||
+ Add<Self, Output=Self>
|
||||
+ Sub<Self, Output=Self>
|
||||
+ Mul<Self, Output=Self>
|
||||
+ Div<Self, Output=Self>
|
||||
+ Ord + PartialEq + Eq
|
||||
+ Debug + Display + Default
|
||||
+ From<u16> + Into<u16>
|
||||
+ Into<usize>
|
||||
+ Into<f64>
|
||||
{
|
||||
#[inline] fn zero () -> Self { 0.into() }
|
||||
#[inline] fn minus (self, other: Self) -> Self {
|
||||
if self >= other {
|
||||
self - other
|
||||
} else {
|
||||
0.into()
|
||||
}
|
||||
}
|
||||
fn plus (self, other: Self) -> Self;
|
||||
}
|
38
output/src/direction.rs
Normal file
38
output/src/direction.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use crate::*;
|
||||
#[cfg(test)] use proptest_derive::Arbitrary;
|
||||
/// A cardinal direction.
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
#[cfg_attr(test, derive(Arbitrary))]
|
||||
pub enum Direction { North, South, East, West, Above, Below }
|
||||
impl Direction {
|
||||
pub fn split_fixed <N: Coordinate> (self, area: impl Area<N>, a: N) -> ([N;4],[N;4]) {
|
||||
let [x, y, w, h] = area.xywh();
|
||||
match self {
|
||||
North => ([x, y.plus(h).minus(a), w, a], [x, y, w, h.minus(a)]),
|
||||
South => ([x, y, w, a], [x, y.plus(a), w, h.minus(a)]),
|
||||
East => ([x, y, a, h], [x.plus(a), y, w.minus(a), h]),
|
||||
West => ([x.plus(w).minus(a), y, a, h], [x, y, w.minus(a), h]),
|
||||
Above | Below => (area.xywh(), area.xywh())
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)] mod test {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
proptest! {
|
||||
#[test] fn proptest_direction (
|
||||
d in prop_oneof![
|
||||
Just(North), Just(South),
|
||||
Just(East), Just(West),
|
||||
Just(Above), Just(Below)
|
||||
],
|
||||
x in u16::MIN..u16::MAX,
|
||||
y in u16::MIN..u16::MAX,
|
||||
w in u16::MIN..u16::MAX,
|
||||
h in u16::MIN..u16::MAX,
|
||||
a in u16::MIN..u16::MAX,
|
||||
) {
|
||||
let _ = d.split_fixed([x, y, w, h], a);
|
||||
}
|
||||
}
|
||||
}
|
55
output/src/lib.rs
Normal file
55
output/src/lib.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
//#![feature(lazy_type_alias)]
|
||||
#![feature(step_trait)]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(impl_trait_in_assoc_type)]
|
||||
mod direction; pub use self::direction::*;
|
||||
mod coordinate; pub use self::coordinate::*;
|
||||
mod size; pub use self::size::*;
|
||||
mod area; pub use self::area::*;
|
||||
mod output; pub use self::output::*;
|
||||
mod measure; pub use self::measure::*;
|
||||
mod thunk; pub use self::thunk::*;
|
||||
mod op_cond; pub use self::op_cond::*;
|
||||
mod op_iter; pub use self::op_iter::*;
|
||||
mod op_align; pub use self::op_align::*;
|
||||
mod op_bsp; pub use self::op_bsp::*;
|
||||
mod op_transform; pub use self::op_transform::*;
|
||||
mod view; pub use self::view::*;
|
||||
pub(crate) use std::marker::PhantomData;
|
||||
pub(crate) use std::error::Error;
|
||||
pub(crate) use ::tek_edn::*;
|
||||
/// Standard result type.
|
||||
pub type Usually<T> = Result<T, Box<dyn Error>>;
|
||||
/// Standard optional result type.
|
||||
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
||||
#[cfg(test)] #[test] fn test_stub_output () -> Usually<()> {
|
||||
use crate::*;
|
||||
struct TestOutput([u16;4]);
|
||||
impl Output for TestOutput {
|
||||
type Unit = u16;
|
||||
type Size = [u16;2];
|
||||
type Area = [u16;4];
|
||||
fn area (&self) -> [u16;4] {
|
||||
self.0
|
||||
}
|
||||
fn area_mut (&mut self) -> &mut [u16;4] {
|
||||
&mut self.0
|
||||
}
|
||||
fn place (&mut self, _: [u16;4], _: &impl Render<TestOutput>) {
|
||||
()
|
||||
}
|
||||
}
|
||||
impl Content<TestOutput> for String {
|
||||
fn render (&self, to: &mut TestOutput) {
|
||||
to.area_mut().set_w(self.len() as u16);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(test)] #[test] fn test_dimensions () {
|
||||
use crate::*;
|
||||
assert_eq!(Area::center(&[10u16, 10, 20, 20]), [20, 20]);
|
||||
}
|
||||
#[cfg(test)] #[test] fn test_layout () -> Usually<()> {
|
||||
Ok(())
|
||||
}
|
144
output/src/measure.rs
Normal file
144
output/src/measure.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
use crate::*;
|
||||
use std::sync::{Arc, atomic::{AtomicUsize, Ordering::Relaxed}};
|
||||
//use ratatui::prelude::{Style, Color};
|
||||
// TODO: 🡘 🡙 ←🡙→ indicator to expand window when too small
|
||||
pub trait HasSize<E: Output> {
|
||||
fn size (&self) -> &Measure<E>;
|
||||
}
|
||||
#[macro_export] macro_rules! has_size {
|
||||
(<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasSize<$E> for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn size (&$self) -> &Measure<$E> { $cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A widget that tracks its render width and height
|
||||
#[derive(Default)]
|
||||
pub struct Measure<E: Output> {
|
||||
_engine: PhantomData<E>,
|
||||
pub x: Arc<AtomicUsize>,
|
||||
pub y: Arc<AtomicUsize>,
|
||||
}
|
||||
impl<E: Output> Content<E> for Measure<E> {
|
||||
fn render (&self, to: &mut E) {
|
||||
self.x.store(to.area().w().into(), Relaxed);
|
||||
self.y.store(to.area().h().into(), Relaxed);
|
||||
}
|
||||
}
|
||||
impl<E: Output> Clone for Measure<E> {
|
||||
fn clone (&self) -> Self {
|
||||
Self {
|
||||
_engine: Default::default(),
|
||||
x: self.x.clone(),
|
||||
y: self.y.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<E: Output> std::fmt::Debug for Measure<E> {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("Measure")
|
||||
.field("width", &self.x)
|
||||
.field("height", &self.y)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
impl<E: Output> Measure<E> {
|
||||
pub fn new () -> Self {
|
||||
Self {
|
||||
_engine: PhantomData::default(),
|
||||
x: Arc::new(0.into()),
|
||||
y: Arc::new(0.into()),
|
||||
}
|
||||
}
|
||||
pub fn set_w (&self, w: impl Into<usize>) -> &Self {
|
||||
self.x.store(w.into(), Relaxed);
|
||||
self
|
||||
}
|
||||
pub fn set_h (&self, h: impl Into<usize>) -> &Self {
|
||||
self.y.store(h.into(), Relaxed);
|
||||
self
|
||||
}
|
||||
pub fn set_wh (&self, w: impl Into<usize>, h: impl Into<usize>) -> &Self {
|
||||
self.set_w(w);
|
||||
self.set_h(h);
|
||||
self
|
||||
}
|
||||
pub fn w (&self) -> usize {
|
||||
self.x.load(Relaxed)
|
||||
}
|
||||
pub fn h (&self) -> usize {
|
||||
self.y.load(Relaxed)
|
||||
}
|
||||
pub fn wh (&self) -> [usize;2] {
|
||||
[self.w(), self.h()]
|
||||
}
|
||||
pub fn format (&self) -> Arc<str> {
|
||||
format!("{}x{}", self.w(), self.h()).into()
|
||||
}
|
||||
pub fn of <T: Content<E>> (&self, item: T) -> Bsp<Fill<&Self>, T> {
|
||||
Bsp::b(Fill::xy(self), item)
|
||||
}
|
||||
}
|
||||
//#[cfg(test)] #[test] fn test_measure () {
|
||||
//use tek_tui::*;
|
||||
//let size: Measure<TuiOut> = Measure::default().set_w(1usize).set_h(1usize).clone();
|
||||
//let size: Measure<TuiOut> = (&Measure::new().set_wh(2usize, 1usize)).clone();
|
||||
//let _ = format!("{:?}", &size);
|
||||
//let _ = size.wh();
|
||||
//let _ = size.format();
|
||||
//let _ = size.of(());
|
||||
//}
|
||||
|
||||
///// A scrollable area.
|
||||
//pub struct Scroll<E, F>(pub F, pub Direction, pub u64, PhantomData<E>)
|
||||
//where
|
||||
//E: Output,
|
||||
//F: Send + Sync + Fn(&mut dyn FnMut(&dyn Content<E>)->Usually<()>)->Usually<()>;
|
||||
|
||||
//pub trait ContentDebug<E: Output> {
|
||||
//fn debug <W: Content<E>> (other: W) -> DebugOverlay<E, W> {
|
||||
//DebugOverlay(Default::default(), other)
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl<E: Output> ContentDebug<E> for E {}
|
||||
|
||||
//impl Render<TuiOut> for Measure<TuiOut> {
|
||||
//fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
//Ok(Some([0u16.into(), 0u16.into()].into()))
|
||||
//}
|
||||
//fn render (&self, to: &mut TuiOut) -> Usually<()> {
|
||||
//self.set_w(to.area().w());
|
||||
//self.set_h(to.area().h());
|
||||
//Ok(())
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl Measure<TuiOut> {
|
||||
//pub fn debug (&self) -> ShowMeasure {
|
||||
//ShowMeasure(&self)
|
||||
//}
|
||||
//}
|
||||
|
||||
//render!(Tui: |self: ShowMeasure<'a>|render(|to: &mut TuiOut|Ok({
|
||||
//let w = self.0.w();
|
||||
//let h = self.0.h();
|
||||
//to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some(
|
||||
//Style::default().bold().italic().bg(Color::Rgb(255, 0, 255)).fg(Color::Rgb(0,0,0))
|
||||
//))
|
||||
//})));
|
||||
|
||||
//pub struct ShowMeasure<'a>(&'a Measure<TuiOut>);
|
||||
|
||||
//pub struct DebugOverlay<E: Output, W: Render<E>>(PhantomData<E>, pub W);
|
||||
|
||||
//impl<T: Render<TuiOut>> Render<TuiOut> for DebugOverlay<Tui, T> {
|
||||
//fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
//self.1.min_size(to)
|
||||
//}
|
||||
//fn render (&self, to: &mut TuiOut) -> Usually<()> {
|
||||
//let [x, y, w, h] = to.area();
|
||||
//self.1.render(to)?;
|
||||
//Ok(to.blit(&format!("{w}x{h}+{x}+{y}"), x, y, Some(Style::default().green())))
|
||||
//}
|
||||
//}
|
102
output/src/op_align.rs
Normal file
102
output/src/op_align.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
//! Aligns things to the container. Comes with caveats.
|
||||
//! ```
|
||||
//! use ::tek_tui::{*, tek_output::*};
|
||||
//! let area: [u16;4] = [10, 10, 20, 20];
|
||||
//! fn test (area: [u16;4], item: &impl Content<TuiOut>, expected: [u16;4]) {
|
||||
//! assert_eq!(Content::layout(item, area), expected);
|
||||
//! assert_eq!(Render::layout(item, area), expected);
|
||||
//! };
|
||||
//!
|
||||
//! let four = ||Fixed::xy(4, 4, "");
|
||||
//! test(area, &Align::nw(four()), [10, 10, 4, 4]);
|
||||
//! test(area, &Align::n(four()), [18, 10, 4, 4]);
|
||||
//! test(area, &Align::ne(four()), [26, 10, 4, 4]);
|
||||
//! test(area, &Align::e(four()), [26, 18, 4, 4]);
|
||||
//! test(area, &Align::se(four()), [26, 26, 4, 4]);
|
||||
//! test(area, &Align::s(four()), [18, 26, 4, 4]);
|
||||
//! test(area, &Align::sw(four()), [10, 26, 4, 4]);
|
||||
//! test(area, &Align::w(four()), [10, 18, 4, 4]);
|
||||
//!
|
||||
//! let two_by_four = ||Fixed::xy(4, 2, "");
|
||||
//! test(area, &Align::nw(two_by_four()), [10, 10, 4, 2]);
|
||||
//! test(area, &Align::n(two_by_four()), [18, 10, 4, 2]);
|
||||
//! test(area, &Align::ne(two_by_four()), [26, 10, 4, 2]);
|
||||
//! test(area, &Align::e(two_by_four()), [26, 19, 4, 2]);
|
||||
//! test(area, &Align::se(two_by_four()), [26, 28, 4, 2]);
|
||||
//! test(area, &Align::s(two_by_four()), [18, 28, 4, 2]);
|
||||
//! test(area, &Align::sw(two_by_four()), [10, 28, 4, 2]);
|
||||
//! test(area, &Align::w(two_by_four()), [10, 19, 4, 2]);
|
||||
//! ```
|
||||
use crate::*;
|
||||
#[derive(Debug, Copy, Clone, Default)] pub enum Alignment { #[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W }
|
||||
pub struct Align<A>(Alignment, A);
|
||||
try_from_expr!(<'a, E>: Align<RenderBox<'a, E>>: |state, iter|
|
||||
if let Some(Token { value: Value::Key(key), .. }) = iter.peek() {
|
||||
match key {
|
||||
"align/c"|"align/x"|"align/y"|
|
||||
"align/n"|"align/s"|"align/e"|"align/w"|
|
||||
"align/nw"|"align/sw"|"align/ne"|"align/se" => {
|
||||
let _ = iter.next().unwrap();
|
||||
let c = iter.next().expect("no content specified");
|
||||
let c = state.get_content(&c.value).expect("no content provided");
|
||||
return Some(match key {
|
||||
"align/c" => Self::c(c),
|
||||
"align/x" => Self::x(c),
|
||||
"align/y" => Self::y(c),
|
||||
"align/n" => Self::n(c),
|
||||
"align/s" => Self::s(c),
|
||||
"align/e" => Self::e(c),
|
||||
"align/w" => Self::w(c),
|
||||
"align/nw" => Self::nw(c),
|
||||
"align/ne" => Self::ne(c),
|
||||
"align/sw" => Self::sw(c),
|
||||
"align/se" => Self::se(c),
|
||||
_ => unreachable!()
|
||||
})
|
||||
},
|
||||
_ => return None
|
||||
}
|
||||
});
|
||||
impl<A> Align<A> {
|
||||
#[inline] pub fn c (a: A) -> Self { Self(Alignment::Center, a) }
|
||||
#[inline] pub fn x (a: A) -> Self { Self(Alignment::X, a) }
|
||||
#[inline] pub fn y (a: A) -> Self { Self(Alignment::Y, a) }
|
||||
#[inline] pub fn n (a: A) -> Self { Self(Alignment::N, a) }
|
||||
#[inline] pub fn s (a: A) -> Self { Self(Alignment::S, a) }
|
||||
#[inline] pub fn e (a: A) -> Self { Self(Alignment::E, a) }
|
||||
#[inline] pub fn w (a: A) -> Self { Self(Alignment::W, a) }
|
||||
#[inline] pub fn nw (a: A) -> Self { Self(Alignment::NW, a) }
|
||||
#[inline] pub fn sw (a: A) -> Self { Self(Alignment::SW, a) }
|
||||
#[inline] pub fn ne (a: A) -> Self { Self(Alignment::NE, a) }
|
||||
#[inline] pub fn se (a: A) -> Self { Self(Alignment::SE, a) }
|
||||
}
|
||||
impl<E: Output, A: Content<E>> Content<E> for Align<A> {
|
||||
fn content (&self) -> impl Render<E> {
|
||||
&self.1
|
||||
}
|
||||
fn layout (&self, on: E::Area) -> E::Area {
|
||||
use Alignment::*;
|
||||
let it = Render::layout(&self.content(), on).xywh();
|
||||
let cx = on.x()+(on.w().minus(it.w())/2.into());
|
||||
let cy = on.y()+(on.h().minus(it.h())/2.into());
|
||||
let fx = (on.x()+on.w()).minus(it.w());
|
||||
let fy = (on.y()+on.h()).minus(it.h());
|
||||
let [x, y] = match self.0 {
|
||||
Center => [cx, cy],
|
||||
X => [cx, it.y()],
|
||||
Y => [it.x(), cy],
|
||||
NW => [on.x(), on.y()],
|
||||
N => [cx, on.y()],
|
||||
NE => [fx, on.y()],
|
||||
W => [on.x(), cy],
|
||||
E => [fx, cy],
|
||||
SW => [on.x(), fy],
|
||||
S => [cx, fy],
|
||||
SE => [fx, fy],
|
||||
}.into();
|
||||
[x, y, it.w(), it.h()].into()
|
||||
}
|
||||
fn render (&self, to: &mut E) {
|
||||
to.place(Content::layout(self, to.area()), &self.content())
|
||||
}
|
||||
}
|
143
output/src/op_bsp.rs
Normal file
143
output/src/op_bsp.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
use crate::*;
|
||||
pub use Direction::*;
|
||||
/// A split or layer.
|
||||
pub struct Bsp<X, Y>(Direction, X, Y);
|
||||
impl<E: Output, A: Content<E>, B: Content<E>> Content<E> for Bsp<A, B> {
|
||||
fn layout (&self, outer: E::Area) -> E::Area {
|
||||
let [_, _, c] = self.areas(outer);
|
||||
c
|
||||
}
|
||||
fn render (&self, to: &mut E) {
|
||||
let [area_a, area_b, _] = self.areas(to.area());
|
||||
let (a, b) = self.contents();
|
||||
match self.0 {
|
||||
Below => { to.place(area_a, a); to.place(area_b, b); },
|
||||
_ => { to.place(area_b, b); to.place(area_a, a); }
|
||||
}
|
||||
}
|
||||
}
|
||||
try_from_expr!(<'a, E>: Bsp<RenderBox<'a, E>, RenderBox<'a, E>>: |state, iter| {
|
||||
if let Some(Token { value: Value::Key(key), .. }) = iter.peek() {
|
||||
match key {
|
||||
"bsp/n"|"bsp/s"|"bsp/e"|"bsp/w"|"bsp/a"|"bsp/b" => {
|
||||
let _ = iter.next().unwrap();
|
||||
let c1 = iter.next().expect("no content1 specified");
|
||||
let c2 = iter.next().expect("no content2 specified");
|
||||
let c1 = state.get_content(&c1.value).expect("no content1 provided");
|
||||
let c2 = state.get_content(&c2.value).expect("no content2 provided");
|
||||
return Some(match key {
|
||||
"bsp/n" => Self::n(c1, c2),
|
||||
"bsp/s" => Self::s(c1, c2),
|
||||
"bsp/e" => Self::e(c1, c2),
|
||||
"bsp/w" => Self::w(c1, c2),
|
||||
"bsp/a" => Self::a(c1, c2),
|
||||
"bsp/b" => Self::b(c1, c2),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
},
|
||||
_ => return None
|
||||
}
|
||||
}
|
||||
});
|
||||
impl<A, B> Bsp<A, B> {
|
||||
#[inline] pub fn n (a: A, b: B) -> Self { Self(North, a, b) }
|
||||
#[inline] pub fn s (a: A, b: B) -> Self { Self(South, a, b) }
|
||||
#[inline] pub fn e (a: A, b: B) -> Self { Self(East, a, b) }
|
||||
#[inline] pub fn w (a: A, b: B) -> Self { Self(West, a, b) }
|
||||
#[inline] pub fn a (a: A, b: B) -> Self { Self(Above, a, b) }
|
||||
#[inline] pub fn b (a: A, b: B) -> Self { Self(Below, a, b) }
|
||||
}
|
||||
pub trait BspAreas<E: Output, A: Content<E>, B: Content<E>> {
|
||||
fn direction (&self) -> Direction;
|
||||
fn contents (&self) -> (&A, &B);
|
||||
fn areas (&self, outer: E::Area) -> [E::Area;3] {
|
||||
let direction = self.direction();
|
||||
let [x, y, w, h] = outer.xywh();
|
||||
let (a, b) = self.contents();
|
||||
let [aw, ah] = a.layout(outer).wh();
|
||||
let [bw, bh] = b.layout(match direction {
|
||||
Above | Below => outer,
|
||||
South => [x, y + ah, w, h.minus(ah)].into(),
|
||||
North => [x, y, w, h.minus(ah)].into(),
|
||||
East => [x + aw, y, w.minus(aw), h].into(),
|
||||
West => [x, y, w.minus(aw), h].into(),
|
||||
}).wh();
|
||||
match direction {
|
||||
Above | Below => {
|
||||
let [x, y, w, h] = outer.center_xy([aw.max(bw), ah.max(bh)]);
|
||||
let a = [(x + w/2.into()).minus(aw/2.into()), (y + h/2.into()).minus(ah/2.into()), aw, ah];
|
||||
let b = [(x + w/2.into()).minus(bw/2.into()), (y + h/2.into()).minus(bh/2.into()), bw, bh];
|
||||
[a.into(), b.into(), [x, y, w, h].into()]
|
||||
},
|
||||
South => {
|
||||
let [x, y, w, h] = outer.center_xy([aw.max(bw), ah + bh]);
|
||||
let a = [(x + w/2.into()).minus(aw/2.into()), y, aw, ah];
|
||||
let b = [(x + w/2.into()).minus(bw/2.into()), y + ah, bw, bh];
|
||||
[a.into(), b.into(), [x, y, w, h].into()]
|
||||
},
|
||||
North => {
|
||||
let [x, y, w, h] = outer.center_xy([aw.max(bw), ah + bh]);
|
||||
let a = [(x + (w/2.into())).minus(aw/2.into()), y + bh, aw, ah];
|
||||
let b = [(x + (w/2.into())).minus(bw/2.into()), y, bw, bh];
|
||||
[a.into(), b.into(), [x, y, w, h].into()]
|
||||
},
|
||||
East => {
|
||||
let [x, y, w, h] = outer.center_xy([aw + bw, ah.max(bh)]);
|
||||
let a = [x, (y + h/2.into()).minus(ah/2.into()), aw, ah];
|
||||
let b = [x + aw, (y + h/2.into()).minus(bh/2.into()), bw, bh];
|
||||
[a.into(), b.into(), [x, y, w, h].into()]
|
||||
},
|
||||
West => {
|
||||
let [x, y, w, h] = outer.center_xy([aw + bw, ah.max(bh)]);
|
||||
let a = [x + bw, (y + h/2.into()).minus(ah/2.into()), aw, ah];
|
||||
let b = [x, (y + h/2.into()).minus(bh/2.into()), bw, bh];
|
||||
[a.into(), b.into(), [x, y, w, h].into()]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<E: Output, A: Content<E>, B: Content<E>> BspAreas<E, A, B> for Bsp<A, B> {
|
||||
fn direction (&self) -> Direction { self.0 }
|
||||
fn contents (&self) -> (&A, &B) { (&self.1, &self.2) }
|
||||
}
|
||||
/// Renders multiple things on top of each other,
|
||||
#[macro_export] macro_rules! lay {
|
||||
($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)*; bsp }}
|
||||
}
|
||||
/// Stack southward.
|
||||
#[macro_export] macro_rules! col {
|
||||
($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)*; bsp }};
|
||||
}
|
||||
/// Stack northward.
|
||||
#[macro_export] macro_rules! col_up {
|
||||
($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)*; bsp }}
|
||||
}
|
||||
/// Stack eastward.
|
||||
#[macro_export] macro_rules! row {
|
||||
($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)*; bsp }};
|
||||
}
|
||||
#[cfg(test)] mod test {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
proptest! {
|
||||
#[test] fn proptest_op_bsp (
|
||||
d in prop_oneof![
|
||||
Just(North), Just(South),
|
||||
Just(East), Just(West),
|
||||
Just(Above), Just(Below)
|
||||
],
|
||||
a in "\\PC*",
|
||||
b in "\\PC*",
|
||||
x in u16::MIN..u16::MAX,
|
||||
y in u16::MIN..u16::MAX,
|
||||
w in u16::MIN..u16::MAX,
|
||||
h in u16::MIN..u16::MAX,
|
||||
) {
|
||||
let bsp = Bsp(d, a, b);
|
||||
assert_eq!(
|
||||
Content::layout(&bsp, [x, y, w, h]),
|
||||
Render::layout(&bsp, [x, y, w, h]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
57
output/src/op_cond.rs
Normal file
57
output/src/op_cond.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use crate::*;
|
||||
/// Show an item only when a condition is true.
|
||||
pub struct When<A>(pub bool, pub A);
|
||||
impl<A> When<A> { #[inline] pub fn new (c: bool, a: A) -> Self { Self(c, a) } }
|
||||
/// Show one item if a condition is true and another if the condition is false
|
||||
pub struct Either<A, B>(pub bool, pub A, pub B);
|
||||
impl<A, B> Either<A, B> { #[inline] pub fn new (c: bool, a: A, b: B) -> Self { Self(c, a, b) } }
|
||||
try_from_expr!(<'a, E>: When<RenderBox<'a, E>>: |state, iter| {
|
||||
if let Some(Token { value: Value::Key("when"), .. }) = iter.peek() {
|
||||
let _ = iter.next().unwrap();
|
||||
let condition = iter.next().expect("no condition specified");
|
||||
let content = iter.next().expect("no content specified");
|
||||
let condition = state.get(&condition.value).expect("no condition provided");
|
||||
let content = state.get_content(&content.value).expect("no content provided");
|
||||
return Some(Self(condition, content))
|
||||
}
|
||||
});
|
||||
try_from_expr!(<'a, E>: Either<RenderBox<'a, E>, RenderBox<'a, E>>: |state, iter| {
|
||||
if let Some(Token { value: Value::Key("either"), .. }) = iter.peek() {
|
||||
let _ = iter.next().unwrap();
|
||||
let condition = iter.next().expect("no condition specified");
|
||||
let content = iter.next().expect("no content specified");
|
||||
let alternate = iter.next().expect("no alternate specified");
|
||||
let condition = state.get(&condition.value).expect("no condition provided");
|
||||
let content = state.get_content(&content.value).expect("no content provided");
|
||||
let alternate = state.get_content(&alternate.value).expect("no alternate provided");
|
||||
return Some(Self(condition, content, alternate))
|
||||
}
|
||||
});
|
||||
impl<E: Output, A: Render<E>> Content<E> for When<A> {
|
||||
fn layout (&self, to: E::Area) -> E::Area {
|
||||
let Self(cond, item) = self;
|
||||
let mut area = E::Area::zero();
|
||||
if *cond {
|
||||
let item_area = item.layout(to);
|
||||
area[0] = item_area.x();
|
||||
area[1] = item_area.y();
|
||||
area[2] = item_area.w();
|
||||
area[3] = item_area.h();
|
||||
}
|
||||
area.into()
|
||||
}
|
||||
fn render (&self, to: &mut E) {
|
||||
let Self(cond, item) = self;
|
||||
if *cond { item.render(to) }
|
||||
}
|
||||
}
|
||||
impl<E: Output, A: Render<E>, B: Render<E>> Content<E> for Either<A, B> {
|
||||
fn layout (&self, to: E::Area) -> E::Area {
|
||||
let Self(cond, a, b) = self;
|
||||
if *cond { a.layout(to) } else { b.layout(to) }
|
||||
}
|
||||
fn render (&self, to: &mut E) {
|
||||
let Self(cond, a, b) = self;
|
||||
if *cond { a.render(to) } else { b.render(to) }
|
||||
}
|
||||
}
|
77
output/src/op_iter.rs
Normal file
77
output/src/op_iter.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use crate::*;
|
||||
#[inline] pub fn map_south<O: Output>(
|
||||
item_offset: O::Unit,
|
||||
item_height: O::Unit,
|
||||
item: impl Content<O>
|
||||
) -> impl Content<O> {
|
||||
Push::y(item_offset, Fixed::y(item_height, Fill::x(item)))
|
||||
}
|
||||
|
||||
#[inline] pub fn map_south_west<O: Output>(
|
||||
item_offset: O::Unit,
|
||||
item_height: O::Unit,
|
||||
item: impl Content<O>
|
||||
) -> impl Content<O> {
|
||||
Push::y(item_offset, Align::nw(Fixed::y(item_height, Fill::x(item))))
|
||||
}
|
||||
|
||||
#[inline] pub fn map_east<O: Output>(
|
||||
item_offset: O::Unit,
|
||||
item_width: O::Unit,
|
||||
item: impl Content<O>
|
||||
) -> impl Content<O> {
|
||||
Push::x(item_offset, Align::w(Fixed::x(item_width, Fill::y(item))))
|
||||
}
|
||||
|
||||
pub struct Map<'a, A, B, I, F, G>(pub PhantomData<&'a()>, pub F, pub G) where
|
||||
I: Iterator<Item = A> + Send + Sync,
|
||||
F: Fn() -> I + Send + Sync + 'a,
|
||||
G: Fn(A, usize)->B + Send + Sync;
|
||||
|
||||
impl<'a, A, B, I, F, G> Map<'a, A, B, I, F, G> where
|
||||
I: Iterator<Item = A> + Send + Sync,
|
||||
F: Fn() -> I + Send + Sync + 'a,
|
||||
G: Fn(A, usize)->B + Send + Sync
|
||||
{
|
||||
pub fn new (f: F, g: G) -> Self {
|
||||
Self(Default::default(), f, g)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, E, A, B, I, F, G> Content<E> for Map<'a, A, B, I, F, G> where
|
||||
E: Output,
|
||||
B: Render<E>,
|
||||
I: Iterator<Item = A> + Send + Sync,
|
||||
F: Fn() -> I + Send + Sync + 'a,
|
||||
G: Fn(A, usize)->B + Send + Sync
|
||||
{
|
||||
fn layout (&self, area: E::Area) -> E::Area {
|
||||
let Self(_, get_iterator, callback) = self;
|
||||
let mut index = 0;
|
||||
let [mut min_x, mut min_y] = area.center();
|
||||
let [mut max_x, mut max_y] = area.center();
|
||||
for item in get_iterator() {
|
||||
let [x,y,w,h] = callback(item, index).layout(area).xywh();
|
||||
min_x = min_x.min(x.into());
|
||||
min_y = min_y.min(y.into());
|
||||
max_x = max_x.max((x + w).into());
|
||||
max_y = max_y.max((y + h).into());
|
||||
index += 1;
|
||||
}
|
||||
let w = max_x - min_x;
|
||||
let h = max_y - min_y;
|
||||
//[min_x.into(), min_y.into(), w.into(), h.into()].into()
|
||||
area.center_xy([w.into(), h.into()].into()).into()
|
||||
}
|
||||
fn render (&self, to: &mut E) {
|
||||
let Self(_, get_iterator, callback) = self;
|
||||
let mut index = 0;
|
||||
let area = Content::layout(self, to.area());
|
||||
for item in get_iterator() {
|
||||
let item = callback(item, index);
|
||||
//to.place(area.into(), &item);
|
||||
to.place(item.layout(area), &item);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
232
output/src/op_transform.rs
Normal file
232
output/src/op_transform.rs
Normal file
|
@ -0,0 +1,232 @@
|
|||
//! [Content] items that modify the inherent
|
||||
//! dimensions of their inner [Render]ables.
|
||||
//!
|
||||
//! Transform may also react to the [Area] provided.
|
||||
//! ```
|
||||
//! use ::tek_tui::{*, tek_output::*};
|
||||
//! let area: [u16;4] = [10, 10, 20, 20];
|
||||
//! fn test (area: [u16;4], item: &impl Content<TuiOut>, expected: [u16;4]) {
|
||||
//! assert_eq!(Content::layout(item, area), expected);
|
||||
//! assert_eq!(Render::layout(item, area), expected);
|
||||
//! };
|
||||
//! test(area, &(), [20, 20, 0, 0]);
|
||||
//!
|
||||
//! test(area, &Fill::xy(()), area);
|
||||
//! test(area, &Fill::x(()), [10, 20, 20, 0]);
|
||||
//! test(area, &Fill::y(()), [20, 10, 0, 20]);
|
||||
//!
|
||||
//! //FIXME:test(area, &Fixed::x(4, ()), [18, 20, 4, 0]);
|
||||
//! //FIXME:test(area, &Fixed::y(4, ()), [20, 18, 0, 4]);
|
||||
//! //FIXME:test(area, &Fixed::xy(4, 4, unit), [18, 18, 4, 4]);
|
||||
//! ```
|
||||
use crate::*;
|
||||
/// Defines an enum that transforms its content
|
||||
/// along either the X axis, the Y axis, or both.
|
||||
macro_rules! transform_xy {
|
||||
($x:literal $y:literal $xy:literal |$self:ident : $Enum:ident, $to:ident|$area:expr) => {
|
||||
pub enum $Enum<T> { X(T), Y(T), XY(T) }
|
||||
impl<T> $Enum<T> {
|
||||
#[inline] pub fn x (item: T) -> Self { Self::X(item) }
|
||||
#[inline] pub fn y (item: T) -> Self { Self::Y(item) }
|
||||
#[inline] pub fn xy (item: T) -> Self { Self::XY(item) }
|
||||
}
|
||||
impl<'a, E: Output + 'a, T: ViewContext<'a, E>> TryFromAtom<'a, T>
|
||||
for $Enum<RenderBox<'a, E>> {
|
||||
fn try_from_expr (state: &'a T, iter: TokenIter<'a>) -> Option<Self> {
|
||||
let mut iter = iter.clone();
|
||||
if let Some(Token { value: Value::Key(k), .. }) = iter.peek() {
|
||||
if k == $x || k == $y || k == $xy {
|
||||
let _ = iter.next().unwrap();
|
||||
let token = iter.next().expect("no content specified");
|
||||
let content = state.get_content(&token.value).expect("no content provided");
|
||||
return Some(match k {
|
||||
$x => Self::x(content),
|
||||
$y => Self::y(content),
|
||||
$xy => Self::xy(content),
|
||||
_ => unreachable!()
|
||||
})
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
impl<E: Output, T: Content<E>> Content<E> for $Enum<T> {
|
||||
fn content (&self) -> impl Render<E> {
|
||||
match self {
|
||||
Self::X(item) => item,
|
||||
Self::Y(item) => item,
|
||||
Self::XY(item) => item,
|
||||
}
|
||||
}
|
||||
fn layout (&$self, $to: <E as Output>::Area) -> <E as Output>::Area {
|
||||
use $Enum::*;
|
||||
$area
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines an enum that parametrically transforms its content
|
||||
/// along either the X axis, the Y axis, or both.
|
||||
macro_rules! transform_xy_unit {
|
||||
($x:literal $y:literal $xy:literal |$self:ident : $Enum:ident, $to:ident|$layout:expr) => {
|
||||
pub enum $Enum<U, T> { X(U, T), Y(U, T), XY(U, U, T), }
|
||||
impl<U, T> $Enum<U, T> {
|
||||
#[inline] pub fn x (x: U, item: T) -> Self { Self::X(x, item) }
|
||||
#[inline] pub fn y (y: U, item: T) -> Self { Self::Y(y, item) }
|
||||
#[inline] pub fn xy (x: U, y: U, item: T) -> Self { Self::XY(x, y, item) }
|
||||
}
|
||||
impl<'a, E: Output + 'a, T: ViewContext<'a, E>> TryFromAtom<'a, T>
|
||||
for $Enum<E::Unit, RenderBox<'a, E>> {
|
||||
fn try_from_expr (state: &'a T, iter: TokenIter<'a>) -> Option<Self> {
|
||||
let mut iter = iter.clone();
|
||||
if let Some(Token { value: Value::Key(k), .. }) = iter.peek() {
|
||||
if k == $x || k == $y {
|
||||
let _ = iter.next().unwrap();
|
||||
let u = iter.next().expect("no unit specified");
|
||||
let c = iter.next().expect("no content specified");
|
||||
let u = state.get(&u.value).expect("no unit provided");
|
||||
let c = state.get_content(&c.value).expect("no content provided");
|
||||
return Some(match k {
|
||||
$x => Self::x(u, c),
|
||||
$y => Self::y(u, c),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
} else if k == $xy {
|
||||
let _ = iter.next().unwrap();
|
||||
let u = iter.next().expect("no unit specified");
|
||||
let v = iter.next().expect("no unit specified");
|
||||
let c = iter.next().expect("no content specified");
|
||||
let u = state.get(&u.value).expect("no unit provided");
|
||||
let v = state.get(&v.value).expect("no unit provided");
|
||||
let c = state.get_content(&c.value).expect("no content provided");
|
||||
return Some(Self::xy(u, v, c))
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
impl<E: Output, T: Content<E>> Content<E> for $Enum<E::Unit, T> {
|
||||
fn content (&self) -> impl Render<E> {
|
||||
Some(match self {
|
||||
Self::X(_, content) => content,
|
||||
Self::Y(_, content) => content,
|
||||
Self::XY(_, _, content) => content,
|
||||
})
|
||||
}
|
||||
fn layout (&$self, $to: E::Area) -> E::Area {
|
||||
$layout.into()
|
||||
}
|
||||
}
|
||||
impl<U: Copy + Coordinate, T> $Enum<U, T> {
|
||||
#[inline] pub fn dx (&self) -> U {
|
||||
match self {
|
||||
Self::X(x, _) => *x, Self::Y(_, _) => 0.into(), Self::XY(x, _, _) => *x,
|
||||
}
|
||||
}
|
||||
#[inline] pub fn dy (&self) -> U {
|
||||
match self {
|
||||
Self::X(_, _) => 0.into(), Self::Y(y, _) => *y, Self::XY(_, y, _) => *y,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
transform_xy!("fill/x" "fill/y" "fill/xy" |self: Fill, to|{
|
||||
let [x0, y0, wmax, hmax] = to.xywh();
|
||||
let [x, y, w, h] = self.content().layout(to).xywh();
|
||||
match self {
|
||||
X(_) => [x0, y, wmax, h],
|
||||
Y(_) => [x, y0, w, hmax],
|
||||
XY(_) => [x0, y0, wmax, hmax],
|
||||
}.into() });
|
||||
transform_xy_unit!("fixed/x" "fixed/y" "fixed/xy"|self: Fixed, area|{
|
||||
let [x, y, w, h] = area.xywh();
|
||||
let fixed_area = match self {
|
||||
Self::X(fw, _) => [x, y, *fw, h],
|
||||
Self::Y(fh, _) => [x, y, w, *fh],
|
||||
Self::XY(fw, fh, _) => [x, y, *fw, *fh],
|
||||
};
|
||||
let [x, y, w, h] = Render::layout(&self.content(), fixed_area.into()).xywh();
|
||||
let fixed_area = match self {
|
||||
Self::X(fw, _) => [x, y, *fw, h],
|
||||
Self::Y(fh, _) => [x, y, w, *fh],
|
||||
Self::XY(fw, fh, _) => [x, y, *fw, *fh],
|
||||
};
|
||||
fixed_area });
|
||||
transform_xy_unit!("min/x" "min/y" "min/xy"|self: Min, area|{
|
||||
let area = Render::layout(&self.content(), area);
|
||||
match self {
|
||||
Self::X(mw, _) => [area.x(), area.y(), area.w().max(*mw), area.h()],
|
||||
Self::Y(mh, _) => [area.x(), area.y(), area.w(), area.h().max(*mh)],
|
||||
Self::XY(mw, mh, _) => [area.x(), area.y(), area.w().max(*mw), area.h().max(*mh)],
|
||||
}});
|
||||
transform_xy_unit!("max/x" "max/y" "max/xy"|self: Max, area|{
|
||||
let [x, y, w, h] = area.xywh();
|
||||
Render::layout(&self.content(), match self {
|
||||
Self::X(fw, _) => [x, y, *fw, h],
|
||||
Self::Y(fh, _) => [x, y, w, *fh],
|
||||
Self::XY(fw, fh, _) => [x, y, *fw, *fh],
|
||||
}.into())});
|
||||
transform_xy_unit!("shrink/x" "shrink/y" "shrink/xy"|self: Shrink, area|Render::layout(
|
||||
&self.content(),
|
||||
[area.x(), area.y(), area.w().minus(self.dx()), area.h().minus(self.dy())].into()));
|
||||
transform_xy_unit!("expand/x" "expand/y" "expand/xy"|self: Expand, area|Render::layout(
|
||||
&self.content(),
|
||||
[area.x(), area.y(), area.w().plus(self.dx()), area.h().plus(self.dy())].into()));
|
||||
transform_xy_unit!("push/x" "push/y" "push/xy"|self: Push, area|{
|
||||
let area = Render::layout(&self.content(), area);
|
||||
[area.x().plus(self.dx()), area.y().plus(self.dy()), area.w(), area.h()] });
|
||||
transform_xy_unit!("pull/x" "pull/y" "pull/xy"|self: Pull, area|{
|
||||
let area = Render::layout(&self.content(), area);
|
||||
[area.x().minus(self.dx()), area.y().minus(self.dy()), area.w(), area.h()] });
|
||||
transform_xy_unit!("margin/x" "margin/y" "margin/xy"|self: Margin, area|{
|
||||
let area = Render::layout(&self.content(), area);
|
||||
let dx = self.dx();
|
||||
let dy = self.dy();
|
||||
[area.x().minus(dx), area.y().minus(dy), area.w().plus(dy.plus(dy)), area.h().plus(dy.plus(dy))] });
|
||||
transform_xy_unit!("padding/x" "padding/y" "padding/xy"|self: Padding, area|{
|
||||
let area = Render::layout(&self.content(), area);
|
||||
let dx = self.dx();
|
||||
let dy = self.dy();
|
||||
[area.x().plus(dx), area.y().plus(dy), area.w().minus(dy.plus(dy)), area.h().minus(dy.plus(dy)), ] });
|
||||
|
||||
#[cfg(test)] mod test_op_transform {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
use proptest::option::of;
|
||||
macro_rules! test_op_transform {
|
||||
($fn:ident, $Op:ident) => {
|
||||
proptest! {
|
||||
#[test] fn $fn (
|
||||
op_x in of(u16::MIN..u16::MAX),
|
||||
op_y in of(u16::MIN..u16::MAX),
|
||||
content in "\\PC*",
|
||||
x in u16::MIN..u16::MAX,
|
||||
y in u16::MIN..u16::MAX,
|
||||
w in u16::MIN..u16::MAX,
|
||||
h in u16::MIN..u16::MAX,
|
||||
) {
|
||||
if let Some(op) = match (op_x, op_y) {
|
||||
(Some(x), Some(y)) => Some($Op::xy(x, y, content)),
|
||||
(Some(x), None) => Some($Op::x(x, content)),
|
||||
(Some(y), None) => Some($Op::y(y, content)),
|
||||
_ => None
|
||||
} {
|
||||
assert_eq!(Content::layout(&op, [x, y, w, h]),
|
||||
Render::layout(&op, [x, y, w, h]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
test_op_transform!(test_op_fixed, Fixed);
|
||||
test_op_transform!(test_op_min, Min);
|
||||
test_op_transform!(test_op_max, Max);
|
||||
test_op_transform!(test_op_push, Push);
|
||||
test_op_transform!(test_op_pull, Pull);
|
||||
test_op_transform!(test_op_shrink, Shrink);
|
||||
test_op_transform!(test_op_expand, Expand);
|
||||
test_op_transform!(test_op_margin, Margin);
|
||||
test_op_transform!(test_op_padding, Padding);
|
||||
}
|
132
output/src/output.rs
Normal file
132
output/src/output.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
use crate::*;
|
||||
use std::ops::Deref;
|
||||
/// Render target.
|
||||
pub trait Output: Send + Sync + Sized {
|
||||
/// Unit of length
|
||||
type Unit: Coordinate;
|
||||
/// Rectangle without offset
|
||||
type Size: Size<Self::Unit>;
|
||||
/// Rectangle with offset
|
||||
type Area: Area<Self::Unit>;
|
||||
/// Current output area
|
||||
fn area (&self) -> Self::Area;
|
||||
/// Mutable pointer to area
|
||||
fn area_mut (&mut self) -> &mut Self::Area;
|
||||
/// Render widget in area
|
||||
fn place (&mut self, area: Self::Area, content: &impl Render<Self>);
|
||||
#[inline] fn x (&self) -> Self::Unit { self.area().x() }
|
||||
#[inline] fn y (&self) -> Self::Unit { self.area().y() }
|
||||
#[inline] fn w (&self) -> Self::Unit { self.area().w() }
|
||||
#[inline] fn h (&self) -> Self::Unit { self.area().h() }
|
||||
#[inline] fn wh (&self) -> Self::Size { self.area().wh().into() }
|
||||
}
|
||||
/// Renderable with dynamic dispatch.
|
||||
pub trait Render<E: Output> {
|
||||
/// Compute layout.
|
||||
fn layout (&self, area: E::Area) -> E::Area;
|
||||
/// Write data to display.
|
||||
fn render (&self, output: &mut E);
|
||||
/// Perform type erasure, turning `self` into an opaque [RenderBox].
|
||||
fn boxed <'a> (self) -> RenderBox<'a, E> where Self: Send + Sync + Sized + 'a {
|
||||
Box::new(self) as RenderBox<'a, E>
|
||||
}
|
||||
}
|
||||
/// Most importantly, every [Content] is also a [Render].
|
||||
///
|
||||
/// However, the converse does not hold true.
|
||||
/// Instead, the [Content::content] method returns an
|
||||
/// opaque [Render] pointer.
|
||||
impl<E: Output, C: Content<E>> Render<E> for C {
|
||||
fn layout (&self, area: E::Area) -> E::Area { Content::layout(self, area) }
|
||||
fn render (&self, output: &mut E) { Content::render(self, output) }
|
||||
}
|
||||
/// Opaque pointer to a renderable living on the heap.
|
||||
///
|
||||
/// Return this from [Content::content] to use dynamic dispatch.
|
||||
pub type RenderBox<'a, E> = Box<RenderDyn<'a, E>>;
|
||||
/// You can render from a box.
|
||||
impl<'a, E: Output> Content<E> for RenderBox<'a, E> {
|
||||
fn content (&self) -> impl Render<E> { self.deref() }
|
||||
//fn boxed <'b> (self) -> RenderBox<'b, E> where Self: Sized + 'b { self }
|
||||
}
|
||||
/// Opaque pointer to a renderable.
|
||||
pub type RenderDyn<'a, E> = dyn Render<E> + Send + Sync + 'a;
|
||||
/// You can render from an opaque pointer.
|
||||
impl<'a, E: Output> Content<E> for &RenderDyn<'a, E> where Self: Sized {
|
||||
fn content (&self) -> impl Render<E> { self.deref() }
|
||||
fn layout (&self, area: E::Area) -> E::Area { Render::layout(self.deref(), area) }
|
||||
fn render (&self, output: &mut E) { Render::render(self.deref(), output) }
|
||||
}
|
||||
/// Composable renderable with static dispatch.
|
||||
pub trait Content<E: Output> {
|
||||
/// Return a [Render]able of a specific type.
|
||||
fn content (&self) -> impl Render<E> { () }
|
||||
/// Perform layout. By default, delegates to [Self::content].
|
||||
fn layout (&self, area: E::Area) -> E::Area { self.content().layout(area) }
|
||||
/// Draw to output. By default, delegates to [Self::content].
|
||||
fn render (&self, output: &mut E) { self.content().render(output) }
|
||||
}
|
||||
/// Every pointer to [Content] is a [Content].
|
||||
impl<E: Output, C: Content<E>> Content<E> for &C {
|
||||
fn content (&self) -> impl Render<E> { (*self).content() }
|
||||
fn layout (&self, area: E::Area) -> E::Area { (*self).layout(area) }
|
||||
fn render (&self, output: &mut E) { (*self).render(output) }
|
||||
}
|
||||
/// The platonic ideal unit of [Content]: total emptiness at dead center (e=1vg^sqrt(-1))
|
||||
impl<E: Output> Content<E> for () {
|
||||
fn layout (&self, area: E::Area) -> E::Area { area.center().to_area_pos().into() }
|
||||
fn render (&self, _: &mut E) {}
|
||||
}
|
||||
impl<E: Output, T: Content<E>> Content<E> for Option<T> {
|
||||
fn content (&self) -> impl Render<E> {
|
||||
self.as_ref()
|
||||
}
|
||||
fn layout (&self, area: E::Area) -> E::Area {
|
||||
self.as_ref()
|
||||
.map(|content|content.layout(area))
|
||||
.unwrap_or([0.into(), 0.into(), 0.into(), 0.into(),].into())
|
||||
}
|
||||
fn render (&self, output: &mut E) {
|
||||
self.as_ref()
|
||||
.map(|content|content.render(output));
|
||||
}
|
||||
}
|
||||
/// Implement [Content] with composable content for a struct.
|
||||
#[macro_export] macro_rules! content {
|
||||
// Implement for all [Output]s.
|
||||
(|$self:ident:$Struct:ty| $content:expr) => {
|
||||
impl<E: Output> Content<E> for $Struct {
|
||||
fn content (&$self) -> impl Render<E> { Some($content) }
|
||||
}
|
||||
};
|
||||
// Implement for specific [Output].
|
||||
($Output:ty:|
|
||||
$self:ident:
|
||||
$Struct:ident$(<$($($L:lifetime)? $($T:ident)? $(:$Trait:path)?),+>)?
|
||||
|$content:expr) => {
|
||||
impl $(<$($($L)? $($T)? $(:$Trait)?),+>)? Content<$Output>
|
||||
for $Struct $(<$($($L)? $($T)?),+>)? {
|
||||
fn content (&$self) -> impl Render<$Output> { $content }
|
||||
}
|
||||
};
|
||||
}
|
||||
/// Implement [Content] with custom rendering for a struct.
|
||||
#[macro_export] macro_rules! render {
|
||||
(|$self:ident:$Struct:ident $(<
|
||||
$($L:lifetime),* $($T:ident $(:$Trait:path)?),*
|
||||
>)?, $to:ident | $render:expr) => {
|
||||
impl <$($($L),*)? E: Output, $($T$(:$Trait)?),*> Content<E>
|
||||
for $Struct $(<$($L),* $($T),*>>)? {
|
||||
fn render (&$self, $to: &mut E) { $render }
|
||||
}
|
||||
};
|
||||
($Output:ty:|
|
||||
$self:ident:
|
||||
$Struct:ident $(<$($($L:lifetime)? $($T:ident)? $(:$Trait:path)?),+>)?, $to:ident
|
||||
|$render:expr) => {
|
||||
impl $(<$($($L)? $($T)? $(:$Trait)?),+>)? Content<$Output>
|
||||
for $Struct $(<$($($L)? $($T)?),+>)? {
|
||||
fn render (&$self, $to: &mut $Output) { $render }
|
||||
}
|
||||
};
|
||||
}
|
93
output/src/reduce.rs
Normal file
93
output/src/reduce.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Reduce<A, B, I, F, G>(pub PhantomData<A>, pub F, pub G) where
|
||||
A: Send + Sync, B: Send + Sync,
|
||||
I: Iterator<Item = B> + Send + Sync,
|
||||
F: Fn() -> I + Send + Sync,
|
||||
G: Fn(A, B, usize)->A + Send + Sync;
|
||||
|
||||
impl<A, B, I, F, G> Reduce<A, B, I, F, G> where
|
||||
A: Send + Sync, B: Send + Sync,
|
||||
I: Iterator<Item = B> + Send + Sync,
|
||||
F: Fn() -> I + Send + Sync,
|
||||
G: Fn(A, B, usize)->A + Send + Sync
|
||||
{
|
||||
pub fn new (f: F, g: G) -> Self { Self(Default::default(), f, g) }
|
||||
}
|
||||
|
||||
impl<E: Output, A, B, I, F, G> Content<E> for Reduce<A, B, I, F, G> where
|
||||
A: Send + Sync, B: Send + Sync,
|
||||
I: Iterator<Item = B> + Send + Sync,
|
||||
F: Fn() -> I + Send + Sync,
|
||||
G: Fn(A, B, usize)->A + Send + Sync
|
||||
{
|
||||
fn content (&self) -> impl Render<E> {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
//pub fn reduce <E, T, I, R, F>(iterator: I, callback: F) -> Reduce<E, T, I, R, F> where
|
||||
//E: Output,
|
||||
//I: Iterator<Item = T> + Send + Sync,
|
||||
//R: Render<E>,
|
||||
//F: Fn(R, T, usize) -> R + Send + Sync
|
||||
//{
|
||||
//Reduce(Default::default(), iterator, callback)
|
||||
//}
|
||||
pub struct Reduce<E, T, I, R, F>(PhantomData<(E, R)>, I, F) where
|
||||
E: Output,
|
||||
I: Iterator<Item = T> + Send + Sync,
|
||||
R: Render<E>,
|
||||
F: Fn(R, T, usize) -> R + Send + Sync;
|
||||
impl<E, T, I, R, F> Content<E> for Reduce<E, T, I, R, F> where
|
||||
E: Output,
|
||||
I: Iterator<Item = T> + Send + Sync,
|
||||
R: Render<E>,
|
||||
F: Fn(R, T, usize) -> R + Send + Sync
|
||||
{
|
||||
fn render (&self, to: &mut E) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
//macro_rules! define_ops {
|
||||
//($Trait:ident<$E:ident:$Output:path> { $(
|
||||
//$(#[$attr:meta $($attr_args:tt)*])*
|
||||
//(
|
||||
//$fn:ident
|
||||
//$(<$($G:ident$(:$Gen:path)?, )+>)?
|
||||
//$Op:ident
|
||||
//($($arg:ident:$Arg:ty),*)
|
||||
//)
|
||||
//)* }) => {
|
||||
//impl<$E: $Output> $Trait<E> for E {}
|
||||
//pub trait $Trait<$E: $Output> {
|
||||
//$(
|
||||
//$(#[$attr $($attr_args)*])*
|
||||
//fn $fn $(<$($G),+>)?
|
||||
//($($arg:$Arg),*)-> $Op<$($(, $G)+)?>
|
||||
//$(where $($G: $($Gen + Send + Sync)?),+)?
|
||||
//{ $Op($($arg),*) }
|
||||
//)*
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
||||
//define_ops! {
|
||||
//Layout<E: Output> {
|
||||
//(when <A: Render<E>,>
|
||||
//When(cond: bool, item: A))
|
||||
///// When `cond` is `true`, render `a`, otherwise render `b`.
|
||||
//(either <A: Render<E>, B: Render<E>,>
|
||||
//Either(cond: bool, a: A, b: B))
|
||||
///// If `opt` is `Some(T)` renders `cb(t)`, otherwise nothing.
|
||||
//(opt <A, F: Fn(A) -> B, B: Render<E>,>
|
||||
//Opt(option: Option<A>, cb: F))
|
||||
///// Maps items of iterator through callback.
|
||||
//(map <A, B: Render<E>, I: Iterator<Item = A>, F: Fn() -> I, G: Fn(A, usize)->B,>
|
||||
//Map(get_iterator: F, callback: G))
|
||||
//}
|
||||
//}
|
||||
|
63
output/src/size.rs
Normal file
63
output/src/size.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use crate::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub trait Size<N: Coordinate>: From<[N;2]> + Debug + Copy {
|
||||
fn x (&self) -> N;
|
||||
fn y (&self) -> N;
|
||||
#[inline] fn w (&self) -> N { self.x() }
|
||||
#[inline] fn h (&self) -> N { self.y() }
|
||||
#[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] }
|
||||
#[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] }
|
||||
#[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] }
|
||||
#[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
|
||||
if self.w() < w || self.h() < h {
|
||||
Err(format!("min {w}x{h}").into())
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
#[inline] fn zero () -> [N;2] {
|
||||
[N::zero(), N::zero()]
|
||||
}
|
||||
#[inline] fn to_area_pos (&self) -> [N;4] {
|
||||
let [x, y] = self.wh();
|
||||
[x, y, 0.into(), 0.into()]
|
||||
}
|
||||
#[inline] fn to_area_size (&self) -> [N;4] {
|
||||
let [w, h] = self.wh();
|
||||
[0.into(), 0.into(), w, h]
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: Coordinate> Size<N> for (N, N) {
|
||||
#[inline] fn x (&self) -> N { self.0 }
|
||||
#[inline] fn y (&self) -> N { self.1 }
|
||||
}
|
||||
|
||||
impl<N: Coordinate> Size<N> for [N;2] {
|
||||
#[inline] fn x (&self) -> N { self[0] }
|
||||
#[inline] fn y (&self) -> N { self[1] }
|
||||
}
|
||||
|
||||
#[cfg(test)] mod test_size {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
proptest! {
|
||||
#[test] fn test_size (
|
||||
x in u16::MIN..u16::MAX,
|
||||
y in u16::MIN..u16::MAX,
|
||||
a in u16::MIN..u16::MAX,
|
||||
b in u16::MIN..u16::MAX,
|
||||
) {
|
||||
let size = [x, y];
|
||||
let _ = size.w();
|
||||
let _ = size.h();
|
||||
let _ = size.wh();
|
||||
let _ = size.clip_w(a);
|
||||
let _ = size.clip_h(b);
|
||||
let _ = size.expect_min(a, b);
|
||||
let _ = size.to_area_pos();
|
||||
let _ = size.to_area_size();
|
||||
}
|
||||
}
|
||||
}
|
50
output/src/thunk.rs
Normal file
50
output/src/thunk.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use crate::*;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Lazily-evaluated [Render]able.
|
||||
pub struct Thunk<E: Output, T: Render<E>, F: Fn()->T + Send + Sync>(PhantomData<E>, F);
|
||||
impl<E: Output, T: Render<E>, F: Fn()->T + Send + Sync> Thunk<E, T, F> {
|
||||
pub fn new (thunk: F) -> Self {
|
||||
Self(Default::default(), thunk)
|
||||
}
|
||||
}
|
||||
impl<E: Output, T: Render<E>, F: Fn()->T + Send + Sync> Content<E> for Thunk<E, T, F> {
|
||||
fn content (&self) -> impl Render<E> { (self.1)() }
|
||||
}
|
||||
|
||||
pub struct ThunkBox<'a, E: Output>(PhantomData<E>, Box<dyn Fn()->RenderBox<'a, E> + Send + Sync + 'a>);
|
||||
impl<'a, E: Output> ThunkBox<'a, E> {
|
||||
pub fn new (thunk: Box<dyn Fn()->RenderBox<'a, E> + Send + Sync + 'a>) -> Self {
|
||||
Self(Default::default(), thunk)
|
||||
}
|
||||
}
|
||||
impl<'a, E: Output> Content<E> for ThunkBox<'a, E> {
|
||||
fn content (&self) -> impl Render<E> { (self.1)() }
|
||||
}
|
||||
impl<'a, E: Output, F: Fn()->T + Send + Sync + 'a, T: Render<E> + Send + Sync + 'a> From<F> for ThunkBox<'a, E> {
|
||||
fn from (f: F) -> Self {
|
||||
Self(Default::default(), Box::new(move||f().boxed()))
|
||||
}
|
||||
}
|
||||
//impl<'a, E: Output, F: Fn()->Box<dyn Render<E> + 'a> + Send + Sync + 'a> From<F> for ThunkBox<'a, E> {
|
||||
//fn from (f: F) -> Self {
|
||||
//Self(Default::default(), Box::new(f))
|
||||
//}
|
||||
//}
|
||||
|
||||
pub struct ThunkRender<E: Output, F: Fn(&mut E) + Send + Sync>(PhantomData<E>, F);
|
||||
impl<E: Output, F: Fn(&mut E) + Send + Sync> ThunkRender<E, F> {
|
||||
pub fn new (render: F) -> Self { Self(Default::default(), render) }
|
||||
}
|
||||
impl<E: Output, F: Fn(&mut E) + Send + Sync> Content<E> for ThunkRender<E, F> {
|
||||
fn render (&self, to: &mut E) { (self.1)(to) }
|
||||
}
|
||||
|
||||
pub struct ThunkLayout<E: Output, F1: Fn(E::Area)->E::Area + Send + Sync, F2: Fn(&mut E) + Send + Sync>(PhantomData<E>, F1, F2);
|
||||
impl<E: Output, F1: Fn(E::Area)->E::Area + Send + Sync, F2: Fn(&mut E) + Send + Sync> ThunkLayout<E, F1, F2> {
|
||||
pub fn new (layout: F1, render: F2) -> Self { Self(Default::default(), layout, render) }
|
||||
}
|
||||
impl<E: Output, F1: Fn(E::Area)->E::Area + Send + Sync, F2: Fn(&mut E) + Send + Sync> Content<E> for ThunkLayout<E, F1, F2> {
|
||||
fn layout (&self, to: E::Area) -> E::Area { (self.1)(to) }
|
||||
fn render (&self, to: &mut E) { (self.2)(to) }
|
||||
}
|
87
output/src/view.rs
Normal file
87
output/src/view.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use crate::*;
|
||||
#[macro_export] macro_rules! view {
|
||||
($Output:ty: |$self:ident: $State:ty| $expr:expr; {
|
||||
$($sym:literal => $body:expr),* $(,)?
|
||||
}) => {
|
||||
impl Content<$Output> for $State {
|
||||
fn content (&$self) -> impl Render<$Output> { $expr }
|
||||
}
|
||||
impl<'a> ViewContext<'a, $Output> for $State {
|
||||
fn get_content_sym (&'a $self, value: &Value<'a>) -> Option<RenderBox<'a, $Output>> {
|
||||
if let Value::Sym(s) = value {
|
||||
match *s {
|
||||
$($sym => Some($body),)*
|
||||
_ => None
|
||||
}
|
||||
} else {
|
||||
panic!("expected content, got: {value:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An ephemeral wrapper around view state and view description,
|
||||
// that is meant to be constructed and returned from [Content::content].
|
||||
pub struct View<'a, T>(pub &'a T, pub SourceIter<'a>);
|
||||
impl<'a, O: Output + 'a, T: ViewContext<'a, O>> Content<O> for View<'a, T> {
|
||||
fn content (&self) -> impl Render<O> {
|
||||
let iter = self.1.clone();
|
||||
while let Some((Token { value, .. }, _)) = iter.next() {
|
||||
if let Some(content) = self.0.get_content(&value) {
|
||||
return Some(content)
|
||||
}
|
||||
}
|
||||
return None
|
||||
}
|
||||
}
|
||||
// Provides components to the view.
|
||||
pub trait ViewContext<'a, E: Output + 'a>: Send + Sync
|
||||
+ Context<bool>
|
||||
+ Context<usize>
|
||||
+ Context<E::Unit>
|
||||
{
|
||||
fn get_content (&'a self, value: &Value<'a>) -> Option<RenderBox<'a, E>> {
|
||||
match value {
|
||||
Value::Sym(_) => self.get_content_sym(value),
|
||||
Value::Exp(_, _) => self.get_content_exp(value),
|
||||
_ => panic!("only :symbols and (expressions) accepted here")
|
||||
}
|
||||
}
|
||||
fn get_content_sym (&'a self, value: &Value<'a>) -> Option<RenderBox<'a, E>>;
|
||||
fn get_content_exp (&'a self, value: &Value<'a>) -> Option<RenderBox<'a, E>> {
|
||||
try_delegate!(self, *value, When::<RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Either::<RenderBox<'a, E>, RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Align::<RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Bsp::<RenderBox<'a, E>, RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Fill::<RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Fixed::<_, RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Min::<_, RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Max::<_, RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Shrink::<_, RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Expand::<_, RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Push::<_, RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Pull::<_, RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Margin::<_, RenderBox<'a, E>>);
|
||||
try_delegate!(self, *value, Padding::<_, RenderBox<'a, E>>);
|
||||
None
|
||||
}
|
||||
}
|
||||
#[macro_export] macro_rules! try_delegate {
|
||||
($s:ident, $atom:expr, $T:ty) => {
|
||||
if let Some(value) = <$T>::try_from_atom($s, $atom) {
|
||||
return Some(value.boxed())
|
||||
}
|
||||
}
|
||||
}
|
||||
#[macro_export] macro_rules! try_from_expr {
|
||||
(<$l:lifetime, $E:ident>: $Struct:ty: |$state:ident, $iter:ident|$body:expr) => {
|
||||
impl<$l, $E: Output + $l, T: ViewContext<$l, $E>> TryFromAtom<$l, T> for $Struct {
|
||||
fn try_from_expr ($state: &$l T, $iter: TokenIter<'a>) -> Option<Self> {
|
||||
let mut $iter = $iter.clone();
|
||||
$body;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
shell.nix
Normal file
13
shell.nix
Normal file
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
{pkgs?import<nixpkgs>{}}:let
|
||||
stdenv = pkgs.clang19Stdenv;
|
||||
name = "tengri";
|
||||
nativeBuildInputs = with pkgs; [ pkg-config libclang ];
|
||||
buildInputs = with pkgs; [ libclang ];
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib";
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (with pkgs; []);
|
||||
in pkgs.mkShell.override {
|
||||
inherit stdenv;
|
||||
} {
|
||||
inherit name nativeBuildInputs buildInputs VST3_SDK_DIR LIBCLANG_PATH LD_LIBRARY_PATH;
|
||||
}
|
19
tui/Cargo.toml
Normal file
19
tui/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "tek_tui"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
palette = { version = "0.7.6", features = [ "random" ] }
|
||||
rand = "0.8.5"
|
||||
crossterm = "0.28.1"
|
||||
ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||
better-panic = "0.3.0"
|
||||
konst = { version = "0.3.16", features = [ "rust_1_83" ] }
|
||||
atomic_float = "1"
|
||||
quanta = "0.12.3"
|
||||
|
||||
tek_edn = { path = "../edn" }
|
||||
tek_input = { path = "../input" }
|
||||
tek_output = { path = "../output" }
|
||||
#tek_time = { path = "../time" }
|
15
tui/README.md
Normal file
15
tui/README.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# `tek_tui`
|
||||
|
||||
the `Tui` struct (the *engine*) implements the
|
||||
`tek_input::Input` and `tek_output::Output` traits.
|
||||
|
||||
at launch, the `Tui` engine spawns two threads,
|
||||
a **render thread** and an **input thread**. (the
|
||||
application may spawn further threads, such as a
|
||||
**jack thread**.)
|
||||
|
||||
all threads communicate using shared ownership,
|
||||
`Arc<RwLock>` and `Arc<Atomic>`. the engine and
|
||||
application instances are expected to be wrapped
|
||||
in `Arc<RwLock>`; internally, those synchronization
|
||||
mechanisms may be used liberally.
|
144
tui/examples/demo.rs.old
Normal file
144
tui/examples/demo.rs.old
Normal file
|
@ -0,0 +1,144 @@
|
|||
use tek::*;
|
||||
|
||||
fn main () -> Usually<()> {
|
||||
Tui::run(Arc::new(RwLock::new(Demo::new())))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Demo<E: Engine> {
|
||||
index: usize,
|
||||
items: Vec<Box<dyn Render<Engine = E>>>
|
||||
}
|
||||
|
||||
impl Demo<Tui> {
|
||||
fn new () -> Self {
|
||||
Self {
|
||||
index: 0,
|
||||
items: vec![
|
||||
//Box::new(tek_sequencer::TransportPlayPauseButton {
|
||||
//_engine: Default::default(),
|
||||
//transport: None,
|
||||
//value: Some(TransportState::Stopped),
|
||||
//focused: true
|
||||
//}),
|
||||
//Box::new(tek_sequencer::TransportPlayPauseButton {
|
||||
//_engine: Default::default(),
|
||||
//transport: None,
|
||||
//value: Some(TransportState::Rolling),
|
||||
//focused: false
|
||||
//}),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for Demo<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> dyn Render<Engine = Tui> {
|
||||
let border_style = Style::default().fg(Color::Rgb(0,0,0));
|
||||
Align::Center(Layers::new(move|add|{
|
||||
|
||||
add(&Background(Color::Rgb(0,128,128)))?;
|
||||
|
||||
add(&Margin::XY(1, 1, Stack::down(|add|{
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(128,96,0)))?;
|
||||
add(&Border(Square(border_style)))?;
|
||||
add(&Margin::XY(2, 1, "..."))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(128,64,0)))?;
|
||||
add(&Border(Lozenge(border_style)))?;
|
||||
add(&Margin::XY(4, 2, "---"))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(96,64,0)))?;
|
||||
add(&Border(SquareBold(border_style)))?;
|
||||
add(&Margin::XY(6, 3, "~~~"))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
Ok(())
|
||||
})).debug())?;
|
||||
|
||||
Ok(())
|
||||
|
||||
}))
|
||||
//Align::Center(Margin::X(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(128,0,0)))?;
|
||||
//add(&Stack::down(|add|{
|
||||
//add(&Margin::Y(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(0,128,0)))?;
|
||||
//add(&Align::Center("12345"))?;
|
||||
//add(&Align::Center("FOO"))
|
||||
//})))?;
|
||||
//add(&Margin::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Align::Center("1234567"))?;
|
||||
//add(&Align::Center("BAR"))?;
|
||||
//add(&Background(Color::Rgb(0,0,128)))
|
||||
//})))
|
||||
//}))
|
||||
//})))
|
||||
|
||||
//Align::Y(Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(128,0,0)))?;
|
||||
//add(&Margin::X(1, Align::Center(Stack::down(|add|{
|
||||
//add(&Align::X(Margin::Y(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(0,128,0)))?;
|
||||
//add(&Align::Center("12345"))?;
|
||||
//add(&Align::Center("FOO"))
|
||||
//})))?;
|
||||
//add(&Margin::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Align::Center("1234567"))?;
|
||||
//add(&Align::Center("BAR"))?;
|
||||
//add(&Background(Color::Rgb(0,0,128)))
|
||||
//})))?;
|
||||
//Ok(())
|
||||
//})))))
|
||||
//}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Handle<TuiIn> for Demo<Tui> {
|
||||
fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
|
||||
use KeyCode::{PageUp, PageDown};
|
||||
match from.event() {
|
||||
kexp!(PageUp) => {
|
||||
self.index = (self.index + 1) % self.items.len();
|
||||
},
|
||||
kexp!(PageDown) => {
|
||||
self.index = if self.index > 1 {
|
||||
self.index - 1
|
||||
} else {
|
||||
self.items.len() - 1
|
||||
};
|
||||
},
|
||||
_ => return Ok(None)
|
||||
}
|
||||
Ok(Some(true))
|
||||
}
|
||||
}
|
||||
|
||||
//lisp!(CONTENT Demo (LET
|
||||
//(BORDER-STYLE (STYLE (FG (RGB 0 0 0))))
|
||||
//(BG-COLOR-0 (RGB 0 128 128))
|
||||
//(BG-COLOR-1 (RGB 128 96 0))
|
||||
//(BG-COLOR-2 (RGB 128 64 0))
|
||||
//(BG-COLOR-3 (RGB 96 64 0))
|
||||
//(CENTER (LAYERS
|
||||
//(BACKGROUND BG-COLOR-0)
|
||||
//(OUTSET-XY 1 1 (SPLIT-DOWN
|
||||
//(LAYERS (BACKGROUND BG-COLOR-1)
|
||||
//(BORDER SQUARE BORDER-STYLE)
|
||||
//(OUTSET-XY 2 1 "..."))
|
||||
//(LAYERS (BACKGROUND BG-COLOR-2)
|
||||
//(BORDER LOZENGE BORDER-STYLE)
|
||||
//(OUTSET-XY 4 2 "---"))
|
||||
//(LAYERS (BACKGROUND BG-COLOR-3)
|
||||
//(BORDER SQUARE-BOLD BORDER-STYLE)
|
||||
//(OUTSET-XY 2 1 "~~~"))))))))
|
1
tui/examples/edn01.edn
Normal file
1
tui/examples/edn01.edn
Normal file
|
@ -0,0 +1 @@
|
|||
:hello-world
|
1
tui/examples/edn02.edn
Normal file
1
tui/examples/edn02.edn
Normal file
|
@ -0,0 +1 @@
|
|||
(bsp/s :hello :world)
|
1
tui/examples/edn03.edn
Normal file
1
tui/examples/edn03.edn
Normal file
|
@ -0,0 +1 @@
|
|||
(fill/xy :hello-world)
|
1
tui/examples/edn04.edn
Normal file
1
tui/examples/edn04.edn
Normal file
|
@ -0,0 +1 @@
|
|||
(fixed/xy 20 10 :hello-world)
|
1
tui/examples/edn05.edn
Normal file
1
tui/examples/edn05.edn
Normal file
|
@ -0,0 +1 @@
|
|||
(bsp/s (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))
|
1
tui/examples/edn06.edn
Normal file
1
tui/examples/edn06.edn
Normal file
|
@ -0,0 +1 @@
|
|||
(bsp/e (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))
|
1
tui/examples/edn07.edn
Normal file
1
tui/examples/edn07.edn
Normal file
|
@ -0,0 +1 @@
|
|||
(bsp/n (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))
|
1
tui/examples/edn08.edn
Normal file
1
tui/examples/edn08.edn
Normal file
|
@ -0,0 +1 @@
|
|||
(bsp/w (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))
|
1
tui/examples/edn09.edn
Normal file
1
tui/examples/edn09.edn
Normal file
|
@ -0,0 +1 @@
|
|||
(bsp/a (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))
|
1
tui/examples/edn10.edn
Normal file
1
tui/examples/edn10.edn
Normal file
|
@ -0,0 +1 @@
|
|||
(bsp/b (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))
|
11
tui/examples/edn11.edn
Normal file
11
tui/examples/edn11.edn
Normal file
|
@ -0,0 +1,11 @@
|
|||
(bsp/s
|
||||
(bsp/e (align/nw (fixed/xy 5 3 :hello))
|
||||
(bsp/e (align/n (fixed/xy 5 3 :hello))
|
||||
(align/ne (fixed/xy 5 3 :hello))))
|
||||
(bsp/s
|
||||
(bsp/e (align/w (fixed/xy 5 3 :hello))
|
||||
(bsp/e (align/c (fixed/xy 5 3 :hello))
|
||||
(align/e (fixed/xy 5 3 :hello))))
|
||||
(bsp/e (align/sw (fixed/xy 5 3 :hello))
|
||||
(bsp/e (align/s (fixed/xy 5 3 :hello))
|
||||
(align/se (fixed/xy 5 3 :hello))))))
|
11
tui/examples/edn12.edn
Normal file
11
tui/examples/edn12.edn
Normal file
|
@ -0,0 +1,11 @@
|
|||
(bsp/s
|
||||
(bsp/e (fixed/xy 8 5 (align/nw :hello))
|
||||
(bsp/e (fixed/xy 8 5 (align/n :hello))
|
||||
(fixed/xy 8 5 (align/ne :hello))))
|
||||
(bsp/s
|
||||
(bsp/e (fixed/xy 8 5 (align/w :hello))
|
||||
(bsp/e (fixed/xy 8 5 (align/c :hello))
|
||||
(fixed/xy 8 5 (align/e :hello))))
|
||||
(bsp/e (fixed/xy 8 5 (align/sw :hello))
|
||||
(bsp/e (fixed/xy 8 5 (align/s :hello))
|
||||
(fixed/xy 8 5 (align/se :hello))))))
|
11
tui/examples/edn13.edn
Normal file
11
tui/examples/edn13.edn
Normal file
|
@ -0,0 +1,11 @@
|
|||
(bsp/s
|
||||
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/nw :hello)))
|
||||
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/n :hello)))
|
||||
(grow/xy 1 1 (fixed/xy 8 5 (align/ne :hello)))))
|
||||
(bsp/s
|
||||
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/w :hello)))
|
||||
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/c :hello)))
|
||||
(grow/xy 1 1 (fixed/xy 8 5 (align/e :hello)))))
|
||||
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/sw :hello)))
|
||||
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/s :hello)))
|
||||
(grow/xy 1 1 (fixed/xy 8 5 (align/se :hello)))))))
|
73
tui/examples/edn99.edn
Normal file
73
tui/examples/edn99.edn
Normal file
|
@ -0,0 +1,73 @@
|
|||
(align/c (bg/behind :bg0 (margin/xy 1 1 (col
|
||||
(bg/behind :bg1 (border/around :border1 (margin/xy 2 1 :label1)))
|
||||
(bg/behind :bg2 (border/around :border2 (margin/xy 4 2 :label2)))
|
||||
(bg/behind :bg3 (border/around :border3 (margin/xy 6 3 :label3)))))))
|
||||
|
||||
fn content (&self) -> dyn Render<Engine = Tui> {
|
||||
let border_style = Style::default().fg(Color::Rgb(0,0,0));
|
||||
Align::Center(Layers::new(move|add|{
|
||||
|
||||
add(&Background(Color::Rgb(0,128,128)))?;
|
||||
|
||||
add(&Margin::XY(1, 1, Stack::down(|add|{
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(128,96,0)))?;
|
||||
add(&Border(Square(border_style)))?;
|
||||
add(&Margin::XY(2, 1, "..."))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(128,64,0)))?;
|
||||
add(&Border(Lozenge(border_style)))?;
|
||||
add(&Margin::XY(4, 2, "---"))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(96,64,0)))?;
|
||||
add(&Border(SquareBold(border_style)))?;
|
||||
add(&Margin::XY(6, 3, "~~~"))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
Ok(())
|
||||
})).debug())?;
|
||||
|
||||
Ok(())
|
||||
|
||||
}))
|
||||
//Align::Center(Margin::X(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(128,0,0)))?;
|
||||
//add(&Stack::down(|add|{
|
||||
//add(&Margin::Y(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(0,128,0)))?;
|
||||
//add(&Align::Center("12345"))?;
|
||||
//add(&Align::Center("FOO"))
|
||||
//})))?;
|
||||
//add(&Margin::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Align::Center("1234567"))?;
|
||||
//add(&Align::Center("BAR"))?;
|
||||
//add(&Background(Color::Rgb(0,0,128)))
|
||||
//})))
|
||||
//}))
|
||||
//})))
|
||||
|
||||
//Align::Y(Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(128,0,0)))?;
|
||||
//add(&Margin::X(1, Align::Center(Stack::down(|add|{
|
||||
//add(&Align::X(Margin::Y(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(0,128,0)))?;
|
||||
//add(&Align::Center("12345"))?;
|
||||
//add(&Align::Center("FOO"))
|
||||
//})))?;
|
||||
//add(&Margin::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Align::Center("1234567"))?;
|
||||
//add(&Align::Center("BAR"))?;
|
||||
//add(&Background(Color::Rgb(0,0,128)))
|
||||
//})))?;
|
||||
//Ok(())
|
||||
//})))))
|
||||
//}))
|
||||
}
|
66
tui/examples/tui.rs
Normal file
66
tui/examples/tui.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use tek_tui::{*, tek_input::*, tek_output::*};
|
||||
use tek_edn::*;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use crossterm::event::{*, KeyCode::*};
|
||||
use crate::ratatui::style::Color;
|
||||
fn main () -> Usually<()> {
|
||||
let state = Arc::new(RwLock::new(Example(10, Measure::new())));
|
||||
Tui::new().unwrap().run(&state)?;
|
||||
Ok(())
|
||||
}
|
||||
#[derive(Debug)] pub struct Example(usize, Measure<TuiOut>);
|
||||
const KEYMAP: &str = "(:left prev) (:right next)";
|
||||
handle!(TuiIn: |self: Example, input|{
|
||||
let keymap = SourceIter::new(KEYMAP);
|
||||
let command = keymap.command::<_, ExampleCommand, _>(self, input);
|
||||
if let Some(command) = command {
|
||||
command.execute(self)?;
|
||||
return Ok(Some(true))
|
||||
}
|
||||
return Ok(None)
|
||||
});
|
||||
enum ExampleCommand { Next, Previous }
|
||||
atom_command!(ExampleCommand: |app: Example| {
|
||||
(":prev" [] Some(Self::Previous))
|
||||
(":next" [] Some(Self::Next))
|
||||
});
|
||||
command!(|self: ExampleCommand, state: Example|match self {
|
||||
Self::Next =>
|
||||
{ state.0 = (state.0 + 1) % EXAMPLES.len(); None },
|
||||
Self::Previous =>
|
||||
{ state.0 = if state.0 > 0 { state.0 - 1 } else { EXAMPLES.len() - 1 }; None },
|
||||
});
|
||||
const EXAMPLES: &'static [&'static str] = &[
|
||||
include_str!("edn01.edn"),
|
||||
include_str!("edn02.edn"),
|
||||
include_str!("edn03.edn"),
|
||||
include_str!("edn04.edn"),
|
||||
include_str!("edn05.edn"),
|
||||
include_str!("edn06.edn"),
|
||||
include_str!("edn07.edn"),
|
||||
include_str!("edn08.edn"),
|
||||
include_str!("edn09.edn"),
|
||||
include_str!("edn10.edn"),
|
||||
include_str!("edn11.edn"),
|
||||
include_str!("edn12.edn"),
|
||||
include_str!("edn13.edn"),
|
||||
];
|
||||
view!(TuiOut: |self: Example|{
|
||||
let index = self.0 + 1;
|
||||
let wh = self.1.wh();
|
||||
let src = EXAMPLES[self.0];
|
||||
let heading = format!("Example {}/{} in {:?}", index, EXAMPLES.len(), &wh);
|
||||
let title = Tui::bg(Color::Rgb(60, 10, 10), Push::y(1, Align::n(heading)));
|
||||
let code = Tui::bg(Color::Rgb(10, 60, 10), Push::y(2, Align::n(format!("{}", src))));
|
||||
let content = Tui::bg(Color::Rgb(10, 10, 60), View(self, SourceIter::new(src)));
|
||||
self.1.of(Bsp::s(title, Bsp::n(""/*code*/, content)))
|
||||
}; {
|
||||
":title" => Tui::bg(Color::Rgb(60, 10, 10), Push::y(1, Align::n(format!("Example {}/{}:", self.0 + 1, EXAMPLES.len())))).boxed(),
|
||||
":code" => Tui::bg(Color::Rgb(10, 60, 10), Push::y(2, Align::n(format!("{}", EXAMPLES[self.0])))).boxed(),
|
||||
":hello" => Tui::bg(Color::Rgb(10, 100, 10), "Hello").boxed(),
|
||||
":world" => Tui::bg(Color::Rgb(100, 10, 10), "world").boxed(),
|
||||
":hello-world" => "Hello world!".boxed()
|
||||
});
|
||||
provide_bool!(bool: |self: Example| {});
|
||||
provide_num!(u16: |self: Example| {});
|
||||
provide_num!(usize: |self: Example| {});
|
59
tui/src/lib.rs
Normal file
59
tui/src/lib.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
mod tui_buffer; pub use self::tui_buffer::*;
|
||||
mod tui_color; pub use self::tui_color::*;
|
||||
mod tui_content; pub use self::tui_content::*;
|
||||
mod tui_engine; pub use self::tui_engine::*;
|
||||
mod tui_file; pub use self::tui_file::*;
|
||||
mod tui_input; pub use self::tui_input::*;
|
||||
mod tui_output; pub use self::tui_output::*;
|
||||
mod tui_perf; pub use self::tui_perf::*;
|
||||
pub use ::tek_edn;// pub(crate) use ::tek_edn::*;
|
||||
//pub use ::tek_time; pub(crate) use ::tek_time::*;
|
||||
pub use ::tek_input; pub(crate) use tek_input::*;
|
||||
pub use ::tek_output; pub(crate) use tek_output::*;
|
||||
pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity};
|
||||
pub use ::palette; pub(crate) use ::palette::{*, convert::*, okhsl::*};
|
||||
pub use ::crossterm; pub(crate) use crossterm::{
|
||||
ExecutableCommand,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
|
||||
event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
|
||||
};
|
||||
pub use ::ratatui; pub(crate) use ratatui::{
|
||||
prelude::{Color, Style, Buffer},
|
||||
style::Modifier,
|
||||
backend::{Backend, CrosstermBackend, ClearType},
|
||||
layout::{Size, Rect},
|
||||
buffer::Cell
|
||||
};
|
||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
|
||||
pub(crate) use std::io::{stdout, Stdout};
|
||||
pub(crate) use std::path::PathBuf;
|
||||
pub(crate) use std::ffi::OsString;
|
||||
pub(crate) use atomic_float::AtomicF64;
|
||||
#[macro_export] macro_rules! from {
|
||||
($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => {
|
||||
impl $(<$($lt),+>)? From<$Source> for $Target {
|
||||
fn from ($state:$Source) -> Self { $cb }
|
||||
}
|
||||
};
|
||||
}
|
||||
#[cfg(test)] #[test] fn test_tui_engine () -> Usually<()> {
|
||||
use crate::*;
|
||||
use std::sync::{Arc, RwLock};
|
||||
struct TestComponent(String);
|
||||
impl Content<TuiOut> for TestComponent {
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
Some(self.0.as_str())
|
||||
}
|
||||
}
|
||||
impl Handle<TuiIn> for TestComponent {
|
||||
fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
let engine = Tui::new()?;
|
||||
engine.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let state = TestComponent("hello world".into());
|
||||
let state = std::sync::Arc::new(std::sync::RwLock::new(state));
|
||||
//engine.run(&state)?;
|
||||
Ok(())
|
||||
}
|
39
tui/src/tui_buffer.rs
Normal file
39
tui/src/tui_buffer.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use crate::*;
|
||||
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
for row in 0..area.h() {
|
||||
let y = area.y() + row;
|
||||
for col in 0..area.w() {
|
||||
let x = area.x() + col;
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
callback(buf.get_mut(x, y), col, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Default)] pub struct BigBuffer {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub content: Vec<Cell>
|
||||
}
|
||||
impl std::fmt::Debug for BigBuffer {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len())
|
||||
}
|
||||
}
|
||||
impl BigBuffer {
|
||||
pub fn new (width: usize, height: usize) -> Self {
|
||||
Self { width, height, content: vec![Cell::default(); width*height] }
|
||||
}
|
||||
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
|
||||
let i = self.index_of(x, y);
|
||||
self.content.get(i)
|
||||
}
|
||||
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
|
||||
let i = self.index_of(x, y);
|
||||
self.content.get_mut(i)
|
||||
}
|
||||
pub fn index_of (&self, x: usize, y: usize) -> usize {
|
||||
y * self.width + x
|
||||
}
|
||||
}
|
||||
from!(|size:(usize, usize)| BigBuffer = Self::new(size.0, size.1));
|
163
tui/src/tui_color.rs
Normal file
163
tui/src/tui_color.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
use crate::*;
|
||||
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
||||
impl Tui {
|
||||
pub const fn null () -> Color { Color::Reset }
|
||||
pub const fn g (g: u8) -> Color { Color::Rgb(g, g, g) }
|
||||
pub const fn red () -> Color { Color::Rgb(255,0, 0) }
|
||||
pub const fn orange () -> Color { Color::Rgb(255,128,0) }
|
||||
pub const fn yellow () -> Color { Color::Rgb(255,255,0) }
|
||||
pub const fn brown () -> Color { Color::Rgb(128,255,0) }
|
||||
pub const fn green () -> Color { Color::Rgb(0,255,0) }
|
||||
pub const fn electric () -> Color { Color::Rgb(0,255,128) }
|
||||
//fn bg0 () -> Color { Color::Rgb(20, 20, 20) }
|
||||
//fn bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
//fn border_bg () -> Color { Color::Rgb(40, 50, 30) }
|
||||
//fn border_fg (f: bool) -> Color { if f { Self::bo1() } else { Self::bo2() } }
|
||||
//fn title_fg (f: bool) -> Color { if f { Self::ti1() } else { Self::ti2() } }
|
||||
//fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
|
||||
//fn mode_bg () -> Color { Color::Rgb(150, 160, 90) }
|
||||
//fn mode_fg () -> Color { Color::Rgb(255, 255, 255) }
|
||||
//fn status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
//fn bo1 () -> Color { Color::Rgb(100, 110, 40) }
|
||||
//fn bo2 () -> Color { Color::Rgb(70, 80, 50) }
|
||||
//fn ti1 () -> Color { Color::Rgb(150, 160, 90) }
|
||||
//fn ti2 () -> Color { Color::Rgb(120, 130, 100) }
|
||||
}
|
||||
pub trait HasColor { fn color (&self) -> ItemColor; }
|
||||
#[macro_export] macro_rules! has_color {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn color (&$self) -> ItemColor { $cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A color in OKHSL and RGB representations.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor {
|
||||
pub okhsl: Okhsl<f32>,
|
||||
pub rgb: Color,
|
||||
}
|
||||
from!(|okhsl: Okhsl<f32>|ItemColor = Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
|
||||
pub fn okhsl_to_rgb (color: Okhsl<f32>) -> Color {
|
||||
let Srgb { red, green, blue, .. }: Srgb<f32> = Srgb::from_color_unclamped(color);
|
||||
Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,)
|
||||
}
|
||||
from!(|rgb: Color|ItemColor = Self { rgb, okhsl: rgb_to_okhsl(rgb) });
|
||||
pub fn rgb_to_okhsl (color: Color) -> Okhsl<f32> {
|
||||
if let Color::Rgb(r, g, b) = color {
|
||||
Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0))
|
||||
} else {
|
||||
unreachable!("only Color::Rgb is supported")
|
||||
}
|
||||
}
|
||||
// A single color within item theme parameters, in OKHSL and RGB representations.
|
||||
impl ItemColor {
|
||||
pub const fn from_rgb (rgb: Color) -> Self {
|
||||
Self { rgb, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) }
|
||||
}
|
||||
pub const fn from_okhsl (okhsl: Okhsl<f32>) -> Self {
|
||||
Self { rgb: Color::Rgb(0, 0, 0), okhsl }
|
||||
}
|
||||
pub fn random () -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let lo = Okhsl::new(-180.0, 0.01, 0.25);
|
||||
let hi = Okhsl::new( 180.0, 0.9, 0.5);
|
||||
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
||||
}
|
||||
pub fn random_dark () -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let lo = Okhsl::new(-180.0, 0.025, 0.075);
|
||||
let hi = Okhsl::new( 180.0, 0.5, 0.150);
|
||||
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
||||
}
|
||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||
color.mix(Self::random(), distance)
|
||||
}
|
||||
pub fn mix (&self, other: Self, distance: f32) -> Self {
|
||||
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
|
||||
self.okhsl.mix(other.okhsl, distance).into()
|
||||
}
|
||||
}
|
||||
/// A color in OKHSL and RGB with lighter and darker variants.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemPalette {
|
||||
pub base: ItemColor,
|
||||
pub light: ItemColor,
|
||||
pub lighter: ItemColor,
|
||||
pub lightest: ItemColor,
|
||||
pub dark: ItemColor,
|
||||
pub darker: ItemColor,
|
||||
pub darkest: ItemColor,
|
||||
}
|
||||
impl ItemPalette {
|
||||
pub const G: [Self;256] = {
|
||||
let mut builder = konst::array::ArrayBuilder::new();
|
||||
while !builder.is_full() {
|
||||
let index = builder.len() as u8;
|
||||
let light = (index as f64 * 1.3) as u8;
|
||||
let lighter = (index as f64 * 1.6) as u8;
|
||||
let lightest = (index as f64 * 1.9) as u8;
|
||||
let dark = (index as f64 * 0.9) as u8;
|
||||
let darker = (index as f64 * 0.6) as u8;
|
||||
let darkest = (index as f64 * 0.3) as u8;
|
||||
builder.push(ItemPalette {
|
||||
base: ItemColor::from_rgb(Color::Rgb(index, index, index )),
|
||||
light: ItemColor::from_rgb(Color::Rgb(light, light, light, )),
|
||||
lighter: ItemColor::from_rgb(Color::Rgb(lighter, lighter, lighter, )),
|
||||
lightest: ItemColor::from_rgb(Color::Rgb(lightest, lightest, lightest, )),
|
||||
dark: ItemColor::from_rgb(Color::Rgb(dark, dark, dark, )),
|
||||
darker: ItemColor::from_rgb(Color::Rgb(darker, darker, darker, )),
|
||||
darkest: ItemColor::from_rgb(Color::Rgb(darkest, darkest, darkest, )),
|
||||
});
|
||||
}
|
||||
builder.build()
|
||||
};
|
||||
pub fn random () -> Self { ItemColor::random().into() }
|
||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||
color.base.mix(ItemColor::random(), distance).into()
|
||||
}
|
||||
pub const G00: Self = {
|
||||
let color: ItemColor = ItemColor {
|
||||
okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 },
|
||||
rgb: Color::Rgb(0, 0, 0)
|
||||
};
|
||||
Self {
|
||||
base: color,
|
||||
light: color,
|
||||
lighter: color,
|
||||
lightest: color,
|
||||
dark: color,
|
||||
darker: color,
|
||||
darkest: color,
|
||||
}
|
||||
};
|
||||
pub fn from_tui_color (base: Color) -> Self {
|
||||
Self::from_item_color(ItemColor::from_rgb(base))
|
||||
}
|
||||
pub fn from_item_color (base: ItemColor) -> Self {
|
||||
let mut light = base.okhsl;
|
||||
light.lightness = (light.lightness * 1.3).min(1.0);
|
||||
let mut lighter = light;
|
||||
lighter.lightness = (lighter.lightness * 1.3).min(1.0);
|
||||
let mut lightest = base.okhsl;
|
||||
lightest.lightness = 0.95;
|
||||
let mut dark = base.okhsl;
|
||||
dark.lightness = (dark.lightness * 0.75).max(0.0);
|
||||
dark.saturation = (dark.saturation * 0.75).max(0.0);
|
||||
let mut darker = dark;
|
||||
darker.lightness = (darker.lightness * 0.66).max(0.0);
|
||||
darker.saturation = (darker.saturation * 0.66).max(0.0);
|
||||
let mut darkest = darker;
|
||||
darkest.lightness = 0.1;
|
||||
darkest.saturation = (darkest.saturation * 0.50).max(0.0);
|
||||
Self {
|
||||
base,
|
||||
light: light.into(),
|
||||
lighter: lighter.into(),
|
||||
lightest: lightest.into(),
|
||||
dark: dark.into(),
|
||||
darker: darker.into(),
|
||||
darkest: darkest.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
from!(|base: Color| ItemPalette = Self::from_tui_color(base));
|
||||
from!(|base: ItemColor|ItemPalette = Self::from_item_color(base));
|
522
tui/src/tui_content.rs
Normal file
522
tui/src/tui_content.rs
Normal file
|
@ -0,0 +1,522 @@
|
|||
use crate::*;
|
||||
use crate::Color::*;
|
||||
use ratatui::prelude::Position;
|
||||
macro_rules! impl_content_layout_render {
|
||||
($Output:ty: |$self:ident: $Struct:ty, $to:ident| layout = $layout:expr; render = $render:expr) => {
|
||||
impl Content<$Output> for $Struct {
|
||||
fn layout (&$self, $to: [u16;4]) -> [u16;4] { $layout }
|
||||
fn render (&$self, $to: &mut $Output) { $render }
|
||||
}
|
||||
}
|
||||
}
|
||||
impl_content_layout_render!(TuiOut: |self: &str, to|
|
||||
layout = to.center_xy([self.chars().count() as u16, 1]);
|
||||
render = {let [x, y, ..] = Content::layout(self, to.area());
|
||||
to.blit(self, x, y, None)});
|
||||
impl_content_layout_render!(TuiOut: |self: String, to|
|
||||
layout = to.center_xy([self.chars().count() as u16, 1]);
|
||||
render = {let [x, y, ..] = Content::layout(self, to.area());
|
||||
to.blit(self, x, y, None)});
|
||||
impl_content_layout_render!(TuiOut: |self: std::sync::RwLock<String>, to|
|
||||
layout = Content::<TuiOut>::layout(&self.read().unwrap(), to);
|
||||
render = Content::<TuiOut>::render(&self.read().unwrap(), to));
|
||||
impl_content_layout_render!(TuiOut: |self: std::sync::RwLockReadGuard<'_, String>, to|
|
||||
layout = Content::<TuiOut>::layout(&**self, to);
|
||||
render = Content::<TuiOut>::render(&**self, to));
|
||||
impl_content_layout_render!(TuiOut: |self: Arc<str>, to|
|
||||
layout = to.center_xy([self.chars().count() as u16, 1]);
|
||||
render = to.blit(self, to.area.x(), to.area.y(), None));
|
||||
impl<T: Content<TuiOut>> Content<TuiOut> for std::sync::Arc<T> {
|
||||
fn layout (&self, to: [u16;4]) -> [u16;4] {
|
||||
Content::<TuiOut>::layout(&**self, to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
Content::<TuiOut>::render(&**self, to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FieldH<T, U>(pub ItemPalette, pub T, pub U);
|
||||
impl<T: Content<TuiOut>, U: Content<TuiOut>> Content<TuiOut> for FieldH<T, U> {
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
let Self(ItemPalette { darkest, dark, lighter, lightest, .. }, title, value) = self;
|
||||
row!(
|
||||
Tui::fg_bg(dark.rgb, darkest.rgb, "▐"),
|
||||
Tui::fg_bg(lighter.rgb, dark.rgb, Tui::bold(true, title)),
|
||||
Tui::fg_bg(dark.rgb, darkest.rgb, "▌"),
|
||||
Tui::fg_bg(lightest.rgb, darkest.rgb, value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FieldV<T, U>(pub ItemPalette, pub T, pub U);
|
||||
impl<T: Content<TuiOut>, U: Content<TuiOut>> Content<TuiOut> for FieldV<T, U> {
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
let Self(ItemPalette { darkest, dark, lighter, lightest, .. }, title, value) = self;
|
||||
let sep1 = Tui::bg(darkest.rgb, Tui::fg(dark.rgb, "▐"));
|
||||
let sep2 = Tui::bg(darkest.rgb, Tui::fg(dark.rgb, "▌"));
|
||||
let title = Tui::bg(dark.rgb, Tui::fg(lighter.rgb, Tui::bold(true, title)));
|
||||
let value = Tui::bg(darkest.rgb, Tui::fg(lightest.rgb, value));
|
||||
Bsp::e(Bsp::s(row!(sep1, title, sep2), value), " ")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Repeat<'a>(pub &'a str);
|
||||
impl Content<TuiOut> for Repeat<'_> {
|
||||
fn layout (&self, to: [u16;4]) -> [u16;4] { to }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
let [x, y, w, h] = to.area().xywh();
|
||||
let a = self.0.len();
|
||||
for (_v, y) in (y..y+h).enumerate() {
|
||||
for (u, x) in (x..x+w).enumerate() {
|
||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||
let u = u % a;
|
||||
cell.set_symbol(&self.0[u..u+1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RepeatV<'a>(pub &'a str);
|
||||
impl Content<TuiOut> for RepeatV<'_> {
|
||||
fn layout (&self, to: [u16;4]) -> [u16;4] { to }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
let [x, y, _w, h] = to.area().xywh();
|
||||
for y in y..y+h {
|
||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RepeatH<'a>(pub &'a str);
|
||||
impl Content<TuiOut> for RepeatH<'_> {
|
||||
fn layout (&self, to: [u16;4]) -> [u16;4] { to }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
let [x, y, w, _h] = to.area().xywh();
|
||||
for x in x..x+w {
|
||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScrollbarV {
|
||||
pub offset: usize,
|
||||
pub length: usize,
|
||||
pub total: usize,
|
||||
}
|
||||
impl ScrollbarV {
|
||||
const ICON_DEC: &[char] = &['▲'];
|
||||
const ICON_INC: &[char] = &['▼'];
|
||||
}
|
||||
impl Content<TuiOut> for ScrollbarV {
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
let [x, y1, _w, h] = to.area().xywh();
|
||||
let y2 = y1 + h;
|
||||
for (i, y) in (y1..=y2).enumerate() {
|
||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||
if (i as usize) < (Self::ICON_DEC.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(Self::ICON_DEC[i as usize]);
|
||||
} else if (i as usize) > (h as usize - Self::ICON_INC.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(Self::ICON_INC[h as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('‖'); // ━
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╎'); // ━
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScrollbarH {
|
||||
pub offset: usize,
|
||||
pub length: usize,
|
||||
pub total: usize,
|
||||
}
|
||||
impl ScrollbarH {
|
||||
const ICON_DEC: &[char] = &[' ', '🞀', ' '];
|
||||
const ICON_INC: &[char] = &[' ', '🞂', ' '];
|
||||
}
|
||||
impl Content<TuiOut> for ScrollbarH {
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
let [x1, y, w, _h] = to.area().xywh();
|
||||
let x2 = x1 + w;
|
||||
for (i, x) in (x1..=x2).enumerate() {
|
||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||
if i < (Self::ICON_DEC.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(Self::ICON_DEC[x as usize]);
|
||||
} else if i > (w as usize - Self::ICON_INC.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(Self::ICON_INC[w as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('━');
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╌');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cell that takes up 3 rows on its own,
|
||||
/// but stacks, giving (N+1)*2 rows per N cells.
|
||||
pub struct Phat<T> {
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub content: T,
|
||||
pub colors: [Color;4],
|
||||
}
|
||||
impl<T> Phat<T> {
|
||||
/// A phat line
|
||||
pub fn lo (fg: Color, bg: Color) -> impl Content<TuiOut> {
|
||||
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▄")))
|
||||
}
|
||||
/// A phat line
|
||||
pub fn hi (fg: Color, bg: Color) -> impl Content<TuiOut> {
|
||||
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▀")))
|
||||
}
|
||||
}
|
||||
impl<T: Content<TuiOut>> Content<TuiOut> for Phat<T> {
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
let [fg, bg, hi, lo] = self.colors;
|
||||
let top = Fixed::y(1, Self::lo(bg, hi));
|
||||
let low = Fixed::y(1, Self::hi(bg, lo));
|
||||
let content = Tui::fg_bg(fg, bg, &self.content);
|
||||
Min::xy(self.width, self.height, Bsp::s(top, Bsp::n(low, Fill::xy(content))))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TuiStyle {
|
||||
fn fg <R: Content<TuiOut>> (color: Color, w: R) -> Foreground<R> {
|
||||
Foreground(color, w)
|
||||
}
|
||||
fn bg <R: Content<TuiOut>> (color: Color, w: R) -> Background<R> {
|
||||
Background(color, w)
|
||||
}
|
||||
fn fg_bg <R: Content<TuiOut>> (fg: Color, bg: Color, w: R) -> Background<Foreground<R>> {
|
||||
Background(bg, Foreground(fg, w))
|
||||
}
|
||||
fn bold <R: Content<TuiOut>> (enable: bool, w: R) -> Bold<R> {
|
||||
Bold(enable, w)
|
||||
}
|
||||
fn border <R: Content<TuiOut>, S: BorderStyle> (enable: bool, style: S, w: R) -> Bordered<S, R> {
|
||||
Bordered(enable, style, w)
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiStyle for Tui {}
|
||||
|
||||
pub struct Bold<R: Content<TuiOut>>(pub bool, R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Bold<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_bold(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Foreground<R: Content<TuiOut>>(pub Color, R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Foreground<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_fg(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Background<R: Content<TuiOut>>(pub Color, R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Background<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_bg(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Styled<R: Content<TuiOut>>(pub Option<Style>, pub R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Styled<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.place(self.content().layout(to.area()), &self.content());
|
||||
// TODO write style over area
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Bordered<S: BorderStyle, W: Content<TuiOut>>(pub bool, pub S, pub W);
|
||||
content!(TuiOut: |self: Bordered<S: BorderStyle, W: Content<TuiOut>>|Fill::xy(
|
||||
lay!(When::new(self.0, Border(self.0, self.1)), Padding::xy(1, 1, &self.2))
|
||||
));
|
||||
|
||||
pub struct Border<S: BorderStyle>(pub bool, pub S);
|
||||
render!(TuiOut: |self: Border<S: BorderStyle>, to| {
|
||||
if self.0 {
|
||||
let area = to.area();
|
||||
if area.w() > 0 && area.y() > 0 {
|
||||
to.blit(&self.1.nw(), area.x(), area.y(), self.1.style());
|
||||
to.blit(&self.1.ne(), area.x() + area.w() - 1, area.y(), self.1.style());
|
||||
to.blit(&self.1.sw(), area.x(), area.y() + area.h() - 1, self.1.style());
|
||||
to.blit(&self.1.se(), area.x() + area.w() - 1, area.y() + area.h() - 1, self.1.style());
|
||||
for x in area.x()+1..area.x()+area.w()-1 {
|
||||
to.blit(&self.1.n(), x, area.y(), self.1.style());
|
||||
to.blit(&self.1.s(), x, area.y() + area.h() - 1, self.1.style());
|
||||
}
|
||||
for y in area.y()+1..area.y()+area.h()-1 {
|
||||
to.blit(&self.1.w(), area.x(), y, self.1.style());
|
||||
to.blit(&self.1.e(), area.x() + area.w() - 1, y, self.1.style());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pub trait BorderStyle: Send + Sync + Copy {
|
||||
fn enabled (&self) -> bool;
|
||||
fn enclose <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
|
||||
Bsp::b(Fill::xy(Border(self.enabled(), self)), w)
|
||||
}
|
||||
fn enclose2 <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
|
||||
Bsp::b(Margin::xy(1, 1, Fill::xy(Border(self.enabled(), self))), w)
|
||||
}
|
||||
fn enclose_bg <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
|
||||
Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
|
||||
Bsp::b(Fill::xy(Border(self.enabled(), self)), w))
|
||||
}
|
||||
const NW: &'static str = "";
|
||||
const N: &'static str = "";
|
||||
const NE: &'static str = "";
|
||||
const E: &'static str = "";
|
||||
const SE: &'static str = "";
|
||||
const S: &'static str = "";
|
||||
const SW: &'static str = "";
|
||||
const W: &'static str = "";
|
||||
|
||||
const N0: &'static str = "";
|
||||
const S0: &'static str = "";
|
||||
const W0: &'static str = "";
|
||||
const E0: &'static str = "";
|
||||
|
||||
fn n (&self) -> &str { Self::N }
|
||||
fn s (&self) -> &str { Self::S }
|
||||
fn e (&self) -> &str { Self::E }
|
||||
fn w (&self) -> &str { Self::W }
|
||||
fn nw (&self) -> &str { Self::NW }
|
||||
fn ne (&self) -> &str { Self::NE }
|
||||
fn sw (&self) -> &str { Self::SW }
|
||||
fn se (&self) -> &str { Self::SE }
|
||||
#[inline] fn draw <'a> (
|
||||
&self, to: &mut TuiOut
|
||||
) -> Usually<()> {
|
||||
if self.enabled() {
|
||||
self.draw_horizontal(to, None)?;
|
||||
self.draw_vertical(to, None)?;
|
||||
self.draw_corners(to, None)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[inline] fn draw_horizontal (
|
||||
&self, to: &mut TuiOut, style: Option<Style>
|
||||
) -> Usually<[u16;4]> {
|
||||
let area = to.area();
|
||||
let style = style.or_else(||self.style_horizontal());
|
||||
let [x, x2, y, y2] = area.lrtb();
|
||||
for x in x..x2.saturating_sub(1) {
|
||||
to.blit(&Self::N, x, y, style);
|
||||
to.blit(&Self::S, x, y2.saturating_sub(1), style)
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline] fn draw_vertical (
|
||||
&self, to: &mut TuiOut, style: Option<Style>
|
||||
) -> Usually<[u16;4]> {
|
||||
let area = to.area();
|
||||
let style = style.or_else(||self.style_vertical());
|
||||
let [x, x2, y, y2] = area.lrtb();
|
||||
let h = y2 - y;
|
||||
if h > 1 {
|
||||
for y in y..y2.saturating_sub(1) {
|
||||
to.blit(&Self::W, x, y, style);
|
||||
to.blit(&Self::E, x2.saturating_sub(1), y, style);
|
||||
}
|
||||
} else if h > 0 {
|
||||
to.blit(&Self::W0, x, y, style);
|
||||
to.blit(&Self::E0, x2.saturating_sub(1), y, style);
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline] fn draw_corners (
|
||||
&self, to: &mut TuiOut, style: Option<Style>
|
||||
) -> Usually<[u16;4]> {
|
||||
let area = to.area();
|
||||
let style = style.or_else(||self.style_corners());
|
||||
let [x, y, width, height] = area.xywh();
|
||||
if width > 1 && height > 1 {
|
||||
to.blit(&Self::NW, x, y, style);
|
||||
to.blit(&Self::NE, x + width - 1, y, style);
|
||||
to.blit(&Self::SW, x, y + height - 1, style);
|
||||
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline] fn style (&self) -> Option<Style> { None }
|
||||
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
|
||||
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
|
||||
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
|
||||
}
|
||||
|
||||
macro_rules! border {
|
||||
($($T:ident {
|
||||
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
||||
$($x:tt)*
|
||||
}),+) => {$(
|
||||
impl BorderStyle for $T {
|
||||
const NW: &'static str = $nw;
|
||||
const N: &'static str = $n;
|
||||
const NE: &'static str = $ne;
|
||||
const W: &'static str = $w;
|
||||
const E: &'static str = $e;
|
||||
const SW: &'static str = $sw;
|
||||
const S: &'static str = $s;
|
||||
const SE: &'static str = $se;
|
||||
$($x)*
|
||||
fn enabled (&self) -> bool { false }
|
||||
}
|
||||
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
|
||||
impl Content<TuiOut> for $T {
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
if self.enabled() { let _ = self.draw(to); }
|
||||
}
|
||||
}
|
||||
)+}
|
||||
}
|
||||
|
||||
border! {
|
||||
Square {
|
||||
"┌" "─" "┐"
|
||||
"│" "│"
|
||||
"└" "─" "┘" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
SquareBold {
|
||||
"┏" "━" "┓"
|
||||
"┃" "┃"
|
||||
"┗" "━" "┛" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
TabLike {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"│" " " "│" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Lozenge {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"╰" "─" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Brace {
|
||||
"╭" "" "╮"
|
||||
"│" "│"
|
||||
"╰" "" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
LozengeDotted {
|
||||
"╭" "┅" "╮"
|
||||
"┇" "┇"
|
||||
"╰" "┅" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Quarter {
|
||||
"▎" "▔" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "▁" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
QuarterV {
|
||||
"▎" "" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Chamfer {
|
||||
"🭂" "▔" "🭍"
|
||||
"▎" "🮇"
|
||||
"🭓" "▁" "🭞" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Corners {
|
||||
"🬆" "" "🬊" // 🬴 🬸
|
||||
"" ""
|
||||
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
CornersTall {
|
||||
"🭽" "" "🭾"
|
||||
"" ""
|
||||
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Outer {
|
||||
"🭽" "▔" "🭾"
|
||||
"▏" "▕"
|
||||
"🭼" "▁" "🭿"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Thick {
|
||||
"▄" "▄" "▄"
|
||||
"█" "█"
|
||||
"▀" "▀" "▀"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Rugged {
|
||||
"▄" "▂" "▄"
|
||||
"▐" "▌"
|
||||
"▀" "🮂" "▀"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Skinny {
|
||||
"▗" "▄" "▖"
|
||||
"▐" "▌"
|
||||
"▝" "▀" "▘"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Brackets {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Reticle {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "╟";
|
||||
const E0: &'static str = "╢";
|
||||
const N0: &'static str = "┯";
|
||||
const S0: &'static str = "┷";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//impl<S: BorderStyle, R: Content<TuiOut>> Content<TuiOut> for Bordered<S, R> {
|
||||
//fn content (&self) -> impl Render<TuiOut> {
|
||||
//let content: &dyn Content<TuiOut> = &self.1;
|
||||
//lay! { content.padding_xy(1, 1), Border(self.0) }.fill_xy()
|
||||
//}
|
||||
//}
|
82
tui/src/tui_engine.rs
Normal file
82
tui/src/tui_engine.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use crate::*;
|
||||
use std::time::Duration;
|
||||
pub struct Tui {
|
||||
pub exited: Arc<AtomicBool>,
|
||||
pub backend: CrosstermBackend<Stdout>,
|
||||
pub buffer: Buffer,
|
||||
pub area: [u16;4],
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
impl Tui {
|
||||
/// Construct a new TUI engine and wrap it for shared ownership.
|
||||
pub fn new () -> Usually<Arc<RwLock<Self>>> {
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let Size { width, height } = backend.size()?;
|
||||
Ok(Arc::new(RwLock::new(Self {
|
||||
exited: Arc::new(AtomicBool::new(false)),
|
||||
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
|
||||
area: [0, 0, width, height],
|
||||
backend,
|
||||
perf: Default::default(),
|
||||
})))
|
||||
}
|
||||
/// True if done
|
||||
pub fn exited (&self) -> bool {
|
||||
self.exited.fetch_and(true, Relaxed)
|
||||
}
|
||||
/// Prepare before run
|
||||
pub fn setup (&mut self) -> Usually<()> {
|
||||
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
|
||||
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||
CrosstermBackend::new(stdout()).show_cursor().unwrap();
|
||||
disable_raw_mode().unwrap();
|
||||
better_panic_handler(info);
|
||||
}));
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
self.backend.hide_cursor()?;
|
||||
enable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
/// Update the display buffer.
|
||||
pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer {
|
||||
if self.buffer.area != size {
|
||||
self.backend.clear_region(ClearType::All).unwrap();
|
||||
self.buffer.resize(size);
|
||||
self.buffer.reset();
|
||||
}
|
||||
let updates = self.buffer.diff(&buffer);
|
||||
self.backend.draw(updates.into_iter()).expect("failed to render");
|
||||
self.backend.flush().expect("failed to flush output buffer");
|
||||
std::mem::swap(&mut self.buffer, &mut buffer);
|
||||
buffer.reset();
|
||||
buffer
|
||||
}
|
||||
/// Clean up after run
|
||||
pub fn teardown (&mut self) -> Usually<()> {
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
self.backend.show_cursor()?;
|
||||
disable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
pub trait TuiRun<R: Render<TuiOut> + Handle<TuiIn> + 'static> {
|
||||
/// Run an app in the main loop.
|
||||
fn run (&self, state: &Arc<RwLock<R>>) -> Usually<()>;
|
||||
}
|
||||
impl<T: Render<TuiOut> + Handle<TuiIn> + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
|
||||
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()> {
|
||||
let _input_thread = TuiIn::run_input(self, state, Duration::from_millis(100));
|
||||
self.write().unwrap().setup()?;
|
||||
let render_thread = TuiOut::run_output(self, state, Duration::from_millis(10));
|
||||
match render_thread.join() {
|
||||
Ok(result) => {
|
||||
self.write().unwrap().teardown()?;
|
||||
println!("\n\rRan successfully: {result:?}\n\r");
|
||||
},
|
||||
Err(error) => {
|
||||
self.write().unwrap().teardown()?;
|
||||
panic!("\n\rRender thread failed: {error:?}.\n\r")
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
87
tui/src/tui_file.rs
Normal file
87
tui/src/tui_file.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use crate::*;
|
||||
/// Browses for phrase to import/export
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileBrowser {
|
||||
pub cwd: PathBuf,
|
||||
pub dirs: Vec<(OsString, String)>,
|
||||
pub files: Vec<(OsString, String)>,
|
||||
pub filter: String,
|
||||
pub index: usize,
|
||||
pub scroll: usize,
|
||||
pub size: Measure<TuiOut>
|
||||
}
|
||||
/// Commands supported by [FileBrowser]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum FileBrowserCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Select(usize),
|
||||
Chdir(PathBuf),
|
||||
Filter(Arc<str>),
|
||||
}
|
||||
content!(TuiOut: |self: FileBrowser| /*Stack::down(|add|{
|
||||
let mut i = 0;
|
||||
for (_, name) in self.dirs.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&Tui::bold(i == self.index, name.as_str()))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
for (_, name) in self.files.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&Tui::bold(i == self.index, name.as_str()))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
add(&format!("{}/{i}", self.index))?;
|
||||
Ok(())
|
||||
})*/"todo");
|
||||
impl FileBrowser {
|
||||
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
||||
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
|
||||
let mut dirs = vec![];
|
||||
let mut files = vec![];
|
||||
for entry in std::fs::read_dir(&cwd)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
|
||||
let meta = entry.metadata()?;
|
||||
if meta.is_dir() {
|
||||
dirs.push((name, format!("📁 {decoded}")));
|
||||
} else if meta.is_file() {
|
||||
files.push((name, format!("📄 {decoded}")));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
cwd,
|
||||
dirs,
|
||||
files,
|
||||
filter: "".to_string(),
|
||||
index: 0,
|
||||
scroll: 0,
|
||||
size: Measure::new(),
|
||||
})
|
||||
}
|
||||
pub fn len (&self) -> usize {
|
||||
self.dirs.len() + self.files.len()
|
||||
}
|
||||
pub fn is_dir (&self) -> bool {
|
||||
self.index < self.dirs.len()
|
||||
}
|
||||
pub fn is_file (&self) -> bool {
|
||||
self.index >= self.dirs.len()
|
||||
}
|
||||
pub fn path (&self) -> PathBuf {
|
||||
self.cwd.join(if self.is_dir() {
|
||||
&self.dirs[self.index].0
|
||||
} else if self.is_file() {
|
||||
&self.files[self.index - self.dirs.len()].0
|
||||
} else {
|
||||
unreachable!()
|
||||
})
|
||||
}
|
||||
pub fn chdir (&self) -> Usually<Self> {
|
||||
Self::new(Some(self.path()))
|
||||
}
|
||||
}
|
306
tui/src/tui_focus.rs
Normal file
306
tui/src/tui_focus.rs
Normal file
|
@ -0,0 +1,306 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum FocusState<T: Copy + Debug + PartialEq> {
|
||||
Focused(T),
|
||||
Entered(T),
|
||||
}
|
||||
|
||||
impl<T: Copy + Debug + PartialEq> FocusState<T> {
|
||||
pub fn inner (&self) -> T {
|
||||
match self {
|
||||
Self::Focused(inner) => *inner,
|
||||
Self::Entered(inner) => *inner,
|
||||
}
|
||||
}
|
||||
pub fn set_inner (&mut self, inner: T) {
|
||||
*self = match self {
|
||||
Self::Focused(_) => Self::Focused(inner),
|
||||
Self::Entered(_) => Self::Entered(inner),
|
||||
}
|
||||
}
|
||||
pub fn is_focused (&self) -> bool { matches!(self, Self::Focused(_)) }
|
||||
pub fn is_entered (&self) -> bool { matches!(self, Self::Entered(_)) }
|
||||
pub fn focus (&mut self) { *self = Self::Focused(self.inner()) }
|
||||
pub fn enter (&mut self) { *self = Self::Entered(self.inner()) }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub enum FocusCommand<T: Send + Sync> {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Next,
|
||||
Prev,
|
||||
Enter,
|
||||
Exit,
|
||||
Set(T)
|
||||
}
|
||||
|
||||
impl<F: HasFocus + HasEnter + FocusGrid + FocusOrder> Command<F> for FocusCommand<F::Item> {
|
||||
fn execute (self, state: &mut F) -> Perhaps<FocusCommand<F::Item>> {
|
||||
match self {
|
||||
Self::Next => { state.focus_next(); },
|
||||
Self::Prev => { state.focus_prev(); },
|
||||
Self::Up => { state.focus_up(); },
|
||||
Self::Down => { state.focus_down(); },
|
||||
Self::Left => { state.focus_left(); },
|
||||
Self::Right => { state.focus_right(); },
|
||||
Self::Enter => { state.focus_enter(); },
|
||||
Self::Exit => { state.focus_exit(); },
|
||||
Self::Set(to) => { state.set_focused(to); },
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that have focusable subparts.
|
||||
pub trait HasFocus {
|
||||
type Item: Copy + PartialEq + Debug + Send + Sync;
|
||||
/// Get the currently focused item.
|
||||
fn focused (&self) -> Self::Item;
|
||||
/// Get the currently focused item.
|
||||
fn set_focused (&mut self, to: Self::Item);
|
||||
/// Loop forward until a specific item is focused.
|
||||
fn focus_to (&mut self, to: Self::Item) {
|
||||
self.set_focused(to);
|
||||
self.focus_updated();
|
||||
}
|
||||
/// Run this on focus update
|
||||
fn focus_updated (&mut self) {}
|
||||
}
|
||||
|
||||
/// Trait for things that have enterable subparts.
|
||||
pub trait HasEnter: HasFocus {
|
||||
/// Get the currently focused item.
|
||||
fn entered (&self) -> bool;
|
||||
/// Get the currently focused item.
|
||||
fn set_entered (&mut self, entered: bool);
|
||||
/// Enter into the currently focused component
|
||||
fn focus_enter (&mut self) {
|
||||
self.set_entered(true);
|
||||
self.focus_updated();
|
||||
}
|
||||
/// Exit the currently entered component
|
||||
fn focus_exit (&mut self) {
|
||||
self.set_entered(false);
|
||||
self.focus_updated();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that implement directional navigation between focusable elements.
|
||||
pub trait FocusGrid: HasFocus {
|
||||
fn focus_layout (&self) -> &[&[Self::Item]];
|
||||
fn focus_cursor (&self) -> (usize, usize);
|
||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize);
|
||||
fn focus_current (&self) -> Self::Item {
|
||||
let (x, y) = self.focus_cursor();
|
||||
self.focus_layout()[y][x]
|
||||
}
|
||||
fn focus_update (&mut self) {
|
||||
self.focus_to(self.focus_current());
|
||||
self.focus_updated()
|
||||
}
|
||||
fn focus_up (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (_, original_y) = self.focus_cursor();
|
||||
loop {
|
||||
let (x, y) = self.focus_cursor();
|
||||
let next_y = if y == 0 {
|
||||
self.focus_layout().len().saturating_sub(1)
|
||||
} else {
|
||||
y - 1
|
||||
};
|
||||
if next_y == original_y {
|
||||
break
|
||||
}
|
||||
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
|
||||
x
|
||||
} else {
|
||||
((x as f32 / self.focus_layout()[original_y].len() as f32)
|
||||
* self.focus_layout()[next_y].len() as f32) as usize
|
||||
};
|
||||
*self.focus_cursor_mut() = (next_x, next_y);
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
fn focus_down (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (_, original_y) = self.focus_cursor();
|
||||
loop {
|
||||
let (x, y) = self.focus_cursor();
|
||||
let next_y = if y >= self.focus_layout().len().saturating_sub(1) {
|
||||
0
|
||||
} else {
|
||||
y + 1
|
||||
};
|
||||
if next_y == original_y {
|
||||
break
|
||||
}
|
||||
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
|
||||
x
|
||||
} else {
|
||||
((x as f32 / self.focus_layout()[original_y].len() as f32)
|
||||
* self.focus_layout()[next_y].len() as f32) as usize
|
||||
};
|
||||
*self.focus_cursor_mut() = (next_x, next_y);
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
fn focus_left (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (original_x, y) = self.focus_cursor();
|
||||
loop {
|
||||
let x = self.focus_cursor().0;
|
||||
let next_x = if x == 0 {
|
||||
self.focus_layout()[y].len().saturating_sub(1)
|
||||
} else {
|
||||
x - 1
|
||||
};
|
||||
if next_x == original_x {
|
||||
break
|
||||
}
|
||||
*self.focus_cursor_mut() = (next_x, y);
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
fn focus_right (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (original_x, y) = self.focus_cursor();
|
||||
loop {
|
||||
let x = self.focus_cursor().0;
|
||||
let next_x = if x >= self.focus_layout()[y].len().saturating_sub(1) {
|
||||
0
|
||||
} else {
|
||||
x + 1
|
||||
};
|
||||
if next_x == original_x {
|
||||
break
|
||||
}
|
||||
self.focus_cursor_mut().0 = next_x;
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that implement next/prev navigation between focusable elements.
|
||||
pub trait FocusOrder {
|
||||
/// Focus the next item.
|
||||
fn focus_next (&mut self);
|
||||
/// Focus the previous item.
|
||||
fn focus_prev (&mut self);
|
||||
}
|
||||
|
||||
/// Next/prev navigation for directional focusables works in the given way.
|
||||
impl<T: FocusGrid + HasEnter> FocusOrder for T {
|
||||
/// Focus the next item.
|
||||
fn focus_next (&mut self) {
|
||||
let current = self.focused();
|
||||
let (x, y) = self.focus_cursor();
|
||||
if x < self.focus_layout()[y].len().saturating_sub(1) {
|
||||
self.focus_right();
|
||||
} else {
|
||||
self.focus_down();
|
||||
self.focus_cursor_mut().0 = 0;
|
||||
}
|
||||
if self.focused() == current { // FIXME: prevent infinite loop
|
||||
self.focus_next()
|
||||
}
|
||||
self.focus_exit();
|
||||
self.focus_update();
|
||||
}
|
||||
/// Focus the previous item.
|
||||
fn focus_prev (&mut self) {
|
||||
let current = self.focused();
|
||||
let (x, _) = self.focus_cursor();
|
||||
if x > 0 {
|
||||
self.focus_left();
|
||||
} else {
|
||||
self.focus_up();
|
||||
let (_, y) = self.focus_cursor();
|
||||
let next_x = self.focus_layout()[y].len().saturating_sub(1);
|
||||
self.focus_cursor_mut().0 = next_x;
|
||||
}
|
||||
if self.focused() == current { // FIXME: prevent infinite loop
|
||||
self.focus_prev()
|
||||
}
|
||||
self.focus_exit();
|
||||
self.focus_update();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FocusWrap<T> {
|
||||
fn wrap <W: Content<TuiOut>> (self, focus: T, content: &'_ W) -> impl Content<TuiOut> + '_;
|
||||
}
|
||||
|
||||
pub fn to_focus_command <T: Send + Sync> (input: &TuiIn) -> Option<FocusCommand<T>> {
|
||||
Some(match input.event() {
|
||||
kpat!(Tab) => FocusCommand::Next,
|
||||
kpat!(Shift-Tab) => FocusCommand::Prev,
|
||||
kpat!(BackTab) => FocusCommand::Prev,
|
||||
kpat!(Shift-BackTab) => FocusCommand::Prev,
|
||||
kpat!(Up) => FocusCommand::Up,
|
||||
kpat!(Down) => FocusCommand::Down,
|
||||
kpat!(Left) => FocusCommand::Left,
|
||||
kpat!(Right) => FocusCommand::Right,
|
||||
kpat!(Enter) => FocusCommand::Enter,
|
||||
kpat!(Esc) => FocusCommand::Exit,
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! impl_focus {
|
||||
($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => {
|
||||
impl HasFocus for $Struct {
|
||||
type Item = $Focus;
|
||||
/// Get the currently focused item.
|
||||
fn focused (&self) -> Self::Item {
|
||||
self.focus.inner()
|
||||
}
|
||||
/// Get the currently focused item.
|
||||
fn set_focused (&mut self, to: Self::Item) {
|
||||
self.focus.set_inner(to)
|
||||
}
|
||||
$(fn focus_updated (&mut $self) { $update_focus })?
|
||||
}
|
||||
impl HasEnter for $Struct {
|
||||
/// Get the currently focused item.
|
||||
fn entered (&self) -> bool {
|
||||
self.focus.is_entered()
|
||||
}
|
||||
/// Get the currently focused item.
|
||||
fn set_entered (&mut self, entered: bool) {
|
||||
if entered {
|
||||
self.focus.to_entered()
|
||||
} else {
|
||||
self.focus.to_focused()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FocusGrid for $Struct {
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
}
|
||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
|
||||
&mut self.cursor
|
||||
}
|
||||
fn focus_layout (&self) -> &[&[$Focus]] {
|
||||
use $Focus::*;
|
||||
&$Grid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
tui/src/tui_input.rs
Normal file
146
tui/src/tui_input.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
use crate::*;
|
||||
use std::time::Duration;
|
||||
use std::thread::{spawn, JoinHandle};
|
||||
use crossterm::event::{poll, read};
|
||||
#[derive(Debug, Clone)] pub struct TuiIn(pub Arc<AtomicBool>, pub Event);
|
||||
impl Input for TuiIn {
|
||||
type Event = Event;
|
||||
type Handled = bool;
|
||||
fn event (&self) -> &Event { &self.1 }
|
||||
fn is_done (&self) -> bool { self.0.fetch_and(true, Relaxed) }
|
||||
fn done (&self) { self.0.store(true, Relaxed); }
|
||||
}
|
||||
impl TuiIn {
|
||||
/// Spawn the input thread.
|
||||
pub fn run_input <T: Handle<TuiIn> + 'static> (
|
||||
engine: &Arc<RwLock<Tui>>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
timer: Duration
|
||||
) -> JoinHandle<()> {
|
||||
let exited = engine.read().unwrap().exited.clone();
|
||||
let state = state.clone();
|
||||
spawn(move || loop {
|
||||
if exited.fetch_and(true, Relaxed) {
|
||||
break
|
||||
}
|
||||
if poll(timer).is_ok() {
|
||||
let event = read().unwrap();
|
||||
match event {
|
||||
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
}) => {
|
||||
exited.store(true, Relaxed);
|
||||
},
|
||||
_ => {
|
||||
let exited = exited.clone();
|
||||
if let Err(e) = state.write().unwrap().handle(&TuiIn(exited, event)) {
|
||||
panic!("{e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
impl AtomInput for TuiIn {
|
||||
fn matches_atom (&self, token: &str) -> bool {
|
||||
if let Some(event) = KeyMatcher::new(token).build() {
|
||||
&event == self.event()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
//fn get_event (item: &AtomItem<impl AsRef<str>>) -> Option<Event> {
|
||||
//match item { AtomItem::Sym(s) => KeyMatcher::new(s).build(), _ => None }
|
||||
//}
|
||||
}
|
||||
struct KeyMatcher {
|
||||
valid: bool,
|
||||
key: Option<KeyCode>,
|
||||
mods: KeyModifiers,
|
||||
}
|
||||
impl KeyMatcher {
|
||||
fn new (token: impl AsRef<str>) -> Self {
|
||||
let token = token.as_ref();
|
||||
if token.len() < 2 {
|
||||
Self { valid: false, key: None, mods: KeyModifiers::NONE }
|
||||
} else if token.chars().next() != Some('@') {
|
||||
Self { valid: false, key: None, mods: KeyModifiers::NONE }
|
||||
} else {
|
||||
Self { valid: true, key: None, mods: KeyModifiers::NONE }.next(&token[1..])
|
||||
}
|
||||
}
|
||||
fn next (mut self, token: &str) -> Self {
|
||||
let mut tokens = token.split('-').peekable();
|
||||
while let Some(token) = tokens.next() {
|
||||
if tokens.peek().is_some() {
|
||||
match token {
|
||||
"ctrl" | "Ctrl" | "c" | "C" => self.mods |= KeyModifiers::CONTROL,
|
||||
"alt" | "Alt" | "m" | "M" => self.mods |= KeyModifiers::ALT,
|
||||
"shift" | "Shift" | "s" | "S" => {
|
||||
self.mods |= KeyModifiers::SHIFT;
|
||||
// + TODO normalize character case, BackTab, etc.
|
||||
},
|
||||
_ => panic!("unknown modifier {token}"),
|
||||
}
|
||||
} else {
|
||||
self.key = if token.len() == 1 {
|
||||
Some(KeyCode::Char(token.chars().next().unwrap()))
|
||||
} else {
|
||||
Some(Self::named_key(token).unwrap_or_else(||panic!("unknown character {token}")))
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
fn named_key (token: &str) -> Option<KeyCode> {
|
||||
use KeyCode::*;
|
||||
Some(match token {
|
||||
"up" => Up,
|
||||
"down" => Down,
|
||||
"left" => Left,
|
||||
"right" => Right,
|
||||
"enter" | "return" => Enter,
|
||||
"delete" | "del" => Delete,
|
||||
"tab" => Tab,
|
||||
"space" => Char(' '),
|
||||
"comma" => Char(','),
|
||||
"period" => Char('.'),
|
||||
"plus" => Char('+'),
|
||||
"minus" | "dash" => Char('-'),
|
||||
"equal" | "equals" => Char('='),
|
||||
"underscore" => Char('_'),
|
||||
"backtick" => Char('`'),
|
||||
"lt" => Char('<'),
|
||||
"gt" => Char('>'),
|
||||
"openbracket" => Char('['),
|
||||
"closebracket" => Char(']'),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
fn build (self) -> Option<Event> {
|
||||
if self.valid && self.key.is_some() {
|
||||
Some(Event::Key(KeyEvent::new(self.key.unwrap(), self.mods)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)] #[test] fn test_parse_key () {
|
||||
use KeyModifiers as Mods;
|
||||
let test = |x: &str, y|assert_eq!(KeyMatcher::new(x).build(), Some(Event::Key(y)));
|
||||
//test(":x",
|
||||
//KeyEvent::new(KeyCode::Char('x'), Mods::NONE));
|
||||
//test(":ctrl-x",
|
||||
//KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL));
|
||||
//test(":alt-x",
|
||||
//KeyEvent::new(KeyCode::Char('x'), Mods::ALT));
|
||||
//test(":shift-x",
|
||||
//KeyEvent::new(KeyCode::Char('x'), Mods::SHIFT));
|
||||
//test(":ctrl-alt-shift-x",
|
||||
//KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL | Mods::ALT | Mods::SHIFT ));
|
||||
}
|
63
tui/src/tui_menu.rs
Normal file
63
tui/src/tui_menu.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use crate::*;
|
||||
pub struct MenuBar<E: Engine, S, C: Command<S>> {
|
||||
pub menus: Vec<Menu<E, S, C>>,
|
||||
pub index: usize,
|
||||
}
|
||||
impl<E: Engine, S, C: Command<S>> MenuBar<E, S, C> {
|
||||
pub fn new () -> Self { Self { menus: vec![], index: 0 } }
|
||||
pub fn add (mut self, menu: Menu<E, S, C>) -> Self {
|
||||
self.menus.push(menu);
|
||||
self
|
||||
}
|
||||
}
|
||||
pub struct Menu<E: Engine, S, C: Command<S>> {
|
||||
pub title: Arc<str>,
|
||||
pub items: Vec<MenuItem<E, S, C>>,
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
|
||||
pub fn new (title: impl AsRef<str>) -> Self {
|
||||
Self {
|
||||
title: title.as_ref().to_string(),
|
||||
items: vec![],
|
||||
index: None,
|
||||
}
|
||||
}
|
||||
pub fn add (mut self, item: MenuItem<E, S, C>) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
pub fn sep (mut self) -> Self {
|
||||
self.items.push(MenuItem::sep());
|
||||
self
|
||||
}
|
||||
pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self {
|
||||
self.items.push(MenuItem::cmd(hotkey, text, command));
|
||||
self
|
||||
}
|
||||
pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self {
|
||||
self.items.push(MenuItem::off(hotkey, text));
|
||||
self
|
||||
}
|
||||
}
|
||||
pub enum MenuItem<E: Engine, S, C: Command<S>> {
|
||||
/// Unused.
|
||||
__(PhantomData<E>, PhantomData<S>),
|
||||
/// A separator. Skip it.
|
||||
Separator,
|
||||
/// A menu item with command, description and hotkey.
|
||||
Command(&'static str, &'static str, C),
|
||||
/// A menu item that can't be activated but has description and hotkey
|
||||
Disabled(&'static str, &'static str)
|
||||
}
|
||||
impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
|
||||
pub fn sep () -> Self {
|
||||
Self::Separator
|
||||
}
|
||||
pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self {
|
||||
Self::Command(hotkey, text, command)
|
||||
}
|
||||
pub fn off (hotkey: &'static str, text: &'static str) -> Self {
|
||||
Self::Disabled(hotkey, text)
|
||||
}
|
||||
}
|
104
tui/src/tui_output.rs
Normal file
104
tui/src/tui_output.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
use crate::*;
|
||||
use std::time::Duration;
|
||||
use std::thread::{spawn, JoinHandle};
|
||||
#[derive(Default)]
|
||||
pub struct TuiOut {
|
||||
pub buffer: Buffer,
|
||||
pub area: [u16;4]
|
||||
}
|
||||
impl Output for TuiOut {
|
||||
type Unit = u16;
|
||||
type Size = [Self::Unit;2];
|
||||
type Area = [Self::Unit;4];
|
||||
#[inline] fn area (&self) -> [u16;4] { self.area }
|
||||
#[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area }
|
||||
#[inline] fn place (&mut self, area: [u16;4], content: &impl Render<TuiOut>) {
|
||||
let last = self.area();
|
||||
*self.area_mut() = area;
|
||||
content.render(self);
|
||||
*self.area_mut() = last;
|
||||
}
|
||||
}
|
||||
impl TuiOut {
|
||||
/// Spawn the output thread.
|
||||
pub fn run_output <T: Render<TuiOut> + Send + Sync + 'static> (
|
||||
engine: &Arc<RwLock<Tui>>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
timer: Duration
|
||||
) -> JoinHandle<()> {
|
||||
let exited = engine.read().unwrap().exited.clone();
|
||||
let engine = engine.clone();
|
||||
let state = state.clone();
|
||||
let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed");
|
||||
let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height });
|
||||
spawn(move || loop {
|
||||
if exited.fetch_and(true, Relaxed) {
|
||||
break
|
||||
}
|
||||
let t0 = engine.read().unwrap().perf.get_t0();
|
||||
let Size { width, height } = engine.read().unwrap().backend.size()
|
||||
.expect("get size failed");
|
||||
if let Ok(state) = state.try_read() {
|
||||
let size = Rect { x: 0, y: 0, width, height };
|
||||
if buffer.area != size {
|
||||
engine.write().unwrap().backend.clear_region(ClearType::All)
|
||||
.expect("clear failed");
|
||||
buffer.resize(size);
|
||||
buffer.reset();
|
||||
}
|
||||
let mut output = TuiOut { buffer, area: [0, 0, width, height] };
|
||||
state.render(&mut output);
|
||||
buffer = engine.write().unwrap().flip(output.buffer, size);
|
||||
}
|
||||
let t1 = engine.read().unwrap().perf.get_t1(t0).unwrap();
|
||||
//buffer.set_string(0, 0, &format!("{:>3}.{:>3}ms", t1.as_millis(), t1.as_micros() % 1000), Style::default());
|
||||
std::thread::sleep(timer);
|
||||
})
|
||||
}
|
||||
pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
buffer_update(&mut self.buffer, area, callback);
|
||||
}
|
||||
pub fn fill_bold (&mut self, area: [u16;4], on: bool) {
|
||||
if on {
|
||||
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD))
|
||||
} else {
|
||||
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD))
|
||||
}
|
||||
}
|
||||
pub fn fill_bg (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
|
||||
}
|
||||
pub fn fill_fg (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
|
||||
}
|
||||
pub fn fill_ul (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{
|
||||
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
|
||||
cell.underline_color = color;
|
||||
})
|
||||
}
|
||||
pub fn fill_char (&mut self, area: [u16;4], c: char) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
|
||||
}
|
||||
pub fn make_dim (&mut self) {
|
||||
for cell in self.buffer.content.iter_mut() {
|
||||
cell.bg = ratatui::style::Color::Rgb(30,30,30);
|
||||
cell.fg = ratatui::style::Color::Rgb(100,100,100);
|
||||
cell.modifier = ratatui::style::Modifier::DIM;
|
||||
}
|
||||
}
|
||||
pub fn blit (
|
||||
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
|
||||
) {
|
||||
let text = text.as_ref();
|
||||
let buf = &mut self.buffer;
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
buf.set_string(x, y, text, style.unwrap_or(Style::default()));
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
|
||||
self.area = area;
|
||||
self
|
||||
}
|
||||
}
|
64
tui/src/tui_perf.rs
Normal file
64
tui/src/tui_perf.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use crate::*;
|
||||
|
||||
/// Performance counter
|
||||
#[derive(Debug)]
|
||||
pub struct PerfModel {
|
||||
pub enabled: bool,
|
||||
clock: quanta::Clock,
|
||||
// In nanoseconds. Time used by last iteration.
|
||||
used: AtomicF64,
|
||||
// In microseconds. Max prescribed time for iteration (frame, chunk...).
|
||||
window: AtomicF64,
|
||||
}
|
||||
|
||||
pub trait HasPerf {
|
||||
fn perf (&self) -> &PerfModel;
|
||||
}
|
||||
|
||||
impl Default for PerfModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
clock: quanta::Clock::new(),
|
||||
used: Default::default(),
|
||||
window: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PerfModel {
|
||||
pub fn get_t0 (&self) -> Option<u64> {
|
||||
if self.enabled {
|
||||
Some(self.clock.raw())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn get_t1 (&self, t0: Option<u64>) -> Option<std::time::Duration> {
|
||||
if let Some(t0) = t0 {
|
||||
if self.enabled {
|
||||
Some(self.clock.delta(t0, self.clock.raw()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn update (&self, t0: Option<u64>, microseconds: f64) {
|
||||
if let Some(t0) = t0 {
|
||||
let t1 = self.clock.raw();
|
||||
self.used.store(self.clock.delta_as_nanos(t0, t1) as f64, Relaxed);
|
||||
self.window.store(microseconds, Relaxed,);
|
||||
}
|
||||
}
|
||||
pub fn percentage (&self) -> Option<f64> {
|
||||
let window = self.window.load(Relaxed) * 1000.0;
|
||||
if window > 0.0 {
|
||||
let used = self.used.load(Relaxed);
|
||||
Some(100.0 * used / window)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue