diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7b08ef3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +root = true +[*] +max_line_length = 132 diff --git a/Cargo.lock b/Cargo.lock index 2a2cf65..e5500fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "allocator-api2" @@ -40,15 +40,15 @@ checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -56,7 +56,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -86,15 +86,15 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "by_address" @@ -110,18 +110,18 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "compact_str" @@ -151,9 +151,34 @@ dependencies = [ [[package]] name = "const_panic" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb8a602185c3c95b52f86dc78e55a6df9a287a7a93ddbcf012509930880cf879" +dependencies = [ + "const_panic_proc_macros", + "typewit", +] + +[[package]] +name = "const_panic_proc_macros" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" +checksum = "0c5b80a80fb52c1a6ca02e3cd829a76b472ff0a15588196fd8da95221f0c1e4b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "crossbeam-utils" @@ -177,6 +202,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.0.8", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -221,6 +264,36 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "either" version = "1.15.0" @@ -241,12 +314,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -275,25 +348,25 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.3+wasi-0.2.4", ] [[package]] @@ -304,9 +377,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -333,9 +406,9 @@ checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "instability" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" dependencies = [ "darling", "indoc", @@ -413,9 +486,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "linux-raw-sys" @@ -430,10 +503,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] -name = "lock_api" -version = "0.4.12" +name = "litrs" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -456,29 +535,29 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -515,7 +594,7 @@ dependencies = [ "fast-srgb8", "palette_derive", "phf", - "rand", + "rand 0.8.5", ] [[package]] @@ -532,9 +611,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -542,15 +621,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -576,7 +655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -612,26 +691,26 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" dependencies = [ "bit-set", "bit-vec", "bitflags", "lazy_static", "num-traits", - "rand", - "rand_chacha", + "rand 0.9.2", + "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -652,15 +731,15 @@ dependencies = [ [[package]] name = "quanta" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" dependencies = [ "crossbeam-utils", "libc", "once_cell", "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "web-sys", "winapi", ] @@ -682,9 +761,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -693,8 +772,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -704,7 +793,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -713,16 +812,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", ] [[package]] name = "rand_xorshift" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core", + "rand_core 0.9.3", ] [[package]] @@ -734,7 +842,7 @@ dependencies = [ "bitflags", "cassowary", "compact_str", - "crossterm", + "crossterm 0.28.1", "indoc", "instability", "itertools 0.13.0", @@ -757,24 +865,24 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustix" @@ -791,22 +899,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" @@ -834,9 +942,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -855,9 +963,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -870,9 +978,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "static_assertions" @@ -910,9 +1018,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -921,90 +1029,117 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.1" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", - "rustix 1.0.5", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] name = "tengri" -version = "0.8.0" +version = "0.14.0" dependencies = [ + "crossterm 0.29.0", + "tengri", + "tengri_core", "tengri_dsl", "tengri_input", "tengri_output", + "tengri_proc", "tengri_tui", ] +[[package]] +name = "tengri_core" +version = "0.14.0" + [[package]] name = "tengri_dsl" -version = "0.8.0" +version = "0.14.0" dependencies = [ + "const_panic", "itertools 0.14.0", "konst", "proptest", + "tengri_core", "tengri_tui", "thiserror", ] [[package]] name = "tengri_input" -version = "0.8.0" +version = "0.14.0" dependencies = [ + "tengri_core", "tengri_dsl", "tengri_tui", ] [[package]] name = "tengri_output" -version = "0.8.0" +version = "0.14.0" dependencies = [ + "bumpalo", "proptest", "proptest-derive", "tengri", + "tengri_core", "tengri_dsl", "tengri_tui", ] +[[package]] +name = "tengri_proc" +version = "0.14.0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", + "tengri_core", +] + [[package]] name = "tengri_tui" -version = "0.8.0" +version = "0.14.0" dependencies = [ "atomic_float", "better-panic", - "crossterm", + "bumpalo", + "crossterm 0.29.0", "konst", "palette", "quanta", - "rand", + "rand 0.8.5", "ratatui", "tengri", + "tengri_core", "tengri_dsl", "tengri_input", "tengri_output", + "tengri_proc", "unicode-width 0.2.0", ] [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -1013,9 +1148,9 @@ dependencies = [ [[package]] name = "typewit" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb77c29baba9e4d3a6182d51fa75e3215c7fd1dab8f4ea9d107c716878e55fc0" +checksum = "4dd91acc53c592cb800c11c83e8e7ee1d48378d05cfa33b5474f5f80c5b236bf" dependencies = [ "typewit_proc_macros", ] @@ -1067,6 +1202,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -1078,17 +1219,17 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -1181,13 +1322,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-link" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-sys" @@ -1195,7 +1333,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", ] [[package]] @@ -1204,14 +1351,31 @@ 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", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -1220,42 +1384,84 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[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_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[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_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1263,28 +1469,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ae648af..f2280f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,52 @@ -[workspace.package] -version = "0.8.0" - -[workspace] -resolver = "2" -members = [ - "./tengri", - "./input", - "./output", - "./tui", - "./dsl" -] - [profile.release] lto = true [profile.coverage] inherits = "test" lto = false + +[workspace] +resolver = "2" +members = [ + "./tengri", + "./core", + "./input", + "./output", + "./tui", + "./dsl", + "./proc", +] + +[workspace.package] +version = "0.14.0" +edition = "2024" + +[workspace.dependencies] +tengri = { path = "./tengri" } +tengri_core = { path = "./core" } +tengri_input = { path = "./input" } +tengri_output = { path = "./output" } +tengri_tui = { path = "./tui" } +tengri_dsl = { path = "./dsl" } +tengri_proc = { path = "./proc" } + +anyhow = { version = "1.0" } +atomic_float = { version = "1" } +better-panic = { version = "0.3.0" } +bumpalo = { version = "3.19.0" } +const_panic = { version = "0.2.12", features = [ "derive" ] } +crossterm = { version = "0.29.0" } +heck = { version = "0.5" } +itertools = { version = "0.14.0" } +konst = { version = "0.3.16", features = [ "rust_1_83" ] } +palette = { version = "0.7.6", features = [ "random" ] } +proc-macro2 = { version = "1", features = ["span-locations"] } +proptest = { version = "^1" } +proptest-derive = { version = "^0.5.1" } +quanta = { version = "0.12.3" } +quote = { version = "1" } +rand = { version = "0.8.5" } +ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] } +syn = { version = "2", features = ["full", "extra-traits"] } +thiserror = { version = "2.0" } +unicode-width = { version = "0.2" } diff --git a/Justfile b/Justfile index af61a41..1d5f794 100644 --- a/Justfile +++ b/Justfile @@ -1,15 +1,34 @@ -covfig := "CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' RUSTDOCFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cov/cargo-test-%p-%m.profraw'" +export 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/*'" + +default: + just -l + +bacon: + bacon -s + cov: - {{covfig}} time cargo test -j4 --workspace --profile coverage + CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' \ + time cargo test -j4 --workspace --profile coverage rm -rf target/coverage/html || true - {{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t html -o target/coverage/html + time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t html -o target/coverage/html + cov-md: - {{covfig}} time cargo test -j4 --workspace --profile coverage - {{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t markdown | sort + CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' \ + time cargo test -j4 --workspace --profile coverage + time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t markdown | sort + cov-md-ci: - {{covfig}} time cargo test -j4 --workspace --profile coverage -- --skip test_tui_engine - {{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t markdown | sort + CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' \ + time cargo test -j4 --workspace --profile coverage -- --skip test_tui_engine + time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t markdown | sort + doc: - cargo doc + CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' RUSTDOCFLAGS='-Cinstrument-coverage' \ + cargo doc + +example-tui-00: + cargo run -p tengri_tui --example tui_00 +example-tui-01: + cargo run -p tengri_tui --example tui_01 diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 0000000..8a5516f --- /dev/null +++ b/bacon.toml @@ -0,0 +1,44 @@ +default_job = "check-all" + +env.CARGO_TERM_COLOR = "always" + +[keybindings] +c = "job:check" +d = "job:doc-open" +t = "job:test" +n = "job:nextest" +l = "job:clippy" + +[jobs] + +[jobs.check] +command = ["cargo", "check"] +need_stdout = false +watch = ["core","dsl","editor","input","output","proc","tengri","tui"] + +[jobs.clippy-all] +command = ["cargo", "clippy"] +need_stdout = false +watch = ["tek", "deps"] + +[jobs.test] +command = ["cargo", "test"] +need_stdout = true +watch = ["tek", "deps"] + +[jobs.doc] +command = ["cargo", "doc", "--no-deps"] +need_stdout = false + +[jobs.doc-open] +command = ["cargo", "doc", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +[skin] +status_fg = 251 +status_bg = 200 +key_fg = 11 +status_key_fg = 11 +project_name_badge_fg = 11 +project_name_badge_bg = 69 diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..b4e9baa --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "tengri_core" +description = "UI metaframework, core definitions." +version = { workspace = true } +edition = { workspace = true } + diff --git a/core/src/core_macros.rs b/core/src/core_macros.rs new file mode 100644 index 0000000..ef56421 --- /dev/null +++ b/core/src/core_macros.rs @@ -0,0 +1,137 @@ +/// Define and reexport submodules. +#[macro_export] macro_rules! modules( + ($($($feat:literal?)? $name:ident),* $(,)?) => { $( + $(#[cfg(feature=$feat)])? mod $name; + $(#[cfg(feature=$feat)])? pub use self::$name::*; + )* }; + ($($($feat:literal?)? $name:ident $body:block),* $(,)?) => { $( + $(#[cfg(feature=$feat)])? mod $name $body + $(#[cfg(feature=$feat)])? pub use self::$name::*; + )* }; +); + +/// Define a trait and implement it for read-only wrapper types. +#[macro_export] macro_rules! flex_trait ( + ($Trait:ident $(<$($A:ident:$T:ident),+>)? $(:$dep:ident $(<$dtt:tt>)? $(+$dep2:ident $(<$dtt2:tt>)?)*)? { + $(fn $fn:ident $(<$($fl:lifetime),*>)? (& $($fl2:lifetime)* $self:ident $(, $arg:ident:$ty:ty)*) $(-> $ret:ty)? $body:block)* + }) => { + pub trait $Trait $(<$($A: $T),+>)? $(:$dep $(<$dtt>)? $(+$dep2 $(<$dtt2>)?)*)? { + $(fn $fn $(<$($fl),*>)? (& $($fl2)* $self $(,$arg:$ty)*) $(-> $ret)? $body)* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &_T_ { + $(fn $fn $(<$($fl),*>)? (& $($fl2)* $self $(,$arg:$ty)*) $(-> $ret)? { (*$self).$fn($($arg),*) })* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &mut _T_ { + $(fn $fn $(<$($fl),*>)? (& $($fl2)* $self $(,$arg:$ty)*) $(-> $ret)? { (**$self).$fn($($arg),*) })* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<_T_> { + $(fn $fn $(<$($fl),*>)? (& $($fl2)* $self $(,$arg:$ty)*) $(-> $ret)? { (*$self).$fn($($arg),*) })* + } + impl<$($($A: $T,)+)?> $Trait $(<$($A),+>)? for Box)?> { + $(fn $fn $(<$($fl),*>)? (& $($fl2)* $self $(,$arg:$ty)*) $(-> $ret)? { (**$self).$fn($($arg),*) })* + } + //impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for Option<_T_> { + //$(fn $fn (&$self $(,$arg:$ty)*) -> $ret { + //if let Some(this) = $self { this.$fn($($arg),*) } else { Ok(None) } + //})* + //} + //impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Mutex<_T_> { + //$(fn $fn (&$self $(,$arg:$ty)*) -> $ret { (*$self).lock().unwrap().$fn($($arg),*) })* + //} + //impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::RwLock<_T_> { + //$(fn $fn (&$self $(,$arg:$ty)*) -> $ret { $self.read().unwrap().$fn($($arg),*) })* + //} + }); +/// Define a trait an implement it for read-only wrapper types. */ +#[macro_export] macro_rules! flex_trait_sized ( + ($Trait:ident $(<$($A:ident:$T:ident),+>)? $(:$dep:ident $(<$dtt:tt>)? $(+$dep2:ident $(<$dtt2:tt>)?)*)? { + $(fn $fn:ident (&$self:ident $(, $arg:ident:$ty:ty)*) $(-> $ret:ty)? $body:block)* + }) => { + pub trait $Trait $(<$($A: $T),+>)? : $($dep $(<$dtt>+)? $($dep2 $(<$dtt2>)?)*+)? Sized { + $(fn $fn (&$self $(,$arg:$ty)*) $(-> $ret)? $body)* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &_T_ { + $(fn $fn (&$self $(,$arg:$ty)*) $(-> $ret)? { (*$self).$fn($($arg),*) })* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &mut _T_ { + $(fn $fn (&$self $(,$arg:$ty)*) $(-> $ret)? { (**$self).$fn($($arg),*) })* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<_T_> { + $(fn $fn (&$self $(,$arg:$ty)*) $(-> $ret)? { (*$self).$fn($($arg),*) })* + } + impl<$($($A: $T,)+)?> $Trait $(<$($A),+>)? for Box)?> { + $(fn $fn (&$self $(,$arg:$ty)*) $(-> $ret)? { (**$self).$fn($($arg),*) })* + } + //impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for Option<_T_> { + //$(fn $fn (&$self $(,$arg:$ty)*) -> $ret { + //if let Some(this) = $self { this.$fn($($arg),*) } else { Ok(None) } + //})* + //} + //impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Mutex<_T_> { + //$(fn $fn (&$self $(,$arg:$ty)*) -> $ret { (*$self).lock().unwrap().$fn($($arg),*) })* + //} + //impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::RwLock<_T_> { + //$(fn $fn (&$self $(,$arg:$ty)*) -> $ret { $self.read().unwrap().$fn($($arg),*) })* + //} + }); + +/// Define a trait an implement it for various mutation-enabled wrapper types. */ +#[macro_export] macro_rules! flex_trait_mut ( + ($Trait:ident $(<$($A:ident:$T:ident),+>)? { + $(fn $fn:ident (&mut $self:ident $(, $arg:ident:$ty:ty)*) -> $ret:ty $body:block)* + })=>{ + pub trait $Trait $(<$($A: $T),+>)? { + $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret $body)* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &mut _T_ { + $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { (*$self).$fn($($arg),*) })* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for Option<_T_> { + $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { + if let Some(this) = $self { this.$fn($($arg),*) } else { Ok(None) } + })* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Mutex<_T_> { + $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.get_mut().unwrap().$fn($($arg),*) })* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::Mutex<_T_>> { + $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.lock().unwrap().$fn($($arg),*) })* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::RwLock<_T_> { + $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })* + } + impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::RwLock<_T_>> { + $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })* + } + }; +); + +/// Implement [`Debug`] in bulk. +#[macro_export] macro_rules! impl_debug(($($S:ty|$self:ident,$w:ident|$body:block)*)=>{ + $(impl std::fmt::Debug for $S { + fn fmt (&$self, $w: &mut std::fmt::Formatter) -> std::fmt::Result $body + })* }); + +/// Implement [`From`] in bulk. +#[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 }}}; + ($($Struct:ty { $( + $(<$($l:lifetime),* $($T:ident$(:$U:ident)?),*>)? ($source:ident: $From:ty) $expr:expr + );+ $(;)? })*) => { $( + $(impl $(<$($l),* $($T$(:$U)?),*>)? From<$From> for $Struct { + fn from ($source: $From) -> Self { $expr } })+ )* }; ); + +/// Implement [Has]. +#[macro_export] macro_rules! has(($T:ty: |$self:ident : $S:ty| $x:expr) => { + impl Has<$T> for $S { + fn get (&$self) -> &$T { &$x } + fn get_mut (&mut $self) -> &mut $T { &mut $x } } };); + +/// Implement [MaybeHas]. +#[macro_export] macro_rules! maybe_has( + ($T:ty: |$self:ident : $S:ty| $x:block; $y:block $(;)?) => { + impl MaybeHas<$T> for $S { + fn get (&$self) -> Option<&$T> $x + fn get_mut (&mut $self) -> Option<&mut $T> $y } };); diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..7bf320e --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,45 @@ +mod core_macros; +pub(crate) use std::error::Error; + +/// Standard result type. +pub type Usually = Result>; + +/// Standard optional result type. +pub type Perhaps = Result, Box>; + +/// Type-dispatched `get` and `get_mut`. +pub trait Has: Send + Sync { fn get (&self) -> &T; fn get_mut (&mut self) -> &mut T; } + +/// Type-dispatched `get` and `get_mut` that return an [Option]-wrapped result. +pub trait MaybeHas: Send + Sync { fn get (&self) -> Option<&T>; fn get_mut (&mut self) -> Option<&mut T>; } + +/// May compute a `RetVal` from `Args`. +pub trait Eval { + /// A custom operation on [Args] that may return [Result::Err] or [Option::None]. + fn try_eval (&self, args: &Args) -> Perhaps; + /// Invoke a custom operation, converting a `None` result to a custom `Box`. + fn eval >> (&self, args: &Args, error: impl Fn()->E) + -> Usually + { + match self.try_eval(args)? { + Some(value) => Ok(value), + _ => Result::Err(format!("Eval: {}", error().into()).into()) + } + } +} + +#[macro_export] macro_rules! as_ref { + ($T:ty: |$self:ident : $S:ty| $x:expr) => { + impl AsRef<$T> for $S { + fn as_ref (&$self) -> &$T { &$x } + } + }; +} + +pub fn wrap_inc (index: usize, count: usize) -> usize { + if count > 0 { (index + 1) % count } else { 0 } +} + +pub fn wrap_dec (index: usize, count: usize) -> usize { + if count > 0 { index.overflowing_sub(1).0.min(count.saturating_sub(1)) } else { 0 } +} diff --git a/dsl/Cargo.toml b/dsl/Cargo.toml index 94238a8..c61a057 100644 --- a/dsl/Cargo.toml +++ b/dsl/Cargo.toml @@ -1,16 +1,18 @@ [package] name = "tengri_dsl" -edition = "2024" description = "UI metaframework, tiny S-expression-based DSL." version = { workspace = true } +edition = { workspace = true } + +[lib] +path = "src/dsl.rs" [dependencies] -konst = { version = "0.3.16", features = [ "rust_1_83" ] } -itertools = "0.14.0" -thiserror = "2.0" - -[features] -default = [] +tengri_core = { path = "../core" } +konst = { workspace = true } +const_panic = { workspace = true } +itertools = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] tengri_tui = { path = "../tui" } diff --git a/dsl/README.md b/dsl/README.md index 97f1d2c..0ef496d 100644 --- a/dsl/README.md +++ b/dsl/README.md @@ -1,14 +1,25 @@ -# `ket` +[***dizzle***](https://codeberg.org/unspeaker/tengri/src/branch/main/dsl) +is a means of adding a tiny interpreted domain-specific language to your programs. -**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. +dizzle currently provides an s-expression based syntax. -## usage +dizzle parses source code by means of the `Dsl`, `DslExpr` and `DslWord` traits. +those are implemented for basic stringy types and their `Option` and `Result` wrapped analogs. +to customize parsing, define and use your own traits on top of the provided ones. -### with `tengri_output` +dizzle evaluates the parsed source code by means of the `DslNs` trait. the methods of +this trait match literals, words, and expressions, against pre-defined lists. the +`dsl_words` and `dsl_exprs` macros let you define those lists slightly less verbosely. -this is a `tengri_output` view layout defined using ket: +## goals + +* [x] const parse +* [ ] live reload +* [ ] serialize modified code back to original indentation + +## examples + +### in [`tengri_output`](../output) ```edn (bsp/s (fixed/y 2 :toolbar) @@ -16,9 +27,7 @@ this is a `tengri_output` view layout defined using ket: (bsp/s :outputs (bsp/s :inputs (bsp/s :tracks :scenes))))))) ``` -### with `tengri_input` - -this is a `tengri_input` keymap defined using ket: +### in [`tengri_input`](../input) ```edn (@u undo 1) @@ -29,66 +38,31 @@ this is a `tengri_input` keymap defined using ket: (@tab pool toggle) ``` -## tokens +## implementation notes -ket has 4 "types", represented by variants of the `Value` enum: +### `DslExpr` trait behavior -* `Num` - numeric literal -* `Sym` - textual symbol -* `Key` - textual key -* `Exp` - parenthesized group of tokens +this is the trait which differentiates "a thing" from +"a thing that is many things". -### numeric literal +|source |key|exp |head |tail | +|---------------|---|-------|---------|---------------| +|`a` |`a`|e0 |`a` |None | +|`(a)` |e1 |`a` |`(a)` |None | +|`a b c` |e2 |e0 |`a` |`b c` | +|`(a b c)` |e1 |`a b c`|`(a b c)`| | +|`(a b c) d e` |e1 |e3 |`(a b c)`|`d e` | +|`a (b c d) e f`|e1 |e0 |`a` |`(b c d) e f` | -numbers are passed through as is. only non-negative integers are supported. +* e0: Unexpected 'a' +* e1: Unexpected '(' +* e2: Unexpected 'b' +* e3: Unexpected 'd' -```edn -0 -123456 -``` +### possible design for operator-based syntax -### 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 `tengri_output`, keys are names of layout primitives -* in `tengri_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 `tengri_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 +* replace: `(:= :name :value1 :valueN)` +* append: `(:+ :name :value2 :valueN)` +* filter: `(:- :name :value2 :valueN)` +* map: `(:* :name op)` +* reduce: `(:/ :name op)` diff --git a/dsl/src/context.rs b/dsl/src/context.rs deleted file mode 100644 index d7adc78..0000000 --- a/dsl/src/context.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::*; -pub trait TryFromAtom<'a, T>: Sized { - fn try_from_expr (_state: &'a T, _iter: TokenIter<'a>) -> Option { None } - fn try_from_atom (state: &'a T, value: Value<'a>) -> Option { - if let Exp(0, iter) = value { return Self::try_from_expr(state, iter) } - None - } -} -pub trait TryIntoAtom: Sized { - fn try_into_atom (&self) -> Option; -} -/// Map EDN tokens to parameters of a given type for a given context -pub trait Context: Sized { - fn get (&self, _atom: &Value) -> Option { - None - } - fn get_or_fail (&self, atom: &Value) -> U { - self.get(atom).expect("no value") - } -} -impl, U> Context for &T { - fn get (&self, atom: &Value) -> Option { - (*self).get(atom) - } - fn get_or_fail (&self, atom: &Value) -> U { - (*self).get_or_fail(atom) - } -} -impl, U> Context for Option { - fn get (&self, atom: &Value) -> Option { - 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)); -} diff --git a/dsl/src/dsl.rs b/dsl/src/dsl.rs new file mode 100644 index 0000000..ffdf38c --- /dev/null +++ b/dsl/src/dsl.rs @@ -0,0 +1,227 @@ +//#![feature(adt_const_params)] +//#![feature(type_alias_impl_trait)] +#![feature(if_let_guard)] +#![feature(impl_trait_in_fn_trait_return)] +#![feature(const_precise_live_drops)] +#![feature(type_alias_impl_trait)] +extern crate const_panic; +use const_panic::PanicFmt; +use std::fmt::Debug; +pub(crate) use ::{ + std::sync::Arc, + //std::error::Error, + konst::iter::for_each, + konst::string::{str_from, str_range, char_indices}, + thiserror::Error, + tengri_core::* +}; +pub(crate) use self::DslError::*; +mod dsl_error; pub use self::dsl_error::*; +mod dsl_ns; pub use self::dsl_ns::*; +mod dsl_word; pub use self::dsl_word::*; +mod dsl_expr; pub use self::dsl_expr::*; +mod dsl_text; pub use self::dsl_text::*; +#[cfg(test)] mod dsl_test; + +// Trait that designates any string-like as potentially parsable DSL. +pub trait Dsl: Debug + Send + Sync { + fn src (&self) -> DslPerhaps<&str> { unreachable!("Dsl::src default impl") } +} +impl<'x> Dsl for &'x str { + fn src (&self) -> DslPerhaps<&str> { Ok(Some(self)) } +} +impl Dsl for Arc { + fn src (&self) -> DslPerhaps<&str> { Ok(Some(self.as_ref())) } +} +impl Dsl for &D { + fn src (&self) -> DslPerhaps<&str> { (*self).src() } +} +impl Dsl for &mut D { + fn src (&self) -> DslPerhaps<&str> { (**self).src() } +} +impl Dsl for Option { + fn src (&self) -> DslPerhaps<&str> {Ok(if let Some(dsl) = self { dsl.src()? } else { None })} +} +impl Dsl for Result { + fn src (&self) -> DslPerhaps<&str> {match self {Ok(dsl) => Ok(dsl.src()?), Err(e) => Err(*e)}} +} + +/// DSL-specific result type. +pub type DslResult = Result; + +/// DSL-specific optional result type. +pub type DslPerhaps = Result, DslError>; + +pub const fn peek (src: &str) -> DslPerhaps<&str> { + Ok(Some(if let Ok(Some(expr)) = expr_peek(src) { expr } else + if let Ok(Some(word)) = word_peek(src) { word } else + if let Ok(Some(text)) = text_peek(src) { text } else + if let Err(e) = no_trailing_non_space(src, 0, Some("peek")) { return Err(e) } + else { return Ok(None) })) +} + +pub const fn seek (src: &str) -> DslPerhaps<(usize, usize)> { + Ok(Some(if let Ok(Some(expr)) = expr_seek(src) { expr } else + if let Ok(Some(word)) = word_seek(src) { word } else + if let Ok(Some(text)) = text_seek(src) { text } else + if let Err(e) = no_trailing_non_space(src, 0, Some("seek")) { return Err(e) } + else { return Ok(None) })) +} + +pub const fn peek_tail (src: &str) -> DslPerhaps<&str> { + match seek(src) { + Err(e) => Err(e), + Ok(None) => Ok(None), + Ok(Some((start, length))) => { + let tail = str_range(src, start + length, src.len()); + for_each!((_i, c) in char_indices(tail) => if !is_space(c) { return Ok(Some(tail)) }); + Ok(None) + }, + } +} + +pub const fn expr_peek_inner (src: &str) -> DslPerhaps<&str> { + match expr_peek(src) { + Ok(Some(peeked)) => { + let len = peeked.len(); + let start = if len > 0 { 1 } else { 0 }; + Ok(Some(str_range(src, start, start + len.saturating_sub(2)))) + }, + e => e + } +} + +pub const fn expr_peek_inner_only (src: &str) -> DslPerhaps<&str> { + match expr_seek(src) { + Err(e) => Err(e), + Ok(None) => Ok(None), + Ok(Some((start, length))) => { + if let Err(e) = no_trailing_non_space(src, start + length, Some("expr_peek_inner_only")) { + Err(e) + } else { + let peeked = str_range(src, start, start + length); + let len = peeked.len(); + let start = if len > 0 { 1 } else { 0 }; + Ok(Some(str_range(peeked, start, start + len.saturating_sub(2)))) + } + }, + } +} + +pub const fn is_space (c: char) -> bool { + matches!(c, ' '|'\n'|'\r'|'\t') +} + +pub const fn no_trailing_non_space ( + src: &str, offset: usize, context: Option<&'static str> +) -> DslResult<()> { + Ok(for_each!((i, c) in char_indices(str_range(src, offset, src.len())) => if !is_space(c) { + return Err(Unexpected(c, Some(offset + i), if let Some(context) = context { + Some(context) + } else { + Some("trailing non-space") + })) + })) +} + +pub const fn is_word_char (c: char) -> bool { + matches!(c, 'a'..='z'|'A'..='Z'|'0'..='9'|'-'|'/'|'@'|':') +} + +pub const fn is_word_end (c: char) -> bool { + is_space(c) || is_expr_end(c) +} + +pub const fn is_text_start (c: char) -> bool { + matches!(c, '"') +} + +pub const fn is_text_end (c: char) -> bool { + matches!(c, '"') +} + +pub const fn is_expr_start (c: char) -> bool { + c == '(' +} + +pub const fn is_expr_end (c: char) -> bool { + c == ')' +} + +pub const fn is_digit (c: char) -> bool { + matches!(c, '0'..='9') +} + +pub const fn to_number (digits: &str) -> Result { + let mut iter = char_indices(digits); + let mut value = 0; + while let Some(((_, c), next)) = iter.next() { + match to_digit(c) { + Ok(digit) => value = 10 * value + digit, + Err(e) => return Err(e), + } + iter = next; + } + Ok(value) +} + +pub const fn to_digit (c: char) -> Result { + Ok(match c { + '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, + '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, + _ => return Err(Unexpected(c, None, Some("parse digit"))) + }) +} + +pub(crate) fn ok_flat (x: Option>) -> DslPerhaps { + Ok(x.transpose()?.flatten()) +} + +#[macro_export] macro_rules! dsl_type (($T:ident { $($trait:tt)* } { + pub const fn $peek:ident $($_1:tt)?; + pub const fn $peek_only:ident $($_2:tt)?; + pub const fn $seek:ident $($_3:tt)?; + pub const fn $seek_start:ident ($source1:ident) $body1:block + pub const fn $seek_length:ident ($source2:ident) $body2:block +})=>{ + pub trait $T: Dsl { $($trait)* } + impl $T for D {} + pub const fn $seek_start ($source1: &str) -> DslPerhaps $body1 + pub const fn $seek_length ($source2: &str) -> DslPerhaps $body2 + /// Find a start and length corresponding to a syntax token. + pub const fn $seek (source: &str) -> DslPerhaps<(usize, usize)> { + match $seek_start(source) { + Err(e) => Err(e), + Ok(None) => Ok(None), + Ok(Some(start)) => match $seek_length(str_from(source, start)) { + Ok(Some(length)) => Ok(Some((start, length))), + Ok(None) => Ok(None), + Err(e) => Err(e), + }, + } + } + /// Find a slice corrensponding to a syntax token. + pub const fn $peek (source: &str) -> DslPerhaps<&str> { + match $seek(source) { + Err(e) => Err(e), + Ok(None) => Ok(None), + Ok(Some((start, length))) => Ok(Some(str_range(source, start, start + length))), + } + } + /// Find a slice corrensponding to a syntax token + /// but return an error if it isn't the only thing + /// in the source. + pub const fn $peek_only (source: &str) -> DslPerhaps<&str> { + match $seek(source) { + Err(e) => Err(e), + Ok(None) => Ok(None), + Ok(Some((start, length))) => { + if let Err(e) = no_trailing_non_space(source, start + length, Some("peek_only")) { + Err(e) + } else { + Ok(Some(str_range(source, start, start + length))) + } + } + } + } +}); diff --git a/dsl/src/error.rs b/dsl/src/dsl_error.rs similarity index 58% rename from dsl/src/error.rs rename to dsl/src/dsl_error.rs index 40b687d..11e00c0 100644 --- a/dsl/src/error.rs +++ b/dsl/src/dsl_error.rs @@ -1,15 +1,25 @@ use crate::*; -use thiserror::Error; -pub type ParseResult = Result; -#[derive(Error, Debug, Copy, Clone, PartialEq)] pub enum ParseError { + +/// DSL-specific error codes. +#[derive(Error, Debug, Copy, Clone, PartialEq, PanicFmt)] +pub enum DslError { + #[error("parse failed: not implemented")] Unimplemented, + #[error("parse failed: empty")] Empty, + #[error("parse failed: incomplete")] Incomplete, + #[error("parse failed: unexpected character '{0}'")] - Unexpected(char), + Unexpected(char, Option, Option<&'static str>), + #[error("parse failed: error #{0}")] Code(u8), + + #[error("end reached")] + End + } diff --git a/dsl/src/dsl_expr.rs b/dsl/src/dsl_expr.rs new file mode 100644 index 0000000..92b093e --- /dev/null +++ b/dsl/src/dsl_expr.rs @@ -0,0 +1,87 @@ +use crate::*; + +pub type GetDslExpr<'a, S, T> = for<'b> fn(&'a S, &'b str)->Perhaps; + +pub type DslExprs<'a, S, T> = &'a [(&'a str, GetDslExpr<'a, S, T>)]; + +dsl_type!(DslExpr { + fn expr (&self) -> DslPerhaps<&str> {ok_flat(self.src()?.map(expr_peek_inner_only))} + fn head (&self) -> DslPerhaps<&str> {ok_flat(self.src()?.map(peek))} + fn tail (&self) -> DslPerhaps<&str> {ok_flat(self.src()?.map(peek_tail))} + /// my other car is a cdr :< + fn each (&self, mut cb: impl FnMut(&str)->Usually<()>) -> Usually<()> { + Ok(if let Some(head) = self.head()? { + cb(head)?; + if let Some(tail) = self.tail()? { + tail.each(cb)?; + } + }) + } +} { + pub const fn expr_peek [generated]; + pub const fn expr_peek_only [generated]; + pub const fn expr_seek [generated]; + pub const fn expr_seek_start (src) { + for_each!((i, c) in char_indices(src) => + if is_expr_start(c) { return Ok(Some(i)) } else + if !is_space(c) { return Err(Unexpected(c, Some(i), Some("expected expression start"))) }); + Ok(None) + } + pub const fn expr_seek_length (src) { + let mut depth = 0; + for_each!((i, c) in char_indices(src) => + if is_expr_start(c) { depth += 1; } else + if is_expr_end(c) { + if depth == 0 { + return Err(Unexpected(c, Some(i), Some("expected expression end"))) + } else if depth == 1 { + return Ok(Some(i + 1)) + } else { + depth -= 1; + } + }); + Err(Incomplete) + } +}); + +#[macro_export] macro_rules!dsl_exprs(($l:lifetime |$state:ident|->$Type:ty$({ + $($name:literal ($($arg:ident:$ty:ty),* $(,)?) => $body:expr),* $(,)? +})?)=>{ + const EXPRS: DslExprs<$l, Self, $Type> = &[$( $({ + let get: GetDslExpr<$l, Self, $Type> = |$state: &$l Self, tail_base|{ + let tail = tail_base; + $( + let head = tail.head()?.unwrap_or_default(); + let tail = tail.tail()?.unwrap_or_default(); + let $arg: $ty = if let Some(arg) = $state.from(&head)? { + arg + } else { + return Err(format!("{}: arg \"{}\" ({}) got: {head} {tail}", + $name, + stringify!($arg), + stringify!($ty), + ).into()) + }; + )* + Ok(Some($body/* as $Type*/)) + }; + ($name, get) + }),* )? ]; +}); + +pub trait DslNsExprs<'a, T: 'a>: 'a { + /// Known expressions. + const EXPRS: DslExprs<'a, Self, T> = &[]; + /// Resolve an expression if known. + fn from_expr (&'a self, dsl: impl DslExpr + 'a) -> Perhaps { + if let Some(dsl) = dsl.expr()? { + let head = dsl.head()?; + for (key, get) in Self::EXPRS.iter() { + if Some(*key) == head { + return get(self, dsl.tail()?.unwrap_or("")) + } + } + } + Ok(None) + } +} diff --git a/dsl/src/dsl_ns.rs b/dsl/src/dsl_ns.rs new file mode 100644 index 0000000..c8b701f --- /dev/null +++ b/dsl/src/dsl_ns.rs @@ -0,0 +1,46 @@ +use crate::*; + +pub trait DslNs<'a, T: 'a>: DslNsWords<'a, T> + DslNsExprs<'a, T> { + /// Resolve an expression or symbol. + fn from (&'a self, dsl: impl Dsl + 'a) -> Perhaps { + if let Ok(Some(literal)) = self.from_literal(&dsl) { + Ok(Some(literal)) + } else if let Ok(Some(meaning)) = self.from_word(&dsl) { + Ok(Some(meaning)) + } else { + self.from_expr(dsl) + } + } + /// Resolve as literal if valid. + fn from_literal (&self, _: impl Dsl) -> Perhaps { + Ok(None) + } +} + +#[macro_export] macro_rules! dsl_ns { + ($State:ty: $Type:ty { + $(literal = |$dsl:ident| $literal:expr;)? + $(word = |$state_w:ident| { + $($word:literal => $body_w:expr),* $(,)? + };)? + $(expr = |$state_e:ident| { + $($head:literal $args:tt => $body_e:expr),* $(,)? + };)? + }) => { + impl<'a> DslNs<'a, $Type> for $State { + $(fn from_literal (&self, $dsl: impl Dsl) -> Perhaps<$Type> { + $literal + })? + } + impl<'a> DslNsWords<'a, $Type> for $State { + $(dsl_words! { 'a |$state_w| -> $Type { + $($word => $body_w),* + } })? + } + impl<'a> DslNsExprs<'a, $Type> for $State { + $(dsl_exprs! { 'a |$state_e| -> $Type { + $($head $args => $body_e),* + } })? + } + } +} diff --git a/dsl/src/dsl_test.rs b/dsl/src/dsl_test.rs new file mode 100644 index 0000000..fb6d6f7 --- /dev/null +++ b/dsl/src/dsl_test.rs @@ -0,0 +1,99 @@ +use crate::*; +macro_rules!is_some(($expr:expr, $val:expr)=>{assert_eq!($expr, Ok(Some($val)))};); +macro_rules!is_none(($expr:expr)=>{assert_eq!($expr, Ok(None))};); +macro_rules!is_err(($expr:expr)=>{assert!($expr.is_err())}; + ($expr:expr, $err:expr)=>{assert_eq!($expr, Err($err))};); +#[test] fn test_expr () -> Result<(), DslError> { + let e0 = DslError::Unexpected('a', None, None); + let e1 = DslError::Unexpected('(', None, None); + let e2 = DslError::Unexpected('b', None, None); + let e3 = DslError::Unexpected('d', None, None); + let check = |src: &str, word, expr, head, tail|{ + assert_eq!(src.word(), word, "{src}"); + assert_eq!(src.expr(), expr, "{src}"); + assert_eq!(src.head(), head, "{src}"); + assert_eq!(src.tail(), tail, "{src}"); + }; + check("a", Ok(Some("a")), Err(e0), Ok(Some("a")), Ok(None)); + check("(a)", Err(e1), Ok(Some("a")), Ok(Some("(a)")), Ok(None)); + check("a b c", Err(e2), Err(e0), Ok(Some("a")), Ok(Some("b c"))); + check("(a b c)", Err(e1), Ok(Some("a b c")), Ok(Some("(a b c)")), Ok(None)); + check("(a b c) d e f", Err(e1), Err(e3), Ok(Some("(a b c)")), Ok(Some("d e f"))); + check("a (b c d) e f", Err(e1), Err(e0), Ok(Some("a")), Ok(Some("(b c d) e f"))); + + is_some!(word_peek("\n :view/transport"), ":view/transport"); + + assert!(is_space(' ')); + assert!(!is_word_char(' ')); + assert!(is_word_char('f')); + + is_some!(word_seek_start("foo"), 0); + is_some!(word_seek_start("foo "), 0); + is_some!(word_seek_start(" foo "), 1); + is_some!(word_seek_length(&" foo "[1..]), 3); + is_some!(word_seek("foo"), (0, 3)); + is_some!(word_peek("foo"), "foo"); + is_some!(word_seek("foo "), (0, 3)); + is_some!(word_peek("foo "), "foo"); + is_some!(word_seek(" foo "), (1, 3)); + is_some!(word_peek(" foo "), "foo"); + + is_err!("(foo)".word()); + is_err!("foo".expr()); + + is_some!("(foo)".expr(), "foo"); + is_some!("(foo)".head(), "(foo)"); + is_none!("(foo)".tail()); + + is_some!("(foo bar baz)".expr(), "foo bar baz"); + is_some!("(foo bar baz)".head(), "(foo bar baz)"); + is_none!("(foo bar baz)".tail()); + + is_some!("(foo bar baz)".expr().head(), "foo"); + is_some!("(foo bar baz)".expr().tail(), "bar baz"); + is_some!("(foo bar baz)".expr().tail().head(), "bar"); + is_some!("(foo bar baz)".expr().tail().tail(), "baz"); + + is_err!("foo".expr()); + is_some!("foo".word(), "foo"); + is_some!(" foo".word(), "foo"); + is_some!(" foo ".word(), "foo"); + + is_some!(" foo ".head(), "foo"); + //assert_eq!(" foo ".head().head(), Ok(None)); + is_none!(" foo ".head().tail()); + is_none!(" foo ".tail()); + is_none!(" foo ".tail().head()); + is_none!(" foo ".tail().tail()); + + assert_eq!(" foo bar ".head(), Ok(Some("foo"))); + //assert_eq!(" foo bar ".head().head(), Ok(None)); + assert_eq!(" foo bar ".head().tail(), Ok(None)); + assert_eq!(" foo bar ".tail(), Ok(Some(" bar "))); + assert_eq!(" foo bar ".tail().head(), Ok(Some("bar"))); + assert_eq!(" foo bar ".tail().tail(), Ok(None)); + + assert_eq!(" (foo) ".head(), Ok(Some("(foo)"))); + //assert_eq!(" (foo) ".head().head(), Ok(Some("foo"))); + //assert_eq!(" (foo) ".head().head().head(), Ok(None)); + assert_eq!(" (foo) ".tail(), Ok(None)); + + assert_eq!(" (foo) (bar) ".head(), Ok(Some("(foo)"))); + //assert_eq!(" (foo) (bar) ".head().head(), Ok(Some("foo"))); + //assert_eq!(" (foo) (bar) ".head().head().head(), Ok(None)); + is_some!(" (foo) (bar) ".tail(), " (bar) "); + is_some!(" (foo) (bar) ".tail().head(), "(bar)"); + is_some!(" (foo) (bar) ".tail().head().head(), "(bar)"); + is_some!(" (foo) (bar) ".tail().head().expr(), "bar"); + is_some!(" (foo) (bar) ".tail().head().expr().head(), "bar"); + + is_some!(" (foo bar baz) ".head(), "(foo bar baz)"); + is_some!(" (foo bar baz) ".head().head(), "(foo bar baz)"); + is_some!(" (foo bar baz) ".expr(), "foo bar baz"); + is_some!(" (foo bar baz) ".expr().head(), "foo"); + is_some!(" (foo bar baz) ".expr().tail(), "bar baz"); + is_some!(" (foo bar baz) ".expr().tail().head(), "bar"); + is_some!(" (foo bar baz) ".expr().tail().tail(), "baz"); + is_none!(" (foo bar baz) ".tail()); + Ok(()) +} diff --git a/dsl/src/dsl_text.rs b/dsl/src/dsl_text.rs new file mode 100644 index 0000000..9c2e51d --- /dev/null +++ b/dsl/src/dsl_text.rs @@ -0,0 +1,20 @@ +use crate::*; + +dsl_type!(DslText { + fn text (&self) -> DslPerhaps<&str> { ok_flat(self.src()?.map(text_peek_only)) } +} { + pub const fn text_peek [generated]; + pub const fn text_peek_only [generated]; + pub const fn text_seek [generated]; + pub const fn text_seek_start (src) { + for_each!((i, c) in char_indices(src) => + if is_text_start(c) { return Ok(Some(i)) } else + if !is_space(c) { return Err(Unexpected(c, Some(i), None)) }); + Ok(None) + } + pub const fn text_seek_length (src) { + for_each!((i, c) in char_indices(src) => + if is_text_end(c) { return Ok(Some(i)) }); + Ok(None) + } +}); diff --git a/dsl/src/dsl_word.rs b/dsl/src/dsl_word.rs new file mode 100644 index 0000000..5abb61a --- /dev/null +++ b/dsl/src/dsl_word.rs @@ -0,0 +1,49 @@ +use crate::*; + +pub type GetDslWord<'a, S, T> = fn(&'a S)->Perhaps; + +pub type DslWords<'a, S, T> = &'a [(&'a str, GetDslWord<'a, S, T>)]; + +dsl_type!(DslWord { + fn word (&self) -> DslPerhaps<&str> {ok_flat(self.src()?.map(word_peek_only))} +} { + pub const fn word_peek [generated]; + pub const fn word_peek_only [generated]; + pub const fn word_seek [generated]; + pub const fn word_seek_start (src) { + for_each!((i, c) in char_indices(src) => if + is_word_char(c) { return Ok(Some(i)) } else + if !is_space(c) { return Err(Unexpected(c, Some(i), Some("word_seek_start"))) }); + Ok(None) + } + pub const fn word_seek_length (src) { + for_each!((i, c) in char_indices(src) => if !is_word_char(c) { return Ok(Some(i)) }); + Ok(Some(src.len())) + } +}); + +#[macro_export] macro_rules!dsl_words(($l:lifetime |$state:ident|->$Type:ty$({ + $($word:literal => $body:expr),* $(,)? +})?)=>{ + const WORDS: DslWords<$l, Self, $Type> = &[$( $({ + let get: GetDslWord<$l, Self, $Type> = |$state: &$l Self|Ok(Some($body)); + ($word, get) + }),* )?]; +}); + +pub trait DslNsWords<'a, T: 'a>: 'a { + /// Known symbols. + const WORDS: DslWords<'a, Self, T> = &[]; + /// Resolve a symbol if known. + fn from_word (&'a self, dsl: impl DslWord) -> Perhaps { + if let Some(dsl) = dsl.word()? { + for (key, get) in Self::WORDS.iter() { + if dsl == *key { + let value = get(self); + return value + } + } + } + return Ok(None) + } +} diff --git a/dsl/src/iter.rs b/dsl/src/iter.rs deleted file mode 100644 index 7f36289..0000000 --- a/dsl/src/iter.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! 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 = tengri_dsl::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> { self.0.peek() } -} -impl<'a> Iterator for TokenIter<'a> { - type Item = Token<'a>; - fn next (&mut self) -> Option> { - 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> { 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> { - 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 { - 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 { - 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(); - //} - //} -} diff --git a/dsl/src/lib.rs b/dsl/src/lib.rs deleted file mode 100644 index 27b542c..0000000 --- a/dsl/src/lib.rs +++ /dev/null @@ -1,93 +0,0 @@ -#![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::fmt::Debug; - -/// 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; - } - } -} - -#[macro_export] macro_rules! expose { - ($([$self:ident:$State:ty] { $($Type:ty => { $($pat:pat => $expr:expr),* $(,)? })* })*) => { - $(expose!(@impl [$self: $State] { $($Type => { $($pat => $expr),* })* });)* - }; - (@impl [$self:ident:$State:ty] { $($Type:ty => { $($pat:pat => $expr:expr),* $(,)? })* }) => { - $(expose!(@type $Type [$self: $State] => { $($pat => $expr),* });)* - }; - (@type bool [$self:ident: $State:ty] => { $($pat:pat => $expr:expr),* $(,)? }) => { - provide_bool!(bool: |$self: $State| { $($pat => $expr),* }); - }; - (@type isize [$self:ident: $State:ty] => { $($pat:pat => $expr:expr),* $(,)? }) => { - provide_num!(u16: |$self: $State| { $($pat => $expr),* }); - }; - (@type usize [$self:ident: $State:ty] => { $($pat:pat => $expr:expr),* $(,)? }) => { - provide_num!(usize: |$self: $State| { $($pat => $expr),* }); - }; - (@type isize [$self:ident: $State:ty] => { $($pat:pat => $expr:expr),* $(,)? }) => { - provide_num!(isize: |$self: $State| { $($pat => $expr),* }); - }; - (@type $Type:ty [$self:ident: $State:ty] => { $($pat:pat => $expr:expr),* $(,)? }) => { - provide!($Type: |$self: $State| { $($pat => $expr),* }); - }; -} - -#[macro_export] macro_rules! impose { - ([$self:ident:$Struct:ty] { $($Command:ty => $variants:tt)* }) => { - $(atom_command!($Command: |$self: $Struct| $variants);)* - }; -} - -//#[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 = >::from(&layout); - ////} - //Ok(()) -//} diff --git a/dsl/src/token.rs b/dsl/src/token.rs deleted file mode 100644 index 467aeeb..0000000 --- a/dsl/src/token.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! [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 tengri_dsl::{*, 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> { - 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(()) -} diff --git a/editor/Cargo.toml b/editor/Cargo.toml new file mode 100644 index 0000000..692cf03 --- /dev/null +++ b/editor/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "tengri_editor" +description = "Embeddable editor for Tengri DSL." +version = { workspace = true } +edition = { workspace = true } diff --git a/editor/src/main.rs b/editor/src/main.rs new file mode 100644 index 0000000..e69de29 diff --git a/input/Cargo.toml b/input/Cargo.toml index a3ca0ce..0c9218d 100644 --- a/input/Cargo.toml +++ b/input/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "tengri_input" -edition = "2024" description = "UI metaframework, input layer." version = { workspace = true } +edition = { workspace = true } + +[lib] +path = "input.rs" [dependencies] -tengri_dsl = { optional = true, path = "../dsl" } - -[features] -dsl = [ "tengri_dsl" ] +tengri_core = { path = "../core" } [dev-dependencies] tengri_tui = { path = "../tui" } diff --git a/input/README.md b/input/README.md index 4261a22..2fa3ef8 100644 --- a/input/README.md +++ b/input/README.md @@ -1,16 +1,6 @@ -# `tengri_engine` +***tengri_input*** is where tengri's input handling is defined. -## 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`, 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. +the following items are provided: +* `Input` trait, for defining for input sources +* `Handle` trait and `handle!` macro, for defining input handlers +* `Command` trait and the `command!` macro, for defining commands that inputs may result in diff --git a/input/input.rs b/input/input.rs new file mode 100644 index 0000000..5a22800 --- /dev/null +++ b/input/input.rs @@ -0,0 +1,94 @@ +#![feature(associated_type_defaults)] +#![feature(if_let_guard)] + +pub(crate) use tengri_core::*; + +#[cfg(test)] mod input_test; + +/// Event source +pub trait Input: Sized { + /// Type of input event + type Event; + /// Result of handling input + type Handled; // TODO: make this an Option> 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); +} + +flex_trait_mut!(Handle { + fn handle (&mut self, _input: &E) -> Perhaps { + Ok(None) + } +}); + +pub trait Command: Send + Sync + Sized { + fn execute (&self, state: &mut S) -> Perhaps; + fn delegate (&self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps + where Self: Sized + { + Ok(self.execute(state)?.map(wrap)) + } +} + +impl> Command for Option { + fn execute (&self, _: &mut S) -> Perhaps { + Ok(None) + } + fn delegate (&self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps + where Self: Sized + { + Ok(None) + } +} + +/// Implement [Command] for given `State` and `handler` +#[macro_export] macro_rules! command { + ($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => { + impl$(<$($l),+>)? ::tengri::input::Command<$State> for $Command { + fn execute (&$self, $state: &mut $State) -> Perhaps { + Ok($handler) + } + } + }; +} + +#[macro_export] macro_rules! def_command (($Command:ident: |$state:ident: $State:ty| { + $($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)? +})=>{ + #[derive(Debug)] + pub enum $Command { + $($Variant $({ $($arg: $Arg),* })?),* + } + impl Command<$State> for $Command { + fn execute (&self, $state: &mut $State) -> Perhaps { + match self { + $(Self::$Variant $({ $($arg),* })? => $body,)* + _ => unimplemented!("Command<{}>: {self:?}", stringify!($State)), + } + } + } +}); + +/// Implement [Handle] for given `State` and `handler`. +#[macro_export] macro_rules! handle { + (|$self:ident:$State:ty,$input:ident|$handler:expr) => { + impl ::tengri::input::Handle for $State { + fn handle (&mut $self, $input: &E) -> Perhaps { + $handler + } + } + }; + ($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => { + impl ::tengri::input::Handle<$E> for $State { + fn handle (&mut $self, $input: &$E) -> + Perhaps<<$E as ::tengri::input::Input>::Handled> + { + $handler + } + } + } +} diff --git a/input/src/lib.rs b/input/input_test.rs similarity index 52% rename from input/src/lib.rs rename to input/input_test.rs index e27a8cb..24576db 100644 --- a/input/src/lib.rs +++ b/input/input_test.rs @@ -1,21 +1,5 @@ -#![feature(associated_type_defaults)] -mod command; pub use self::command::*; -mod handle; pub use self::handle::*; -mod keymap; pub use self::keymap::*; +use crate::*; -#[cfg(feature = "dsl")] pub(crate) use ::tengri_dsl::*; - -/// Standard error trait. -pub(crate) use std::error::Error; - -/// Standard optional result type. -pub(crate) type Perhaps = Result, Box>; - -/// Standard result type. -#[cfg(test)] -pub(crate) type Usually = Result>; - -#[cfg(test)] #[test] fn test_stub_input () -> Usually<()> { use crate::*; struct TestInput(bool); @@ -36,3 +20,9 @@ pub(crate) type Usually = Result>; assert!(!TestInput(false).is_done()); Ok(()) } + +//#[cfg(all(test, feature = "dsl"))] #[test] fn test_dsl_keymap () -> Usually<()> { + //let _keymap = CstIter::new(""); + //Ok(()) +//} + diff --git a/input/src/command.rs b/input/src/command.rs deleted file mode 100644 index 23b2464..0000000 --- a/input/src/command.rs +++ /dev/null @@ -1,28 +0,0 @@ -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 { - Ok($handler) - } - } - }; -} -pub trait Command: Send + Sync + Sized { - fn execute (self, state: &mut S) -> Perhaps; - fn delegate (self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps - where Self: Sized - { - Ok(self.execute(state)?.map(wrap)) - } -} -impl> Command for Option { - fn execute (self, _: &mut S) -> Perhaps { - Ok(None) - } - fn delegate (self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps - where Self: Sized - { - Ok(None) - } -} diff --git a/input/src/event_map.rs b/input/src/event_map.rs deleted file mode 100644 index fc9644d..0000000 --- a/input/src/event_map.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::*; - -pub struct EventMap<'a, S, I: PartialEq, C> { - pub bindings: &'a [(I, &'a dyn Fn(&S) -> Option)], - pub fallback: Option<&'a dyn Fn(&S, &I) -> Option> -} - -impl<'a, S, I: PartialEq, C> EventMap<'a, S, I, C> { - pub fn handle (&self, state: &S, input: &I) -> Option { - 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 { - 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 { - Some($handler) - } - } - } -} - -pub trait InputToCommand: Command + Sized { - fn input_to_command (state: &S, input: &I) -> Option; - fn execute_with_state (state: &mut S, input: &I) -> Perhaps { - Ok(if let Some(command) = Self::input_to_command(state, input) { - let _undo = command.execute(state)?; - Some(true) - } else { - None - }) - } -} diff --git a/input/src/handle.rs b/input/src/handle.rs deleted file mode 100644 index 2a8fbcb..0000000 --- a/input/src/handle.rs +++ /dev/null @@ -1,75 +0,0 @@ -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> 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 Handle for $Struct { - fn handle (&mut $self, $input: &E) -> Perhaps { - $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: Send + Sync { - fn handle (&mut self, _input: &E) -> Perhaps { - Ok(None) - } -} -impl> Handle for &mut H { - fn handle (&mut self, context: &E) -> Perhaps { - (*self).handle(context) - } -} -impl> Handle for Option { - fn handle (&mut self, context: &E) -> Perhaps { - if let Some(handle) = self { - handle.handle(context) - } else { - Ok(None) - } - } -} -impl Handle for Mutex where H: Handle { - fn handle (&mut self, context: &E) -> Perhaps { - self.get_mut().unwrap().handle(context) - } -} -impl Handle for Arc> where H: Handle { - fn handle (&mut self, context: &E) -> Perhaps { - self.lock().unwrap().handle(context) - } -} -impl Handle for RwLock where H: Handle { - fn handle (&mut self, context: &E) -> Perhaps { - self.write().unwrap().handle(context) - } -} -impl Handle for Arc> where H: Handle { - fn handle (&mut self, context: &E) -> Perhaps { - self.write().unwrap().handle(context) - } -} diff --git a/input/src/input.rs b/input/src/input.rs deleted file mode 100644 index 24dd927..0000000 --- a/input/src/input.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::*; -use std::time::Duration; -use std::thread::JoinHandle; - -/// 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> 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); -} - -/// Input thread entrypoint. -pub trait InputRun { - fn run_input (engine: T, state: Self, timer: Duration) -> JoinHandle<()>; -} - -/// Handle input through a mutable reference. -pub trait Handle: Send + Sync { - fn handle (&mut self, _input: &E) -> Perhaps { - Ok(None) - } -} - -/// Handle input through an immutable reference (e.g. [Arc] or [Arc]) -pub trait HandleRef: Send + Sync { - fn handle (&self, _input: &E) -> Perhaps { - Ok(None) - } -} diff --git a/input/src/keymap.rs b/input/src/keymap.rs deleted file mode 100644 index 629f8ac..0000000 --- a/input/src/keymap.rs +++ /dev/null @@ -1,177 +0,0 @@ -use crate::*; - -/// [Input] state that can be matched against a [Value]. -pub trait AtomInput: Input { - fn matches_atom (&self, token: &str) -> bool; -} - -#[cfg(feature = "dsl")] -pub trait KeyMap<'a> { - /// Try to find a command that matches the current input event. - fn command , I: AtomInput> (&'a self, state: &'a S, input: &'a I) - -> Option; -} - -#[cfg(feature = "dsl")] -impl<'a> KeyMap<'a> for SourceIter<'a> { - fn command , I: AtomInput> (&'a self, state: &'a S, input: &'a I) - -> Option - { - 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 - } -} - -#[cfg(feature = "dsl")] -impl<'a> KeyMap<'a> for TokenIter<'a> { - fn command , I: AtomInput> (&'a self, state: &'a S, input: &'a I) - -> Option - { - 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]. -#[cfg(feature = "dsl")] -pub trait AtomCommand<'a, C>: TryFromAtom<'a, C> + Command {} - -#[cfg(feature = "dsl")] -impl<'a, C, T: TryFromAtom<'a, C> + Command> AtomCommand<'a, C> for T {} - -/** Implement `AtomCommand` for given `State` and `Command` */ -#[cfg(feature = "dsl")] -#[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 { - 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 { - 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); - }; -} - -#[cfg(all(test, feature = "dsl"))] -#[test] fn test_atom_keymap () -> Usually<()> { - let keymap = SourceIter::new(""); - Ok(()) -} diff --git a/output/Cargo.toml b/output/Cargo.toml index b208a5b..ed7f210 100644 --- a/output/Cargo.toml +++ b/output/Cargo.toml @@ -1,18 +1,24 @@ [package] name = "tengri_output" -edition = "2024" description = "UI metaframework, output layer." version = { workspace = true } +edition = { workspace = true } -[dependencies] -tengri_dsl = { optional = true, path = "../dsl" } +[lib] +path = "src/output.rs" [features] -dsl = [ "tengri_dsl" ] +bumpalo = [ "dep:bumpalo" ] +dsl = [ "dep:tengri_dsl" ] + +[dependencies] +tengri_core = { path = "../core" } +tengri_dsl = { optional = true, path = "../dsl" } +bumpalo = { optional = true, workspace = true } [dev-dependencies] tengri = { path = "../tengri", features = [ "dsl", "tui" ] } tengri_tui = { path = "../tui" } tengri_dsl = { path = "../dsl" } -proptest = "^1" -proptest-derive = "^0.5.1" +proptest = { workspace = true } +proptest-derive = { workspace = true } diff --git a/output/README.md b/output/README.md index dc19511..e9669a4 100644 --- a/output/README.md +++ b/output/README.md @@ -1,75 +1,20 @@ -# `tengri_output` +***tengri_output*** is an abstract interface layout framework. -## free floating layout primitives +it expresses the following notions: -this crate exposes several layout operators -which work entirely in unsigned coordinates -and are generic over the trait `Content`. -most importantly, they are not dependent on rendering framework. +* [**space:**](./src/space.rs) `Direction`, `Coordinate`, `Area`, `Size`, `Measure` -|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| +* [**output:**](./src/output.rs) `Out`, `Draw`, `Content` + * the layout operators are generic over `Draw` and/or `Content` + * the traits `Draw` and `Content` are generic over `Out` + * implement `Out` to bring a layout to a new backend: + [see `TuiOut` in `tengri_tui`](../tui/src/tui_engine/tui_output.rs) -**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`, 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. +* [**layout:**](./src/layout.rs) + * conditionals: `When`, `Either` + * iteration: `Map` + * concatenation: `Bsp` + * positioning: `Align`, `Push`, `Pull` + * sizing: `Fill`, `Fixed`, `Expand`, `Shrink`, `Min`, `Max` + * implement custom components (that may be backend-dependent): + [see `tui_content` in `tengri_tui`](../tui/src/tui_content) diff --git a/output/src/content.rs b/output/src/content.rs new file mode 100644 index 0000000..2925259 --- /dev/null +++ b/output/src/content.rs @@ -0,0 +1,26 @@ +use crate::*; + +pub trait Content: Draw + Layout {} + +impl + Layout> Content for T {} + +impl<'a, O: Out> AsRef + 'a> for dyn Content + 'a { + fn as_ref (&self) -> &(dyn Draw + 'a) { self } +} + +impl<'a, O: Out> AsRef + 'a> for dyn Content + 'a { + fn as_ref (&self) -> &(dyn Layout + 'a) { self } +} + +pub trait HasContent { + fn content (&self) -> impl Content; +} + +//impl> Draw for T { + //fn draw (&self, to: &mut O) { + //let area = to.area(); + //*to.area_mut() = self.0; + //self.content().draw(to); + //*to.area_mut() = area; + //} +//} diff --git a/output/src/draw.rs b/output/src/draw.rs new file mode 100644 index 0000000..12ef066 --- /dev/null +++ b/output/src/draw.rs @@ -0,0 +1,30 @@ +use crate::*; + +/// Drawable with dynamic dispatch. +pub trait Draw { + fn draw (&self, to: &mut O); +} +impl Draw for () { + fn draw (&self, _: &mut O) {} +} +impl Draw for fn(&mut O) { + fn draw (&self, to: &mut O) { (*self)(to) } +} +impl Draw for Box> { + fn draw (&self, to: &mut O) { (**self).draw(to) } +} +impl> Draw for &D { + fn draw (&self, to: &mut O) { (*self).draw(to) } +} +impl> Draw for &mut D { + fn draw (&self, to: &mut O) { (**self).draw(to) } +} +impl> Draw for Arc { + fn draw (&self, to: &mut O) { (**self).draw(to) } +} +impl> Draw for RwLock { + fn draw (&self, to: &mut O) { self.read().unwrap().draw(to) } +} +impl> Draw for Option { + fn draw (&self, to: &mut O) { if let Some(draw) = self { draw.draw(to) } } +} diff --git a/output/src/group.rs b/output/src/group.rs new file mode 100644 index 0000000..4c949c7 --- /dev/null +++ b/output/src/group.rs @@ -0,0 +1,12 @@ +#[allow(unused)] use crate::*; + +pub struct Group(T); + +impl Group { + pub const fn new () -> Group<()> { + Group(()) + } + pub const fn add (self, value: U) -> Group<(T, U)> { + Group((self.0, value)) + } +} diff --git a/output/src/layout.rs b/output/src/layout.rs new file mode 100644 index 0000000..3130385 --- /dev/null +++ b/output/src/layout.rs @@ -0,0 +1,161 @@ +use crate::*; + +mod layout_align; pub use self::layout_align::*; +mod layout_bsp; pub use self::layout_bsp::*; +mod layout_cond; pub use self::layout_cond::*; +mod layout_map; pub use self::layout_map::*; +mod layout_pad; pub use self::layout_pad::*; +mod layout_move; pub use self::layout_move::*; +mod layout_size; pub use self::layout_size::*; +mod layout_stack; //pub use self::layout_stack::*; + +/// Drawable area of display. +pub trait Layout { + fn x (&self, to: O::Area) -> O::Unit { + to.x() + } + fn y (&self, to: O::Area) -> O::Unit { + to.y() + } + fn min_w (&self, _to: O::Area) -> O::Unit { + 0.into() + } + fn max_w (&self, to: O::Area) -> O::Unit { + to.w() + } + fn w (&self, to: O::Area) -> O::Unit { + to.w().max(self.min_w(to)).min(self.max_w(to)) + } + fn min_h (&self, _to: O::Area) -> O::Unit { + 0.into() + } + fn max_h (&self, to: O::Area) -> O::Unit { + to.h() + } + fn h (&self, to: O::Area) -> O::Unit { + to.h().max(self.min_h(to)).min(self.max_h(to)) + } + fn layout (&self, to: O::Area) -> O::Area { + [self.x(to), self.y(to), self.w(to), self.h(to)].into() + } +} + +impl Layout for () { + fn x (&self, a: O::Area) -> O::Unit { a.x() } + fn y (&self, a: O::Area) -> O::Unit { a.y() } + fn w (&self, _: O::Area) -> O::Unit { 0.into() } + fn min_w (&self, _: O::Area) -> O::Unit { 0.into() } + fn max_w (&self, _: O::Area) -> O::Unit { 0.into() } + fn h (&self, _: O::Area) -> O::Unit { 0.into() } + fn min_h (&self, _: O::Area) -> O::Unit { 0.into() } + fn max_h (&self, _: O::Area) -> O::Unit { 0.into() } + fn layout (&self, a: O::Area) -> O::Area { [a.x(), a.y(), 0.into(), 0.into()].into() } +} + +impl> Layout for &L { + fn x (&self, a: O::Area) -> O::Unit { (*self).x(a) } + fn y (&self, a: O::Area) -> O::Unit { (*self).y(a) } + fn w (&self, a: O::Area) -> O::Unit { (*self).w(a) } + fn min_w (&self, a: O::Area) -> O::Unit { (*self).min_w(a) } + fn max_w (&self, a: O::Area) -> O::Unit { (*self).max_w(a) } + fn h (&self, a: O::Area) -> O::Unit { (*self).h(a) } + fn min_h (&self, a: O::Area) -> O::Unit { (*self).min_h(a) } + fn max_h (&self, a: O::Area) -> O::Unit { (*self).max_h(a) } + fn layout (&self, a: O::Area) -> O::Area { (*self).layout(a) } +} + +impl> Layout for &mut L { + fn x (&self, a: O::Area) -> O::Unit { (**self).x(a) } + fn y (&self, a: O::Area) -> O::Unit { (**self).y(a) } + fn w (&self, a: O::Area) -> O::Unit { (**self).w(a) } + fn min_w (&self, a: O::Area) -> O::Unit { (**self).min_w(a) } + fn max_w (&self, a: O::Area) -> O::Unit { (**self).max_w(a) } + fn h (&self, a: O::Area) -> O::Unit { (**self).h(a) } + fn min_h (&self, a: O::Area) -> O::Unit { (**self).min_h(a) } + fn max_h (&self, a: O::Area) -> O::Unit { (**self).max_h(a) } + fn layout (&self, a: O::Area) -> O::Area { (**self).layout(a) } +} + +impl> Layout for Arc { + fn x (&self, a: O::Area) -> O::Unit { (**self).x(a) } + fn y (&self, a: O::Area) -> O::Unit { (**self).y(a) } + fn w (&self, a: O::Area) -> O::Unit { (**self).w(a) } + fn min_w (&self, a: O::Area) -> O::Unit { (**self).min_w(a) } + fn max_w (&self, a: O::Area) -> O::Unit { (**self).max_w(a) } + fn h (&self, a: O::Area) -> O::Unit { (**self).h(a) } + fn min_h (&self, a: O::Area) -> O::Unit { (**self).min_h(a) } + fn max_h (&self, a: O::Area) -> O::Unit { (**self).max_h(a) } + fn layout (&self, a: O::Area) -> O::Area { (**self).layout(a) } +} + +impl Layout for Box> { + fn x (&self, a: O::Area) -> O::Unit { (**self).x(a) } + fn y (&self, a: O::Area) -> O::Unit { (**self).y(a) } + fn w (&self, a: O::Area) -> O::Unit { (**self).w(a) } + fn min_w (&self, a: O::Area) -> O::Unit { (**self).min_w(a) } + fn max_w (&self, a: O::Area) -> O::Unit { (**self).max_w(a) } + fn h (&self, a: O::Area) -> O::Unit { (**self).h(a) } + fn min_h (&self, a: O::Area) -> O::Unit { (**self).min_h(a) } + fn max_h (&self, a: O::Area) -> O::Unit { (**self).max_h(a) } + fn layout (&self, a: O::Area) -> O::Area { (**self).layout(a) } +} + +impl> Layout for RwLock { + fn x (&self, a: O::Area) -> O::Unit { self.read().unwrap().x(a) } + fn y (&self, a: O::Area) -> O::Unit { self.read().unwrap().y(a) } + fn w (&self, a: O::Area) -> O::Unit { self.read().unwrap().w(a) } + fn min_w (&self, a: O::Area) -> O::Unit { self.read().unwrap().min_w(a) } + fn max_w (&self, a: O::Area) -> O::Unit { self.read().unwrap().max_w(a) } + fn h (&self, a: O::Area) -> O::Unit { self.read().unwrap().h(a) } + fn min_h (&self, a: O::Area) -> O::Unit { self.read().unwrap().min_h(a) } + fn max_h (&self, a: O::Area) -> O::Unit { self.read().unwrap().max_h(a) } + fn layout (&self, a: O::Area) -> O::Area { self.read().unwrap().layout(a) } +} + +impl> Layout for Option { + fn x (&self, to: O::Area) -> O::Unit { + self.as_ref().map(|c|c.x(to)).unwrap_or(to.x()) + } + fn y (&self, to: O::Area) -> O::Unit { + self.as_ref().map(|c|c.y(to)).unwrap_or(to.y()) + } + fn min_w (&self, to: O::Area) -> O::Unit { + self.as_ref().map(|c|c.min_w(to)).unwrap_or(0.into()) + } + fn max_w (&self, to: O::Area) -> O::Unit { + self.as_ref().map(|c|c.max_w(to)).unwrap_or(0.into()) + } + fn w (&self, to: O::Area) -> O::Unit { + self.as_ref().map(|c|c.w(to)).unwrap_or(0.into()) + } + fn min_h (&self, to: O::Area) -> O::Unit { + self.as_ref().map(|c|c.min_h(to)).unwrap_or(0.into()) + } + fn max_h (&self, to: O::Area) -> O::Unit { + self.as_ref().map(|c|c.max_h(to)).unwrap_or(0.into()) + } + fn h (&self, to: O::Area) -> O::Unit { + self.as_ref().map(|c|c.h(to)).unwrap_or(0.into()) + } + fn layout (&self, to: O::Area) -> O::Area { + self.as_ref().map(|c|c.layout([self.x(to), self.y(to), self.w(to), self.h(to)].into())) + .unwrap_or([to.x(), to.y(), 0.into(), 0.into()].into()) + } +} + +pub struct Bounded(pub O::Area, pub D); + +impl> HasContent for Bounded { + fn content (&self) -> impl Content { + &self.1 + } +} + +impl> Draw for Bounded { + fn draw (&self, to: &mut O) { + let area = to.area(); + *to.area_mut() = self.0; + self.1.draw(to); + *to.area_mut() = area; + } +} diff --git a/output/src/layout/layout_align.rs b/output/src/layout/layout_align.rs new file mode 100644 index 0000000..0ccdd20 --- /dev/null +++ b/output/src/layout/layout_align.rs @@ -0,0 +1,69 @@ +//! ``` +//! use ::tengri::{output::*, tui::*}; +//! let area: [u16;4] = [10, 10, 20, 20]; +//! fn test (area: [u16;4], item: &impl Draw, expected: [u16;4]) { +//! assert_eq!(Content::layout(item, area), expected); +//! assert_eq!(Draw::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::*; +use Alignment::*; + +/// 9th of area to place. +#[derive(Debug, Copy, Clone, Default)] +pub enum Alignment { #[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W } +pub struct Align(Alignment, T); +impl Align { + #[inline] pub const fn c (a: T) -> Self { Self(Alignment::Center, a) } + #[inline] pub const fn x (a: T) -> Self { Self(Alignment::X, a) } + #[inline] pub const fn y (a: T) -> Self { Self(Alignment::Y, a) } + #[inline] pub const fn n (a: T) -> Self { Self(Alignment::N, a) } + #[inline] pub const fn s (a: T) -> Self { Self(Alignment::S, a) } + #[inline] pub const fn e (a: T) -> Self { Self(Alignment::E, a) } + #[inline] pub const fn w (a: T) -> Self { Self(Alignment::W, a) } + #[inline] pub const fn nw (a: T) -> Self { Self(Alignment::NW, a) } + #[inline] pub const fn sw (a: T) -> Self { Self(Alignment::SW, a) } + #[inline] pub const fn ne (a: T) -> Self { Self(Alignment::NE, a) } + #[inline] pub const fn se (a: T) -> Self { Self(Alignment::SE, a) } +} +impl> Draw for Align { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), &self.1).draw(to) } +} +impl> Layout for Align { + fn x (&self, to: O::Area) -> O::Unit { + match self.0 { + NW | W | SW => to.x(), + N | Center | S => to.x().plus(to.w() / 2.into()).minus(self.1.w(to) / 2.into()), + NE | E | SE => to.x().plus(to.w()).minus(self.1.w(to)), + _ => todo!(), + } + } + fn y (&self, to: O::Area) -> O::Unit { + match self.0 { + NW | N | NE => to.y(), + W | Center | E => to.y().plus(to.h() / 2.into()).minus(self.1.h(to) / 2.into()), + SW | S | SE => to.y().plus(to.h()).minus(self.1.h(to)), + _ => todo!(), + } + } +} diff --git a/output/src/layout/layout_bsp.rs b/output/src/layout/layout_bsp.rs new file mode 100644 index 0000000..3de4cfc --- /dev/null +++ b/output/src/layout/layout_bsp.rs @@ -0,0 +1,152 @@ +use crate::*; + +/// A binary split or layer. +pub struct Bsp( + pub(crate) Direction, + /// First element. + pub(crate) Head, + /// Second element. + pub(crate) Tail, +); + +impl Bsp { + #[inline] pub const fn n (a: Head, b: Tail) -> Self { Self(North, a, b) } + #[inline] pub const fn s (a: Head, b: Tail) -> Self { Self(South, a, b) } + #[inline] pub const fn e (a: Head, b: Tail) -> Self { Self(East, a, b) } + #[inline] pub const fn w (a: Head, b: Tail) -> Self { Self(West, a, b) } + #[inline] pub const fn a (a: Head, b: Tail) -> Self { Self(Above, a, b) } + #[inline] pub const fn b (a: Head, b: Tail) -> Self { Self(Below, a, b) } +} + +impl, Tail: Content> Draw for Bsp { + fn draw (&self, to: &mut O) { + match self.0 { + South => { + panic!("{}", self.1.h(to.area())); + let area_1 = self.1.layout(to.area()); + let area_2 = self.2.layout([ + to.area().x(), + to.area().y().plus(area_1.h()), + to.area().w(), + to.area().h().minus(area_1.h()) + ].into()); + panic!("{area_1:?} {area_2:?}"); + to.place_at(area_1, &self.1); + to.place_at(area_2, &self.2); + }, + _ => todo!("{:?}", self.0) + } + //let [a, b, _] = bsp_areas(to.area(), self.0, &self.1, &self.2); + //panic!("{a:?} {b:?}"); + //if self.0 == Below { + //to.place_at(a, &self.1); + //to.place_at(b, &self.2); + //} else { + //to.place_at(b, &self.2); + //to.place_at(a, &self.1); + //} + } +} +impl, Tail: Layout> Layout for Bsp { + fn w (&self, area: O::Area) -> O::Unit { + match self.0 { + North | South | Above | Below => self.1.w(area).max(self.2.w(area)), + East | West => self.1.min_w(area).plus(self.2.w(area)), + } + } + fn min_w (&self, area: O::Area) -> O::Unit { + match self.0 { + North | South | Above | Below => self.1.min_w(area).max(self.2.min_w(area)), + East | West => self.1.min_w(area).plus(self.2.min_w(area)), + } + } + fn max_w (&self, area: O::Area) -> O::Unit { + match self.0 { + North | South | Above | Below => self.1.max_w(area).max(self.2.max_w(area)), + East | West => self.1.max_w(area).plus(self.2.max_w(area)), + } + } + fn h (&self, area: O::Area) -> O::Unit { + match self.0 { + East | West | Above | Below => self.1.h(area).max(self.2.h(area)), + North | South => self.1.h(area).plus(self.2.h(area)), + } + } + fn min_h (&self, area: O::Area) -> O::Unit { + match self.0 { + East | West | Above | Below => self.1.min_h(area).max(self.2.min_h(area)), + North | South => self.1.min_h(area).plus(self.2.min_h(area)), + } + } + fn max_h (&self, area: O::Area) -> O::Unit { + match self.0 { + North | South | Above | Below => self.1.max_h(area).max(self.2.max_h(area)), + East | West => self.1.max_h(area).plus(self.2.max_h(area)), + } + } + fn layout (&self, area: O::Area) -> O::Area { + bsp_areas(area, self.0, &self.1, &self.2)[2] + } +} + +fn bsp_areas , B: Layout> ( + area: O::Area, direction: Direction, a: &A, b: &B, +) -> [O::Area;3] { + let [x, y, w, h] = area.xywh(); + let [aw, ah] = a.layout(area).wh(); + let [bw, bh] = b.layout(match direction { + Above | Below => area, + 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] = area.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] = area.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] = area.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] = area.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] = area.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()] + }, + } +} + +/// Stack 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 }}); diff --git a/output/src/layout/layout_cond.rs b/output/src/layout/layout_cond.rs new file mode 100644 index 0000000..485d5b3 --- /dev/null +++ b/output/src/layout/layout_cond.rs @@ -0,0 +1,211 @@ +use crate::*; + +/// Show an item only when a condition is true. +pub struct When(bool, T, PhantomData); +impl> When { + /// Create a binary condition. + pub const fn new (c: bool, a: T) -> Self { Self(c, a, PhantomData) } +} +impl> Layout for When { + fn layout (&self, to: O::Area) -> O::Area { + let Self(cond, item, ..) = self; + if *cond { item.layout(to) } else { O::Area::zero().into() } + } +} +impl> Draw for When { + fn draw (&self, to: &mut O) { + let Self(cond, item, ..) = self; + if *cond { Bounded(self.layout(to.area()), item).draw(to) } + } +} + +/// Show one item if a condition is true and another if the condition is false +pub struct Either(pub bool, pub A, pub B, pub PhantomData); +impl, B: Content> Either { + /// Create a ternary view condition. + pub const fn new (c: bool, a: A, b: B) -> Self { + Self(c, a, b, PhantomData) + } +} +impl, B: Layout> Layout for Either { + fn layout (&self, to: E::Area) -> E::Area { + let Self(cond, a, b, ..) = self; + if *cond { a.layout(to) } else { b.layout(to) } + } +} +impl, B: Content> Draw for Either { + fn draw (&self, to: &mut E) { + let Self(cond, a, b, ..) = self; + let area = self.layout(to.area()); + if *cond { Bounded(area, a).draw(to) } else { Bounded(area, b).draw(to) } + } +} + +/////////////////////////////////////////////////////////////////////////////// + + + ///// The syntagm `(when :condition :content)` corresponds to a [When] layout element. + //impl FromDsl for When where bool: FromDsl, A: FromDsl { + //fn try_provide (state: &S, source: &DslVal) -> Perhaps { + //source.exp_match("when", |_, tail|Ok(Some(Self( + //FromDsl::::provide(state, + //tail.nth(0, ||"no condition".into())?, ||"no condition".into())?, + //FromDsl::::provide(state, + //tail.nth(1, ||"no content".into())?, ||"no content".into())?, + //)))) + //} + //} + ///// The syntagm `(either :condition :content1 :content2)` corresponds to an [Either] layout element. + //impl FromDsl for Either where S: Eval + Eval + Eval { + //fn try_provide (state: &S, source: &DslVal) -> Perhaps { + //source.exp_match("either", |_, tail|Ok(Some(Self( + //state.eval(tail.nth(0, ||"no condition")?, ||"no condition")?, + //state.eval(tail.nth(1, ||"no content 1")?, ||"no content 1")?, + //state.eval(tail.nth(2, ||"no content 1")?, ||"no content 2")?, + //)))) + //} + //} + ///// The syntagm `(align/* :content)` corresponds to an [Align] layout element, + ///// where `*` specifies the direction of the alignment. + //impl FromDsl for Align where S: Eval, A> { + //fn try_provide (state: &S, source: &DslVal) -> Perhaps { + //source.exp_match("align/", |head, tail|Ok(Some(match head { + //"c" => Self::c(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //"x" => Self::x(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //"y" => Self::y(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //"n" => Self::n(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //"s" => Self::s(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //"e" => Self::e(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //"w" => Self::w(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //"nw" => Self::nw(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //"ne" => Self::ne(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //"sw" => Self::sw(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //"se" => Self::se(state.eval(tail.nth(0, ||"no content")?, ||"no content")), + //_ => return Err("invalid align variant".into()) + //}))) + //} + //} + ///// The syntagm `(bsp/* :content1 :content2)` corresponds to a [Bsp] layout element, + ///// where `*` specifies the direction of the split. + //impl FromDsl for Bsp where S: Eval, A> + Eval, B> { + //fn try_provide (state: &S, source: &DslVal) -> Perhaps { + //source.exp_match("bsp/", |head, tail|Ok(Some(match head { + //"n" => Self::n(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")), + //"s" => Self::s(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")), + //"e" => Self::e(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")), + //"w" => Self::w(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")), + //"a" => Self::a(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")), + //"b" => Self::b(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")), + //_ => return Ok(None), + //}))) + //} + //} + //#[cfg(feature = "dsl")] take!($Enum, A|state, words|Ok( + //if let Some(Token { value: Key(k), .. }) = words.peek() { + //let mut base = words.clone(); + //let content = state.give_or_fail(words, ||format!("{k}: no content"))?; + //return Ok(Some(match words.next() { + //Some(Token{value: Key($x),..}) => Self::x(content), + //Some(Token{value: Key($y),..}) => Self::y(content), + //Some(Token{value: Key($xy),..}) => Self::XY(content), + //_ => unreachable!() + //})) + //} else { + //None + //})); + //#[cfg(feature = "dsl")] take!($Enum, U, A|state, words|Ok( + //if let Some(Token { value: Key($x|$y|$xy), .. }) = words.peek() { + //let mut base = words.clone(); + //Some(match words.next() { + //Some(Token { value: Key($x), .. }) => Self::x( + //state.give_or_fail(words, ||"x: no unit")?, + //state.give_or_fail(words, ||"x: no content")?, + //), + //Some(Token { value: Key($y), .. }) => Self::y( + //state.give_or_fail(words, ||"y: no unit")?, + //state.give_or_fail(words, ||"y: no content")?, + //), + //Some(Token { value: Key($x), .. }) => Self::XY( + //state.give_or_fail(words, ||"xy: no unit x")?, + //state.give_or_fail(words, ||"xy: no unit y")?, + //state.give_or_fail(words, ||"xy: no content")? + //), + //_ => unreachable!(), + //}) + //} else { + //None + //})); + //if let Exp(_, exp) = source.value() { + //let mut rest = exp.clone(); + //return Ok(Some(match rest.next().as_ref().and_then(|x|x.key()) { + //Some("bsp/n") => Self::n( + //state.eval(rest.next(), ||"bsp/n: no content 1")?, + //state.eval(rest.next(), ||"bsp/n: no content 2")?, + //), + //Some("bsp/s") => Self::s( + //state.eval(rest.next(), ||"bsp/s: no content 1")?, + //state.eval(rest.next(), ||"bsp/s: no content 2")?, + //), + //Some("bsp/e") => Self::e( + //state.eval(rest.next(), ||"bsp/e: no content 1")?, + //state.eval(rest.next(), ||"bsp/e: no content 2")?, + //), + //Some("bsp/w") => Self::w( + //state.eval(rest.next(), ||"bsp/w: no content 1")?, + //state.eval(rest.next(), ||"bsp/w: no content 2")?, + //), + //Some("bsp/a") => Self::a( + //state.eval(rest.next(), ||"bsp/a: no content 1")?, + //state.eval(rest.next(), ||"bsp/a: no content 2")?, + //), + //Some("bsp/b") => Self::b( + //state.eval(rest.next(), ||"bsp/b: no content 1")?, + //state.eval(rest.next(), ||"bsp/b: no content 2")?, + //), + //_ => return Ok(None), + //})) + //} + //Ok(None) + //if let Exp(_, source) = source.value() { + //let mut rest = source.clone(); + //return Ok(Some(match rest.next().as_ref().and_then(|x|x.key()) { + //Some("align/c") => Self::c(state.eval(rest.next(), ||"align/c: no content")?), + //Some("align/x") => Self::x(state.eval(rest.next(), ||"align/x: no content")?), + //Some("align/y") => Self::y(state.eval(rest.next(), ||"align/y: no content")?), + //Some("align/n") => Self::n(state.eval(rest.next(), ||"align/n: no content")?), + //Some("align/s") => Self::s(state.eval(rest.next(), ||"align/s: no content")?), + //Some("align/e") => Self::e(state.eval(rest.next(), ||"align/e: no content")?), + //Some("align/w") => Self::w(state.eval(rest.next(), ||"align/w: no content")?), + //Some("align/nw") => Self::nw(state.eval(rest.next(), ||"align/nw: no content")?), + //Some("align/ne") => Self::ne(state.eval(rest.next(), ||"align/ne: no content")?), + //Some("align/sw") => Self::sw(state.eval(rest.next(), ||"align/sw: no content")?), + //Some("align/se") => Self::se(state.eval(rest.next(), ||"align/se: no content")?), + //_ => return Ok(None), + //})) + //} + //Ok(None) + //Ok(match source.exp_head().and_then(|e|e.key()) { + //Some("either") => Some(Self( + //source.exp_tail().and_then(|t|t.get(0)).map(|x|state.eval(x, ||"when: no condition"))?, + //source.exp_tail().and_then(|t|t.get(1)).map(|x|state.eval(x, ||"when: no content 1"))?, + //source.exp_tail().and_then(|t|t.get(2)).map(|x|state.eval(x, ||"when: no content 2"))?, + //)), + //_ => None + //}) + //if let Exp(_, mut exp) = source.value() + //&& let Some(Ast(Key(id))) = exp.peek() && *id == *"either" { + //let _ = exp.next(); + //return Ok(Some(Self( + //state.eval(exp.next().unwrap(), ||"either: no condition")?, + //state.eval(exp.next().unwrap(), ||"either: no content 1")?, + //state.eval(exp.next().unwrap(), ||"either: no content 2")?, + //))) + //} + //Ok(None) + //Ok(match source.exp_head().and_then(|e|e.key()) { + //Some("when") => Some(Self( + //source.exp_tail().and_then(|t|t.get(0)).map(|x|state.eval(x, ||"when: no condition"))?, + //source.exp_tail().and_then(|t|t.get(1)).map(|x|state.eval(x, ||"when: no content"))?, + //)), + //_ => None + //}) diff --git a/output/src/layout/layout_iter.rs b/output/src/layout/layout_iter.rs new file mode 100644 index 0000000..e69de29 diff --git a/output/src/layout/layout_map.rs b/output/src/layout/layout_map.rs new file mode 100644 index 0000000..ffd7005 --- /dev/null +++ b/output/src/layout/layout_map.rs @@ -0,0 +1,139 @@ +use crate::*; + +/// Draws items from an iterator. +pub struct Map +where + I: Iterator + Send + Sync, + F: Fn() -> I + Send + Sync, +{ + __: PhantomData<(O, B)>, + /// Function that returns iterator over stacked components + get_iter: F, + /// Function that returns each stacked component + get_item: G, +} + +impl<'a, O, A, B, I, F, G> Map where + I: Iterator + Send + Sync + 'a, + F: Fn() -> I + Send + Sync + 'a, +{ + pub const fn new (get_iter: F, get_item: G) -> Self { + Self { + __: PhantomData, + get_iter, + get_item + } + } +} + +macro_rules! impl_map_direction (($name:ident, $axis:ident, $align:ident)=>{ + impl<'a, O, A, B, I, F> Map< + O, A, Push>>>, I, F, fn(A, usize)->B + > where + O: Out, + B: Draw, + I: Iterator + Send + Sync + 'a, + F: Fn() -> I + Send + Sync + 'a + { + pub const fn $name ( + size: O::Unit, + get_iter: F, + get_item: impl Fn(A, usize)->B + Send + Sync + ) -> Map< + O, A, + Push>>, + I, F, + impl Fn(A, usize)->Push>> + Send + Sync + > { + Map { + __: PhantomData, + get_iter, + get_item: move |item: A, index: usize|{ + // FIXME: multiply + let mut push: O::Unit = O::Unit::from(0u16); + for _ in 0..index { + push = push + size; + } + Push::$axis(push, Align::$align(Fixed::$axis(size, get_item(item, index)))) + } + } + } + } +}); + +impl_map_direction!(east, X, w); +impl_map_direction!(south, Y, n); +impl_map_direction!(west, X, e); +impl_map_direction!(north, Y, s); + +impl<'a, O, A, B, I, F, G> Layout for Map where + O: Out, + B: Layout, + I: Iterator + Send + Sync + 'a, + F: Fn() -> I + Send + Sync + 'a, + G: Fn(A, usize)->B + Send + Sync +{ + fn layout (&self, area: O::Area) -> O::Area { + let Self { get_iter, get_item, .. } = 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_iter() { + let [x,y,w,h] = get_item(item, index).layout(area).xywh(); + min_x = min_x.min(x); + min_y = min_y.min(y); + max_x = max_x.max(x + w); + max_y = max_y.max(y + h); + 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() + } +} +impl<'a, O, A, B, I, F, G> Draw for Map where + O: Out, + B: Content, + I: Iterator + Send + Sync + 'a, + F: Fn() -> I + Send + Sync + 'a, + G: Fn(A, usize)->B + Send + Sync +{ + fn draw (&self, to: &mut O) { + let Self { get_iter, get_item, .. } = self; + let mut index = 0; + let area = self.layout(to.area()); + for item in get_iter() { + let item = get_item(item, index); + //to.place_at(area.into(), &item); + to.place_at(item.layout(area), &item); + index += 1; + } + } +} + +#[inline] pub fn map_south( + item_offset: O::Unit, + item_height: O::Unit, + item: impl Content +) -> impl Content { + Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item))) +} + +#[inline] pub fn map_south_west( + item_offset: O::Unit, + item_height: O::Unit, + item: impl Content +) -> impl Content { + Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item)))) +} + +#[inline] pub fn map_east( + item_offset: O::Unit, + item_width: O::Unit, + item: impl Content +) -> impl Content { + Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item)))) +} + + diff --git a/output/src/layout/layout_move.rs b/output/src/layout/layout_move.rs new file mode 100644 index 0000000..c634cc3 --- /dev/null +++ b/output/src/layout/layout_move.rs @@ -0,0 +1,33 @@ +use crate::*; +/// Increment X and/or Y coordinate. +pub enum Push { X(U, A), Y(U, A), XY(U, U, A), } +impl Push { + #[inline] pub const fn inner (&self) -> &A { match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, } } +} +impl Push { + #[inline] pub fn dx (&self) -> U { match self { Self::X(x, ..) | Self::XY(x, _, _) => *x, Self::Y(_, _) => 0.into() } } + #[inline] pub fn dy (&self) -> U { match self { Self::Y(y, ..) | Self::XY(_, y, _) => *y, Self::X(_, _) => 0.into() } } +} +impl> Draw for Push { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} +impl> Layout for Push { + fn x (&self, area: O::Area) -> O::Unit { area.x().plus(self.dx()) } + fn y (&self, area: O::Area) -> O::Unit { area.y().plus(self.dy()) } +} +/// Decrement X and/or Y coordinate. +pub enum Pull { X(U, A), Y(U, A), XY(U, U, A), } +impl Pull { + #[inline] pub const fn inner (&self) -> &A { match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, } } +} +impl Pull { + #[inline] pub fn dx (&self) -> U { match self { Self::X(x, ..) | Self::XY(x, _, _) => *x, Self::Y(_, _) => 0.into() } } + #[inline] pub fn dy (&self) -> U { match self { Self::Y(y, ..) | Self::XY(_, y, _) => *y, Self::X(_, _) => 0.into() } } +} +impl> Draw for Pull { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} +impl> Layout for Pull { + fn x (&self, area: O::Area) -> O::Unit { area.x().minus(self.dx()) } + fn y (&self, area: O::Area) -> O::Unit { area.y().minus(self.dy()) } +} diff --git a/output/src/layout/layout_pad.rs b/output/src/layout/layout_pad.rs new file mode 100644 index 0000000..b625ef8 --- /dev/null +++ b/output/src/layout/layout_pad.rs @@ -0,0 +1,27 @@ +use crate::*; +use Pad::*; +pub enum Pad { X(U, A), Y(U, A), XY(U, U, A), } +impl Pad { + #[inline] pub const fn inner (&self) -> &A { match self { X(_, c) | Y(_, c) | XY(_, _, c) => c, } } +} +impl Pad { + #[inline] pub fn dx (&self) -> U { match self { X(x, _) => *x, Y(_, _) => 0.into(), XY(x, _, _) => *x, } } + #[inline] pub fn dy (&self) -> U { match self { X(_, _) => 0.into(), Y(y, _) => *y, XY(_, y, _) => *y, } } +} +impl> Draw for Pad { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} +impl> Layout for Pad { + fn x (&self, area: O::Area) -> O::Unit { + area.x().plus(self.dx()) + } + fn y (&self, area: O::Area) -> O::Unit { + area.x().plus(self.dx()) + } + fn w (&self, area: O::Area) -> O::Unit { + area.w().minus(self.dx() * 2.into()) + } + fn h (&self, area: O::Area) -> O::Unit { + area.h().minus(self.dy() * 2.into()) + } +} diff --git a/output/src/layout/layout_size.rs b/output/src/layout/layout_size.rs new file mode 100644 index 0000000..6ef5f7c --- /dev/null +++ b/output/src/layout/layout_size.rs @@ -0,0 +1,121 @@ +use crate::*; + +pub enum Fill { X(A), Y(A), XY(A) } +impl Fill { + #[inline] pub const fn inner (&self) -> &A { match self { Self::X(c) | Self::Y(c) | Self::XY(c) => c } } + #[inline] pub const fn dx (&self) -> bool { match self { Self::X(_) | Self::XY(_) => true, _ => false } } + #[inline] pub const fn dy (&self) -> bool { match self { Self::Y(_) | Self::XY(_) => true, _ => false } } +} +impl> Draw for Fill { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} +impl> Layout for Fill { + fn x (&self, area: O::Area) -> O::Unit { if self.dx() { area.x() } else { self.inner().x(area) } } + fn y (&self, area: O::Area) -> O::Unit { if self.dy() { area.y() } else { self.inner().y(area) } } + fn w (&self, area: O::Area) -> O::Unit { if self.dx() { area.w() } else { self.inner().w(area) } } + fn min_w (&self, area: O::Area) -> O::Unit { if self.dx() { area.w() } else { self.inner().min_w(area) } } + fn max_w (&self, area: O::Area) -> O::Unit { if self.dx() { area.w() } else { self.inner().max_w(area) } } + fn h (&self, area: O::Area) -> O::Unit { if self.dy() { area.h() } else { self.inner().h(area) } } + fn min_h (&self, area: O::Area) -> O::Unit { if self.dy() { area.h() } else { self.inner().min_h(area) } } + fn max_h (&self, area: O::Area) -> O::Unit { if self.dy() { area.h() } else { self.inner().max_h(area) } } +} +/// Set fixed size for content. +pub enum Fixed { X(U, A), Y(U, A), XY(U, U, A), } +impl Fixed { + #[inline] pub const fn inner (&self) -> &A { match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c } } +} +impl Fixed { + #[inline] pub const fn dx (&self) -> Option { match self { Self::X(x, _) | Self::XY(x, ..) => Some(*x), _ => None } } + #[inline] pub const fn dy (&self) -> Option { match self { Self::Y(y, _) | Self::XY(y, ..) => Some(*y), _ => None } } +} +impl> Draw for Fixed { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} +impl> Layout for Fixed { + fn w (&self, area: O::Area) -> O::Unit { self.dx().unwrap_or(self.inner().w(area)) } + fn min_w (&self, area: O::Area) -> O::Unit { self.dx().unwrap_or(self.inner().min_w(area)) } + fn max_w (&self, area: O::Area) -> O::Unit { self.dx().unwrap_or(self.inner().max_w(area)) } + fn h (&self, area: O::Area) -> O::Unit { self.dy().unwrap_or(self.inner().h(area)) } + fn min_h (&self, area: O::Area) -> O::Unit { self.dy().unwrap_or(self.inner().min_h(area)) } + fn max_h (&self, area: O::Area) -> O::Unit { self.dy().unwrap_or(self.inner().max_h(area)) } +} + +pub enum Max { X(U, A), Y(U, A), XY(U, U, A), } +impl Max { + #[inline] pub const fn inner (&self) -> &A { match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c } } +} +impl Max { + #[inline] pub const fn dx (&self) -> Option { match self { Self::X(x, _) | Self::XY(x, ..) => Some(*x), _ => None } } + #[inline] pub const fn dy (&self) -> Option { match self { Self::Y(y, _) | Self::XY(y, ..) => Some(*y), _ => None } } +} +impl> Draw for Max { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} +impl> Layout for Max { + fn layout (&self, area: E::Area) -> E::Area { + let [x, y, w, h] = self.inner().layout(area).xywh(); + match self { + Self::X(mw, _) => [x, y, w.min(*mw), h], + Self::Y(mh, _) => [x, y, w, h.min(*mh)], + Self::XY(mw, mh, _) => [x, y, w.min(*mw), h.min(*mh)], + }.into() + } +} + +pub enum Min { X(U, A), Y(U, A), XY(U, U, A), } +impl Min { + #[inline] pub const fn inner (&self) -> &A { match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c } } +} +impl Min { + #[inline] pub const fn dx (&self) -> Option { match self { Self::X(x, _) | Self::XY(x, ..) => Some(*x), _ => None } } + #[inline] pub const fn dy (&self) -> Option { match self { Self::Y(y, _) | Self::XY(y, ..) => Some(*y), _ => None } } +} +impl> Draw for Min { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} +impl> Layout for Min { + fn layout (&self, area: E::Area) -> E::Area { + let [x, y, w, h] = self.inner().layout(area).xywh(); + match self { + Self::X(mw, _) => [x, y, w.max(*mw), h], + Self::Y(mh, _) => [x, y, w, h.max(*mh)], + Self::XY(mw, mh, _) => [x, y, w.max(*mw), h.max(*mh)], + }.into() + } +} + +pub enum Expand { X(U, A), Y(U, A), XY(U, U, A), } +impl Expand { + #[inline] pub const fn inner (&self) -> &A { match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c } } +} +impl Expand { + #[inline] pub const fn dx (&self) -> Option { match self { Self::X(x, _) | Self::XY(x, ..) => Some(*x), _ => None } } + #[inline] pub const fn dy (&self) -> Option { match self { Self::Y(y, _) | Self::XY(y, ..) => Some(*y), _ => None } } +} +impl> Draw for Expand { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} +impl> Layout for Expand { + fn w (&self, to: O::Area) -> O::Unit { self.inner().w(to).plus(self.dx().unwrap_or_default()) } + fn h (&self, to: O::Area) -> O::Unit { self.inner().w(to).plus(self.dy().unwrap_or_default()) } +} + +pub enum Shrink { X(U, A), Y(U, A), XY(U, U, A), } +impl Shrink { + #[inline] pub const fn inner (&self) -> &A { match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c } } +} +impl Shrink { + #[inline] pub const fn dx (&self) -> Option { match self { Self::X(x, _) | Self::XY(x, ..) => Some(*x), _ => None } } + #[inline] pub const fn dy (&self) -> Option { match self { Self::Y(y, _) | Self::XY(y, ..) => Some(*y), _ => None } } +} +impl> Draw for Shrink { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} +impl> Layout for Shrink { + fn layout (&self, to: E::Area) -> E::Area { + let area = self.inner().layout(to); + let dx = self.dx().unwrap_or_default(); + let dy = self.dy().unwrap_or_default(); + [area.x(), area.y(), area.w().minus(dx), area.h().minus(dy)].into() + } +} diff --git a/output/src/layout/layout_stack.rs b/output/src/layout/layout_stack.rs new file mode 100644 index 0000000..d5a311d --- /dev/null +++ b/output/src/layout/layout_stack.rs @@ -0,0 +1,170 @@ +//use crate::*; +//use Direction::*; + +//pub struct Stack<'x, E, F1> { + //__: PhantomData<&'x (E, F1)>, + //direction: Direction, + //callback: F1 +//} + +//impl<'x, E, F1> Stack<'x, E, F1> { + //pub fn new (direction: Direction, callback: F1) -> Self { + //Self { direction, callback, __: Default::default(), } + //} + //pub fn above (callback: F1) -> Self { + //Self::new(Above, callback) + //} + //pub fn below (callback: F1) -> Self { + //Self::new(Below, callback) + //} + //pub fn north (callback: F1) -> Self { + //Self::new(North, callback) + //} + //pub fn south (callback: F1) -> Self { + //Self::new(South, callback) + //} + //pub fn east (callback: F1) -> Self { + //Self::new(East, callback) + //} + //pub fn west (callback: F1) -> Self { + //Self::new(West, callback) + //} +//} + +//impl<'x, E: Out, F1: Fn(&mut dyn FnMut(&dyn Layout))> Layout for Stack<'x, E, F1> { + //fn layout (&self, to: E::Area) -> E::Area { + //let state = StackLayoutState::::new(self.direction, to); + //(self.callback)(&mut |component: &dyn Layout|{ + //let StackLayoutState { x, y, w_remaining, h_remaining, .. } = *state.borrow(); + //let [_, _, w, h] = component.layout([x, y, w_remaining, h_remaining].into()).xywh(); + //state.borrow_mut().grow(w, h); + //}); + //let StackLayoutState { w_used, h_used, .. } = *state.borrow(); + //match self.direction { + //North | West => { todo!() }, + //South | East => { [to.x(), to.y(), w_used, h_used].into() }, + //_ => unreachable!(), + //} + //} +//} +//impl<'x, E: Out, F1: Fn(&mut dyn FnMut(&dyn Draw))> Draw for Stack<'x, E, F1> { + //fn draw (&self, to: &mut E) { + //let state = StackLayoutState::::new(self.direction, to.area()); + //let to = Rc::new(RefCell::new(to)); + //(self.callback)(&mut |component: &dyn Draw|{ + //let StackLayoutState { x, y, w_remaining, h_remaining, .. } = *state.borrow(); + //let layout = component.layout([x, y, w_remaining, h_remaining].into()); + //state.borrow_mut().grow(layout.w(), layout.h()); + //to.borrow_mut().place_at(layout, component); + //}); + //} +//} + +//#[derive(Copy, Clone)] +//struct StackLayoutState { + //direction: Direction, + //x: E::Unit, + //y: E::Unit, + //w_used: E::Unit, + //h_used: E::Unit, + //w_remaining: E::Unit, + //h_remaining: E::Unit, +//} +//impl StackLayoutState { + //fn new (direction: Direction, area: E::Area) -> std::rc::Rc> { + //let [x, y, w_remaining, h_remaining] = area.xywh(); + //std::rc::Rc::new(std::cell::RefCell::new(Self { + //direction, + //x, y, w_remaining, h_remaining, + //w_used: E::Unit::zero(), h_used: E::Unit::zero() + //})) + //} + //fn grow (&mut self, w: E::Unit, h: E::Unit) -> &mut Self { + //match self.direction { + //South => { self.y = self.y.plus(h); + //self.h_used = self.h_used.plus(h); + //self.h_remaining = self.h_remaining.minus(h); + //self.w_used = self.w_used.max(w); }, + //East => { self.x = self.x.plus(w); + //self.w_used = self.w_used.plus(w); + //self.w_remaining = self.w_remaining.minus(w); + //self.h_used = self.h_used.max(h); }, + //North | West => { todo!() }, + //Above | Below => {}, + //}; + //self + //} + //fn area_remaining (&self) -> E::Area { + //[self.x, self.y, self.w_remaining, self.h_remaining].into() + //} +//} + +////pub struct Stack<'a, E, F1> { + ////__: PhantomData<&'a (E, F1)>, + ////direction: Direction, + ////callback: F1 +////} +////impl<'a, E, F1> Stack<'a, E, F1> where + ////E: Out, F1: Fn(&mut dyn FnMut(&'a dyn Draw)) + Send + Sync, +////{ + ////pub fn north (callback: F1) -> Self { Self::new(North, callback) } + ////pub fn south (callback: F1) -> Self { Self::new(South, callback) } + ////pub fn east (callback: F1) -> Self { Self::new(East, callback) } + ////pub fn west (callback: F1) -> Self { Self::new(West, callback) } + ////pub fn above (callback: F1) -> Self { Self::new(Above, callback) } + ////pub fn below (callback: F1) -> Self { Self::new(Below, callback) } + ////pub fn new (direction: Direction, callback: F1) -> Self { + ////Self { direction, callback, __: Default::default(), } + ////} +////} +////impl<'a, E, F1> Draw for Stack<'a, E, F1> where + ////E: Out, F1: Fn(&mut dyn FnMut(&'a dyn Draw)) + Send + Sync, +////{ + ////fn layout (&self, to: E::Area) -> E::Area { + ////let state = StackLayoutState::::new(self.direction, to); + ////let mut adder = { + ////let state = state.clone(); + ////move|component: &dyn Draw|{ + ////let [w, h] = component.layout(state.borrow().area_remaining()).wh(); + ////state.borrow_mut().grow(w, h); + ////} + ////}; + ////(self.callback)(&mut adder); + ////let StackLayoutState { w_used, h_used, .. } = *state.borrow(); + ////match self.direction { + ////North | West => { todo!() }, + ////South | East => { [to.x(), to.y(), w_used, h_used].into() }, + ////Above | Below => { [to.x(), to.y(), to.w(), to.h()].into() }, + ////} + ////} + ////fn draw (&self, to: &mut E) { + ////let state = StackLayoutState::::new(self.direction, to.area()); + ////let mut adder = { + ////let state = state.clone(); + ////move|component: &dyn Draw|{ + ////let [x, y, w, h] = component.layout(state.borrow().area_remaining()).xywh(); + ////state.borrow_mut().grow(w, h); + ////to.place_at([x, y, w, h].into(), component); + ////} + ////}; + ////(self.callback)(&mut adder); + ////} +////} + +//[>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(()) +//}));*/ diff --git a/output/src/lib.rs b/output/src/lib.rs deleted file mode 100644 index baaf657..0000000 --- a/output/src/lib.rs +++ /dev/null @@ -1,44 +0,0 @@ -#![feature(step_trait)] -#![feature(type_alias_impl_trait)] -#![feature(impl_trait_in_assoc_type)] - -mod space; pub use self::space::*; -mod ops; pub use self::ops::*; -mod output; pub use self::output::*; - -pub(crate) use std::marker::PhantomData; -pub(crate) use std::error::Error; - -#[cfg(feature = "dsl")] pub(crate) use ::tengri_dsl::*; -#[cfg(feature = "dsl")] mod view; -#[cfg(feature = "dsl")] pub use self::view::*; - -/// Standard result type. -pub type Usually = Result>; -/// Standard optional result type. -pub type Perhaps = Result, Box>; - -#[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) { - () - } - } - impl Content for String { - fn render (&self, to: &mut TestOutput) { - to.area_mut().set_w(self.len() as u16); - } - } - Ok(()) -} diff --git a/output/src/ops.rs b/output/src/ops.rs deleted file mode 100644 index b10d8e2..0000000 --- a/output/src/ops.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::*; - -mod align; pub use self::align::*; -mod bsp; pub use self::bsp::*; -mod cond; pub use self::cond::*; -mod map; pub use self::map::*; -//mod reduce; pub use self::reduce::*; -mod thunk; pub use self::thunk::*; -mod transform; pub use self::transform::*; - -#[cfg(test)] #[test] fn test_ops () -> Usually<()> { - Ok(()) -} diff --git a/output/src/ops/align.rs b/output/src/ops/align.rs deleted file mode 100644 index e9ff5a7..0000000 --- a/output/src/ops/align.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Aligns things to the container. Comes with caveats. -//! ``` -//! use ::tengri::{output::*, tui::*}; -//! let area: [u16;4] = [10, 10, 20, 20]; -//! fn test (area: [u16;4], item: &impl Content, 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(Alignment, A); - -#[cfg(feature = "dsl")] -try_from_expr!(<'a, E>: Align>: |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 content = iter.next().expect("no content specified"); - let content = if let Some(content) = state.get_content(&content.value) { - content - } else { - panic!("no content corresponding to {:?}", &content); - }; - return Some(match key { - "align/c" => Self::c(content), - "align/x" => Self::x(content), - "align/y" => Self::y(content), - "align/n" => Self::n(content), - "align/s" => Self::s(content), - "align/e" => Self::e(content), - "align/w" => Self::w(content), - "align/nw" => Self::nw(content), - "align/ne" => Self::ne(content), - "align/sw" => Self::sw(content), - "align/se" => Self::se(content), - _ => unreachable!() - }) - }, - _ => return None - } - } -}); - -impl Align { - #[inline] pub const fn c (a: A) -> Self { Self(Alignment::Center, a) } - #[inline] pub const fn x (a: A) -> Self { Self(Alignment::X, a) } - #[inline] pub const fn y (a: A) -> Self { Self(Alignment::Y, a) } - #[inline] pub const fn n (a: A) -> Self { Self(Alignment::N, a) } - #[inline] pub const fn s (a: A) -> Self { Self(Alignment::S, a) } - #[inline] pub const fn e (a: A) -> Self { Self(Alignment::E, a) } - #[inline] pub const fn w (a: A) -> Self { Self(Alignment::W, a) } - #[inline] pub const fn nw (a: A) -> Self { Self(Alignment::NW, a) } - #[inline] pub const fn sw (a: A) -> Self { Self(Alignment::SW, a) } - #[inline] pub const fn ne (a: A) -> Self { Self(Alignment::NE, a) } - #[inline] pub const fn se (a: A) -> Self { Self(Alignment::SE, a) } -} -impl> Content for Align { - fn content (&self) -> impl Render { - &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()) - } -} diff --git a/output/src/ops/bsp.rs b/output/src/ops/bsp.rs deleted file mode 100644 index 52e697d..0000000 --- a/output/src/ops/bsp.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::*; -pub use Direction::*; -/// A split or layer. -pub struct Bsp(Direction, X, Y); -impl, B: Content> Content for Bsp { - 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); } - } - } -} -#[cfg(feature = "dsl")] -try_from_expr!(<'a, E>: Bsp, 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 Bsp { - #[inline] pub const fn n (a: A, b: B) -> Self { Self(North, a, b) } - #[inline] pub const fn s (a: A, b: B) -> Self { Self(South, a, b) } - #[inline] pub const fn e (a: A, b: B) -> Self { Self(East, a, b) } - #[inline] pub const fn w (a: A, b: B) -> Self { Self(West, a, b) } - #[inline] pub const fn a (a: A, b: B) -> Self { Self(Above, a, b) } - #[inline] pub const fn b (a: A, b: B) -> Self { Self(Below, a, b) } -} -pub trait BspAreas, B: Content> { - 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, B: Content> BspAreas for Bsp { - 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]), - ); - } - } -} diff --git a/output/src/ops/collect.rs b/output/src/ops/collect.rs deleted file mode 100644 index 5559afe..0000000 --- a/output/src/ops/collect.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Groupings of elements. -use crate::*; - -/// A function or closure that emits renderables. -pub trait Collector: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)) {} - -/// Any function or closure that emits renderables for the given engine matches [CollectCallback]. -impl Collector for F -where E: Engine, F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)) {} - -pub trait Render { - fn area (&self, to: E::Area) -> E::Area; - fn render (&self, to: &mut E::Output); -} - -impl> Render 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) - } -} diff --git a/output/src/ops/cond.rs b/output/src/ops/cond.rs deleted file mode 100644 index f92f157..0000000 --- a/output/src/ops/cond.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::*; - -/// Show an item only when a condition is true. -pub struct When(pub bool, pub A); -impl When { - /// Create a binary condition. - pub const 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(pub bool, pub A, pub B); -impl Either { - /// Create a ternary condition. - pub const fn new (c: bool, a: A, b: B) -> Self { - Self(c, a, b) - } -} - -#[cfg(feature = "dsl")] -try_from_expr!(<'a, E>: When>: |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 condition = state.get(&condition.value).expect("no condition provided"); - - let content = iter.next().expect("no content specified"); - let content = if let Some(content) = state.get_content(&content.value) { - content - } else { - panic!("no content corresponding to for {:?}", &content); - }; - - return Some(Self(condition, content)) - } -}); - -#[cfg(feature = "dsl")] -try_from_expr!(<'a, E>: Either, 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 condition = state.get(&condition.value).expect("no condition provided"); - - let content = iter.next().expect("no content specified"); - let content = if let Some(content) = state.get_content(&content.value) { - content - } else { - panic!("no content 1 corresponding to {:?}", &content); - }; - - let alternate = iter.next().expect("no alternate specified"); - let alternate = if let Some(alternate) = state.get_content(&alternate.value) { - alternate - } else { - panic!("no content 2 corresponding to {:?}", &alternate); - }; - - return Some(Self(condition, content, alternate)) - } -}); - -impl> Content for When { - 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, B: Render> Content for Either { - 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) } - } -} diff --git a/output/src/ops/map.rs b/output/src/ops/map.rs deleted file mode 100644 index 32202bf..0000000 --- a/output/src/ops/map.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::*; -#[inline] pub fn map_south( - item_offset: O::Unit, - item_height: O::Unit, - item: impl Content -) -> impl Content { - Push::y(item_offset, Fixed::y(item_height, Fill::x(item))) -} - -#[inline] pub fn map_south_west( - item_offset: O::Unit, - item_height: O::Unit, - item: impl Content -) -> impl Content { - Push::y(item_offset, Align::nw(Fixed::y(item_height, Fill::x(item)))) -} - -#[inline] pub fn map_east( - item_offset: O::Unit, - item_width: O::Unit, - item: impl Content -) -> impl Content { - Push::x(item_offset, Align::w(Fixed::x(item_width, Fill::y(item)))) -} - -/// Renders items from an iterator. -pub struct Map -where - I: Iterator + Send + Sync, - F: Fn() -> I + Send + Sync, -{ - __: PhantomData<(E, B)>, - /// Function that returns iterator over stacked components - get_iterator: F, - /// Function that returns each stacked component - get_item: G, -} - -impl<'a, E, A, B, I, F, G> Map where - I: Iterator + Send + Sync + 'a, - F: Fn() -> I + Send + Sync + 'a, -{ - pub const fn new (get_iterator: F, get_item: G) -> Self { - Self { - __: PhantomData, - get_iterator, - get_item - } - } -} - -impl<'a, E, A, B, I, F, G> Content for Map where - E: Output, - B: Render, - I: Iterator + Send + Sync + 'a, - 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, get_item, .. } = 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] = get_item(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, get_item, .. } = self; - let mut index = 0; - let area = Content::layout(self, to.area()); - for item in get_iterator() { - let item = get_item(item, index); - //to.place(area.into(), &item); - to.place(item.layout(area), &item); - index += 1; - } - } -} - -#[cfg(test)] #[test] fn test_iter_map () { - struct Foo; - impl Content for Foo {} - fn make_map + Send + Sync> (data: &Vec) -> impl Content { - Map::new(||data.iter(), |foo, index|{}) - } - let data = vec![Foo, Foo, Foo]; - //let map = make_map(&data); -} diff --git a/output/src/ops/reduce.rs b/output/src/ops/reduce.rs deleted file mode 100644 index dfcc00e..0000000 --- a/output/src/ops/reduce.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::*; - -pub struct Reduce(pub PhantomData, pub F, pub G) where - A: Send + Sync, B: Send + Sync, - I: Iterator + Send + Sync, - F: Fn() -> I + Send + Sync, - G: Fn(A, B, usize)->A + Send + Sync; - -impl Reduce where - A: Send + Sync, B: Send + Sync, - I: Iterator + Send + Sync, - F: Fn() -> I + Send + Sync, - G: Fn(A, B, usize)->A + Send + Sync -{ - pub const fn new (f: F, g: G) -> Self { Self(Default::default(), f, g) } -} - -impl Content for Reduce where - A: Send + Sync, B: Send + Sync, - I: Iterator + Send + Sync, - F: Fn() -> I + Send + Sync, - G: Fn(A, B, usize)->A + Send + Sync -{ - fn content (&self) -> impl Render { - } -} - -/* - - //pub fn reduce (iterator: I, callback: F) -> Reduce where - //E: Output, - //I: Iterator + Send + Sync, - //R: Render, - //F: Fn(R, T, usize) -> R + Send + Sync - //{ - //Reduce(Default::default(), iterator, callback) - //} -pub struct Reduce(PhantomData<(E, R)>, I, F) where - E: Output, - I: Iterator + Send + Sync, - R: Render, - F: Fn(R, T, usize) -> R + Send + Sync; -impl Content for Reduce where - E: Output, - I: Iterator + Send + Sync, - R: Render, - 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 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 { - //(when ,> - //When(cond: bool, item: A)) - ///// When `cond` is `true`, render `a`, otherwise render `b`. - //(either , B: Render,> - //Either(cond: bool, a: A, b: B)) - ///// If `opt` is `Some(T)` renders `cb(t)`, otherwise nothing. - //(opt B, B: Render,> - //Opt(option: Option, cb: F)) - ///// Maps items of iterator through callback. - //(map , I: Iterator, F: Fn() -> I, G: Fn(A, usize)->B,> - //Map(get_iterator: F, callback: G)) - //} -//} - diff --git a/output/src/ops/thunk.rs b/output/src/ops/thunk.rs deleted file mode 100644 index 9d2c0bf..0000000 --- a/output/src/ops/thunk.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::*; -use std::marker::PhantomData; - -/// Lazily-evaluated [Render]able. -pub struct Thunk, F: Fn()->T + Send + Sync>( - PhantomData, - F -); -impl, F: Fn()->T + Send + Sync> Thunk { - pub const fn new (thunk: F) -> Self { - Self(PhantomData, thunk) - } -} -impl, F: Fn()->T + Send + Sync> Content for Thunk { - fn content (&self) -> impl Render { (self.1)() } -} - -pub struct ThunkBox<'a, E: Output>( - PhantomData, - BoxRenderBox<'a, E> + Send + Sync + 'a> -); -impl<'a, E: Output> ThunkBox<'a, E> { - pub const fn new (thunk: BoxRenderBox<'a, E> + Send + Sync + 'a>) -> Self { - Self(PhantomData, thunk) - } -} -impl<'a, E: Output> Content for ThunkBox<'a, E> { - fn content (&self) -> impl Render { (self.1)() } -} -impl<'a, E, F, T> From for ThunkBox<'a, E> -where - E: Output, - F: Fn()->T + Send + Sync + 'a, - T: Render + Send + Sync + 'a -{ - fn from (f: F) -> Self { - Self(PhantomData, Box::new(move||f().boxed())) - } -} -//impl<'a, E: Output, F: Fn()->Box + 'a> + Send + Sync + 'a> From for ThunkBox<'a, E> { - //fn from (f: F) -> Self { - //Self(Default::default(), Box::new(f)) - //} -//} - -pub struct ThunkRender(PhantomData, F); -impl ThunkRender { - pub fn new (render: F) -> Self { Self(PhantomData, render) } -} -impl Content for ThunkRender { - 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, - F1, - F2 -); -implE::Area + Send + Sync, F2: Fn(&mut E) + Send + Sync> ThunkLayout { - pub fn new (layout: F1, render: F2) -> Self { Self(PhantomData, layout, render) } -} -impl Content for ThunkLayout -where - E: Output, - F1: Fn(E::Area)->E::Area + Send + Sync, - F2: Fn(&mut E) + Send + Sync -{ - fn layout (&self, to: E::Area) -> E::Area { (self.1)(to) } - fn render (&self, to: &mut E) { (self.2)(to) } -} diff --git a/output/src/ops/transform.rs b/output/src/ops/transform.rs deleted file mode 100644 index 3dd5b6e..0000000 --- a/output/src/ops/transform.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! [Content] items that modify the inherent -//! dimensions of their inner [Render]ables. -//! -//! Transform may also react to the [Area] provided. -//! ``` -//! use ::tengri::{output::*, tui::*}; -//! let area: [u16;4] = [10, 10, 20, 20]; -//! fn test (area: [u16;4], item: &impl Content, 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 { X(T), Y(T), XY(T) } - impl $Enum { - #[inline] pub const fn x (item: T) -> Self { Self::X(item) } - #[inline] pub const fn y (item: T) -> Self { Self::Y(item) } - #[inline] pub const fn xy (item: T) -> Self { Self::XY(item) } - } - #[cfg(feature = "dsl")] - impl<'a, E: Output + 'a, T: ViewContext<'a, E>> TryFromAtom<'a, T> - for $Enum> { - fn try_from_expr (state: &'a T, iter: TokenIter<'a>) -> Option { - 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 = if let Some(content) = state.get_content(&token.value) { - content - } else { - panic!("no content corresponding to for {:?}", &token); - }; - return Some(match k { - $x => Self::x(content), - $y => Self::y(content), - $xy => Self::xy(content), - _ => unreachable!() - }) - } - } - None - } - } - impl> Content for $Enum { - fn content (&self) -> impl Render { - match self { - Self::X(item) => item, - Self::Y(item) => item, - Self::XY(item) => item, - } - } - fn layout (&$self, $to: ::Area) -> ::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 { X(U, T), Y(U, T), XY(U, U, T), } - impl $Enum { - #[inline] pub const fn x (x: U, item: T) -> Self { Self::X(x, item) } - #[inline] pub const fn y (y: U, item: T) -> Self { Self::Y(y, item) } - #[inline] pub const fn xy (x: U, y: U, item: T) -> Self { Self::XY(x, y, item) } - } - #[cfg(feature = "dsl")] - impl<'a, E: Output + 'a, T: ViewContext<'a, E>> TryFromAtom<'a, T> - for $Enum> { - fn try_from_expr (state: &'a T, iter: TokenIter<'a>) -> Option { - 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 u = state.get(&u.value).expect("no unit provided"); - - let c = iter.next().expect("no content specified"); - let c = if let Some(c) = state.get_content(&c.value) { - c - } else { - panic!("no content corresponding to {:?}", &c); - }; - - 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 u = state.get(&u.value).expect("no x unit provided"); - - let v = iter.next().expect("no unit specified"); - let v = state.get(&v.value).expect("no y unit provided"); - - let c = iter.next().expect("no content specified"); - let c = if let Some(c) = state.get_content(&c.value) { - c - } else { - panic!("no content corresponding to {:?}", &c); - }; - - return Some(Self::xy(u, v, c)) - } - } - None - } - } - impl> Content for $Enum { - fn content (&self) -> impl Render { - 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 $Enum { - #[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); -} diff --git a/output/src/output.rs b/output/src/output.rs index af31438..5da748f 100644 --- a/output/src/output.rs +++ b/output/src/output.rs @@ -1,153 +1,65 @@ -use crate::*; -use std::ops::Deref; -/// Render target. -pub trait Output: Send + Sync + Sized { +#![feature(step_trait)] +#![feature(type_alias_impl_trait)] +#![feature(impl_trait_in_assoc_type)] +#![feature(const_precise_live_drops)] +#![feature(type_changing_struct_update)] +#![feature(anonymous_lifetime_in_impl_trait)] +#![feature(const_option_ops)] +#![feature(const_trait_impl)] +#![feature(const_default)] +#![feature(trait_alias)] +//#![feature(non_lifetime_binders)] + +pub(crate) use self::Direction::*; +pub(crate) use std::fmt::{Debug, Display}; +pub(crate) use std::marker::PhantomData; +pub(crate) use std::ops::{Add, Sub, Mul, Div}; +pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}}; +pub(crate) use tengri_core::*; + +/// Drawing target. +pub trait Out: Send + Sync + Sized { /// Unit of length type Unit: Coordinate; + /// Rectangle without offset type Size: Size; + /// Rectangle with offset type Area: Area; + + /// Render drawable in area specified by `T::layout(self.area())` + #[inline] fn place <'t, T: Content + ?Sized> ( + &mut self, content: &'t T + ) { + self.place_at(content.layout(self.area()), content) + } + + /// Render drawable in area specified by `area` + fn place_at <'t, T: Draw + ?Sized> (&mut self, area: Self::Area, content: &'t T); + /// 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); #[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() } + + /// Mutable pointer to area. + fn area_mut (&mut self) -> &mut Self::Area; } -/// Renderable with dynamic dispatch. -pub trait Render { - /// 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> - } -} +#[cfg(test)] mod output_test; +#[cfg(test)] pub(crate) use proptest_derive::Arbitrary; -/// 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> Render for C { - fn layout (&self, area: E::Area) -> E::Area { Content::layout(self, area) } - fn render (&self, output: &mut E) { Content::render(self, output) } -} +mod content; pub use self::content::*; +mod draw; pub use self::draw::*; +mod group; pub use self::group::*; +mod layout; pub use self::layout::*; +mod space; pub use self::space::*; +mod thunk; pub use self::thunk::*; +mod widget; pub use self::widget::*; -/// Opaque pointer to a renderable living on the heap. -/// -/// Return this from [Content::content] to use dynamic dispatch. -pub type RenderBox<'a, E> = Box>; - -/// You can render from a box. -impl<'a, E: Output> Content for RenderBox<'a, E> { - fn content (&self) -> impl Render { 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 + Send + Sync + 'a; - -/// You can render from an opaque pointer. -impl<'a, E: Output> Content for &RenderDyn<'a, E> where Self: Sized { - fn content (&self) -> impl Render { - #[allow(suspicious_double_ref_op)] - self.deref() - } - fn layout (&self, area: E::Area) -> E::Area { - #[allow(suspicious_double_ref_op)] - Render::layout(self.deref(), area) - } - fn render (&self, output: &mut E) { - #[allow(suspicious_double_ref_op)] - Render::render(self.deref(), output) - } -} - -/// Composable renderable with static dispatch. -pub trait Content { - /// Return a [Render]able of a specific type. - fn content (&self) -> impl Render { () } - /// 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> Content for &C { - fn content (&self) -> impl Render { (*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 Content for () { - fn layout (&self, area: E::Area) -> E::Area { area.center().to_area_pos().into() } - fn render (&self, _: &mut E) {} -} - -impl> Content for Option { - fn content (&self) -> impl Render { - 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 Content for $Struct { - fn content (&$self) -> impl Render { 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 - 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 } - } - }; -} +#[cfg(feature = "dsl")] mod view; +#[cfg(feature = "dsl")] pub use self::view::*; diff --git a/output/src/output_test.rs b/output/src/output_test.rs new file mode 100644 index 0000000..932e87a --- /dev/null +++ b/output/src/output_test.rs @@ -0,0 +1,170 @@ +use crate::{*, Direction::*}; +//use proptest_derive::Arbitrary; +use proptest::{prelude::*, option::of}; + +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); + } +} + +proptest! { + #[test] fn proptest_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(); + } +} + +proptest! { + #[test] fn proptest_area ( + 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>::zero(); + let _: [u16;4] = <[u16;4] as Area>::from_position([a, b]); + let _: [u16;4] = <[u16;4] as Area>::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(); + } +} + +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)), + (None, Some(y)) => Some($Op::y(y, content)), + _ => None + } { + //assert_eq!(Content::layout(&op, [x, y, w, h]), + //Draw::layout(&op, [x, y, w, h])); + } + } + } + } +} + +test_op_transform!(proptest_op_fixed, Fixed); +test_op_transform!(proptest_op_min, Min); +test_op_transform!(proptest_op_max, Max); +test_op_transform!(proptest_op_push, Push); +test_op_transform!(proptest_op_pull, Pull); +test_op_transform!(proptest_op_shrink, Shrink); +test_op_transform!(proptest_op_expand, Expand); +test_op_transform!(proptest_op_padding, Pad); + +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]), + //Draw::layout(&bsp, [x, y, w, h]), + //); + } +} + +#[test] fn test_stub_output () -> Usually<()> { + use crate::*; + struct TestOut([u16;4]); + impl Out for TestOut { + 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_at + ?Sized> (&mut self, area: [u16;4], _: &T) { + println!("place_at: {area:?}"); + () + } + } + impl Draw for String { + fn draw (&self, to: &mut TestOut) { + to.area_mut().set_w(self.len() as u16); + } + } + Ok(()) +} + +#[test] fn test_space () { + use crate::*; + assert_eq!(Area::center(&[10u16, 10, 20, 20]), [20, 20]); +} + +#[test] fn test_iter_map () { + struct Foo; + impl Content for Foo {} + fn _make_map + Send + Sync> (data: &Vec) -> impl Draw { + Map::new(||data.iter(), |_foo, _index|{}) + } + let _data = vec![Foo, Foo, Foo]; + //let map = make_map(&data); +} diff --git a/output/src/space.rs b/output/src/space.rs index 8edcb63..8875438 100644 --- a/output/src/space.rs +++ b/output/src/space.rs @@ -1,10 +1,5 @@ -mod area; pub use self::area::*; -mod coordinate; pub use self::coordinate::*; -mod direction; pub use self::direction::*; -mod measure; pub use self::measure::*; -mod size; pub use self::size::*; - -#[cfg(test)] #[test] fn test_space () { - use crate::*; - assert_eq!(Area::center(&[10u16, 10, 20, 20]), [20, 20]); -} +mod space_area; pub use self::space_area::*; +mod space_coordinate; pub use self::space_coordinate::*; +mod space_direction; pub use self::space_direction::*; +mod space_measure; pub use self::space_measure::*; +mod space_size; pub use self::space_size::*; diff --git a/output/src/space/direction.rs b/output/src/space/direction.rs deleted file mode 100644 index afa2139..0000000 --- a/output/src/space/direction.rs +++ /dev/null @@ -1,38 +0,0 @@ -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 (self, area: impl Area, 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); - } - } -} diff --git a/output/src/space/area.rs b/output/src/space/space_area.rs similarity index 70% rename from output/src/space/area.rs rename to output/src/space/space_area.rs index aefcfe8..e958857 100644 --- a/output/src/space/area.rs +++ b/output/src/space/space_area.rs @@ -1,5 +1,4 @@ use crate::*; -use std::fmt::Debug; pub trait Area: From<[N;4]> + Debug + Copy { fn x (&self) -> N; @@ -96,40 +95,3 @@ impl Area for [N;4] { fn w (&self) -> N { self[2] } 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>::zero(); - let _: [u16;4] = <[u16;4] as Area>::from_position([a, b]); - let _: [u16;4] = <[u16;4] as Area>::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(); - } - } -} diff --git a/output/src/space/coordinate.rs b/output/src/space/space_coordinate.rs similarity index 90% rename from output/src/space/coordinate.rs rename to output/src/space/space_coordinate.rs index 262be9e..d977318 100644 --- a/output/src/space/coordinate.rs +++ b/output/src/space/space_coordinate.rs @@ -1,5 +1,4 @@ -use std::fmt::{Debug, Display}; -use std::ops::{Add, Sub, Mul, Div}; +use crate::*; /// A linear coordinate. pub trait Coordinate: Send + Sync + Copy diff --git a/output/src/space/space_direction.rs b/output/src/space/space_direction.rs new file mode 100644 index 0000000..90dc95e --- /dev/null +++ b/output/src/space/space_direction.rs @@ -0,0 +1,21 @@ +use crate::*; + +/// 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 (self, area: impl Area, 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()) + } + } +} diff --git a/output/src/space/measure.rs b/output/src/space/space_measure.rs similarity index 67% rename from output/src/space/measure.rs rename to output/src/space/space_measure.rs index 2a1e415..b785bdb 100644 --- a/output/src/space/measure.rs +++ b/output/src/space/space_measure.rs @@ -1,35 +1,31 @@ use crate::*; -use std::sync::{Arc, atomic::{AtomicUsize, Ordering::Relaxed}}; - -pub trait HasSize { - fn size (&self) -> &Measure; -} - -#[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 { - _engine: PhantomData, +pub struct Measure { + _engine: PhantomData, pub x: Arc, pub y: Arc, } +impl PartialEq for Measure { + fn eq (&self, other: &Self) -> bool { + self.x.load(Relaxed) == other.x.load(Relaxed) && + self.y.load(Relaxed) == other.y.load(Relaxed) + } +} + +impl Layout for Measure {} + // TODO: 🡘 🡙 ←🡙→ indicator to expand window when too small -impl Content for Measure { - fn render (&self, to: &mut E) { +impl Draw for Measure { + fn draw (&self, to: &mut O) { self.x.store(to.area().w().into(), Relaxed); self.y.store(to.area().h().into(), Relaxed); } } -impl Clone for Measure { +impl Clone for Measure { fn clone (&self) -> Self { Self { _engine: Default::default(), @@ -39,7 +35,7 @@ impl Clone for Measure { } } -impl std::fmt::Debug for Measure { +impl std::fmt::Debug for Measure { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Measure") .field("width", &self.x) @@ -48,7 +44,7 @@ impl std::fmt::Debug for Measure { } } -impl Measure { +impl Measure { pub fn new () -> Self { Self { _engine: PhantomData::default(), @@ -81,7 +77,7 @@ impl Measure { pub fn format (&self) -> Arc { format!("{}x{}", self.w(), self.h()).into() } - pub fn of > (&self, item: T) -> Bsp, T> { - Bsp::b(Fill::xy(self), item) + pub fn of > (&self, item: T) -> Bsp, T> { + Bsp::b(Fill::XY(self), item) } } diff --git a/output/src/space/size.rs b/output/src/space/space_size.rs similarity index 63% rename from output/src/space/size.rs rename to output/src/space/space_size.rs index cfeeeed..235f098 100644 --- a/output/src/space/size.rs +++ b/output/src/space/space_size.rs @@ -1,5 +1,4 @@ use crate::*; -use std::fmt::Debug; pub trait Size: From<[N;2]> + Debug + Copy { fn x (&self) -> N; @@ -39,25 +38,18 @@ impl Size for [N;2] { 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(); - } +pub trait HasSize { + fn size (&self) -> &Measure; + fn width (&self) -> usize { + self.size().w() + } + fn height (&self) -> usize { + self.size().h() + } +} + +impl>> HasSize for T { + fn size (&self) -> &Measure { + self.get() } } diff --git a/output/src/thunk.rs b/output/src/thunk.rs new file mode 100644 index 0000000..29b7321 --- /dev/null +++ b/output/src/thunk.rs @@ -0,0 +1,27 @@ +use crate::*; + +pub struct Lazy(F, PhantomData<(O, T)>); +impl, F: Fn()->T> Lazy { pub const fn new (thunk: F) -> Self { Self(thunk, PhantomData) } } + +pub struct Thunk(PhantomData, F); +impl Thunk { pub const fn new (draw: F) -> Self { Self(PhantomData, draw) } } +impl Layout for Thunk {} +impl Draw for Thunk { + fn draw (&self, to: &mut O) { (self.1)(to) } +} + +#[derive(Debug, Default)] pub struct Memo { pub value: T, pub view: Arc> } +impl Memo { + pub fn new (value: T, view: U) -> Self { Self { value, view: Arc::new(view.into()) } } + pub fn update (&mut self, newval: T, draw: impl Fn(&mut U, &T, &T)->R) -> Option { + if newval != self.value { + let result = draw(&mut*self.view.write().unwrap(), &newval, &self.value); + self.value = newval; + return Some(result); + } + None + } +} + +/// Clear a pre-allocated buffer, then write into it. +#[macro_export] macro_rules! rewrite { ($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } } } diff --git a/output/src/view.rs b/output/src/view.rs index f0c88f9..f8a182f 100644 --- a/output/src/view.rs +++ b/output/src/view.rs @@ -1,100 +1,147 @@ use crate::*; +use ::tengri_dsl::{Dsl, DslExpr, DslWord, DslNs}; -#[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> { - if let Value::Sym(s) = value { - match *s { - $($sym => Some($body),)* - _ => None - } - } else { - panic!("expected content, got: {value:?}") - } - } +pub trait View { + fn view_expr <'a> (&'a self, output: &mut O, expr: &'a impl DslExpr) -> Usually { + Err(format!("View::view_expr: no exprs defined: {expr:?}").into()) + } + fn view_word <'a> (&'a self, output: &mut O, word: &'a impl DslWord) -> Usually { + Err(format!("View::view_word: no words defined: {word:?}").into()) + } + fn view <'a> (&'a self, output: &mut O, dsl: &'a impl Dsl) -> Usually { + if let Ok(Some(expr)) = dsl.expr() { + self.view_expr(output, &expr) + } else if let Ok(Some(word)) = dsl.word() { + self.view_word(output, &word) + } else { + panic!("{dsl:?}: invalid") } } } -// An ephemeral wrapper around view state and view description, -// that is meant to be constructed and returned from [Content::content]. -#[cfg(feature = "dsl")] -pub struct View<'a, T>( - pub &'a T, - pub SourceIter<'a> -); - -#[cfg(feature = "dsl")] -impl<'a, O: Output + 'a, T: ViewContext<'a, O>> Content for View<'a, T> { - fn content (&self) -> impl Render { - 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. -#[cfg(feature = "dsl")] -pub trait ViewContext<'a, E: Output + 'a>: Send + Sync - + Context - + Context - + Context +pub fn evaluate_output_expression <'a, O: Out + 'a, S> ( + state: &S, output: &mut O, expr: &'a impl DslExpr +) -> Usually where + S: View + + for<'b>DslNs<'b, bool> + + for<'b>DslNs<'b, O::Unit> { - fn get_content (&'a self, value: &Value<'a>) -> Option> { - 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>; - fn get_content_exp (&'a self, value: &Value<'a>) -> Option> { - try_delegate!(self, *value, When::>); - try_delegate!(self, *value, Either::, RenderBox<'a, E>>); - try_delegate!(self, *value, Align::>); - try_delegate!(self, *value, Bsp::, RenderBox<'a, E>>); - try_delegate!(self, *value, Fill::>); - 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 - } -} + // First element of expression is used for dispatch. + // Dispatch is proto-namespaced using separator character + let head = expr.head()?; + let mut frags = head.src()?.unwrap_or_default().split("/"); + // The rest of the tokens in the expr are arguments. + // Their meanings depend on the dispatched operation + let args = expr.tail(); + let arg0 = args.head(); + let tail0 = args.tail(); + let arg1 = tail0.head(); + let tail1 = tail0.tail(); + let arg2 = tail1.head(); + // And we also have to do the above binding dance + // so that the Perhapss remain in scope. + match frags.next() { -#[cfg(feature = "dsl")] -#[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()) - } - } -} + Some("when") => output.place(&When::new( + state.from(arg0?)?.unwrap(), + Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap()) + )), -#[cfg(feature = "dsl")] -#[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 { - let mut $iter = $iter.clone(); - $body; - None + Some("either") => output.place(&Either::new( + state.from(arg0?)?.unwrap(), + Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap()), + Thunk::new(move|output: &mut O|state.view(output, &arg2).unwrap()) + )), + + Some("bsp") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap()); + let b = Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap()); + match frags.next() { + Some("n") => Bsp::n(a, b), + Some("s") => Bsp::s(a, b), + Some("e") => Bsp::e(a, b), + Some("w") => Bsp::w(a, b), + Some("a") => Bsp::a(a, b), + Some("b") => Bsp::b(a, b), + frag => unimplemented!("bsp/{frag:?}") } - } - } + }), + + Some("align") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap()); + match frags.next() { + Some("n") => Align::n(a), + Some("s") => Align::s(a), + Some("e") => Align::e(a), + Some("w") => Align::w(a), + Some("x") => Align::x(a), + Some("y") => Align::y(a), + Some("c") => Align::c(a), + frag => unimplemented!("align/{frag:?}") + } + }), + + Some("fill") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap()); + match frags.next() { + Some("xy") | None => Fill::XY(a), + Some("x") => Fill::X(a), + Some("y") => Fill::Y(a), + frag => unimplemented!("fill/{frag:?}") + } + }), + + Some("fixed") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap()); + match axis { + Some("xy") | None => Fixed::XY(state.from(arg0?)?.unwrap(), state.from(arg1?)?.unwrap(), cb), + Some("x") => Fixed::X(state.from(arg0?)?.unwrap(), cb), + Some("y") => Fixed::Y(state.from(arg0?)?.unwrap(), cb), + frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})", + head.src()?.unwrap_or_default().split("/").next()) + } + }), + + Some("min") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap()); + match axis { + Some("xy") | None => Min::XY(state.from(arg0?)?.unwrap(), state.from(arg1?)?.unwrap(), cb), + Some("x") => Min::X(state.from(arg0?)?.unwrap(), cb), + Some("y") => Min::Y(state.from(arg0?)?.unwrap(), cb), + frag => unimplemented!("min/{frag:?}") + } + }), + + Some("max") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap()); + match axis { + Some("xy") | None => Max::XY(state.from(arg0?)?.unwrap(), state.from(arg1?)?.unwrap(), cb), + Some("x") => Max::X(state.from(arg0?)?.unwrap(), cb), + Some("y") => Max::Y(state.from(arg0?)?.unwrap(), cb), + frag => unimplemented!("max/{frag:?}") + } + }), + + Some("push") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap()); + match axis { + Some("xy") | None => Push::XY(state.from(arg0?)?.unwrap(), state.from(arg1?)?.unwrap(), cb), + Some("x") => Push::X(state.from(arg0?)?.unwrap(), cb), + Some("y") => Push::Y(state.from(arg0?)?.unwrap(), cb), + frag => unimplemented!("push/{frag:?}") + } + }), + + _ => return Ok(false) + + }; + Ok(true) } diff --git a/output/src/widget.rs b/output/src/widget.rs new file mode 100644 index 0000000..e8fc4c4 --- /dev/null +++ b/output/src/widget.rs @@ -0,0 +1,4 @@ +mod widget_border; pub use self::widget_border::*; +mod widget_form; pub use self::widget_form::*; +mod widget_style; pub use self::widget_style::*; +mod widget_tryptich; pub use self::widget_tryptich::*; diff --git a/output/src/widget/widget_border.rs b/output/src/widget/widget_border.rs new file mode 100644 index 0000000..7f44a17 --- /dev/null +++ b/output/src/widget/widget_border.rs @@ -0,0 +1,10 @@ +use crate::*; + +pub struct Border(pub bool, pub S); +impl> Layout for Border { + fn layout (&self, area: O::Area) -> O::Area { + self.1.layout(area) + } +} + +pub struct Bordered(pub bool, pub S, pub W); diff --git a/output/src/widget/widget_form.rs b/output/src/widget/widget_form.rs new file mode 100644 index 0000000..960c77d --- /dev/null +++ b/output/src/widget/widget_form.rs @@ -0,0 +1,55 @@ +use crate::*; + +pub struct FieldH(pub Theme, pub Label, pub Value); +impl, V: Content> HasContent for FieldH { + fn content (&self) -> impl Content { Bsp::e(&self.1, &self.2) } +} +impl, V: Content> Layout for FieldH { + fn layout (&self, to: O::Area) -> O::Area { self.content().layout(to) } +} +impl, V: Content> Draw for FieldH { + fn draw (&self, to: &mut O) { self.content().draw(to) } +} + +pub struct FieldV(pub Theme, pub Label, pub Value); +impl, V: Content> HasContent for FieldV { + fn content (&self) -> impl Content { Bsp::s(&self.1, &self.2) } +} +impl, V: Content> Layout for FieldV { + fn layout (&self, to: O::Area) -> O::Area { self.content().layout(to) } +} +impl, V: Content> Draw for FieldV { + fn draw (&self, to: &mut O) { self.content().draw(to) } +} + +// TODO: +pub struct Field { + pub direction: Direction, + pub label: Option, + pub label_fg: Option, + pub label_bg: Option, + pub label_align: Option, + pub value: Option, + pub value_fg: Option, + pub value_bg: Option, + pub value_align: Option, +} +impl Field { + pub fn new (direction: Direction) -> Field { + Field:: { + direction, + label: None, label_fg: None, label_bg: None, label_align: None, + value: None, value_fg: None, value_bg: None, value_align: None, + } + } + pub fn label ( + self, label: Option, align: Option, fg: Option, bg: Option + ) -> Field { + Field:: { label, label_fg: fg, label_bg: bg, label_align: align, ..self } + } + pub fn value ( + self, value: Option, align: Option, fg: Option, bg: Option + ) -> Field { + Field:: { value, value_fg: fg, value_bg: bg, value_align: align, ..self } + } +} diff --git a/output/src/widget/widget_style.rs b/output/src/widget/widget_style.rs new file mode 100644 index 0000000..b7ba4ba --- /dev/null +++ b/output/src/widget/widget_style.rs @@ -0,0 +1,15 @@ +#[allow(unused)] use crate::*; + +pub struct Foreground(pub Color, pub Item); +impl> Layout for Foreground { + fn layout (&self, to: O::Area) -> O::Area { + self.1.layout(to) + } +} + +pub struct Background(pub Color, pub Item); +impl> Layout for Background { + fn layout (&self, to: O::Area) -> O::Area { + self.1.layout(to) + } +} diff --git a/output/src/widget/widget_tryptich.rs b/output/src/widget/widget_tryptich.rs new file mode 100644 index 0000000..3039f5e --- /dev/null +++ b/output/src/widget/widget_tryptich.rs @@ -0,0 +1,31 @@ +#[allow(unused)] use crate::*; + +/// A three-column layout. +pub struct Tryptich { + pub top: bool, + pub h: u16, + pub left: (u16, A), + pub middle: (u16, B), + pub right: (u16, C), +} + +impl Tryptich<(), (), ()> { + pub fn center (h: u16) -> Self { + Self { h, top: false, left: (0, ()), middle: (0, ()), right: (0, ()) } + } + pub fn top (h: u16) -> Self { + Self { h, top: true, left: (0, ()), middle: (0, ()), right: (0, ()) } + } +} + +impl Tryptich { + pub fn left (self, w: u16, content: D) -> Tryptich { + Tryptich { left: (w, content), ..self } + } + pub fn middle (self, w: u16, content: D) -> Tryptich { + Tryptich { middle: (w, content), ..self } + } + pub fn right (self, w: u16, content: D) -> Tryptich { + Tryptich { right: (w, content), ..self } + } +} diff --git a/proc/Cargo.toml b/proc/Cargo.toml new file mode 100644 index 0000000..19fe50c --- /dev/null +++ b/proc/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tengri_proc" +description = "UI metaframework, procedural macros." +version = { workspace = true } +edition = { workspace = true } + +[lib] +proc-macro = true + +[dependencies] +tengri_core = { path = "../core" } +syn = { workspace = true } +quote = { workspace = true } +proc-macro2 = { workspace = true } +heck = { workspace = true } + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/proc/src/lib.rs b/proc/src/lib.rs new file mode 100644 index 0000000..7ae7699 --- /dev/null +++ b/proc/src/lib.rs @@ -0,0 +1,243 @@ +#![feature(str_as_str)] +#![feature(box_patterns)] +extern crate proc_macro; + +pub(crate) use std::collections::BTreeMap; +pub(crate) use std::cmp::Ordering; +pub(crate) use std::sync::Arc; +pub(crate) use proc_macro::TokenStream; +pub(crate) use proc_macro2::{ + TokenStream as TokenStream2, Ident, Span, Punct, Group, Delimiter, Spacing::* +}; +pub(crate) use syn::{ + parse_macro_input, ImplItem, ImplItemFn, LitStr, Type, TypePath, + ItemImpl, ReturnType, Signature, FnArg, Pat, PatType, PatIdent, + parse::{Parse, ParseStream, Result}, +}; +pub(crate) use quote::{quote, TokenStreamExt, ToTokens}; +pub(crate) use heck::{AsKebabCase, AsUpperCamelCase}; + +mod proc_view; +mod proc_expose; +mod proc_command; + +#[cfg(test)] use syn::parse_quote as pq; + +#[proc_macro_attribute] +pub fn expose (meta: TokenStream, item: TokenStream) -> TokenStream { + use self::proc_expose::{ExposeDef, ExposeMeta, ExposeImpl}; + write(ExposeDef( + parse_macro_input!(meta as ExposeMeta), + parse_macro_input!(item as ExposeImpl), + )) +} + +#[proc_macro_attribute] +pub fn command (meta: TokenStream, item: TokenStream) -> TokenStream { + use self::proc_command::{CommandDef, CommandMeta, CommandImpl}; + write(CommandDef( + parse_macro_input!(meta as CommandMeta), + parse_macro_input!(item as CommandImpl), + )) +} + +#[proc_macro_attribute] +pub fn view (meta: TokenStream, item: TokenStream) -> TokenStream { + use self::proc_view::{ViewDef, ViewMeta, ViewImpl}; + write(ViewDef( + parse_macro_input!(meta as ViewMeta), + parse_macro_input!(item as ViewImpl), + )) +} + +pub(crate) fn write (t: T) -> TokenStream { + let mut out = TokenStream2::new(); + t.to_tokens(&mut out); + out.into() +} + +pub(crate) fn write_quote (quote: TokenStream2) -> TokenStream2 { + let mut out = TokenStream2::new(); + for token in quote { + out.append(token); + } + out +} + +pub(crate) fn write_quote_to (out: &mut TokenStream2, quote: TokenStream2) { + for token in quote { + out.append(token); + } +} + +#[cfg(test)] #[test] fn test_proc_view () { + let x: crate::proc_view::ViewMeta = pq! { SomeOut }; + let output: Ident = pq! { SomeOut }; + assert_eq!(x.output, output); + + // TODO + let _x: crate::proc_view::ViewImpl = pq! { + impl Foo { + /// docstring1 + #[tengri::view(":view1")] #[bar] fn a_view () {} + + #[baz] + /// docstring2 + #[baz] fn is_not_view () {} + } + }; + let _expected_target: Ident = pq! { Foo }; + //assert_eq!(x.target, expected_target); + //assert_eq!(x.items.len(), 2); + //assert_eq!(x.items[0].item, pq! { + ///// docstring1 + //#[bar] fn a_view () {} + //}); + //assert_eq!(x.items[1].item, pq! { + //#[baz] + ///// docstring2 + //#[baz] fn is_not_view () {} + //}); + //assert_eq!(x.syms, vec![ + //ViewArm( { symbol: pq! { ":view1" }, name: pq! { a_view }, }, + //]); + // FIXME + //let parsed: ViewDefinition = pq! { + //#[tengri_proc::view(SomeOut)] + //impl SomeView { + //#[tengri::view(":view-1")] + //fn view_1 (&self) -> impl Content + use<'_> { + //"view-1" + //} + //} + //}; + //let written = quote! { #parsed }; + //assert_eq!(format!("{written}"), format!("{}", quote! { + //impl SomeView { + //fn view_1 (&self) -> impl Content + use<'_> { + //"view-1" + //} + //} + ///// Generated by [tengri_proc]. + //impl ::tengri::output::Content for SomeView { + //fn content (&self) -> impl Content { + //self.size.of(::tengri::output::View(self, self.config.view)) + //} + //} + ///// Generated by [tengri_proc]. + //impl<'a> ::tengri::dsl::ViewContext<'a, SomeOut> for SomeView { + //fn get_content_sym (&'a self, value: &Value<'a>) -> Option> { + //match value { + //::tengri::dsl::Value::Sym(":view-1") => self.view_1().boxed(), + //_ => panic!("expected Sym(content), got: {value:?}") + //} + //} + //} + //})); +} + +//#[cfg(test)] #[test] fn test_expose_definition () { + // TODO + //let parsed: ExposeImpl = pq! { + ////#[tengri_proc::expose] + //impl Something { + //fn something () -> bool {} + //} + //}; + //// FIXME: + ////assert_eq!( + ////format!("{}", quote! { #parsed }), + ////format!("{}", quote! { + ////impl Something { + ////fn something () {} + ////} + ////impl ::tengri::Context for Something { + ////fn get (&self, dsl: &::tengri::Value) -> Option { + ////Some(match dsl { + ////::tengri::Value::Sym(":true") => true, + ////::tengri::Value::Sym(":false") => false, + ////::tengri::Value::Sym(":bool1") => true || false, + ////_ => return None + ////}) + ////} + ////} + ////}) + ////); + + //let parsed: ExposeImpl = pq! { + ////#[tengri_proc::expose] + //impl Something { + //#[tengri::expose(bool)] { + //":bool1" => true || false, + //} + //#[tengri::expose(u16)] { + //":u161" => 0 + 1, + //} + //#[tengri::expose(usize)] { + //":usize1" => 1 + 2, + //} + //#[tengri::expose(Arc)] { + //":arcstr1" => "foo".into(), + //} + //#[tengri::expose(Option>)] { + //":optarcstr1" => Some("bar".into()), + //":optarcstr2" => Some("baz".into()), + //} + //fn something () {} + //} + //}; + //// FIXME: + ////assert_eq!( + ////format!("{}", quote! { #parsed }), + ////format!("{}", quote! { + ////impl Something { + ////fn something () {} + ////} + ////impl ::tengri::Context> for Something { + ////fn get (&self, dsl: &::tengri::Value) -> Option> { + ////Some(match dsl { + ////::tengri::Value::Sym(":arcstr1") => "foo".into(), + ////_ => return None + ////}) + ////} + ////} + ////impl ::tengri::Context>> for Something { + ////fn get (&self, dsl: &::tengri::Value) -> Option>> { + ////Some(match dsl { + ////::tengri::Value::Sym(":optarcstr1") => Some("bar".into()), + ////::tengri::Value::Sym(":optarcstr2") => Some("baz".into()), + ////_ => return None + ////}) + ////} + ////} + ////impl ::tengri::Context for Something { + ////fn get (&self, dsl: &::tengri::Value) -> Option { + ////Some(match dsl { + ////::tengri::Value::Sym(":true") => true, + ////::tengri::Value::Sym(":false") => false, + ////::tengri::Value::Sym(":bool1") => true || false, + ////_ => return None + ////}) + ////} + ////} + ////impl ::tengri::Context for Something { + ////fn get (&self, dsl: &::tengri::Value) -> Option { + ////Some(match dsl { + ////::tengri::Value::Num(n) => *n as u16, + ////::tengri::Value::Sym(":u161") => 0 + 1, + ////_ => return None + ////}) + ////} + ////} + ////impl ::tengri::Context for Something { + ////fn get (&self, dsl: &::tengri::Value) -> Option { + ////Some(match dsl { + ////::tengri::Value::Num(n) => *n as usize, + ////::tengri::Value::Sym(":usize1") => 1 + 2, + ////_ => return None + ////}) + ////} + ////} + ////}) + ////) +//} diff --git a/proc/src/proc_command.rs b/proc/src/proc_command.rs new file mode 100644 index 0000000..12347ee --- /dev/null +++ b/proc/src/proc_command.rs @@ -0,0 +1,212 @@ +use crate::*; + +#[derive(Debug, Clone)] +pub(crate) struct CommandDef(pub(crate) CommandMeta, pub(crate) CommandImpl); + +#[derive(Debug, Clone)] +pub(crate) struct CommandMeta(TypePath); + +#[derive(Debug, Clone)] +pub(crate) struct CommandImpl(ItemImpl, BTreeMap, CommandArm>); + +#[derive(Debug, Clone)] +struct CommandArm(Ident, Vec, #[allow(unused)] ReturnType); + +impl Parse for CommandMeta { + fn parse (input: ParseStream) -> Result { + Ok(Self(input.parse()?)) + } +} + +impl Parse for CommandImpl { + fn parse (input: ParseStream) -> Result { + let block = input.parse::()?; + let exposed = Self::collect(&block.items).map_err(|e|input.error(e))?; + Ok(Self(block, exposed)) + } +} + +impl CommandImpl { + fn collect (items: &Vec) + -> std::result::Result, CommandArm>, String> + { + let mut exposed: BTreeMap, CommandArm> = Default::default(); + for item in items.iter() { + if let ImplItem::Fn( + ImplItemFn { sig: Signature { ident, inputs, output, .. }, .. } + ) = item { + let key = CommandArm::ident_to_key(&ident); + if exposed.contains_key(&key) { + return Err(format!("already defined: {ident}")); + } + exposed.insert(key, CommandArm( + ident.clone(), + inputs.iter().map(|x|x.clone()).collect(), + output.clone(), + )); + } + } + Ok(exposed) + } +} + +impl ToTokens for CommandDef { + fn to_tokens (&self, out: &mut TokenStream2) { + let Self(CommandMeta(state), CommandImpl(block, exposed)) = self; + + let command_enum = &block.self_ty; + + let variants = exposed.values().map(|arm|{ + let mut out = TokenStream2::new(); + out.append(arm.to_enum_variant_ident()); + //let ident = &arm.0; + if arm.has_args() { + out.append(Group::new(Delimiter::Brace, { + let mut out = TokenStream2::new(); + for (arg, ty) in arm.args() { + write_quote_to(&mut out, quote! { #arg : #ty , }); + } + out + })); + } + out.append(Punct::new(',', Alone)); + out + }); + + let _matchers = exposed.values().map(|arm|{ + let key = LitStr::new(&arm.to_key(), Span::call_site()); + let variant = { + let mut out = TokenStream2::new(); + out.append(arm.to_enum_variant_ident()); + let _ident = &arm.0; + if arm.has_args() { + out.append(Group::new(Delimiter::Brace, { + let mut out = TokenStream2::new(); + for (arg, _ty) in arm.args() { + write_quote_to(&mut out, quote! { + #arg: FromDsl::from_dsl(self, words, ||"command error")?, + }); + } + out + })); + } + out + }; + write_quote(quote! { + Some(::tengri::dsl::Token { value: ::tengri::dsl::Val::Key(#key), .. }) => { + let mut words = words.clone(); + Some(#command_enum::#variant) + }, + //Some(::tengri::dsl::Token { + //value: ::tengri::dsl::Value::Key(#key), .. + //}) => { + //let mut iter = iter.clone(); Some(#command_enum::#variant) + //}, + }) + }); + + let implementations = exposed.values().map(|arm|{ + let ident = &arm.0; + let variant = { + let mut out = TokenStream2::new(); + out.append(arm.to_enum_variant_ident()); + //let ident = &arm.0; + if arm.has_args() { + out.append(Group::new(Delimiter::Brace, { + let mut out = TokenStream2::new(); + for (arg, _ty) in arm.args() { + write_quote_to(&mut out, quote! { #arg , }); + } + out + })); + } + out + }; + let give_rest = write_quote(quote! { /*TODO*/ }); + let give_args = arm.args() + .map(|(arg, _ty)|write_quote(quote! { #arg, })) + .collect::>(); + write_quote(quote! { + #command_enum::#variant => + #command_enum::#ident(state, #(#give_args)* #give_rest), + }) + }); + + write_quote_to(out, quote! { + /// Generated by [tengri_proc]. + #[derive(Clone, Debug)] pub enum #command_enum { #(#variants)* } + /// Not generated by [tengri_proc]. + #block + /// Generated by [tengri_proc::command]. + /// + /// Means [#command_enum] is now a [Command] over [#state]. + /// Instances of [#command_enum] can be consumed by a + /// mutable pointer to [#state], invoking predefined operations + /// and optionally returning undo history data. + impl ::tengri::input::Command<#state> for #command_enum { + fn execute (&self, state: &mut #state) -> Perhaps { + match self { #(#implementations)* } + } + } + /// Generated by [tengri_proc::command]. + impl ::tengri::dsl::FromDsl<#state> for #command_enum { + fn from_dsl (state: &#state, value: &impl ::tengri::dsl::Dsl) + -> Perhaps + { + todo!()//Ok(match token { #(#matchers)* _ => None }) + } + } + }); + + //if exposed.len() > 0 { + //panic!("{:#?}", block.self_ty); + //if let Type::Path(ref path) = *block.self_ty { + //if path.path.segments.get(0).unwrap().ident == "TekCommand" { + //panic!("\n{}", quote! {#out}); + //} + //} + } +} + +impl CommandArm { + fn to_key (&self) -> Arc { + Self::ident_to_key(&self.0) + } + fn to_enum_variant_ident (&self) -> Ident { + Ident::new(&Self::ident_to_enum_variant(&self.0), Span::call_site()) + } + fn ident_to_key (ident: &Ident) -> Arc { + format!("{}", AsKebabCase(format!("{ident}"))).into() + } + fn ident_to_enum_variant (ident: &Ident) -> Arc { + format!("{}", AsUpperCamelCase(format!("{ident}"))).into() + } + fn has_args (&self) -> bool { + self.1.len() > 1 + } + fn args (&self) -> impl Iterator)> { + self.1.iter().skip(1).filter_map(|arg|if let FnArg::Typed(PatType { + ty, pat: box Pat::Ident(PatIdent { ident: arg, .. }), .. + }) = arg { + Some((arg, ty)) + } else { + unreachable!("only typed args should be present at this position"); + }) + } + fn _to_enum_variant_def (&self) -> TokenStream2 { + let mut out = TokenStream2::new(); + out.append(self.to_enum_variant_ident()); + //let ident = &self.0; + if self.has_args() { + out.append(Group::new(Delimiter::Brace, { + let mut out = TokenStream2::new(); + for (arg, ty) in self.args() { + write_quote_to(&mut out, quote! { #arg : #ty , }); + } + out + })); + } + out.append(Punct::new(',', Alone)); + out + } +} diff --git a/proc/src/proc_expose.rs b/proc/src/proc_expose.rs new file mode 100644 index 0000000..2756b28 --- /dev/null +++ b/proc/src/proc_expose.rs @@ -0,0 +1,232 @@ +use crate::*; + +#[derive(Debug, Clone)] +pub(crate) struct ExposeDef(pub(crate) ExposeMeta, pub(crate) ExposeImpl); + +#[derive(Debug, Clone)] +pub(crate) struct ExposeMeta; + +#[derive(Debug, Clone)] +pub(crate) struct ExposeImpl(ItemImpl, BTreeMap>); + +//#[derive(Debug, Clone)] +//struct ExposeSym(LitStr); + +#[derive(Debug, Clone)] +struct ExposeType(Box); + +impl Parse for ExposeMeta { + fn parse (_input: ParseStream) -> Result { + Ok(Self) + } +} + +impl Parse for ExposeImpl { + fn parse (input: ParseStream) -> Result { + let block = input.parse::()?; + let mut exposed: BTreeMap> = Default::default(); + for item in block.items.iter() { + if let ImplItem::Fn(ImplItemFn { sig: Signature { ident, output, .. }, .. }) = item { + if let ReturnType::Type(_, return_type) = output { + let return_type = ExposeType(return_type.clone()); + if !exposed.contains_key(&return_type) { + exposed.insert(return_type.clone(), Default::default()); + } + let values = exposed.get_mut(&return_type).unwrap(); + let key = format!(":{}", AsKebabCase(format!("{}", &ident))); + if values.contains_key(&key) { + return Err(input.error(format!("already defined: {key}"))) + } + values.insert(key, ident.clone()); + } else { + return Err(input.error("output type must be specified")) + } + } + } + Ok(Self(block, exposed)) + } +} + +impl ToTokens for ExposeDef { + fn to_tokens (&self, out: &mut TokenStream2) { + let Self(_meta, data) = self; + write_quote_to(out, quote! { #data }); + } +} + +impl ToTokens for ExposeImpl { + fn to_tokens (&self, out: &mut TokenStream2) { + let Self(block, exposed) = self; + write_quote_to(out, quote! { #block }); + for (t, variants) in exposed.iter() { + self.expose_variants(out, t, variants); + } + if exposed.len() > 0 { + //panic!("{}", quote! {#out}); + } + } +} + +fn is_num (t: &impl AsRef) -> bool { + return matches!(t.as_ref(), + | "u8" | "u16" | "u32" | "u64" | "usize" + | "i8" | "i16" | "i32" | "i64" | "isize") } + +impl ExposeImpl { + fn expose_variants ( + &self, out: &mut TokenStream2, t: &ExposeType, variants: &BTreeMap + ) { + let Self(ItemImpl { self_ty: state, .. }, ..) = self; + let type_is = format!("{}", quote! { #t }); + let variants = variants.iter().map(|(key, value)|{ + let key = LitStr::new(&key, Span::call_site()); + quote! { Some(#key) => Ok(Some(state.#value())), } + }); + write_quote_to(out, if &type_is == "bool" { + quote! { + /// Generated by [tengri_proc::expose]. + impl ::tengri::dsl::FromDsl<#state> for #t { + fn from_dsl (state: &#state, dsl: &impl Dsl) -> Perhaps { + match dsl.word()? { + Some("true") => Ok(Some(true)), + Some("false") => Ok(Some(false)), + _ => match dsl.word()? { #(#variants)* _ => Ok(None) } + } + } + } + } + } else if is_num(&type_is) { + quote! {} + //quote! { + ///// Generated by [tengri_proc::expose]. + //impl ::tengri::dsl::FromDsl<#state> for #t { + //fn from_dsl (state: &#state, dsl: &impl Dsl) -> Perhaps { + //match dsl.num()? { + //Some(n) => Ok(Some(n.parse::<#t>()?)), + //_ => match dsl.word()? { #(#variants)* _ => Ok(None) } + //} + //} + //} + //} + } else { + quote! { + /// Generated by [tengri_proc::expose]. + impl ::tengri::dsl::FromDsl<#state> for #t { + fn from_dsl (state: &#state, dsl: &impl Dsl) -> Perhaps { + match dsl.word()? { #(#variants)* _ => Ok(None) } + } + } + } + }) + //let arms = variants.iter().map(|(key, value)|{ + //let key = LitStr::new(&key, Span::call_site()); + //quote! { #key => state.#value(), } + //}); + //if &type_is == "bool" { + //return quote! { + //::tengri::dsl::Val::Sym(s) => match s.as_ref() { + //":true" => true, + //":false" => false, + //#variants + //_ => { return Ok(None) } + //}, + //} + //} + //if matches!(type_is.as_str(), + //"u8" | "u16" | "u32" | "u64" | "usize" | + //"i8" | "i16" | "i32" | "i64" | "isize") + //{ + //let num_err = LitStr::new( + //&format!("{{n}}: failed to convert to {type_is}"), + //Span::call_site() + //); + //return quote! { + //::tengri::dsl::Val::Num(n) => TryInto::<#t>::try_into(n) + //.unwrap_or_else(|_|panic!(#num_err)), + //::tengri::dsl::Val::Sym(s) => match s.as_ref() { + //#variants + //_ => { return Ok(None) } + //}, + //} + //} + //let arms = quote! { + //::tengri::dsl::Val::Sym(s) => match s.as_ref() { + //#variants + //_ => { return Ok(None) } + //}, + //}; + //let builtins = + //write_quote_to(out, quote! { + ///// Generated by [tengri_proc::expose]. + //impl ::tengri::dsl::FromDsl<#state> for #t { + //fn from_dsl (state: &#state, dsl: &impl Dsl) -> Perhaps { + //$builtins + //$defined + //Ok(None) + //} + //} + //}); + } +} + +//impl From for ExposeSym { fn from (this: LitStr) -> Self { Self(this) } } + +//impl PartialOrd for ExposeSym { + //fn partial_cmp (&self, other: &Self) -> Option { + //let this = &self.0; + //let that = &other.0; + //Some(format!("{}", quote! { #this }).cmp(&format!("{}", quote! { #that }))) + //} +//} + +//impl Ord for ExposeSym { + //fn cmp (&self, other: &Self) -> Ordering { + //let this = &self.0; + //let that = &other.0; + //format!("{}", quote! { #this }).cmp(&format!("{}", quote! { #that })) + //} +//} + +//impl PartialEq for ExposeSym { + //fn eq (&self, other: &Self) -> bool { + //let this = &self.0; + //let that = &other.0; + //format!("{}", quote! { #this }) == format!("{}", quote! { #that }) + //} +//} + +//impl Eq for ExposeSym {} + +impl From for ExposeType { fn from (this: Type) -> Self { Self(Box::new(this)) } } + +impl PartialOrd for ExposeType { + fn partial_cmp (&self, other: &Self) -> Option { + let this = &self.0; + let that = &other.0; + Some(format!("{}", quote! { #this }).cmp(&format!("{}", quote! { #that }))) + } +} + +impl Ord for ExposeType { + fn cmp (&self, other: &Self) -> Ordering { + let this = &self.0; + let that = &other.0; + format!("{}", quote! { #this }).cmp(&format!("{}", quote! { #that })) + } +} + +impl PartialEq for ExposeType { + fn eq (&self, other: &Self) -> bool { + let this = &self.0; + let that = &other.0; + format!("{}", quote! { #this }) == format!("{}", quote! { #that }) + } +} + +impl Eq for ExposeType {} + +impl ToTokens for ExposeType { + fn to_tokens (&self, out: &mut TokenStream2) { + self.0.to_tokens(out) + } +} diff --git a/proc/src/proc_flex.rs b/proc/src/proc_flex.rs new file mode 100644 index 0000000..039dd7d --- /dev/null +++ b/proc/src/proc_flex.rs @@ -0,0 +1,2 @@ +// TODO: #[derive(Flex, FlexMut, FlexOption, FlexResult ... ?)] +// better derive + annotations diff --git a/proc/src/proc_view.rs b/proc/src/proc_view.rs new file mode 100644 index 0000000..b9e1017 --- /dev/null +++ b/proc/src/proc_view.rs @@ -0,0 +1,136 @@ +use crate::*; + +#[derive(Debug, Clone)] +pub(crate) struct ViewDef(pub(crate) ViewMeta, pub(crate) ViewImpl); + +#[derive(Debug, Clone)] +pub(crate) struct ViewMeta { pub(crate) output: Ident } + +#[derive(Debug, Clone)] +pub(crate) struct ViewImpl { block: ItemImpl, exposed: BTreeMap, } + +/// `#[view]` takes 1 arg ([Engine] implementation for which it's valid). +impl Parse for ViewMeta { + fn parse (input: ParseStream) -> Result { + Ok(Self { output: input.parse::()?, }) + } +} + +/// `#[view]` exposes each function as corresponding symbol. +/// +/// Maybe it's best to genericize that pattern rather than have +/// 3 whole things for the layers of the program. +impl Parse for ViewImpl { + fn parse (input: ParseStream) -> Result { + let block = input.parse::()?; + let mut exposed: BTreeMap = Default::default(); + for item in block.items.iter() { + if let ImplItem::Fn(ImplItemFn { sig: Signature { ident, inputs, .. }, .. }) = item + && inputs.len() == 1 { + let key = format!(":{}", AsKebabCase(format!("{}", &ident))); + if exposed.contains_key(&key) { + return Err(input.error(format!("already defined: {ident}"))); + } + exposed.insert(key, ident.clone()); + } + } + Ok(Self { block, exposed }) + } +} + +/// `#[view]`'s output is a generated block of symbol bindings. +impl ToTokens for ViewDef { + fn to_tokens (&self, out: &mut TokenStream2) { + let Self(_, ViewImpl { block, .. }) = self; + let generated = self.generated(); + write_quote_to(out, quote! { + #block + #generated + }) + } +} + +impl ViewDef { + fn generated (&self) -> impl ToTokens { + let Self(ViewMeta { output }, ViewImpl { block, exposed }) = self; + let self_ty = &block.self_ty; + // Expressions are handled by built-in functions that operate over constants and symbols. + let builtins = builtins_with_boxes_output(quote! { #output }) + .map(|(builtin, builtin_ty)|match builtin { + Single(name) => quote! { + if dsl.head()?.key()? == Some(#name) { + return Ok(Some(#builtin_ty::from_dsl_or_else(self, dsl, + ||format!("failed to load builtin").into())?.boxed())) + } + }, + Prefix(name) => quote! { + if let Some(key) = dsl.head()?.key()? && key.starts_with(#name) { + return Ok(Some(#builtin_ty::from_dsl_or_else(self, dsl, + ||format!("failed to load builtin").into())?.boxed())) + } + }, + }); + // Symbols are handled by user-provided functions that take no parameters but `&self`. + let exposed = exposed.iter().map(|(key, value)|write_quote(quote! { + #key => return Ok(Some(self.#value().boxed())), + })); + quote! { + /// Generated by [tengri_proc]. + /// + /// Makes [#self_ty] able to construct the [Draw]able + /// which might correspond to a given [TokenStream], + /// while taking [#self_ty]'s state into consideration. + impl<'state> ::tengri::dsl::DslInto + 'state>> for #self_ty { + fn dsl_into (&self, dsl: &impl ::tengri::dsl::Dsl) + -> Perhaps + 'state>> + { + #(#builtins)* + if let Some(sym) = dsl.sym()? { + match sym { + #(#exposed)* + _ => return Err(format!("unknown symbol {sym}").into()) + } + } + Ok(None) + } + } + } + } +} + +enum Builtin { + Single(TokenStream2), + Prefix(TokenStream2), +} +use Builtin::*; + +fn builtins_with (n: TokenStream2, c: TokenStream2) -> impl Iterator { + [ + (Single(quote!("when")), quote!(When::< #c >)), + (Single(quote!("either")), quote!(Either::< #c, #c>)), + (Prefix(quote!("align/")), quote!(Align::< #c >)), + (Prefix(quote!("bsp/")), quote!(Bsp::< #c, #c>)), + (Prefix(quote!("fill/")), quote!(Fill::< #c >)), + (Prefix(quote!("fixed/")), quote!(Fixed::<#n, #c >)), + (Prefix(quote!("min/")), quote!(Min::<#n, #c >)), + (Prefix(quote!("max/")), quote!(Max::<#n, #c >)), + (Prefix(quote!("shrink/")), quote!(Shrink::<#n, #c >)), + (Prefix(quote!("expand/")), quote!(Expand::<#n, #c >)), + (Prefix(quote!("push/")), quote!(Push::<#n, #c >)), + (Prefix(quote!("pull/")), quote!(Pull::<#n, #c >)), + (Prefix(quote!("margin/")), quote!(Margin::<#n, #c >)), + (Prefix(quote!("padding/")), quote!(Padding::<#n, #c >)), + ].into_iter() +} + +fn _builtins_with_holes () -> impl Iterator { + builtins_with(quote! { _ }, quote! { _ }) +} + +fn _builtins_with_boxes () -> impl Iterator { + builtins_with(quote! { _ }, quote! { Box> }) +} + +fn builtins_with_boxes_output (o: TokenStream2) -> impl Iterator { + builtins_with(quote! { _ }, quote! { Box> }) +} diff --git a/shell.nix b/shell.nix index a84dcd7..a4c3248 100644 --- a/shell.nix +++ b/shell.nix @@ -2,10 +2,10 @@ {pkgs?import{}}:let stdenv = pkgs.clang19Stdenv; name = "tengri"; - nativeBuildInputs = with pkgs; [ pkg-config libclang ]; - buildInputs = with pkgs; [ libclang ]; + nativeBuildInputs = [ pkgs.pkg-config pkgs.libclang pkgs.mold ]; + buildInputs = [ pkgs.libclang ]; LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (with pkgs; []); + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath []; in pkgs.mkShell.override { inherit stdenv; } { diff --git a/tengri.svg b/tengri.svg new file mode 100644 index 0000000..7ff60dd --- /dev/null +++ b/tengri.svg @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + Daily Timestamp Heatmap + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 23:00 + 22:00 + 21:00 + 20:00 + 19:00 + 18:00 + 17:00 + 16:00 + 15:00 + 14:00 + 13:00 + 12:00 + 11:00 + 10:00 + 9:00 + 8:00 + 7:00 + 6:00 + 5:00 + 4:00 + 3:00 + 2:00 + 1:00 + 0:00 + 03/02 + 03/09 + 03/16 + 03/23 + 03/30 + 04/06 + 04/13 + 04/20 + 04/27 + 05/04 + 05/11 + 05/18 + 05/25 + 06/01 + 06/08 + 06/15 + 06/22 + 06/29 + 07/06 + 07/13 + 07/20 + 07/27 + 08/03 + 08/10 + 08/17 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tengri/Cargo.toml b/tengri/Cargo.toml index 1c7d4eb..74b46ad 100644 --- a/tengri/Cargo.toml +++ b/tengri/Cargo.toml @@ -4,15 +4,24 @@ edition = "2024" description = "UI metaframework." version = { workspace = true } -[dependencies] -tengri_dsl = { optional = true, path = "../dsl" } -tengri_input = { optional = true, path = "../input" } -tengri_output = { optional = true, path = "../output" } -tengri_tui = { optional = true, path = "../tui" } - [features] default = [ "input", "output", "tui" ] input = [ "tengri_input" ] output = [ "tengri_output" ] tui = [ "tengri_tui" ] -dsl = [ "tengri_dsl", "tengri_input/dsl", "tengri_output/dsl", "tengri_tui/dsl" ] +dsl = [ "tengri_dsl", "tengri_output/dsl", "tengri_tui/dsl" ] + +[dependencies] +tengri_core = { workspace = true } +tengri_dsl = { optional = true, path = "../dsl" } +tengri_input = { optional = true, path = "../input" } +tengri_output = { optional = true, path = "../output" } +tengri_tui = { optional = true, path = "../tui" } + +[dev-dependencies] +tengri_proc = { path = "../proc" } +tengri = { path = ".", features = [ "dsl" ] } +crossterm = { workspace = true } + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/tengri/README.md b/tengri/README.md index 4500aef..962f196 100644 --- a/tengri/README.md +++ b/tengri/README.md @@ -1,3 +1,17 @@ -# tengri +***tengri*** is a metaframework for building interactive applications with rust. (aren't we all?) -an interface metaframework. currently supports ratatui. +tengri is developed as part of [***tek***](https://codeberg.org/unspeaker/tek), +a music program for terminals. + +tengri contains: +* [***dizzle***](./dsl), a framework for defining domain-specific languages +* [***output***](./output), an abstract UI layout framework +* [***input***](./input), an abstract UI event framework +* [***tui***](./tui), an implementation of tengri over [***ratatui***](https://ratatui.rs/). + +as well as: +* [***core***](./core), the shared definitions ("utils") module +* [***proc***](./proc), the space for procedural macros +* [***tengri***](./tengri), the top-level reexport crate + +tengri is published under [**AGPL3**](./LICENSE). diff --git a/tengri/src/lib.rs b/tengri/src/lib.rs index c1c72a0..972a702 100644 --- a/tengri/src/lib.rs +++ b/tengri/src/lib.rs @@ -1,4 +1,8 @@ +pub use ::tengri_core::*; #[cfg(feature="output")] pub use ::tengri_output as output; #[cfg(feature="input")] pub use ::tengri_input as input; #[cfg(feature="dsl")] pub use ::tengri_dsl as dsl; #[cfg(feature="tui")] pub use ::tengri_tui as tui; + +#[cfg(test)] extern crate tengri_proc; +#[cfg(test)] mod test; diff --git a/tengri/src/test.rs b/tengri/src/test.rs new file mode 100644 index 0000000..2df3c8a --- /dev/null +++ b/tengri/src/test.rs @@ -0,0 +1,103 @@ +// FIXME +//use crate::*; +//use crate::{dsl::*, input::*, tui::TuiIn}; +//use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}; +//use std::cmp::Ordering; + +//#[test] fn test_subcommand () -> Usually<()> { + //#[derive(Debug)] struct Event(crossterm::event::Event); + //impl Eq for Event {} + //impl PartialEq for Event { fn eq (&self, other: &Self) -> bool { todo!() } } + //impl Ord for Event { fn cmp (&self, other: &Self) -> Ordering { todo!() } } + //impl PartialOrd for Event { fn partial_cmp (&self, other: &Self) -> Option { None } } + //struct Test { keys: InputMap } + + //handle!(TuiIn: |self: Test, input|Ok(None));[>if let Some(command) = self.keys.command(self, input) { + //Ok(Some(true)) + //} else { + //Ok(None) + //});*/ + + //#[tengri_proc::command(Test)] + //impl TestCommand { + //fn do_thing (_state: &mut Test) -> Perhaps { + //Ok(None) + //} + //fn do_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps { + //Ok(None) + //} + //fn do_sub (state: &mut Test, command: TestSubcommand) -> Perhaps { + //Ok(command.execute(state)?.map(|command|Self::DoSub { command })) + //} + //} + + //#[tengri_proc::command(Test)] + //impl TestSubcommand { + //fn do_other_thing (_state: &mut Test) -> Perhaps { + //Ok(None) + //} + //fn do_other_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps { + //Ok(None) + //} + //} + + //let mut test = Test { + //keys: InputMap::from_source(" + //(@a do-thing) + //(@b do-thing-arg 0) + //(@c do-sub do-other-thing) + //(@d do-sub do-other-thing-arg 0) + //")? + //}; + + ////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent { + ////kind: KeyEventKind::Press, + ////code: KeyCode::Char('a'), + ////modifiers: KeyModifiers::NONE, + ////state: KeyEventState::NONE, + ////})))?); + ////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent { + ////kind: KeyEventKind::Press, + ////code: KeyCode::Char('b'), + ////modifiers: KeyModifiers::NONE, + ////state: KeyEventState::NONE, + ////})))?); + ////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent { + ////kind: KeyEventKind::Press, + ////code: KeyCode::Char('c'), + ////modifiers: KeyModifiers::NONE, + ////state: KeyEventState::NONE, + ////})))?); + ////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent { + ////kind: KeyEventKind::Press, + ////code: KeyCode::Char('d'), + ////modifiers: KeyModifiers::NONE, + ////state: KeyEventState::NONE, + ////})))?); + ////assert_eq!(None, test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent { + ////kind: KeyEventKind::Press, + ////code: KeyCode::Char('z'), + ////modifiers: KeyModifiers::NONE, + ////state: KeyEventState::NONE, + ////})))?); + //Ok(()) +//} + +//FIXME: +//#[cfg(test)] #[test] fn test_dsl_context () { + //use crate::dsl::{Dsl, Value}; + + //struct Test; + //#[tengri_proc::expose] + //impl Test { + //fn some_bool (&self) -> bool { + //true + //} + //} + //assert_eq!(Dsl::get(&Test, &Value::Sym(":false")), Some(false)); + //assert_eq!(Dsl::get(&Test, &Value::Sym(":true")), Some(true)); + //assert_eq!(Dsl::get(&Test, &Value::Sym(":some-bool")), Some(true)); + //assert_eq!(Dsl::get(&Test, &Value::Sym(":missing-bool")), None); + //assert_eq!(Dsl::get(&Test, &Value::Num(0)), Some(false)); + //assert_eq!(Dsl::get(&Test, &Value::Num(1)), Some(true)); +//} diff --git a/tui/Cargo.toml b/tui/Cargo.toml index e005d37..3e9ab61 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -1,27 +1,34 @@ [package] name = "tengri_tui" -edition = "2024" description = "UI metaframework, Ratatui backend." version = { workspace = true } +edition = { workspace = true } -[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" -unicode-width = "0.2" - -tengri_input = { path = "../input" } -tengri_output = { path = "../output" } -tengri_dsl = { optional = true, path = "../dsl" } - -[dev-dependencies] -tengri = { path = "../tengri", features = [ "dsl" ] } -tengri_dsl = { path = "../dsl" } +[lib] +path = "src/tui.rs" [features] -dsl = [ "tengri_dsl", "tengri_input/dsl", "tengri_output/dsl" ] +dsl = [ "dep:tengri_dsl", "tengri_output/dsl" ] +bumpalo = [ "dep:bumpalo" ] + +[dependencies] +tengri_core = { workspace = true } +tengri_input = { workspace = true } +tengri_output = { workspace = true } +tengri_dsl = { workspace = true, optional = true } + +atomic_float = { workspace = true } +better-panic = { workspace = true } +bumpalo = { workspace = true, optional = true } +crossterm = { workspace = true } +konst = { workspace = true } +palette = { workspace = true } +quanta = { workspace = true } +rand = { workspace = true } +ratatui = { workspace = true } +unicode-width = { workspace = true } + +[dev-dependencies] +tengri = { workspace = true, features = [ "dsl" ] } +tengri_dsl = { workspace = true } +tengri_proc = { workspace = true } diff --git a/tui/README.md b/tui/README.md index d8c2f29..8e227d8 100644 --- a/tui/README.md +++ b/tui/README.md @@ -1,15 +1,4 @@ -# `tengri_tui` +***tengri_tui*** is an implementation of [tengri_output](../output) and [tengri_input](../input) +on top of [ratatui](https://ratatui.rs/) and [crossterm](https://github.com/crossterm-rs/crossterm). -the `Tui` struct (the *engine*) implements the -`tengri_input::Input` and `tengri_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` and `Arc`. the engine and -application instances are expected to be wrapped -in `Arc`; internally, those synchronization -mechanisms may be used liberally. +tengri is published under [**AGPL3**](../LICENSE). diff --git a/tui/examples/demo.rs.old b/tui/examples/demo.rs.old deleted file mode 100644 index ba013de..0000000 --- a/tui/examples/demo.rs.old +++ /dev/null @@ -1,131 +0,0 @@ -use tek::*; - -fn main () -> Usually<()> { - Tui::run(Arc::new(RwLock::new(Demo::new())))?; - Ok(()) -} - -pub struct Demo { - index: usize, - items: Vec>> -} - -impl Demo { - fn new () -> Self { - Self { - index: 0, - items: vec![] - } - } -} - -impl Content for Demo { - type Engine = Tui; - fn content (&self) -> dyn Render { - 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 for Demo { - fn handle (&mut self, from: &TuiIn) -> Perhaps { - 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 "~~~")))))))) diff --git a/tui/examples/edn01.edn b/tui/examples/edn01.edn deleted file mode 100644 index 7ff93d1..0000000 --- a/tui/examples/edn01.edn +++ /dev/null @@ -1 +0,0 @@ -:hello-world diff --git a/tui/examples/edn02.edn b/tui/examples/edn02.edn deleted file mode 100644 index 4f352a6..0000000 --- a/tui/examples/edn02.edn +++ /dev/null @@ -1 +0,0 @@ -(bsp/s :hello :world) diff --git a/tui/examples/edn03.edn b/tui/examples/edn03.edn deleted file mode 100644 index 1622275..0000000 --- a/tui/examples/edn03.edn +++ /dev/null @@ -1 +0,0 @@ -(fill/xy :hello-world) diff --git a/tui/examples/edn04.edn b/tui/examples/edn04.edn deleted file mode 100644 index 9393669..0000000 --- a/tui/examples/edn04.edn +++ /dev/null @@ -1 +0,0 @@ -(fixed/xy 20 10 :hello-world) diff --git a/tui/examples/edn05.edn b/tui/examples/edn05.edn deleted file mode 100644 index 90313ad..0000000 --- a/tui/examples/edn05.edn +++ /dev/null @@ -1 +0,0 @@ -(bsp/s (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn06.edn b/tui/examples/edn06.edn deleted file mode 100644 index e35abf0..0000000 --- a/tui/examples/edn06.edn +++ /dev/null @@ -1 +0,0 @@ -(bsp/e (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn07.edn b/tui/examples/edn07.edn deleted file mode 100644 index 7370353..0000000 --- a/tui/examples/edn07.edn +++ /dev/null @@ -1 +0,0 @@ -(bsp/n (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn08.edn b/tui/examples/edn08.edn deleted file mode 100644 index afcde46..0000000 --- a/tui/examples/edn08.edn +++ /dev/null @@ -1 +0,0 @@ -(bsp/w (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn09.edn b/tui/examples/edn09.edn deleted file mode 100644 index 3ced769..0000000 --- a/tui/examples/edn09.edn +++ /dev/null @@ -1 +0,0 @@ -(bsp/a (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn10.edn b/tui/examples/edn10.edn deleted file mode 100644 index 07af31e..0000000 --- a/tui/examples/edn10.edn +++ /dev/null @@ -1 +0,0 @@ -(bsp/b (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn11.edn b/tui/examples/edn11.edn deleted file mode 100644 index 1c47cb5..0000000 --- a/tui/examples/edn11.edn +++ /dev/null @@ -1,11 +0,0 @@ -(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)))))) diff --git a/tui/examples/edn12.edn b/tui/examples/edn12.edn deleted file mode 100644 index 26e3f5f..0000000 --- a/tui/examples/edn12.edn +++ /dev/null @@ -1,11 +0,0 @@ -(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)))))) diff --git a/tui/examples/edn13.edn b/tui/examples/edn13.edn deleted file mode 100644 index 8fe81c3..0000000 --- a/tui/examples/edn13.edn +++ /dev/null @@ -1,11 +0,0 @@ -(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))))))) diff --git a/tui/examples/edn99.edn b/tui/examples/edn99.edn deleted file mode 100644 index cac4726..0000000 --- a/tui/examples/edn99.edn +++ /dev/null @@ -1,73 +0,0 @@ -(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 { - 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(()) - //}))))) - //})) - } diff --git a/tui/examples/tui.rs b/tui/examples/tui.rs deleted file mode 100644 index a52fbb4..0000000 --- a/tui/examples/tui.rs +++ /dev/null @@ -1,65 +0,0 @@ -use tengri::{self, input::*, output::*, tui::*, dsl::*}; -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); -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| {}); diff --git a/tui/examples/tui_00.rs b/tui/examples/tui_00.rs new file mode 100644 index 0000000..f13049e --- /dev/null +++ b/tui/examples/tui_00.rs @@ -0,0 +1,123 @@ +use tengri::{self, Usually, Perhaps, input::*, output::*, tui::*, dsl::*}; +use std::sync::{Arc, RwLock}; +use crate::ratatui::style::Color; +//use crossterm::event::{*, KeyCode::*}; + +fn main () -> Usually<()> { + let state = Example::new(); + Tui::new().unwrap().run(&state)?; + Ok(()) +} + +#[derive(Debug)] struct Example(usize, Measure); + +handle!(TuiIn: |self: Example, input|Ok(None)); +enum ExampleCommand { Next, Prev } +impl ExampleCommand { + fn eval (&self, state: &mut Example) -> Perhaps { + match self { + Self::Next => { + state.0 = (state.0 + 1) % Example::VIEWS.len(); + Ok(Some(Self::Prev)) + }, + Self::Prev => { + state.0 = if state.0 > 0 { state.0 - 1 } else { Example::VIEWS.len() - 1 }; + Ok(Some(Self::Next)) + } + } + } +} + +impl Draw for Example { + fn content (&self) -> impl Draw { + let index = self.0 + 1; + let wh = self.1.wh(); + let src = Self::VIEWS.get(self.0).unwrap_or(&""); + let heading = format!("Example {}/{} in {:?}", index, Self::VIEWS.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, CstIter::new(src))); + self.1.of(Bsp::s(title, Bsp::n(""/*code*/, content))) + } +} +impl View for Example { + fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> { + if evaluate_output_expression(self, to, expr)? + || evaluate_output_expression_tui(self, to, expr)? { + Ok(()) + } else { + Err(format!("Example::view_expr: unexpected: {expr:?}").into()) + } + } +} + +impl Example { + fn new () -> Arc> { + Arc::new(RwLock::new(Example(10, Measure::new()))) + } + const BINDS: &'static str = stringify! { + (@left prev) + (@right next) + }; + const VIEWS: &'static [&'static str] = &[ + stringify! { :hello-world }, + stringify! { (fill/xy :hello-world) }, + stringify! { (bsp/s :hello :world) }, + stringify! { (fixed/xy 20 10 :hello-world) }, + stringify! { (bsp/s (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + stringify! { (bsp/e (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + stringify! { (bsp/n (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + stringify! { (bsp/w (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + stringify! { (bsp/a (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + stringify! { (bsp/b (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + stringify! { + (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)))))) + }, + stringify! { + (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)))))) + }, + stringify! { + (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))))))) + }, + stringify! { :map-e }, + stringify! { (align/c :map-e) }, + stringify! { :map-s }, + stringify! { (align/c :map-s) }, + stringify! { + (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))))))) + }, + ]; +} diff --git a/tui/examples/tui_01.rs b/tui/examples/tui_01.rs new file mode 100644 index 0000000..9d62bdf --- /dev/null +++ b/tui/examples/tui_01.rs @@ -0,0 +1,105 @@ +fn main () {} + +//#[tengri_proc::expose] +//impl Example { + //fn _todo_u16_stub (&self) -> u16 { todo!() } + //fn _todo_bool_stub (&self) -> bool { todo!() } + //fn _todo_usize_stub (&self) -> usize { todo!() } + ////[bool] => {} + ////[u16] => {} + ////[usize] => {} +//} + +//#[tengri_proc::view(TuiOut)] +//impl Example { + //pub fn title (&self) -> impl Content + use<'_> { + //Tui::bg(Color::Rgb(60, 10, 10), Push::y(1, Align::n(format!("Example {}/{}:", self.0 + 1, VIEWS.len())))).boxed() + //} + //pub fn code (&self) -> impl Content + use<'_> { + //Tui::bg(Color::Rgb(10, 60, 10), Push::y(2, Align::n(format!("{}", VIEWS[self.0])))).boxed() + //} + //pub fn hello (&self) -> impl Content + use<'_> { + //Tui::bg(Color::Rgb(10, 100, 10), "Hello").boxed() + //} + //pub fn world (&self) -> impl Content + use<'_> { + //Tui::bg(Color::Rgb(100, 10, 10), "world").boxed() + //} + //pub fn hello_world (&self) -> impl Content + use<'_> { + //"Hello world!".boxed() + //} + //pub fn map_e (&self) -> impl Content + use<'_> { + //Map::east(5u16, ||0..5u16, |n, _i|format!("{n}")).boxed() + //} + //pub fn map_s (&self) -> impl Content + use<'_> { + //Map::south(5u16, ||0..5u16, |n, _i|format!("{n}")).boxed() + //} +//} + + //fn content (&self) -> dyn Draw { + //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(()) + ////}))))) + ////})) + //} diff --git a/tui/src/lib.rs b/tui/src/lib.rs deleted file mode 100644 index 6ac4926..0000000 --- a/tui/src/lib.rs +++ /dev/null @@ -1,68 +0,0 @@ -mod tui_engine; pub use self::tui_engine::*; -mod tui_content; pub use self::tui_content::*; - -pub use ::tengri_input as input; -pub(crate) use ::tengri_input::*; - -pub use ::tengri_output as output; -pub(crate) use ::tengri_output::*; - -#[cfg(feature = "dsl")] -pub use ::tengri_dsl; -#[cfg(feature = "dsl")] -pub(crate) use ::tengri_dsl::*; - -pub(crate) use atomic_float::AtomicF64; - -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; - -#[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 for TestComponent { - fn content (&self) -> impl Render { - Some(self.0.as_str()) - } - } - impl Handle for TestComponent { - fn handle (&mut self, from: &TuiIn) -> Perhaps { - 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(()) -} diff --git a/tui/src/tui.rs b/tui/src/tui.rs new file mode 100644 index 0000000..853d99d --- /dev/null +++ b/tui/src/tui.rs @@ -0,0 +1,79 @@ +#![feature(type_changing_struct_update)] +#![feature(trait_alias)] +#[cfg(test)] mod tui_test; +mod tui_engine; pub use self::tui_engine::*; +mod tui_content; pub use self::tui_content::*; +pub use ::{ + tengri_input, + tengri_output, + ratatui, + crossterm, + palette, + better_panic +}; +pub(crate) use ::{ + tengri_core::*, + tengri_input::*, + tengri_output::*, + atomic_float::AtomicF64, + std::{io::{stdout, Stdout}, sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}}}, + better_panic::{Settings, Verbosity}, + palette::{*, convert::*, okhsl::*}, + ratatui::{ + prelude::{Color, Style, Buffer}, + style::Modifier, + backend::{Backend, CrosstermBackend, ClearType}, + layout::{Size, Rect}, + buffer::Cell + }, + crossterm::{ + ExecutableCommand, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, + event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, + } +}; + +#[cfg(feature = "dsl")] use tengri_dsl::*; +#[cfg(feature = "dsl")] +pub fn evaluate_output_expression_tui <'a, S> ( + state: &S, mut output: &mut TuiOut, expr: impl DslExpr + 'a +) -> Usually where + S: View + + for<'b>DslNs<'b, bool> + + for<'b>DslNs<'b, u16> + + for<'b>DslNs<'b, Color> +{ + // See `tengri_output::evaluate_output_expression` + let head = expr.head()?; + let mut frags = head.src()?.unwrap_or_default().split("/"); + let args = expr.tail(); + let arg0 = args.head(); + let tail0 = args.tail(); + let arg1 = tail0.head(); + let tail1 = tail0.tail(); + let arg2 = tail1.head(); + match frags.next() { + + Some("text") => if let Some(src) = args?.src()? { output.place(&src) }, + + Some("fg") => { + let arg0 = arg0?.expect("fg: expected arg 0 (color)"); + output.place(&Tui::fg( + DslNs::::from(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")), + Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()), + )) + }, + + Some("bg") => { + let arg0 = arg0?.expect("bg: expected arg 0 (color)"); + output.place(&Tui::bg( + DslNs::::from(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")), + Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()), + )) + }, + + _ => return Ok(false) + + }; + Ok(true) +} diff --git a/tui/src/tui_content.rs b/tui/src/tui_content.rs index 0b15a95..f1c812b 100644 --- a/tui/src/tui_content.rs +++ b/tui/src/tui_content.rs @@ -1,32 +1,74 @@ -use crate::*; - -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 } - } - } +#[allow(unused)] use crate::*; +impl Tui { + pub const fn fg (color: Color, w: T) -> Foreground { Foreground(color, w) } + pub const fn bg (color: Color, w: T) -> Background { Background(color, w) } + pub const fn fg_bg (fg: Color, bg: Color, w: T) -> Background> { Background(bg, Foreground(fg, w)) } + pub const fn modify (enable: bool, modifier: Modifier, w: T) -> Modify { Modify(enable, modifier, w) } + pub const fn bold (enable: bool, w: T) -> Modify { Self::modify(enable, Modifier::BOLD, w) } + pub const fn border (enable: bool, style: S, w: T) -> Bordered { Bordered(enable, style, w) } } - -impl> Content for std::sync::Arc { - fn layout (&self, to: [u16;4]) -> [u16;4] { - Content::::layout(&**self, to) - } - fn render (&self, to: &mut TuiOut) { - Content::::render(&**self, to) - } -} - mod tui_border; pub use self::tui_border::*; +mod tui_button; pub use self::tui_button::*; mod tui_color; pub use self::tui_color::*; -mod tui_field; pub use self::tui_field::*; -mod tui_file; pub use self::tui_file::*; +mod tui_error; pub use self::tui_error::*; mod tui_phat; pub use self::tui_phat::*; mod tui_repeat; pub use self::tui_repeat::*; mod tui_scroll; pub use self::tui_scroll::*; mod tui_string; pub use self::tui_string::*; -mod tui_style; pub use self::tui_style::*; +mod tui_number; //pub use self::tui_number::*; +mod tui_tryptich; //pub use self::tui_tryptich::*; +impl> Draw for Foreground { + fn draw (&self, to: &mut TuiOut) { + let area = self.layout(to.area()); + to.fill_fg(area, self.0); + to.place_at(area, &self.1); + } +} +impl> Draw for Background { + fn draw (&self, to: &mut TuiOut) { + let area = self.layout(to.area()); + to.fill_bg(area, self.0); + to.place_at(area, &self.1); + } +} +pub struct Modify(pub bool, pub Modifier, pub T); +impl> Layout for Modify {} +impl> Draw for Modify { + fn draw (&self, to: &mut TuiOut) { + to.fill_mod(to.area(), self.0, self.1); + self.2.draw(to) + } +} +pub struct Styled(pub Option