Compare commits
45 commits
Author | SHA1 | Date | |
---|---|---|---|
fe4401c899 | |||
5ce50b76f5 | |||
6d23afe41f | |||
3f411e071f | |||
6a39c0441d | |||
5ef37275ec | |||
caeac280eb | |||
fb4a142c9c | |||
f3880d77d2 | |||
e02def6bc1 | |||
a9a8b8b951 | |||
95ceaa8231 | |||
b752d77815 | |||
e98242addf | |||
01e33afd93 | |||
ddd2e363c2 | |||
bc27b457ea | |||
35669c423c | |||
5d4aa21edd | |||
f7db3333c5 | |||
ec10e5a89d | |||
11e1bd975f | |||
79f08fd202 | |||
28fa0f21dc | |||
302daacc82 | |||
15e70da512 | |||
0444726a2d | |||
bf72429ac5 | |||
340f014365 | |||
8887eb07c1 | |||
cd3d4a5a6d | |||
18c51e88d1 | |||
bcb69805ef | |||
86f61ff3f6 | |||
f3a85de02e | |||
78e3d6b798 | |||
7b12788a92 | |||
a8a23db252 | |||
b0c6daf56e | |||
f7668c5c54 | |||
b1c6ab8b7d | |||
e9a9dad037 | |||
ed739d792f | |||
8208fa8899 | |||
4a537cd933 |
66 changed files with 12622 additions and 483 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
||||||
/target
|
/target
|
||||||
src/htmx.min.js
|
src/htmx.min.js
|
||||||
|
src/flowbite.min.css
|
||||||
|
src/flowbite.min.js
|
378
Cargo.lock
generated
378
Cargo.lock
generated
|
@ -17,18 +17,6 @@ version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ahash"
|
|
||||||
version = "0.8.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"once_cell",
|
|
||||||
"version_check",
|
|
||||||
"zerocopy",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
|
@ -83,9 +71,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.83"
|
version = "0.1.85"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
|
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -191,7 +179,7 @@ checksum = "2b1866ecef4f2d06a0bb77880015fdf2b89e25a1c2e5addacb87e459c86dc67e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"blowfish",
|
"blowfish",
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
@ -210,9 +198,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.6.0"
|
version = "2.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -238,15 +226,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.16.0"
|
version = "3.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.20.0"
|
version = "1.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a"
|
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
|
@ -262,9 +250,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.4"
|
version = "1.2.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf"
|
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
@ -344,9 +332,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.16"
|
version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
@ -426,9 +414,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.6.0"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
|
checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
|
@ -476,7 +464,7 @@ version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
|
checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.8.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"proc-macro2-diagnostics",
|
"proc-macro2-diagnostics",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -572,9 +560,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "event-listener"
|
name = "event-listener"
|
||||||
version = "5.3.1"
|
version = "5.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
|
checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"parking",
|
"parking",
|
||||||
|
@ -618,6 +606,12 @@ version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -773,7 +767,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi 0.13.3+wasi-0.2.2",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -784,9 +790,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
|
@ -812,24 +818,25 @@ name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
dependencies = [
|
|
||||||
"ahash",
|
|
||||||
"allocator-api2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.9.1"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.14.5",
|
"hashbrown 0.15.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -918,9 +925,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.9.5"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
|
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpdate"
|
name = "httpdate"
|
||||||
|
@ -1135,9 +1142,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.7.0"
|
version = "2.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.15.2",
|
"hashbrown 0.15.2",
|
||||||
|
@ -1161,19 +1168,19 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.10.1"
|
version = "2.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
|
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is-terminal"
|
name = "is-terminal"
|
||||||
version = "0.4.13"
|
version = "0.4.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
|
checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi 0.4.0",
|
"hermit-abi 0.4.0",
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1184,9 +1191,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.76"
|
version = "0.3.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
|
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
@ -1203,9 +1210,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.168"
|
version = "0.2.169"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
|
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
|
@ -1219,16 +1226,15 @@ version = "0.30.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.14"
|
version = "0.4.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
|
@ -1248,9 +1254,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.22"
|
version = "0.4.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "loom"
|
name = "loom"
|
||||||
|
@ -1320,17 +1326,11 @@ version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimal-lexical"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
|
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler2",
|
"adler2",
|
||||||
]
|
]
|
||||||
|
@ -1342,7 +1342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1367,9 +1367,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.12"
|
version = "0.2.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
|
checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
@ -1382,16 +1382,6 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "7.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"minimal-lexical",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
|
@ -1467,9 +1457,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.5"
|
version = "0.36.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
|
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -1482,11 +1472,11 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.68"
|
version = "0.10.69"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
|
checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.8.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1508,9 +1498,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
|
@ -1559,12 +1549,6 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paste"
|
|
||||||
version = "1.0.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pear"
|
name = "pear"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
|
@ -1605,9 +1589,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.15"
|
version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-utils"
|
name = "pin-utils"
|
||||||
|
@ -1682,9 +1666,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.92"
|
version = "1.0.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
@ -1704,9 +1688,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.37"
|
version = "1.0.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
@ -1738,7 +1722,7 @@ version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1767,7 +1751,7 @@ version = "0.5.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2016,11 +2000,11 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.42"
|
version = "0.38.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
|
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.8.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
|
@ -2038,15 +2022,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.18"
|
version = "1.0.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
|
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.18"
|
version = "1.0.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
|
@ -2084,7 +2068,7 @@ version = "2.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.8.0",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -2093,9 +2077,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework-sys"
|
name = "security-framework-sys"
|
||||||
version = "2.12.1"
|
version = "2.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
|
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -2103,18 +2087,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.216"
|
version = "1.0.217"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
|
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.216"
|
version = "1.0.217"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
|
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2123,9 +2107,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.133"
|
version = "1.0.138"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -2263,21 +2247,11 @@ dependencies = [
|
||||||
"der",
|
"der",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sqlformat"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
|
|
||||||
dependencies = [
|
|
||||||
"nom",
|
|
||||||
"unicode_categories",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx"
|
name = "sqlx"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e"
|
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-macros",
|
"sqlx-macros",
|
||||||
|
@ -2288,38 +2262,32 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-core"
|
name = "sqlx-core"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e"
|
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
|
||||||
"byteorder",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"either",
|
"either",
|
||||||
"event-listener",
|
"event-listener",
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-intrusive",
|
"futures-intrusive",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hashbrown 0.14.5",
|
"hashbrown 0.15.2",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
"hex",
|
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlformat",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
@ -2330,9 +2298,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-macros"
|
name = "sqlx-macros"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657"
|
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2343,9 +2311,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-macros-core"
|
name = "sqlx-macros-core"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5"
|
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"either",
|
"either",
|
||||||
|
@ -2369,13 +2337,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-mysql"
|
name = "sqlx-mysql"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
|
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.8.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -2413,13 +2381,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-postgres"
|
name = "sqlx-postgres"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
|
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.8.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
|
@ -2427,7 +2395,6 @@ dependencies = [
|
||||||
"etcetera",
|
"etcetera",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
|
@ -2453,9 +2420,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-sqlite"
|
name = "sqlx-sqlite"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680"
|
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -2519,9 +2486,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.90"
|
version = "2.0.96"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2568,12 +2535,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.14.0"
|
version = "3.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
"getrandom 0.3.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
|
@ -2590,18 +2558,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "2.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.69"
|
version = "2.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2661,9 +2629,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.8.0"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
|
checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tinyvec_macros",
|
"tinyvec_macros",
|
||||||
]
|
]
|
||||||
|
@ -2676,9 +2644,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.42.0"
|
version = "1.43.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -2694,9 +2662,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2760,9 +2728,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.22"
|
version = "0.22.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -2872,9 +2840,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.8.0"
|
version = "2.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
|
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase_serde"
|
name = "unicase_serde"
|
||||||
|
@ -2894,9 +2862,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.14"
|
version = "1.0.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
|
@ -2919,12 +2887,6 @@ version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode_categories"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -2956,19 +2918,19 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.11.0"
|
version = "1.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
|
@ -3007,6 +2969,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.13.3+wasi-0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasite"
|
name = "wasite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -3015,20 +2986,21 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.99"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
|
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.99"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
|
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
|
@ -3040,9 +3012,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.49"
|
version = "0.4.50"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
|
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
@ -3053,9 +3025,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.99"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
|
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
|
@ -3063,9 +3035,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.99"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
|
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -3076,15 +3048,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.99"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
|
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.76"
|
version = "0.3.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
|
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
@ -3299,9 +3274,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.6.20"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
|
checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -3316,6 +3291,15 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rt"
|
||||||
|
version = "0.33.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.8.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "write16"
|
name = "write16"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
|
@ -31,4 +31,3 @@ reqwest = { version = "0.11", features = ["blocking"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
cache = []
|
cache = []
|
||||||
htmx = []
|
|
||||||
|
|
26
build.rs
26
build.rs
|
@ -1,11 +1,9 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
fn main() {
|
pub fn download_file(url: &str, dest_path: &str) {
|
||||||
let url = "https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js";
|
println!("Downloading {dest_path} from {url}");
|
||||||
let dest_path = Path::new("src/htmx.min.js");
|
let dest_path = Path::new(dest_path);
|
||||||
|
|
||||||
println!("Downloading htmx.min.js from {url}");
|
|
||||||
let response = reqwest::blocking::get(url)
|
let response = reqwest::blocking::get(url)
|
||||||
.expect("Failed to send HTTP request")
|
.expect("Failed to send HTTP request")
|
||||||
.error_for_status()
|
.error_for_status()
|
||||||
|
@ -13,7 +11,23 @@ fn main() {
|
||||||
|
|
||||||
let content = response.bytes().expect("Failed to read response body");
|
let content = response.bytes().expect("Failed to read response body");
|
||||||
|
|
||||||
fs::write(dest_path, &content).expect("Failed to write htmx.min.js to destination");
|
fs::write(dest_path, &content).expect("Failed to write file to destination");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
download_file(
|
||||||
|
"https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js",
|
||||||
|
"src/htmx.min.js",
|
||||||
|
);
|
||||||
|
|
||||||
|
download_file(
|
||||||
|
"https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.css",
|
||||||
|
"src/flowbite.min.css",
|
||||||
|
);
|
||||||
|
download_file(
|
||||||
|
"https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.js",
|
||||||
|
"src/flowbite.min.js",
|
||||||
|
);
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
|
use based::get_pg;
|
||||||
use based::request::{RequestContext, StringResponse};
|
use based::request::{RequestContext, StringResponse};
|
||||||
use based::{
|
use based::ui::components::Shell;
|
||||||
get_pg,
|
use based::ui::prelude::Nothing;
|
||||||
page::{Shell, render_page},
|
|
||||||
};
|
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use rocket::get;
|
use rocket::get;
|
||||||
use rocket::routes;
|
use rocket::routes;
|
||||||
|
@ -13,13 +12,9 @@ pub async fn index_page(ctx: RequestContext) -> StringResponse {
|
||||||
h1 { "Hello World!" };
|
h1 { "Hello World!" };
|
||||||
);
|
);
|
||||||
|
|
||||||
render_page(
|
Shell::new(Nothing(), Nothing(), Nothing())
|
||||||
content,
|
.render_page(content, "Hello World", ctx)
|
||||||
"Hello World",
|
.await
|
||||||
ctx,
|
|
||||||
&Shell::new(html! {}, html! {}, Some(String::new())),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::launch]
|
#[rocket::launch]
|
||||||
|
|
54
examples/ui.rs
Normal file
54
examples/ui.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use based::asset::AssetRoutes;
|
||||||
|
use based::request::{RequestContext, StringResponse};
|
||||||
|
use based::ui::components::{AppBar, Shell};
|
||||||
|
use based::ui::htmx::{Event, HTMXAttributes};
|
||||||
|
use based::ui::prelude::*;
|
||||||
|
use maud::Render;
|
||||||
|
use maud::html;
|
||||||
|
use rocket::routes;
|
||||||
|
use rocket::{State, get};
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub async fn index_page(ctx: RequestContext, shell: &State<Shell>) -> StringResponse {
|
||||||
|
let content = AppBar("MyApp", None).render();
|
||||||
|
|
||||||
|
let content = html!(
|
||||||
|
h1 { "Hello World!" };
|
||||||
|
|
||||||
|
(
|
||||||
|
Screen::medium(Hover(Background(Nothing()).color(Red::_700))).on(
|
||||||
|
Background(Text("HELLO!")).color(Blue::_700)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(Hover(
|
||||||
|
Cursor::NorthEastResize.on(
|
||||||
|
Padding(Text("").color(&Gray::_400)).x(ScreenValue::_10)
|
||||||
|
)
|
||||||
|
).on(
|
||||||
|
Link("/test", Text("Hello")).hx_get("/test").hx_get("/test").hx_trigger(
|
||||||
|
Event::on_load().delay("2s")
|
||||||
|
.and(Event::on_revealed())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(content)
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
shell.render_page(content, "Hello World", ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::launch]
|
||||||
|
async fn launch() -> _ {
|
||||||
|
// Logging
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let shell = Shell::new(Nothing(), Nothing(), Nothing()).use_ui();
|
||||||
|
|
||||||
|
rocket::build()
|
||||||
|
.mount("/", routes![index_page])
|
||||||
|
.mount_assets() // Mount included assets routes
|
||||||
|
.manage(shell) // Manage global shell reference
|
||||||
|
}
|
43
src/asset.rs
Normal file
43
src/asset.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use crate::request::assets::DataResponse;
|
||||||
|
use rocket::{Build, get, routes};
|
||||||
|
|
||||||
|
#[get("/assets/htmx.min.js")]
|
||||||
|
pub fn htmx_script_route() -> DataResponse {
|
||||||
|
DataResponse::new(
|
||||||
|
include_str!("htmx.min.js").as_bytes().to_vec(),
|
||||||
|
"application/javascript".to_string(),
|
||||||
|
Some(60 * 60 * 24 * 3),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/assets/flowbite.min.css")]
|
||||||
|
pub fn flowbite_css() -> DataResponse {
|
||||||
|
DataResponse::new(
|
||||||
|
include_str!("flowbite.min.css").as_bytes().to_vec(),
|
||||||
|
"text/css".to_string(),
|
||||||
|
Some(60 * 60 * 24 * 3),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/assets/flowbite.min.js")]
|
||||||
|
pub fn flowbite_js() -> DataResponse {
|
||||||
|
DataResponse::new(
|
||||||
|
include_str!("flowbite.min.js").as_bytes().to_vec(),
|
||||||
|
"application/javascript".to_string(),
|
||||||
|
Some(60 * 60 * 24 * 3),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AssetRoutes {
|
||||||
|
fn mount_assets(self) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetRoutes for rocket::Rocket<Build> {
|
||||||
|
fn mount_assets(self) -> Self {
|
||||||
|
self.mount("/", routes![
|
||||||
|
crate::asset::htmx_script_route,
|
||||||
|
crate::asset::flowbite_css,
|
||||||
|
crate::asset::flowbite_js
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
use data_encoding::HEXUPPER;
|
||||||
|
use rand::RngCore;
|
||||||
|
|
||||||
pub mod csrf;
|
pub mod csrf;
|
||||||
pub mod profile_pic;
|
pub mod profile_pic;
|
||||||
mod session;
|
mod session;
|
||||||
|
@ -10,6 +13,14 @@ pub use user::MaybeUser;
|
||||||
pub use user::User;
|
pub use user::User;
|
||||||
pub use user::UserRole;
|
pub use user::UserRole;
|
||||||
|
|
||||||
|
fn gen_token(token_length: usize) -> String {
|
||||||
|
let mut token_bytes = vec![0u8; token_length];
|
||||||
|
|
||||||
|
rand::thread_rng().fill_bytes(&mut token_bytes);
|
||||||
|
|
||||||
|
HEXUPPER.encode(&token_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
/// A macro to check if a user has admin privileges.
|
/// A macro to check if a user has admin privileges.
|
||||||
///
|
///
|
||||||
/// This macro checks whether the provided user has admin privileges by calling the `is_admin` method on it.
|
/// This macro checks whether the provided user has admin privileges by calling the `is_admin` method on it.
|
||||||
|
|
|
@ -2,9 +2,9 @@ use chrono::Utc;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
|
|
||||||
use crate::{gen_random, get_pg};
|
use crate::get_pg;
|
||||||
|
|
||||||
use super::{User, UserRole};
|
use super::{User, UserRole, gen_token};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
|
@ -50,7 +50,7 @@ impl Sessions for User {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
"INSERT INTO user_session (token, \"user\", kind, name) VALUES ($1, $2, $3, $4) RETURNING *",
|
"INSERT INTO user_session (token, \"user\", kind, name) VALUES ($1, $2, $3, $4) RETURNING *",
|
||||||
)
|
)
|
||||||
.bind(gen_random(64))
|
.bind(gen_token(64))
|
||||||
.bind(&self.username)
|
.bind(&self.username)
|
||||||
.bind(SessionKind::API)
|
.bind(SessionKind::API)
|
||||||
.bind(name)
|
.bind(name)
|
||||||
|
@ -108,7 +108,7 @@ impl Sessions for User {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
"INSERT INTO user_session (token, \"user\", kind) VALUES ($1, $2, $3) RETURNING *",
|
"INSERT INTO user_session (token, \"user\", kind) VALUES ($1, $2, $3) RETURNING *",
|
||||||
)
|
)
|
||||||
.bind(gen_random(64))
|
.bind(gen_token(64))
|
||||||
.bind(&self.username)
|
.bind(&self.username)
|
||||||
.bind(SessionKind::USER)
|
.bind(SessionKind::USER)
|
||||||
.fetch_one(get_pg!())
|
.fetch_one(get_pg!())
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
use rocket::get;
|
|
||||||
|
|
||||||
use crate::request::{StringResponse, respond_script};
|
|
||||||
|
|
||||||
#[get("/assets/htmx.min.js")]
|
|
||||||
pub fn htmx_script_route() -> StringResponse {
|
|
||||||
respond_script(include_str!("htmx.min.js").to_string())
|
|
||||||
}
|
|
23
src/lib.rs
23
src/lib.rs
|
@ -1,19 +1,22 @@
|
||||||
use data_encoding::HEXUPPER;
|
#![feature(const_vec_string_slice)]
|
||||||
use rand::RngCore;
|
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
|
pub mod asset;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod format;
|
pub mod format;
|
||||||
#[cfg(feature = "htmx")]
|
|
||||||
pub mod htmx;
|
|
||||||
pub mod page;
|
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod result;
|
pub mod result;
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
// TODO : CORS?
|
// TODO : CORS?
|
||||||
|
|
||||||
// Postgres
|
// Postgres
|
||||||
|
|
||||||
|
// TODO : IDEA
|
||||||
|
// more efficient table join using WHERE ANY instead of multiple SELECTs
|
||||||
|
// map_tables(Vec<T>, Fn(&T) -> U) -> Vec<U>
|
||||||
|
|
||||||
pub static PG: OnceCell<sqlx::PgPool> = OnceCell::const_new();
|
pub static PG: OnceCell<sqlx::PgPool> = OnceCell::const_new();
|
||||||
|
|
||||||
/// A macro to retrieve or initialize the `PostgreSQL` connection pool.
|
/// A macro to retrieve or initialize the `PostgreSQL` connection pool.
|
||||||
|
@ -34,7 +37,7 @@ macro_rules! get_pg {
|
||||||
client
|
client
|
||||||
} else {
|
} else {
|
||||||
let client = sqlx::postgres::PgPoolOptions::new()
|
let client = sqlx::postgres::PgPoolOptions::new()
|
||||||
.max_connections(12)
|
.max_connections(5)
|
||||||
.connect(&std::env::var("DATABASE_URL").unwrap())
|
.connect(&std::env::var("DATABASE_URL").unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -43,11 +46,3 @@ macro_rules! get_pg {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gen_random(token_length: usize) -> String {
|
|
||||||
let mut token_bytes = vec![0u8; token_length];
|
|
||||||
|
|
||||||
rand::thread_rng().fill_bytes(&mut token_bytes);
|
|
||||||
|
|
||||||
HEXUPPER.encode(&token_bytes)
|
|
||||||
}
|
|
||||||
|
|
157
src/page/mod.rs
157
src/page/mod.rs
|
@ -1,157 +0,0 @@
|
||||||
use maud::{PreEscaped, html};
|
|
||||||
|
|
||||||
pub mod search;
|
|
||||||
|
|
||||||
use crate::request::{RequestContext, StringResponse};
|
|
||||||
|
|
||||||
use rocket::http::{ContentType, Status};
|
|
||||||
|
|
||||||
/// Represents the HTML structure of a page shell, including the head, body class, and body content.
|
|
||||||
///
|
|
||||||
/// This structure is used to construct the overall HTML structure of a page, making it easier to generate consistent HTML pages dynamically.
|
|
||||||
pub struct Shell {
|
|
||||||
/// The HTML content for the `<head>` section of the page.
|
|
||||||
head: PreEscaped<String>,
|
|
||||||
/// An optional class attribute for the `<body>` element.
|
|
||||||
body_class: Option<String>,
|
|
||||||
/// The HTML content for the static body portion.
|
|
||||||
body_content: PreEscaped<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Shell {
|
|
||||||
/// Constructs a new `Shell` instance with the given head content, body content, and body class.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `head` - The HTML content for the page's head.
|
|
||||||
/// * `body_content` - The HTML content for the body of the page.
|
|
||||||
/// * `body_class` - An optional class to apply to the `<body>` element.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A `Shell` instance encapsulating the provided HTML content and attributes.
|
|
||||||
#[must_use]
|
|
||||||
pub const fn new(
|
|
||||||
head: PreEscaped<String>,
|
|
||||||
body_content: PreEscaped<String>,
|
|
||||||
body_class: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
head,
|
|
||||||
body_class,
|
|
||||||
body_content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders the full HTML page using the shell structure, with additional content and a title.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `content` - The additional HTML content to render inside the main content div.
|
|
||||||
/// * `title` - The title of the page, rendered inside the `<title>` element.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A `PreEscaped<String>` containing the full HTML page content.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render(&self, content: PreEscaped<String>, title: &str) -> PreEscaped<String> {
|
|
||||||
html! {
|
|
||||||
html {
|
|
||||||
head {
|
|
||||||
title { (title) };
|
|
||||||
(self.head)
|
|
||||||
};
|
|
||||||
@if self.body_class.is_some() {
|
|
||||||
body class=(self.body_class.as_ref().unwrap()) {
|
|
||||||
(self.body_content);
|
|
||||||
|
|
||||||
div id="main_content" {
|
|
||||||
(content)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} @else {
|
|
||||||
body {
|
|
||||||
(self.body_content);
|
|
||||||
|
|
||||||
div id="main_content" {
|
|
||||||
(content)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders a full page or an HTMX-compatible fragment based on the request context.
|
|
||||||
///
|
|
||||||
/// If the request is not an HTMX request, this function uses the provided shell to generate
|
|
||||||
/// a full HTML page. If it is an HTMX request, only the provided content is rendered.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `content` - The HTML content to render.
|
|
||||||
/// * `title` - The title of the page for full-page rendering.
|
|
||||||
/// * `ctx` - The `RequestContext` containing request metadata.
|
|
||||||
/// * `shell` - The `Shell` instance used for full-page rendering.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A `StringResponse`
|
|
||||||
pub async fn render_page(
|
|
||||||
content: PreEscaped<String>,
|
|
||||||
title: &str,
|
|
||||||
ctx: RequestContext,
|
|
||||||
shell: &Shell,
|
|
||||||
) -> StringResponse {
|
|
||||||
if ctx.is_htmx {
|
|
||||||
(Status::Ok, (ContentType::HTML, content.into_string()))
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
Status::Ok,
|
|
||||||
(
|
|
||||||
ContentType::HTML,
|
|
||||||
shell.render(content, title).into_string(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates an HTML link with HTMX attributes for dynamic behavior.
|
|
||||||
///
|
|
||||||
/// This function creates an `<a>` element with attributes that enable HTMX behavior for navigation without reload.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `url` - The URL to link to.
|
|
||||||
/// * `class` - The CSS class for styling the link.
|
|
||||||
/// * `onclick` - The JavaScript `onclick` handler for the link.
|
|
||||||
/// * `content` - The content inside the link element.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A `PreEscaped<String>` containing the rendered HTML link element.
|
|
||||||
#[must_use]
|
|
||||||
pub fn htmx_link(
|
|
||||||
url: &str,
|
|
||||||
class: &str,
|
|
||||||
onclick: &str,
|
|
||||||
content: PreEscaped<String>,
|
|
||||||
) -> PreEscaped<String> {
|
|
||||||
html!(
|
|
||||||
a class=(class) onclick=(onclick) href=(url) hx-get=(url) hx-target="#main_content" hx-push-url="true" hx-swap="innerHTML" {
|
|
||||||
(content);
|
|
||||||
};
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a `<script>` element containing the provided JavaScript code.
|
|
||||||
///
|
|
||||||
/// This function wraps the provided JavaScript code in a `<script>` tag,
|
|
||||||
/// allowing for easy inclusion of custom scripts in the rendered HTML.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `script` - The JavaScript code to include.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A `PreEscaped<String>` containing the rendered `<script>` element.
|
|
||||||
#[must_use]
|
|
||||||
pub fn script(script: &str) -> PreEscaped<String> {
|
|
||||||
html!(
|
|
||||||
script {
|
|
||||||
(PreEscaped(script))
|
|
||||||
};
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
use rocket::response::status::BadRequest;
|
use rocket::response::status::BadRequest;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::str::FromStr;
|
use std::{str::FromStr, sync::Arc};
|
||||||
|
|
||||||
/// API error response with a JSON payload.
|
/// API error response with a JSON payload.
|
||||||
pub type ApiError = BadRequest<serde_json::Value>;
|
pub type ApiError = BadRequest<serde_json::Value>;
|
||||||
|
@ -85,8 +85,9 @@ pub fn api_error(msg: &str) -> ApiError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `Pager` that manages paginated items, with the ability to handle incomplete data.
|
/// A `Pager` that manages paginated items, with the ability to handle incomplete data.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Pager<T> {
|
pub struct Pager<T> {
|
||||||
inner: Vec<T>,
|
inner: Arc<Vec<T>>,
|
||||||
pub items_per_page: u64,
|
pub items_per_page: u64,
|
||||||
complete_at: Option<u64>,
|
complete_at: Option<u64>,
|
||||||
}
|
}
|
||||||
|
@ -101,9 +102,9 @@ impl<T> Pager<T> {
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A new `Pager` instance.
|
/// A new `Pager` instance.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new(items: Vec<T>, per_page: u64) -> Self {
|
pub fn new(items: Vec<T>, per_page: u64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: items,
|
inner: Arc::new(items),
|
||||||
items_per_page: per_page,
|
items_per_page: per_page,
|
||||||
complete_at: None,
|
complete_at: None,
|
||||||
}
|
}
|
||||||
|
@ -119,9 +120,9 @@ impl<T> Pager<T> {
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A new `Pager` instance.
|
/// A new `Pager` instance.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new_incomplete(items: Vec<T>, per_page: u64, at_page: u64) -> Self {
|
pub fn new_incomplete(items: Vec<T>, per_page: u64, at_page: u64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: items,
|
inner: Arc::new(items),
|
||||||
items_per_page: per_page,
|
items_per_page: per_page,
|
||||||
complete_at: Some(at_page),
|
complete_at: Some(at_page),
|
||||||
}
|
}
|
||||||
|
@ -146,6 +147,25 @@ impl<T> Pager<T> {
|
||||||
self.items_per_page * (page - 1)
|
self.items_per_page * (page - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Estimated total size of the pager
|
||||||
|
///
|
||||||
|
/// If the pager is generated, this is only a rough estimation since not all elements are loaded.
|
||||||
|
#[must_use]
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
if let Some(start_page) = self.complete_at {
|
||||||
|
return (start_page * self.items_per_page) as usize + self.inner.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inner.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimated total page count of the pager
|
||||||
|
///
|
||||||
|
/// If the pager is generated, this is only a rough estimation since not all elements are loaded.
|
||||||
|
pub fn total_pages(&self) -> usize {
|
||||||
|
((self.size() as f64 / self.items_per_page as f64).ceil()) as usize
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieves the items for a specific page.
|
/// Retrieves the items for a specific page.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
@ -158,6 +178,11 @@ impl<T> Pager<T> {
|
||||||
/// A vector of items on the requested page.
|
/// A vector of items on the requested page.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn page(&self, page: u64) -> Vec<&T> {
|
pub fn page(&self, page: u64) -> Vec<&T> {
|
||||||
|
self.page_with_context(page).0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the elements at the current page and wether the next page has content
|
||||||
|
pub fn page_with_context(&self, page: u64) -> (Vec<&T>, bool) {
|
||||||
if let Some(incomplete) = self.complete_at {
|
if let Some(incomplete) = self.complete_at {
|
||||||
assert!(
|
assert!(
|
||||||
page >= incomplete,
|
page >= incomplete,
|
||||||
|
@ -165,18 +190,33 @@ impl<T> Pager<T> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.inner
|
let res = self
|
||||||
|
.inner
|
||||||
.iter()
|
.iter()
|
||||||
.skip(self.offset(page).try_into().unwrap())
|
.skip(self.offset(page).try_into().unwrap())
|
||||||
.take(self.items_per_page.try_into().unwrap())
|
.take(self.items_per_page as usize + 1);
|
||||||
.collect()
|
|
||||||
|
let has_next_page = if res.clone().count() > (self.items_per_page as usize) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
res.take(self.items_per_page as usize).collect::<Vec<_>>(),
|
||||||
|
has_next_page,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_next_page(&self, page: u64) -> bool {
|
||||||
|
self.page_with_context(page).1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `GeneratedPager` is a paginated generator that fetches items dynamically from a generator function.
|
/// A `GeneratedPager` is a paginated generator that fetches items dynamically from a generator function.
|
||||||
pub struct GeneratedPager<T, G, I>
|
pub struct GeneratedPager<T, G, I>
|
||||||
where
|
where
|
||||||
G: Fn(I, u64, u64) -> futures::future::BoxFuture<'static, Vec<T>>,
|
G: Fn(&I, u64, u64) -> futures::future::BoxFuture<'static, Vec<T>>,
|
||||||
{
|
{
|
||||||
generator: G,
|
generator: G,
|
||||||
pub items_per_page: u64,
|
pub items_per_page: u64,
|
||||||
|
@ -185,7 +225,7 @@ where
|
||||||
|
|
||||||
impl<T, G, I> GeneratedPager<T, G, I>
|
impl<T, G, I> GeneratedPager<T, G, I>
|
||||||
where
|
where
|
||||||
G: Fn(I, u64, u64) -> futures::future::BoxFuture<'static, Vec<T>>,
|
G: Fn(&I, u64, u64) -> futures::future::BoxFuture<'static, Vec<T>>,
|
||||||
{
|
{
|
||||||
/// Creates a new `GeneratedPager` instance with the provided generator and pagination settings.
|
/// Creates a new `GeneratedPager` instance with the provided generator and pagination settings.
|
||||||
///
|
///
|
||||||
|
@ -228,9 +268,9 @@ where
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A vector of items on the requested page.
|
/// A vector of items on the requested page.
|
||||||
pub async fn page(&self, page: u64, input: I) -> Vec<T> {
|
pub async fn page(&self, page: u64, input: &I) -> Vec<T> {
|
||||||
let offset = self.offset(page);
|
let offset = self.offset(page);
|
||||||
(self.generator)(input, offset, self.items_per_page).await
|
(self.generator)(input, offset, self.items_per_page + 1).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts the `GeneratedPager` into a regular `Pager` for a given page of items.
|
/// Converts the `GeneratedPager` into a regular `Pager` for a given page of items.
|
||||||
|
@ -244,7 +284,13 @@ where
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A `Pager` instance containing the requested page of items.
|
/// A `Pager` instance containing the requested page of items.
|
||||||
pub async fn pager(&self, page: u64, input: I) -> Pager<T> {
|
pub async fn pager(&self, page: u64, input: I) -> Pager<T> {
|
||||||
let content = self.page(page, input).await;
|
let mut content = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..=5 {
|
||||||
|
let fetch = self.page(page + i, &input).await;
|
||||||
|
content.extend(fetch);
|
||||||
|
}
|
||||||
|
|
||||||
Pager::new_incomplete(content, self.items_per_page, page)
|
Pager::new_incomplete(content, self.items_per_page, page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ use rocket::http::Status;
|
||||||
use rocket::response::Responder;
|
use rocket::response::Responder;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use crate::gen_random;
|
|
||||||
|
|
||||||
// TODO: Implement file based response
|
// TODO: Implement file based response
|
||||||
|
|
||||||
pub struct DataResponse {
|
pub struct DataResponse {
|
||||||
|
@ -31,62 +29,20 @@ impl<'r> Responder<'r, 'static> for DataResponse {
|
||||||
fn respond_to(self, req: &'r Request<'_>) -> rocket::response::Result<'static> {
|
fn respond_to(self, req: &'r Request<'_>) -> rocket::response::Result<'static> {
|
||||||
// Handle Range requests
|
// Handle Range requests
|
||||||
if let Some(range) = req.headers().get_one("Range") {
|
if let Some(range) = req.headers().get_one("Range") {
|
||||||
let ranges = range.split(",").collect::<Vec<_>>();
|
if let Some((start, end)) = parse_range_header(range, self.data.len()) {
|
||||||
if ranges.len() == 1 {
|
// TODO : Reject invalid ranges
|
||||||
if let Some((start, end)) = parse_range_header(range, self.data.len()) {
|
// TODO : Multiple ranges?
|
||||||
let sliced_data = &self.data[start..=end];
|
|
||||||
return Ok(Response::build()
|
|
||||||
.header(Header::new(
|
|
||||||
"Content-Range",
|
|
||||||
format!("bytes {}-{}/{}", start, end, self.data.len()),
|
|
||||||
))
|
|
||||||
.header(Header::new("Accept-Ranges", "bytes"))
|
|
||||||
.header(Header::new("Content-Type", self.content_type.clone()))
|
|
||||||
.status(Status::PartialContent)
|
|
||||||
.streamed_body(Cursor::new(sliced_data.to_vec()))
|
|
||||||
.finalize());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut multipart_body: Vec<u8> = Vec::new();
|
|
||||||
let boundary = gen_random(32);
|
|
||||||
|
|
||||||
for range in ranges {
|
|
||||||
if let Some((start, end)) = parse_range_header(range, self.data.len()) {
|
|
||||||
let sliced_data = &self.data[start..=end];
|
|
||||||
|
|
||||||
let mut body: Vec<u8> = Vec::new();
|
|
||||||
|
|
||||||
body.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
|
|
||||||
body.extend_from_slice(
|
|
||||||
format!(
|
|
||||||
"Content-Range: bytes {}-{}/{}\r\n",
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
self.data.len()
|
|
||||||
)
|
|
||||||
.as_bytes(),
|
|
||||||
);
|
|
||||||
body.extend_from_slice(
|
|
||||||
format!("Content-Type: {}\r\n\r\n", self.content_type.clone())
|
|
||||||
.as_bytes(),
|
|
||||||
);
|
|
||||||
body.extend_from_slice(sliced_data);
|
|
||||||
body.extend_from_slice("\r\n".as_bytes());
|
|
||||||
|
|
||||||
multipart_body.extend_from_slice(&body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
multipart_body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
|
|
||||||
|
|
||||||
|
let sliced_data = &self.data[start..=end];
|
||||||
return Ok(Response::build()
|
return Ok(Response::build()
|
||||||
.header(Header::new("Accept-Ranges", "bytes"))
|
|
||||||
.header(Header::new(
|
.header(Header::new(
|
||||||
"Content-Type",
|
"Content-Range",
|
||||||
format!("multipart/byteranges; boundary={boundary}"),
|
format!("bytes {}-{}/{}", start, end, self.data.len()),
|
||||||
))
|
))
|
||||||
|
.header(Header::new("Accept-Ranges", "bytes"))
|
||||||
|
.header(Header::new("Content-Type", self.content_type.clone()))
|
||||||
.status(Status::PartialContent)
|
.status(Status::PartialContent)
|
||||||
.streamed_body(Cursor::new(multipart_body.to_vec()))
|
.streamed_body(Cursor::new(sliced_data.to_vec()))
|
||||||
.finalize());
|
.finalize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,16 +69,10 @@ fn parse_range_header(range: &str, total_len: usize) -> Option<(usize, usize)> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let range = &range[6..];
|
let range = &range[6..];
|
||||||
|
|
||||||
if range.starts_with('-') {
|
|
||||||
let neg: usize = range.trim_start_matches('-').parse().ok()?;
|
|
||||||
return Some((total_len - neg, total_len));
|
|
||||||
}
|
|
||||||
|
|
||||||
let parts: Vec<&str> = range.split('-').collect();
|
let parts: Vec<&str> = range.split('-').collect();
|
||||||
|
|
||||||
if parts.len() != 2 {
|
if parts.len() != 2 {
|
||||||
return Some((parts[0].parse().ok()?, total_len));
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = parts[0].parse::<usize>().ok();
|
let start = parts[0].parse::<usize>().ok();
|
||||||
|
|
|
@ -32,24 +32,27 @@ impl<T, E: std::fmt::Debug> LogAndIgnore for Result<T, E> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait LogNoneAndPass {
|
pub trait LogNoneAndPass {
|
||||||
|
#[must_use]
|
||||||
fn log_warn_none_and_pass(self, msg: impl Fn() -> String) -> Self;
|
fn log_warn_none_and_pass(self, msg: impl Fn() -> String) -> Self;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
fn log_err_none_and_pass(self, msg: impl Fn() -> String) -> Self;
|
fn log_err_none_and_pass(self, msg: impl Fn() -> String) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> LogNoneAndPass for Option<T> {
|
impl<T> LogNoneAndPass for Option<T> {
|
||||||
fn log_warn_none_and_pass(self, msg: impl Fn() -> String) -> Option<T> {
|
fn log_warn_none_and_pass(self, msg: impl Fn() -> String) -> Option<T> {
|
||||||
if matches!(self, None) {
|
if self.is_none() {
|
||||||
log::warn!("{}", msg());
|
log::warn!("{}", msg());
|
||||||
}
|
}
|
||||||
|
|
||||||
return self;
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_err_none_and_pass(self, msg: impl Fn() -> String) -> Option<T> {
|
fn log_err_none_and_pass(self, msg: impl Fn() -> String) -> Option<T> {
|
||||||
if matches!(self, None) {
|
if self.is_none() {
|
||||||
log::error!("{}", msg());
|
log::error!("{}", msg());
|
||||||
}
|
}
|
||||||
|
|
||||||
return self;
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
283
src/ui/color.rs
Normal file
283
src/ui/color.rs
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
/// UI Color
|
||||||
|
pub trait UIColor {
|
||||||
|
#[must_use]
|
||||||
|
fn color_class(&self) -> &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ColorCircle {
|
||||||
|
#[must_use]
|
||||||
|
fn previous(&self) -> Self;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn middle(&self) -> Self;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn start(&self) -> Self;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn end(&self) -> Self;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn next(&self) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn RGB(r: u8, g: u8, b: u8) -> RGBColor {
|
||||||
|
RGBColor(format!("[#{r:02x}{g:02x}{b:02x}]"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RGBColor(String);
|
||||||
|
|
||||||
|
impl UIColor for RGBColor {
|
||||||
|
fn color_class(&self) -> &str {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait UIColorCircle: UIColor + ColorCircle + Sized {}
|
||||||
|
|
||||||
|
macro_rules! color_map {
|
||||||
|
($name:ident, $id:literal) => {
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum $name {
|
||||||
|
_50,
|
||||||
|
_100,
|
||||||
|
_200,
|
||||||
|
_300,
|
||||||
|
_400,
|
||||||
|
_500,
|
||||||
|
_600,
|
||||||
|
_700,
|
||||||
|
_800,
|
||||||
|
_900,
|
||||||
|
_950,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIColor for $name {
|
||||||
|
fn color_class(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
$name::_50 => concat!($id, "-50"),
|
||||||
|
$name::_100 => concat!($id, "-100"),
|
||||||
|
$name::_200 => concat!($id, "-200"),
|
||||||
|
$name::_300 => concat!($id, "-300"),
|
||||||
|
$name::_400 => concat!($id, "-400"),
|
||||||
|
$name::_500 => concat!($id, "-500"),
|
||||||
|
$name::_600 => concat!($id, "-600"),
|
||||||
|
$name::_700 => concat!($id, "-700"),
|
||||||
|
$name::_800 => concat!($id, "-800"),
|
||||||
|
$name::_900 => concat!($id, "-900"),
|
||||||
|
$name::_950 => concat!($id, "-950"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorCircle for $name {
|
||||||
|
fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
$name::_50 => $name::_100,
|
||||||
|
$name::_100 => $name::_200,
|
||||||
|
$name::_200 => $name::_300,
|
||||||
|
$name::_300 => $name::_400,
|
||||||
|
$name::_400 => $name::_500,
|
||||||
|
$name::_500 => $name::_600,
|
||||||
|
$name::_600 => $name::_700,
|
||||||
|
$name::_700 => $name::_800,
|
||||||
|
$name::_800 => $name::_900,
|
||||||
|
$name::_900 => $name::_950,
|
||||||
|
$name::_950 => $name::_50,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn middle(&self) -> Self {
|
||||||
|
Self::_500
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&self) -> Self {
|
||||||
|
Self::_50
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(&self) -> Self {
|
||||||
|
Self::_950
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
$name::_50 => $name::_950,
|
||||||
|
$name::_100 => $name::_50,
|
||||||
|
$name::_200 => $name::_100,
|
||||||
|
$name::_300 => $name::_200,
|
||||||
|
$name::_400 => $name::_300,
|
||||||
|
$name::_500 => $name::_400,
|
||||||
|
$name::_600 => $name::_500,
|
||||||
|
$name::_700 => $name::_600,
|
||||||
|
$name::_800 => $name::_700,
|
||||||
|
$name::_900 => $name::_800,
|
||||||
|
$name::_950 => $name::_900,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
color_map!(Slate, "slate");
|
||||||
|
color_map!(Gray, "gray");
|
||||||
|
color_map!(Zinc, "zinc");
|
||||||
|
color_map!(Neutral, "neutral");
|
||||||
|
color_map!(Stone, "stone");
|
||||||
|
color_map!(Red, "red");
|
||||||
|
color_map!(Orange, "orange");
|
||||||
|
color_map!(Amber, "amber");
|
||||||
|
color_map!(Yellow, "yellow");
|
||||||
|
color_map!(Lime, "lime");
|
||||||
|
color_map!(Green, "green");
|
||||||
|
color_map!(Emerald, "emerald");
|
||||||
|
color_map!(Teal, "teal");
|
||||||
|
color_map!(Cyan, "cyan");
|
||||||
|
color_map!(Sky, "sky");
|
||||||
|
color_map!(Blue, "blue");
|
||||||
|
color_map!(Indigo, "indigo");
|
||||||
|
color_map!(Violet, "violet");
|
||||||
|
color_map!(Purple, "purple");
|
||||||
|
color_map!(Fuchsia, "fuchsia");
|
||||||
|
color_map!(Pink, "pink");
|
||||||
|
color_map!(Rose, "rose");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Colors {
|
||||||
|
/// Inherit a color
|
||||||
|
Inherit,
|
||||||
|
/// Use current color
|
||||||
|
Current,
|
||||||
|
/// Transparency
|
||||||
|
Transparent,
|
||||||
|
/// Black
|
||||||
|
Black,
|
||||||
|
/// White
|
||||||
|
White,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorCircle for Colors {
|
||||||
|
fn previous(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Colors::Black => Colors::White,
|
||||||
|
Colors::White => Colors::Black,
|
||||||
|
_ => self.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn middle(&self) -> Self {
|
||||||
|
self.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Colors::Black => Colors::White,
|
||||||
|
Colors::White => Colors::White,
|
||||||
|
_ => self.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Colors::Black => Colors::Black,
|
||||||
|
Colors::White => Colors::Black,
|
||||||
|
_ => self.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Colors::Black => Colors::White,
|
||||||
|
Colors::White => Colors::Black,
|
||||||
|
_ => self.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIColor for Colors {
|
||||||
|
fn color_class(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Inherit => "inherit",
|
||||||
|
Self::Current => "current",
|
||||||
|
Self::Transparent => "transparent",
|
||||||
|
Self::Black => "black",
|
||||||
|
Self::White => "white",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Gradient {
|
||||||
|
start: Box<dyn UIColor>,
|
||||||
|
middle: Option<Box<dyn UIColor>>,
|
||||||
|
end: Option<Box<dyn UIColor>>,
|
||||||
|
pos_start: Option<u8>,
|
||||||
|
pos_middle: Option<u8>,
|
||||||
|
pos_end: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gradient {
|
||||||
|
pub fn from<C: UIColor + 'static>(start: C) -> Self {
|
||||||
|
Self {
|
||||||
|
start: Box::new(start),
|
||||||
|
middle: None,
|
||||||
|
end: None,
|
||||||
|
pos_end: None,
|
||||||
|
pos_middle: None,
|
||||||
|
pos_start: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn via<C: UIColor + 'static>(mut self, middle: C) -> Self {
|
||||||
|
self.middle = Some(Box::new(middle));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to<C: UIColor + 'static>(mut self, end: C) -> Self {
|
||||||
|
self.end = Some(Box::new(end));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step_start(mut self, percentage: u8) -> Self {
|
||||||
|
assert!(percentage <= 100, "Percentage should be under 100%");
|
||||||
|
self.pos_start = Some(percentage);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step_middle(mut self, percentage: u8) -> Self {
|
||||||
|
assert!(percentage <= 100, "Percentage should be under 100%");
|
||||||
|
self.pos_middle = Some(percentage);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step_end(mut self, percentage: u8) -> Self {
|
||||||
|
assert!(percentage <= 100, "Percentage should be under 100%");
|
||||||
|
self.pos_end = Some(percentage);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color_class(&self) -> Vec<String> {
|
||||||
|
let mut classes = vec![format!("from-{}", self.start.color_class())];
|
||||||
|
|
||||||
|
if let Some(via) = &self.middle {
|
||||||
|
classes.push(format!("via-{}", via.color_class()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(end) = &self.end {
|
||||||
|
classes.push(format!("to-{}", end.color_class()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(step) = &self.pos_start {
|
||||||
|
classes.push(format!("from-{step}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(step) = &self.pos_middle {
|
||||||
|
classes.push(format!("via-{step}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(step) = &self.pos_end {
|
||||||
|
classes.push(format!("to-{step}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
classes
|
||||||
|
}
|
||||||
|
}
|
78
src/ui/components/appbar.rs
Normal file
78
src/ui/components/appbar.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
use maud::{Markup, Render};
|
||||||
|
|
||||||
|
use crate::auth::User;
|
||||||
|
|
||||||
|
use crate::ui::{UIWidget, prelude::*};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn AppBar(name: &str, user: Option<User>) -> AppBarWidget {
|
||||||
|
AppBarWidget {
|
||||||
|
name: name.to_owned(),
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppBarWidget {
|
||||||
|
name: String,
|
||||||
|
user: Option<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AppBarWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for AppBarWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
self.base_class()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
Padding(Shadow::medium(
|
||||||
|
Background(Header(
|
||||||
|
Padding(
|
||||||
|
Flex(
|
||||||
|
Div()
|
||||||
|
.vanish()
|
||||||
|
.push(
|
||||||
|
SpaceBetween(
|
||||||
|
Flex(Link(
|
||||||
|
"/",
|
||||||
|
Div()
|
||||||
|
.vanish()
|
||||||
|
.push(Sized(
|
||||||
|
ScreenValue::_10,
|
||||||
|
ScreenValue::_10,
|
||||||
|
Rounded(Image("/favicon").alt("Logo"))
|
||||||
|
.size(Size::Medium),
|
||||||
|
))
|
||||||
|
.push(Span(&self.name).semibold().xl().white()),
|
||||||
|
))
|
||||||
|
.items_center(),
|
||||||
|
)
|
||||||
|
.x(ScreenValue::_2),
|
||||||
|
)
|
||||||
|
.push_some(self.user.as_ref(), |user| Text(&user.username).white()),
|
||||||
|
)
|
||||||
|
.group()
|
||||||
|
.justify(Justify::Between)
|
||||||
|
.items_center(),
|
||||||
|
)
|
||||||
|
.x(ScreenValue::_6),
|
||||||
|
))
|
||||||
|
.color(Gray::_800),
|
||||||
|
))
|
||||||
|
.y(ScreenValue::_2)
|
||||||
|
.render()
|
||||||
|
}
|
||||||
|
}
|
171
src/ui/components/avatar.rs
Normal file
171
src/ui/components/avatar.rs
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
use maud::{Render, html};
|
||||||
|
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Avatar<T: Into<String>>(image: T, name: T) -> AvatarWidget {
|
||||||
|
AvatarWidget {
|
||||||
|
users: vec![(image.into(), name.into())],
|
||||||
|
ring: false,
|
||||||
|
use_initials: false,
|
||||||
|
indicator: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn AvatarStack(users: Vec<(String, String)>) -> AvatarWidget {
|
||||||
|
AvatarWidget {
|
||||||
|
users,
|
||||||
|
ring: false,
|
||||||
|
use_initials: false,
|
||||||
|
indicator: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AvatarWidget {
|
||||||
|
users: Vec<(String, String)>,
|
||||||
|
ring: bool,
|
||||||
|
use_initials: bool,
|
||||||
|
indicator: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AvatarWidget {
|
||||||
|
pub fn with_ring(mut self) -> Self {
|
||||||
|
self.ring = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn online(mut self, value: bool) -> Self {
|
||||||
|
self.indicator = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_initials(mut self) -> Self {
|
||||||
|
self.use_initials = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_avatar(&self, image: &str, name: &str) -> maud::Markup {
|
||||||
|
let mut img_class = "w-10 h-10 rounded-full".to_string();
|
||||||
|
|
||||||
|
if self.ring {
|
||||||
|
img_class.push_str(" ring-2 ring-gray-300 dark:ring-gray-500");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.indicator.is_some() {
|
||||||
|
let online = self.indicator.unwrap();
|
||||||
|
|
||||||
|
let indicator_class = match online {
|
||||||
|
true => {
|
||||||
|
"bottom-0 left-7 absolute w-3.5 h-3.5 bg-green-400 border-2 border-white dark:border-gray-800 rounded-full"
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
"bottom-0 left-7 absolute w-3.5 h-3.5 bg-red-400 border-2 border-white dark:border-gray-800 rounded-full"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return html! {
|
||||||
|
@if image.is_empty() {
|
||||||
|
@if self.use_initials {
|
||||||
|
div class="relative" title=(name) {
|
||||||
|
div class=(format!("relative inline-flex items-center justify-center w-10 h-10 overflow-hidden bg-gray-100 rounded-full dark:bg-gray-600 {img_class}")) {
|
||||||
|
span class="font-medium text-gray-600 dark:text-gray-300" { (initials(name)) };
|
||||||
|
};
|
||||||
|
span class=(indicator_class) {};
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
div class="relative" title=(name) {
|
||||||
|
div class=(format!("relative w-10 h-10 overflow-hidden bg-gray-100 rounded-full dark:bg-gray-600 {img_class}")){
|
||||||
|
svg class="absolute w-12 h-12 text-gray-400 -left-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {
|
||||||
|
path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
span class=(indicator_class) {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} @else {
|
||||||
|
div class="relative" title=(name) {
|
||||||
|
img class=(img_class) src=(image) alt=(name) {};
|
||||||
|
span class=(indicator_class) {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return html! {
|
||||||
|
@if image.is_empty() {
|
||||||
|
@if self.use_initials {
|
||||||
|
div class="relative" title=(name) {
|
||||||
|
div class=(format!("relative inline-flex items-center justify-center w-10 h-10 overflow-hidden bg-gray-100 rounded-full dark:bg-gray-600 {img_class}")) {
|
||||||
|
span class="font-medium text-gray-600 dark:text-gray-300" { (initials(name)) };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
div class="relative" title=(name) {
|
||||||
|
div class=(format!("relative w-10 h-10 overflow-hidden bg-gray-100 rounded-full dark:bg-gray-600 {img_class}")){
|
||||||
|
svg class="absolute w-12 h-12 text-gray-400 -left-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {
|
||||||
|
path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} @else {
|
||||||
|
div class="relative" title=(name) {
|
||||||
|
img class=(img_class) src=(image) alt=(name) {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initials(txt: &str) -> String {
|
||||||
|
let elements = txt.split_whitespace();
|
||||||
|
let mut initials = Vec::new();
|
||||||
|
|
||||||
|
for e in elements {
|
||||||
|
initials.push(e.chars().next().unwrap().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
initials.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AvatarWidget {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for AvatarWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> maud::Markup {
|
||||||
|
if self.users.len() == 1 {
|
||||||
|
let (image, name) = &self.users[0];
|
||||||
|
self.build_avatar(image, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="flex -space-x-4 rtl:space-x-reverse" {
|
||||||
|
@for (image, name) in self.users.iter().take(4) {
|
||||||
|
(self.build_avatar(&image, &name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@if self.users.len() > 4 {
|
||||||
|
p class="flex items-center justify-center w-11 h-11 text-xs font-medium text-white bg-gray-700 border-2 border-white rounded-full hover:bg-gray-600 dark:border-gray-800 z-10 -translate-y-[2px]" {
|
||||||
|
(format!("+{}", self.users.len()-4))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
205
src/ui/components/htmx.rs
Normal file
205
src/ui/components/htmx.rs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
use maud::{PreEscaped, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::{
|
||||||
|
UIWidget,
|
||||||
|
htmx::{Event, HTMXAttributes, SwapStrategy},
|
||||||
|
prelude::Div,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn ClickToLoad<T: UIWidget + 'static>(reference: &str, widget: T) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
button hx-get=(reference)
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="outerHTML" {
|
||||||
|
(widget)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn LazyLoad<T: UIWidget + 'static>(reference: &str, widget: T) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div hx-get=(reference) hx-trigger="load" {
|
||||||
|
div class="htmx-indicator" {
|
||||||
|
(widget)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn InfinityScroll<T: UIWidget + 'static>(content: T, fetch_url: &str) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
(content)
|
||||||
|
(Div()
|
||||||
|
.hx_get(fetch_url)
|
||||||
|
.hx_swap(SwapStrategy::outerHTML)
|
||||||
|
.hx_trigger(Event::on_revealed()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : htmx elements
|
||||||
|
/*
|
||||||
|
Progress Bar
|
||||||
|
|
||||||
|
This example shows how to implement a smoothly scrolling progress bar.
|
||||||
|
|
||||||
|
We start with an initial state with a button that issues a POST to /start to begin the job:
|
||||||
|
|
||||||
|
<div hx-target="this" hx-swap="outerHTML">
|
||||||
|
<h3>Start Progress</h3>
|
||||||
|
<button class="btn primary" hx-post="/start">
|
||||||
|
Start Job
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
This div is then replaced with a new div containing status and a progress bar that reloads itself every 600ms:
|
||||||
|
|
||||||
|
<div hx-trigger="done" hx-get="/job" hx-swap="outerHTML" hx-target="this">
|
||||||
|
<h3 role="status" id="pblabel" tabindex="-1" autofocus>Running</h3>
|
||||||
|
|
||||||
|
<div
|
||||||
|
hx-get="/job/progress"
|
||||||
|
hx-trigger="every 600ms"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-labelledby="pblabel">
|
||||||
|
<div id="pb" class="progress-bar" style="width:0%">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
This progress bar is updated every 600 milliseconds, with the “width” style attribute and aria-valuenow attributed set to current progress value. Because there is an id on the progress bar div, htmx will smoothly transition between requests by settling the style attribute into its new value. This, when coupled with CSS transitions, makes the visual transition continuous rather than jumpy.
|
||||||
|
|
||||||
|
Finally, when the process is complete, a server returns HX-Trigger: done header, which triggers an update of the UI to “Complete” state with a restart button added to the UI (we are using the class-tools extension in this example to add fade-in effect on the button):
|
||||||
|
|
||||||
|
<div hx-trigger="done" hx-get="/job" hx-swap="outerHTML" hx-target="this">
|
||||||
|
<h3 role="status" id="pblabel" tabindex="-1" autofocus>Complete</h3>
|
||||||
|
|
||||||
|
<div
|
||||||
|
hx-get="/job/progress"
|
||||||
|
hx-trigger="none"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="122" aria-labelledby="pblabel">
|
||||||
|
<div id="pb" class="progress-bar" style="width:122%">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="restart-btn" class="btn primary" hx-post="/start" classes="add show:600ms">
|
||||||
|
Restart Job
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
This example uses styling cribbed from the bootstrap progress bar:
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
height: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
float: left;
|
||||||
|
width: 0%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #337ab7;
|
||||||
|
-webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
|
||||||
|
-webkit-transition: width .6s ease;
|
||||||
|
-o-transition: width .6s ease;
|
||||||
|
transition: width .6s ease;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Cascading Selects
|
||||||
|
|
||||||
|
In this example we show how to make the values in one select depend on the value selected in another select.
|
||||||
|
|
||||||
|
To begin we start with a default value for the make select: Audi. We render the model select for this make. We then have the make select trigger a GET to /models to retrieve the models options and target the models select.
|
||||||
|
|
||||||
|
Here is the code:
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label >Make</label>
|
||||||
|
<select name="make" hx-get="/models" hx-target="#models" hx-indicator=".htmx-indicator">
|
||||||
|
<option value="audi">Audi</option>
|
||||||
|
<option value="toyota">Toyota</option>
|
||||||
|
<option value="bmw">BMW</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Model</label>
|
||||||
|
<select id="models" name="model">
|
||||||
|
<option value="a1">A1</option>
|
||||||
|
...
|
||||||
|
</select>
|
||||||
|
<img class="htmx-indicator" width="20" src="/img/bars.svg">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
When a request is made to the /models end point, we return the models for that make:
|
||||||
|
|
||||||
|
<option value='325i'>325i</option>
|
||||||
|
<option value='325ix'>325ix</option>
|
||||||
|
<option value='X5'>X5</option>
|
||||||
|
|
||||||
|
And they become available in the model select.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Sortable
|
||||||
|
|
||||||
|
In this example we show how to integrate the Sortable javascript library with htmx.
|
||||||
|
|
||||||
|
To begin we initialize the .sortable class with the Sortable javascript library:
|
||||||
|
|
||||||
|
htmx.onLoad(function(content) {
|
||||||
|
var sortables = content.querySelectorAll(".sortable");
|
||||||
|
for (var i = 0; i < sortables.length; i++) {
|
||||||
|
var sortable = sortables[i];
|
||||||
|
var sortableInstance = new Sortable(sortable, {
|
||||||
|
animation: 150,
|
||||||
|
ghostClass: 'blue-background-class',
|
||||||
|
|
||||||
|
// Make the `.htmx-indicator` unsortable
|
||||||
|
filter: ".htmx-indicator",
|
||||||
|
onMove: function (evt) {
|
||||||
|
return evt.related.className.indexOf('htmx-indicator') === -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Disable sorting on the `end` event
|
||||||
|
onEnd: function (evt) {
|
||||||
|
this.option("disabled", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-enable sorting on the `htmx:afterSwap` event
|
||||||
|
sortable.addEventListener("htmx:afterSwap", function() {
|
||||||
|
sortableInstance.option("disabled", false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Next, we create a form that has some sortable divs within it, and we trigger an ajax request on the end event, fired by Sortable.js:
|
||||||
|
|
||||||
|
<form class="sortable" hx-post="/items" hx-trigger="end">
|
||||||
|
<div class="htmx-indicator">Updating...</div>
|
||||||
|
<div><input type='hidden' name='item' value='1'/>Item 1</div>
|
||||||
|
<div><input type='hidden' name='item' value='2'/>Item 2</div>
|
||||||
|
<div><input type='hidden' name='item' value='3'/>Item 3</div>
|
||||||
|
<div><input type='hidden' name='item' value='4'/>Item 4</div>
|
||||||
|
<div><input type='hidden' name='item' value='5'/>Item 5</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
Note that each div has a hidden input inside of it that specifies the item id for that row.
|
||||||
|
|
||||||
|
When the list is reordered via the Sortable.js drag-and-drop, the end event will be fired. htmx will then post the item ids in the new order to /items, to be persisted by the server.
|
||||||
|
|
||||||
|
That’s it!
|
||||||
|
*/
|
63
src/ui/components/indicator.rs
Normal file
63
src/ui/components/indicator.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use maud::{PreEscaped, html};
|
||||||
|
|
||||||
|
use crate::ui::{UIWidget, color::UIColor};
|
||||||
|
|
||||||
|
use super::ColorCircle;
|
||||||
|
|
||||||
|
pub fn Indicator<C: UIColor + 'static>(color: C) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
span class=(format!("flex w-3 h-3 me-3 bg-{} rounded-full", color.color_class())) {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn IndicatorLegend<C: UIColor + 'static>(color: C, legend: &str) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
span class="flex items-center text-sm font-medium text-gray-900 dark:text-white me-3" {
|
||||||
|
span class=(format!("flex w-2.5 h-2.5 bg-{} rounded-full me-1.5 shrink-0", color.color_class())) {};
|
||||||
|
(legend)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn NumberIndicator<T: UIWidget + 'static>(on: T, amount: u32) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div class="relative items-center max-w-fit" {
|
||||||
|
(on)
|
||||||
|
div class="absolute inline-flex items-center justify-center px-2 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 -end-2 dark:border-gray-900" { (amount) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn BadgeIndicator<C: UIColor + 'static + ColorCircle>(
|
||||||
|
color: C,
|
||||||
|
dark_color: C,
|
||||||
|
txt: &str,
|
||||||
|
) -> PreEscaped<String> {
|
||||||
|
// BG -100
|
||||||
|
let bg_color = color.color_class();
|
||||||
|
// Text -800
|
||||||
|
let text_color = color.next().next().next().next().next().next().next();
|
||||||
|
let text_color = text_color.color_class();
|
||||||
|
// Dark BG -900
|
||||||
|
let dark_bg = dark_color.color_class();
|
||||||
|
// Dark Text -300
|
||||||
|
let dark_text = dark_color
|
||||||
|
.previous()
|
||||||
|
.previous()
|
||||||
|
.previous()
|
||||||
|
.previous()
|
||||||
|
.previous()
|
||||||
|
.previous();
|
||||||
|
let dark_text = dark_text.color_class();
|
||||||
|
// Indicator -500
|
||||||
|
let indicator_color = color.middle();
|
||||||
|
let indicator_color = indicator_color.color_class();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
span
|
||||||
|
class=(format!("inline-flex items-center bg-{bg_color} text-{text_color} text-xs font-medium px-2.5 py-0.5 rounded-full dark:bg-{dark_bg} dark:text-{dark_text}")) {
|
||||||
|
span class=(format!("w-2 h-2 me-1 bg-{indicator_color} rounded-full")) {};
|
||||||
|
(txt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
705
src/ui/components/mod.rs
Normal file
705
src/ui/components/mod.rs
Normal file
|
@ -0,0 +1,705 @@
|
||||||
|
mod appbar;
|
||||||
|
mod avatar;
|
||||||
|
mod htmx;
|
||||||
|
mod indicator;
|
||||||
|
mod modal;
|
||||||
|
mod overlay;
|
||||||
|
mod pagination;
|
||||||
|
mod placeholder;
|
||||||
|
mod search;
|
||||||
|
mod shell;
|
||||||
|
mod timeline;
|
||||||
|
|
||||||
|
// TODO : ENSURE: prelude
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::avatar::{Avatar, AvatarStack};
|
||||||
|
pub use super::htmx::*;
|
||||||
|
pub use super::indicator::*;
|
||||||
|
pub use super::modal::*;
|
||||||
|
pub use super::overlay::*;
|
||||||
|
pub use super::pagination::*;
|
||||||
|
pub use super::placeholder::{
|
||||||
|
CardPlaceholder, ImagePlaceholder, ListPlaceholder, Placeholder, TextPlaceholder,
|
||||||
|
VideoPlaceholder,
|
||||||
|
};
|
||||||
|
pub use super::timeline::*;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use super::color::ColorCircle;
|
||||||
|
pub use appbar::AppBar;
|
||||||
|
use maud::{PreEscaped, Render, html};
|
||||||
|
pub use search::Search;
|
||||||
|
pub use shell::*;
|
||||||
|
|
||||||
|
use crate::ui::prelude::script;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
UIWidget,
|
||||||
|
color::{Gray, UIColor},
|
||||||
|
htmx::{Event, HTMXAttributes, SwapStrategy},
|
||||||
|
prelude::Div,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ENSURE: doc pages? test
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn HorizontalLine() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
hr class="h-px my-8 bg-gray-200 border-0 dark:bg-gray-700" {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn FnKey(key: &str) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
kbd class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500" { (key) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ColoredSpinner<T: UIColor + 'static>(color: T) -> PreEscaped<String> {
|
||||||
|
let col = color.color_class();
|
||||||
|
html! {
|
||||||
|
div role="status" {
|
||||||
|
svg aria-hidden="true" class=(format!("w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-{col}")) viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg" {
|
||||||
|
path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" {};
|
||||||
|
path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" {};
|
||||||
|
};
|
||||||
|
span class="sr-only" { "Loading..." }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Spinner() -> PreEscaped<String> {
|
||||||
|
ColoredSpinner(super::color::Blue::_600)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn CopyText(txt: &str) -> PreEscaped<String> {
|
||||||
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let gscript = format!(
|
||||||
|
"
|
||||||
|
window.addEventListener('load', function () {{
|
||||||
|
const clipboard = FlowbiteInstances.getInstance('CopyClipboard', '{id}-copy');
|
||||||
|
const tooltip = FlowbiteInstances.getInstance('Tooltip', '{id}-copy-tooltip');
|
||||||
|
|
||||||
|
const $defaultIcon = document.getElementById('default-icon');
|
||||||
|
const $successIcon = document.getElementById('success-icon');
|
||||||
|
|
||||||
|
const $defaultTooltipMessage = document.getElementById('default-tooltip-message');
|
||||||
|
const $successTooltipMessage = document.getElementById('success-tooltip-message');
|
||||||
|
|
||||||
|
clipboard.updateOnCopyCallback((clipboard) => {{
|
||||||
|
showSuccess();
|
||||||
|
|
||||||
|
// reset to default state
|
||||||
|
setTimeout(() => {{
|
||||||
|
resetToDefault();
|
||||||
|
}}, 2000);
|
||||||
|
}})
|
||||||
|
|
||||||
|
const showSuccess = () => {{
|
||||||
|
$defaultIcon.classList.add('hidden');
|
||||||
|
$successIcon.classList.remove('hidden');
|
||||||
|
$defaultTooltipMessage.classList.add('hidden');
|
||||||
|
$successTooltipMessage.classList.remove('hidden');
|
||||||
|
tooltip.show();
|
||||||
|
}}
|
||||||
|
|
||||||
|
const resetToDefault = () => {{
|
||||||
|
$defaultIcon.classList.remove('hidden');
|
||||||
|
$successIcon.classList.add('hidden');
|
||||||
|
$defaultTooltipMessage.classList.remove('hidden');
|
||||||
|
$successTooltipMessage.classList.add('hidden');
|
||||||
|
tooltip.hide();
|
||||||
|
}}
|
||||||
|
}})
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
|
||||||
|
(script(&gscript))
|
||||||
|
|
||||||
|
div class="w-full max-w-[16rem]" {
|
||||||
|
div class="relative" {
|
||||||
|
input id=(format!("{id}-copy")) type="text" class="col-span-6 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500" value=(txt) disabled readonly {};
|
||||||
|
button data-copy-to-clipboard-target=(format!("{id}-copy")) data-tooltip-target=(format!("{id}-copy-tooltip")) class="absolute end-2 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg p-2 inline-flex items-center justify-center" {
|
||||||
|
span id="default-icon" {
|
||||||
|
svg class="w-3.5 h-3.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20" {
|
||||||
|
path d="M16 1h-3.278A1.992 1.992 0 0 0 11 0H7a1.993 1.993 0 0 0-1.722 1H2a2 2 0 0 0-2 2v15a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2Zm-3 14H5a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2Zm0-4H5a1 1 0 0 1 0-2h8a1 1 0 1 1 0 2Zm0-5H5a1 1 0 0 1 0-2h2V2h4v2h2a1 1 0 1 1 0 2Z" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
span id="success-icon" class="hidden inline-flex items-center" {
|
||||||
|
svg class="w-3.5 h-3.5 text-blue-700 dark:text-blue-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 12" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5.917 5.724 10.5 15 1.5" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
div id=(format!("{id}-copy-tooltip")) role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700" {
|
||||||
|
span id="default-tooltip-message" { "Copy to clipboard" };
|
||||||
|
span id="success-tooltip-message" class="hidden" { "Copied!" };
|
||||||
|
div class="tooltip-arrow" data-popper-arrow {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOCOMMIT ---
|
||||||
|
|
||||||
|
// TODO : test + fix accordion
|
||||||
|
pub fn Accordion<H: UIWidget + 'static, B: UIWidget + 'static>(
|
||||||
|
title: H,
|
||||||
|
body: B,
|
||||||
|
) -> PreEscaped<String> {
|
||||||
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
html! {
|
||||||
|
div id=(format!("accordion-collapse-{id}")) data-accordion="collapse" {
|
||||||
|
h2 id=(format!("accordion-collapse-heading-{id}")) {
|
||||||
|
button type="button"
|
||||||
|
class="flex items-center justify-between w-full p-5 font-medium rtl:text-right rounded-t-xl gap-3" data-accordion-target=(format!("#accordion-collapse-body-{id}")) aria-expanded="true" aria-controls=(format!("accordion-collapse-body-{id}")) {
|
||||||
|
(title)
|
||||||
|
svg data-accordion-icon class="w-3 h-3 rotate-180 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5 5 1 1 5" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
div id=(format!("accordion-collapse-body-{id}")) class="hidden" aria-labelledby=(format!("accordion-collapse-heading-{id}")) {
|
||||||
|
div class="p-5 border border-b-0 border-gray-200 dark:border-gray-700 dark:bg-gray-900" {
|
||||||
|
(body)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn InfoIcon() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
svg class="shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20" {
|
||||||
|
path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn X_Icon() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn ColoredAlert<T: UIWidget + 'static, C: UIColor + ColorCircle + 'static>(
|
||||||
|
color: C,
|
||||||
|
inner: T,
|
||||||
|
) -> PreEscaped<String> {
|
||||||
|
let dark_color = color.previous().previous().previous().previous();
|
||||||
|
let dark_color = dark_color.color_class();
|
||||||
|
let bg_light = color.start();
|
||||||
|
let bg_light = bg_light.color_class();
|
||||||
|
let btn_bg_light = color
|
||||||
|
.previous()
|
||||||
|
.previous()
|
||||||
|
.previous()
|
||||||
|
.previous()
|
||||||
|
.previous()
|
||||||
|
.previous();
|
||||||
|
let btn_bg_light = btn_bg_light.color_class();
|
||||||
|
let ring_light = color.previous().previous().previous().previous();
|
||||||
|
let ring_light = ring_light.color_class();
|
||||||
|
let btn_light = color.previous().previous().previous();
|
||||||
|
let btn_light = btn_light.color_class();
|
||||||
|
let color = color.color_class();
|
||||||
|
|
||||||
|
let id = format!("alert-{}", uuid::Uuid::new_v4().to_string());
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div id=(id) class=(format!("flex items-center p-4 mb-4 text-{color} rounded-lg bg-{bg_light} dark:bg-gray-800 dark:text-{dark_color}")) role="alert" {
|
||||||
|
(inner)
|
||||||
|
|
||||||
|
button type="button"
|
||||||
|
class=(format!("ms-auto -mx-1.5 -my-1.5 bg-{bg_light} text-{btn_light} rounded-lg focus:ring-2 focus:ring-{ring_light} p-1.5 hover:bg-{btn_bg_light} inline-flex items-center justify-center h-8 w-8 dark:bg-gray-800 dark:text-{dark_color} dark:hover:bg-gray-700"))
|
||||||
|
data-dismiss-target=(format!("#{id}")) aria-label="Close" {
|
||||||
|
span class="sr-only" { "Close" };
|
||||||
|
(X_Icon())
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Alert<T: UIWidget + 'static>(inner: T) -> PreEscaped<String> {
|
||||||
|
ColoredAlert(Gray::_800, inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn FetchAlert(reference: &str) -> PreEscaped<String> {
|
||||||
|
Div()
|
||||||
|
.hx_get(reference)
|
||||||
|
.hx_target(super::htmx::Selector::Query(
|
||||||
|
"#notification_area".to_string(),
|
||||||
|
))
|
||||||
|
.hx_swap(SwapStrategy::beforeend)
|
||||||
|
.hx_trigger(Event::on_load())
|
||||||
|
.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BreadcrumbWidget {
|
||||||
|
elements: Vec<(String, String)>,
|
||||||
|
seperator: Option<Box<dyn UIWidget>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BreadcrumbWidget {
|
||||||
|
pub fn seperator<T: UIWidget + 'static>(mut self, seperator: T) -> Self {
|
||||||
|
self.seperator = Some(Box::new(seperator));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arrow_seperator() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
svg class="rtl:rotate-180 w-3 h-3 text-gray-400 mx-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4" {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for BreadcrumbWidget {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for BreadcrumbWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> maud::Markup {
|
||||||
|
html! {
|
||||||
|
nav class="flex" aria-label="Breadcrumb" {
|
||||||
|
|
||||||
|
ol class="inline-flex items-center space-x-1 md:space-x-2 rtl:space-x-reverse p-2" {
|
||||||
|
|
||||||
|
@for (index, (name, url)) in self.elements.iter().enumerate() {
|
||||||
|
@if index == 0 {
|
||||||
|
li class="inline-flex items-center" {
|
||||||
|
a href=(url) class="inline-flex items-center text-sm font-medium hover:text-blue-600" { (name) };
|
||||||
|
};
|
||||||
|
} @else if index == (self.elements.len()-1) {
|
||||||
|
|
||||||
|
li aria-current="page" {
|
||||||
|
div class="flex items-center" {
|
||||||
|
@if let Some(s) = self.seperator.as_ref() {
|
||||||
|
(s)
|
||||||
|
} @else {
|
||||||
|
(Self::arrow_seperator())
|
||||||
|
}
|
||||||
|
span class="ms-1 text-sm font-medium text-gray-500 md:ms-2 dark:text-gray-400" { (name) };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} @else {
|
||||||
|
li {
|
||||||
|
div class="flex items-center" {
|
||||||
|
@if let Some(s) = self.seperator.as_ref() {
|
||||||
|
(s)
|
||||||
|
} @else {
|
||||||
|
(Self::arrow_seperator())
|
||||||
|
}
|
||||||
|
|
||||||
|
a href=(url) class="ms-1 text-sm font-medium hover:text-blue-600 md:ms-2" { (name) };
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Breadcrumb(paths: Vec<(String, String)>) -> BreadcrumbWidget {
|
||||||
|
BreadcrumbWidget {
|
||||||
|
elements: paths,
|
||||||
|
seperator: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Card<T: UIWidget + 'static>(inner: T) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div class="w-fit p-4 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700" {
|
||||||
|
(inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Banner<T: UIWidget + 'static>(inner: T) -> PreEscaped<String> {
|
||||||
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
html! {
|
||||||
|
div id=(format!("banner-{id}")) tabindex="-1" class="fixed top-0 start-0 z-40 flex justify-between w-full p-4 border-b border-gray-200 bg-gray-50 dark:bg-gray-700 dark:border-gray-600" {
|
||||||
|
div class="flex items-center mx-auto" {
|
||||||
|
p class="flex items-center text-sm font-normal text-gray-500 dark:text-gray-400" {
|
||||||
|
(inner)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
div class="flex items-center" {
|
||||||
|
button data-dismiss-target=(format!("#banner-{id}")) type="button" class="flex-shrink-0 inline-flex justify-center w-7 h-7 items-center text-gray-400 hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 dark:hover:bg-gray-600 dark:hover:text-white" {
|
||||||
|
(X_Icon())
|
||||||
|
span class="sr-only" { "Close banner" };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [NOCOMMIT]
|
||||||
|
// COMPONENTS
|
||||||
|
|
||||||
|
pub enum CarouselMode {
|
||||||
|
Slide,
|
||||||
|
Static,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Carousel<T: UIWidget + 'static>(elements: Vec<T>) -> CarouselWidget {
|
||||||
|
let mut boxed_elements: Vec<Box<dyn UIWidget>> = Vec::with_capacity(elements.len());
|
||||||
|
|
||||||
|
for e in elements {
|
||||||
|
boxed_elements.push(Box::new(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
CarouselWidget {
|
||||||
|
elements: boxed_elements,
|
||||||
|
mode: CarouselMode::Slide,
|
||||||
|
controls: false,
|
||||||
|
indicators: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CarouselWidget {
|
||||||
|
elements: Vec<Box<dyn UIWidget>>,
|
||||||
|
mode: CarouselMode,
|
||||||
|
controls: bool,
|
||||||
|
indicators: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CarouselWidget {
|
||||||
|
pub fn mode(mut self, mode: CarouselMode) -> Self {
|
||||||
|
self.mode = mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_controls(mut self) -> Self {
|
||||||
|
self.controls = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_indicators(mut self) -> Self {
|
||||||
|
self.indicators = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for CarouselWidget {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for CarouselWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> maud::Markup {
|
||||||
|
let mode = match self.mode {
|
||||||
|
CarouselMode::Slide => "slide",
|
||||||
|
CarouselMode::Static => "static",
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
|
||||||
|
div id="default-carousel" class="relative w-full" data-carousel=(mode) {
|
||||||
|
|
||||||
|
// Carousel wrapper
|
||||||
|
div class="relative h-56 overflow-hidden rounded-lg md:h-96" {
|
||||||
|
|
||||||
|
@for element in &self.elements {
|
||||||
|
div class="hidden duration-700 ease-in-out absolute block w-full flex justify-center items-center" data-carousel-item {
|
||||||
|
(element)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@if self.indicators {
|
||||||
|
// Slider indicators
|
||||||
|
div class="absolute z-30 flex -translate-x-1/2 bottom-5 left-1/2 space-x-3 rtl:space-x-reverse" {
|
||||||
|
@for i in 0..self.elements.len() {
|
||||||
|
@if i == 0 {
|
||||||
|
button type="button" class="w-3 h-3 rounded-full" aria-current="true" aria-label=(format!("Slide {i}")) data-carousel-slide-to=(i) {};
|
||||||
|
} @else {
|
||||||
|
button type="button" class="w-3 h-3 rounded-full" aria-current="false" aria-label=(format!("Slide {i}")) data-carousel-slide-to=(i) {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if self.controls {
|
||||||
|
// Slider controls
|
||||||
|
button type="button" class="absolute top-0 start-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none" data-carousel-prev {
|
||||||
|
span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none" {
|
||||||
|
svg class="w-4 h-4 text-white dark:text-gray-800 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4" {};
|
||||||
|
};
|
||||||
|
span class="sr-only" { "Previous" };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
button type="button" class="absolute top-0 end-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none" data-carousel-next {
|
||||||
|
span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none" {
|
||||||
|
svg class="w-4 h-4 text-white dark:text-gray-800 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4" {};
|
||||||
|
};
|
||||||
|
span class="sr-only" { "Next" };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn HelpIcon() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
svg class="w-4 h-4 text-gray-400 hover:text-gray-500" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {
|
||||||
|
path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : test + struct progressbar
|
||||||
|
pub fn ProgressBar(percentage: u8, label: bool) -> PreEscaped<String> {
|
||||||
|
assert!(percentage < 100, "Percentage must be less than 100");
|
||||||
|
html! {
|
||||||
|
@if label {
|
||||||
|
div class="w-full bg-gray-200 rounded-full dark:bg-gray-700" {
|
||||||
|
div class="bg-blue-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full" style=(format!("width: {percentage}%")) { (format!("{percentage}%")) };
|
||||||
|
};
|
||||||
|
} @else {
|
||||||
|
div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700" {
|
||||||
|
div class="bg-blue-600 h-2.5 rounded-full" style=(format!("width: {percentage}%")) {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : timeline stepper
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Stepper<S: Into<String>>(steps: Vec<S>) -> StepperWidget {
|
||||||
|
let mut boxed_elements: Vec<Box<dyn UIWidget>> = Vec::with_capacity(steps.len());
|
||||||
|
|
||||||
|
for e in steps {
|
||||||
|
boxed_elements.push(Box::new(e.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
StepperWidget {
|
||||||
|
elements: boxed_elements,
|
||||||
|
icons: false,
|
||||||
|
progress: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn IconStepper<T: UIWidget + 'static>(elements: Vec<T>) -> StepperWidget {
|
||||||
|
let mut boxed_elements: Vec<Box<dyn UIWidget>> = Vec::with_capacity(elements.len());
|
||||||
|
|
||||||
|
for e in elements {
|
||||||
|
boxed_elements.push(Box::new(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
StepperWidget {
|
||||||
|
elements: boxed_elements,
|
||||||
|
icons: true,
|
||||||
|
progress: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StepperWidget {
|
||||||
|
elements: Vec<Box<dyn UIWidget>>,
|
||||||
|
icons: bool,
|
||||||
|
progress: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StepperWidget {
|
||||||
|
pub fn step(mut self, step: u8) -> Self {
|
||||||
|
if step != 0 {
|
||||||
|
self.progress = step - 1;
|
||||||
|
} else {
|
||||||
|
self.progress = step;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_icon() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
svg class="w-3.5 h-3.5 sm:w-4 sm:h-4 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20" {
|
||||||
|
path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z" {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_final(index: usize, element: &Box<dyn UIWidget>) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
li class="flex items-center" {
|
||||||
|
span class="me-2" { (index+1) };
|
||||||
|
(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_middle(index: usize, element: &Box<dyn UIWidget>) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
li class="flex md:w-full items-center after:content-[''] after:w-full after:h-1 after:border-b after:border-gray-200 after:border-1 after:hidden sm:after:inline-block after:mx-6 xl:after:mx-10 dark:after:border-gray-700" {
|
||||||
|
span class="flex items-center after:content-['/'] sm:after:hidden after:mx-2 after:text-gray-200 dark:after:text-gray-500" {
|
||||||
|
span class="me-2" { (index+1) };
|
||||||
|
(element)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_done(element: &Box<dyn UIWidget>) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
li class="flex md:w-full items-center text-blue-600 dark:text-blue-500 sm:after:content-[''] after:w-full after:h-1 after:border-b after:border-gray-200 after:border-1 after:hidden sm:after:inline-block after:mx-6 xl:after:mx-10 dark:after:border-gray-700" {
|
||||||
|
span class="flex items-center after:content-['/'] sm:after:hidden after:mx-2 after:text-gray-200 dark:after:text-gray-500" {
|
||||||
|
(Self::check_icon())
|
||||||
|
(element)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_final_icon(element: &Box<dyn UIWidget>) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
li class="flex items-center w-full" {
|
||||||
|
span class="flex items-center justify-center w-10 h-10 bg-gray-100 rounded-full lg:h-12 lg:w-12 dark:bg-gray-700 shrink-0" {
|
||||||
|
(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_middle_icon(element: &Box<dyn UIWidget>) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
li class="flex w-full items-center after:content-[''] after:w-full after:h-1 after:border-b after:border-gray-100 after:border-4 after:inline-block dark:after:border-gray-700" {
|
||||||
|
span class="flex items-center justify-center w-10 h-10 bg-gray-100 rounded-full lg:h-12 lg:w-12 dark:bg-gray-700 shrink-0" {
|
||||||
|
(element)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_done_icon(element: &Box<dyn UIWidget>) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
li class="flex w-full items-center text-blue-600 dark:text-blue-500 after:content-[''] after:w-full after:h-1 after:border-b after:border-blue-100 after:border-4 after:inline-block dark:after:border-blue-800" {
|
||||||
|
span class="flex items-center justify-center w-10 h-10 bg-blue-100 rounded-full lg:h-12 lg:w-12 dark:bg-blue-800 shrink-0" {
|
||||||
|
(element)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for StepperWidget {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for StepperWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> maud::Markup {
|
||||||
|
html! {
|
||||||
|
@if self.icons {
|
||||||
|
ol class="flex items-center w-full" {
|
||||||
|
|
||||||
|
@for (index, e) in self.elements.iter().enumerate() {
|
||||||
|
@if index == (self.elements.len()-1) {
|
||||||
|
(Self::build_final_icon(e))
|
||||||
|
} @else if index <= self.progress as usize {
|
||||||
|
(Self::build_done_icon(e))
|
||||||
|
} @else {
|
||||||
|
(Self::build_middle_icon(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} @else {
|
||||||
|
|
||||||
|
|
||||||
|
ol class="flex items-center w-full text-sm font-medium text-center text-gray-500 dark:text-gray-400 sm:text-base" {
|
||||||
|
|
||||||
|
@for (index, e) in self.elements.iter().enumerate() {
|
||||||
|
@if index == (self.elements.len()-1) {
|
||||||
|
(Self::build_final(index, e))
|
||||||
|
} @else if index <= self.progress as usize {
|
||||||
|
(Self::build_done(e))
|
||||||
|
} @else {
|
||||||
|
(Self::build_middle(index, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : impl tabs
|
||||||
|
// https://flowbite.com/docs/components/tabs/
|
||||||
|
// tabs (with inline + htmx content)
|
||||||
|
pub struct TabWidget {
|
||||||
|
pub htmx_content: Vec<(String, String)>,
|
||||||
|
pub content: Vec<(String, Box<dyn UIWidget>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : Material Icons
|
||||||
|
// TODO : Common SVG Icons
|
56
src/ui/components/modal.rs
Normal file
56
src/ui/components/modal.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use maud::{PreEscaped, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
|
// TODO : rework modal
|
||||||
|
pub fn ModalCloseButton<T: UIWidget + 'static>(modal: &str, inner: T) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
button
|
||||||
|
data-modal-hide=(modal)
|
||||||
|
type="button"
|
||||||
|
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" { (inner) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ModalOpenButton<T: UIWidget + 'static>(modal: &str, inner: T) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
button
|
||||||
|
data-modal-target=(modal)
|
||||||
|
data-modal-toggle=(modal)
|
||||||
|
class="block text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button" { (inner) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn Modal<T: UIWidget + 'static, E: UIWidget + 'static, F: FnOnce(String) -> E>(
|
||||||
|
title: &str,
|
||||||
|
body: T,
|
||||||
|
footer: F,
|
||||||
|
) -> (String, PreEscaped<String>) {
|
||||||
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
(format!("modal-{id}"), html! {
|
||||||
|
div id=(format!("modal-{id}")) tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full" {
|
||||||
|
div class="relative p-4 w-full max-w-2xl max-h-full" {
|
||||||
|
|
||||||
|
div class="relative bg-white rounded-lg shadow dark:bg-gray-700" {
|
||||||
|
div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600" {
|
||||||
|
h3 class="text-xl font-semibold text-gray-900 dark:text-white" { (title) }
|
||||||
|
button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="default-modal" {
|
||||||
|
svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" {};
|
||||||
|
};
|
||||||
|
span class="sr-only" { "Close modal" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="p-4 md:p-5 space-y-4" {
|
||||||
|
(body)
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600" {
|
||||||
|
(footer(format!("modal-{id}")))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}};
|
||||||
|
})
|
||||||
|
}
|
334
src/ui/components/overlay.rs
Normal file
334
src/ui/components/overlay.rs
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Popover<T: UIWidget + 'static, I: UIWidget + 'static>(on: T, inner: I) -> PopoverWidget {
|
||||||
|
PopoverWidget {
|
||||||
|
on: Box::new(on),
|
||||||
|
inner: Box::new(inner),
|
||||||
|
placement: Placement::Top,
|
||||||
|
arrow: true,
|
||||||
|
animated: true,
|
||||||
|
trigger: PopoverTrigger::Hover,
|
||||||
|
offset: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PopoverTrigger {
|
||||||
|
Click,
|
||||||
|
Hover,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PopoverWidget {
|
||||||
|
on: Box<dyn UIWidget>,
|
||||||
|
inner: Box<dyn UIWidget>,
|
||||||
|
placement: Placement,
|
||||||
|
arrow: bool,
|
||||||
|
animated: bool,
|
||||||
|
trigger: PopoverTrigger,
|
||||||
|
offset: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PopoverWidget {
|
||||||
|
pub fn place(mut self, placement: Placement) -> Self {
|
||||||
|
self.placement = placement;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn no_arrow(mut self) -> Self {
|
||||||
|
self.arrow = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animate(mut self, value: bool) -> Self {
|
||||||
|
self.animated = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger(mut self, trigger: PopoverTrigger) -> Self {
|
||||||
|
self.trigger = trigger;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn offset(mut self, offset: i32) -> Self {
|
||||||
|
self.offset = offset;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PopoverWidget {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for PopoverWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> maud::Markup {
|
||||||
|
let id = format!("popover-{}", uuid::Uuid::new_v4().to_string());
|
||||||
|
|
||||||
|
let el_class = "absolute z-10 invisible inline-block w-fit p-2 text-sm text-gray-500 bg-white border border-gray-200 rounded-lg shadow-xs opacity-0 dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800";
|
||||||
|
|
||||||
|
let el_class = if self.animated {
|
||||||
|
format!("{el_class} transition-opacity duration-200")
|
||||||
|
} else {
|
||||||
|
el_class.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let trigger = match self.trigger {
|
||||||
|
PopoverTrigger::Click => "click",
|
||||||
|
PopoverTrigger::Hover => "hover",
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
|
||||||
|
div data-popover-target=(id)
|
||||||
|
data-popover-trigger=(trigger)
|
||||||
|
data-popover-placement=(self.placement.to_value())
|
||||||
|
data-popover-offset=(self.offset)
|
||||||
|
class=(format!("{class} my-auto max-w-fit")) { (self.on) };
|
||||||
|
|
||||||
|
div data-popover id=(id) role="tooltip" class=(el_class) {
|
||||||
|
(self.inner)
|
||||||
|
|
||||||
|
@if self.arrow {
|
||||||
|
div data-popper-arrow {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use maud::{Render, html};
|
||||||
|
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Tooltip<T: UIWidget + 'static, I: UIWidget + 'static>(on: T, inner: I) -> TooltipWidget {
|
||||||
|
TooltipWidget {
|
||||||
|
on: Box::new(on),
|
||||||
|
inner: Box::new(inner),
|
||||||
|
placement: Placement::Top,
|
||||||
|
arrow: true,
|
||||||
|
dark: true,
|
||||||
|
animated: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Placement {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Placement {
|
||||||
|
pub fn to_value(&self) -> &str {
|
||||||
|
match *self {
|
||||||
|
Placement::Left => "left",
|
||||||
|
Placement::Right => "right",
|
||||||
|
Placement::Top => "top",
|
||||||
|
Placement::Bottom => "bottom",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TooltipWidget {
|
||||||
|
on: Box<dyn UIWidget>,
|
||||||
|
inner: Box<dyn UIWidget>,
|
||||||
|
placement: Placement,
|
||||||
|
arrow: bool,
|
||||||
|
dark: bool,
|
||||||
|
animated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TooltipWidget {
|
||||||
|
pub fn place(mut self, placement: Placement) -> Self {
|
||||||
|
self.placement = placement;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn no_arrow(mut self) -> Self {
|
||||||
|
self.arrow = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animate(mut self, value: bool) -> Self {
|
||||||
|
self.animated = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn white(mut self) -> Self {
|
||||||
|
self.dark = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TooltipWidget {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for TooltipWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> maud::Markup {
|
||||||
|
let id = format!("tooltip-{}", uuid::Uuid::new_v4().to_string());
|
||||||
|
|
||||||
|
let tt_class = match self.dark {
|
||||||
|
true => {
|
||||||
|
"absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-xs opacity-0 tooltip dark:bg-gray-700"
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
"absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg shadow-xs opacity-0 tooltip"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tt_class = if self.animated {
|
||||||
|
format!("{tt_class} transition-opacity duration-200")
|
||||||
|
} else {
|
||||||
|
tt_class.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
@if self.dark {
|
||||||
|
div data-tooltip-target=(id) data-tooltip-placement=(self.placement.to_value())
|
||||||
|
class=(format!("{class} my-auto max-w-fit")) { (self.on) };
|
||||||
|
} @else {
|
||||||
|
div data-tooltip-target=(id) data-tooltip-style="light" data-tooltip-placement=(self.placement.to_value())
|
||||||
|
class=(format!("{class} my-auto max-w-fit")) { (self.on) };
|
||||||
|
}
|
||||||
|
|
||||||
|
div id=(id) role="tooltip" class=(tt_class) {
|
||||||
|
(self.inner)
|
||||||
|
@if self.arrow {
|
||||||
|
div class="tooltip-arrow" data-popper-arrow {};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn DropDown<T: UIWidget + 'static, I: UIWidget + 'static>(on: T, inner: I) -> DropDownWidget {
|
||||||
|
DropDownWidget {
|
||||||
|
on: Box::new(on),
|
||||||
|
inner: Box::new(inner),
|
||||||
|
delay: 0,
|
||||||
|
placement: Placement::Bottom,
|
||||||
|
trigger: PopoverTrigger::Click,
|
||||||
|
distance: 10,
|
||||||
|
skidding: 0,
|
||||||
|
stretch: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DropDownWidget {
|
||||||
|
on: Box<dyn UIWidget>,
|
||||||
|
inner: Box<dyn UIWidget>,
|
||||||
|
placement: Placement,
|
||||||
|
delay: u32,
|
||||||
|
trigger: PopoverTrigger,
|
||||||
|
distance: u32,
|
||||||
|
skidding: u32,
|
||||||
|
stretch: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DropDownWidget {
|
||||||
|
pub fn place(mut self, placement: Placement) -> Self {
|
||||||
|
self.placement = placement;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delay(mut self, delay: u32) -> Self {
|
||||||
|
self.delay = delay;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stretch(mut self) -> Self {
|
||||||
|
self.stretch = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn distance(mut self, distance: u32) -> Self {
|
||||||
|
self.distance = distance;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skidding(mut self, skidding: u32) -> Self {
|
||||||
|
self.skidding = skidding;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger(mut self, trigger: PopoverTrigger) -> Self {
|
||||||
|
self.trigger = trigger;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for DropDownWidget {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for DropDownWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> maud::Markup {
|
||||||
|
let id = format!("dropdown-{}", uuid::Uuid::new_v4().to_string());
|
||||||
|
|
||||||
|
let trigger = match self.trigger {
|
||||||
|
PopoverTrigger::Click => "click",
|
||||||
|
PopoverTrigger::Hover => "hover",
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div
|
||||||
|
data-dropdown-toggle=(id)
|
||||||
|
data-dropdown-trigger=(trigger)
|
||||||
|
data-dropdown-delay=(self.delay)
|
||||||
|
data-dropdown-placement=(self.placement.to_value())
|
||||||
|
data-dropdown-offset-distance=(self.distance)
|
||||||
|
data-dropdown-offset-skidding=(self.skidding)
|
||||||
|
class=(format!("{class} hover:cursor-pointer my-auto{}", if self.stretch { "" } else { " max-w-fit" })) {
|
||||||
|
(self.on)
|
||||||
|
};
|
||||||
|
|
||||||
|
div id=(id) class="z-50 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700" {
|
||||||
|
ul class="py-2 text-sm text-gray-700 dark:text-gray-200" {
|
||||||
|
(self.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
213
src/ui/components/pagination.rs
Normal file
213
src/ui/components/pagination.rs
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
use maud::{PreEscaped, Render, html};
|
||||||
|
|
||||||
|
use crate::{request::api::Pager, ui::UIWidget};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Pagination<T, F>(
|
||||||
|
current_page: usize,
|
||||||
|
pager: Pager<T>,
|
||||||
|
url_builder: F,
|
||||||
|
) -> PaginationWidget<T, F>
|
||||||
|
where
|
||||||
|
F: Fn(usize) -> String,
|
||||||
|
{
|
||||||
|
PaginationWidget {
|
||||||
|
icons: true,
|
||||||
|
pager,
|
||||||
|
current_page,
|
||||||
|
url_builder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PaginationWidget<T, F>
|
||||||
|
where
|
||||||
|
F: Fn(usize) -> String,
|
||||||
|
{
|
||||||
|
icons: bool,
|
||||||
|
pager: Pager<T>,
|
||||||
|
current_page: usize,
|
||||||
|
url_builder: F,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F> PaginationWidget<T, F>
|
||||||
|
where
|
||||||
|
F: Fn(usize) -> String,
|
||||||
|
{
|
||||||
|
pub fn no_icons(mut self) -> Self {
|
||||||
|
self.icons = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_page_link_active(page_num: usize, link: &str, last: bool) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
li {
|
||||||
|
a href=(link) aria-current="page" class=(
|
||||||
|
format!("flex items-center justify-center px-3 h-8 text-blue-600 border border-gray-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white {} {}",
|
||||||
|
if page_num == 1 { "rounded-s-lg" } else { "" },
|
||||||
|
if last { "rounded-e-lg" } else { "" }
|
||||||
|
)) { (page_num) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_page_link(page_num: usize, link: &str, last: bool) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
li {
|
||||||
|
a href=(link) class=
|
||||||
|
(format!("flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white {}", if last { "rounded-e-lg" } else { "" })) { (page_num) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_page_links(&self, current_page: usize) -> PreEscaped<String> {
|
||||||
|
let total_pages = self.pager.total_pages();
|
||||||
|
let mut start_page = if current_page <= 3 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
current_page - 2
|
||||||
|
};
|
||||||
|
let mut end_page = if total_pages - current_page < 2 {
|
||||||
|
total_pages
|
||||||
|
} else {
|
||||||
|
current_page + 2
|
||||||
|
};
|
||||||
|
|
||||||
|
if end_page - start_page < 4 {
|
||||||
|
if start_page > 1 {
|
||||||
|
start_page = start_page.saturating_sub(4 - (end_page - start_page));
|
||||||
|
} else {
|
||||||
|
end_page = (start_page + 4).min(total_pages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
@for page in start_page..=end_page {
|
||||||
|
@if page == current_page {
|
||||||
|
(Self::build_page_link_active(page, &(self.url_builder)(page), current_page == total_pages && page == current_page))
|
||||||
|
} @else {
|
||||||
|
(Self::build_page_link(page, &(self.url_builder)(page), current_page == total_pages && page == current_page))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(&self, page: usize) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
nav {
|
||||||
|
ul class="inline-flex -space-x-px text-sm" {
|
||||||
|
@if page != 1 {
|
||||||
|
li {
|
||||||
|
a href=((self.url_builder)(page-1)) class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white" {
|
||||||
|
@if self.icons {
|
||||||
|
span class="sr-only" { "Previous" };
|
||||||
|
svg class="w-2.5 h-2.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4" {};
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
"Previous"
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(self.build_page_links(page))
|
||||||
|
|
||||||
|
|
||||||
|
@if self.pager.has_next_page(page as u64) {
|
||||||
|
li {
|
||||||
|
a href=((self.url_builder)(page+1)) class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white" {
|
||||||
|
|
||||||
|
@if self.icons {
|
||||||
|
span class="sr-only" { "Next" };
|
||||||
|
svg class="w-2.5 h-2.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4" {};
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
"Next"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F> Render for PaginationWidget<T, F>
|
||||||
|
where
|
||||||
|
F: Fn(usize) -> String,
|
||||||
|
{
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F> UIWidget for PaginationWidget<T, F>
|
||||||
|
where
|
||||||
|
F: Fn(usize) -> String,
|
||||||
|
{
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> maud::Markup {
|
||||||
|
self.build(self.current_page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn PaginationButtonsOnly<T, F>(
|
||||||
|
page: u64,
|
||||||
|
pager: &Pager<T>,
|
||||||
|
url_builder: F,
|
||||||
|
) -> PreEscaped<String>
|
||||||
|
where
|
||||||
|
F: Fn(usize) -> String,
|
||||||
|
{
|
||||||
|
let previous = page > 1;
|
||||||
|
let next = pager.page_with_context(page).1;
|
||||||
|
|
||||||
|
let buttons = html! {
|
||||||
|
@if previous {
|
||||||
|
a href=(url_builder((page-1) as usize)) class="flex items-center justify-center px-3 h-8 me-3 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white" {
|
||||||
|
svg class="w-3.5 h-3.5 me-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5H1m0 0 4 4M1 5l4-4" {};
|
||||||
|
}
|
||||||
|
"Previous"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@if next {
|
||||||
|
a href=(url_builder((page+1) as usize)) class="flex items-center justify-center px-3 h-8 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white" {
|
||||||
|
"Next"
|
||||||
|
svg class="w-3.5 h-3.5 ms-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9" {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
@if previous {
|
||||||
|
div class="flex justify-between" {
|
||||||
|
(buttons)
|
||||||
|
};
|
||||||
|
} @else {
|
||||||
|
div class="flex justify-end" {
|
||||||
|
(buttons)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
147
src/ui/components/placeholder.rs
Normal file
147
src/ui/components/placeholder.rs
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
use maud::{PreEscaped, html};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Placeholder() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div role="status" class="max-w-sm animate-pulse" {
|
||||||
|
div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4" {};
|
||||||
|
div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5" {};
|
||||||
|
div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5" {};
|
||||||
|
div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[330px] mb-2.5" {};
|
||||||
|
div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[300px] mb-2.5" {};
|
||||||
|
div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]" {};
|
||||||
|
span class="sr-only" { "Loading..." };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn ImagePlaceholder() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div class="flex items-center justify-center w-full h-48 animate-pulse bg-gray-300 rounded-sm sm:w-96 dark:bg-gray-700" {
|
||||||
|
svg class="w-10 h-10 text-gray-200 dark:text-gray-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 18" {
|
||||||
|
path d="M18 0H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-5.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm4.376 10.481A1 1 0 0 1 16 15H4a1 1 0 0 1-.895-1.447l3.5-7A1 1 0 0 1 7.468 6a.965.965 0 0 1 .9.5l2.775 4.757 1.546-1.887a1 1 0 0 1 1.618.1l2.541 4a1 1 0 0 1 .028 1.011Z" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn VideoPlaceholder() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div role="status" class="flex items-center justify-center h-56 max-w-sm bg-gray-300 rounded-lg animate-pulse dark:bg-gray-700" {
|
||||||
|
svg class="w-10 h-10 text-gray-200 dark:text-gray-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 20" {
|
||||||
|
path d="M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z" {}
|
||||||
|
path d="M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM9 13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2Zm4 .382a1 1 0 0 1-1.447.894L10 13v-2l1.553-1.276a1 1 0 0 1 1.447.894v2.764Z" {};
|
||||||
|
};
|
||||||
|
span class="sr-only" { "Loading..." }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn TextPlaceholder() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div role="status" class="space-y-2.5 animate-pulse max-w-lg" {
|
||||||
|
div class="flex items-center w-full" {
|
||||||
|
div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-32" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-24" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-full" {};
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="flex items-center w-full max-w-[480px]" {
|
||||||
|
div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-full" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-full" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-24" {};
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="flex items-center w-full max-w-[400px]" {
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-full" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-200 rounded-full dark:bg-gray-700 w-80" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-full" {};
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="flex items-center w-full max-w-[480px]" {
|
||||||
|
div class="h-2.5 ms-2 bg-gray-200 rounded-full dark:bg-gray-700 w-full" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-full" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-24" {};
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="flex items-center w-full max-w-[440px]" {
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-32" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-24" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-200 rounded-full dark:bg-gray-700 w-full" {};
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="flex items-center w-full max-w-[360px]" {
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-full" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-200 rounded-full dark:bg-gray-700 w-80" {};
|
||||||
|
div class="h-2.5 ms-2 bg-gray-300 rounded-full dark:bg-gray-600 w-full" {};
|
||||||
|
};
|
||||||
|
span class="sr-only" { "Loading..." };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn CardPlaceholder() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div role="status" class="max-w-sm p-4 border border-gray-200 rounded-sm shadow-sm animate-pulse md:p-6 dark:border-gray-700" {
|
||||||
|
div class="flex items-center justify-center h-48 mb-4 bg-gray-300 rounded-sm dark:bg-gray-700" {
|
||||||
|
svg class="w-10 h-10 text-gray-200 dark:text-gray-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 20" {
|
||||||
|
path d="M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM10.5 6a1.5 1.5 0 1 1 0 2.999A1.5 1.5 0 0 1 10.5 6Zm2.221 10.515a1 1 0 0 1-.858.485h-8a1 1 0 0 1-.9-1.43L5.6 10.039a.978.978 0 0 1 .936-.57 1 1 0 0 1 .9.632l1.181 2.981.541-1a.945.945 0 0 1 .883-.522 1 1 0 0 1 .879.529l1.832 3.438a1 1 0 0 1-.031.988Z" {};
|
||||||
|
path d="M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z" {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4" {};
|
||||||
|
div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5" {};
|
||||||
|
div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5" {};
|
||||||
|
div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700" {};
|
||||||
|
span class="sr-only" { "Loading..." };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn ListPlaceholder() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div role="status" class="max-w-md p-4 space-y-4 border border-gray-200 divide-y divide-gray-200 rounded-sm shadow-sm animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700" {
|
||||||
|
div class="flex items-center justify-between" {
|
||||||
|
div {
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5" {};
|
||||||
|
div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700" {};
|
||||||
|
}
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12" {};
|
||||||
|
}
|
||||||
|
div class="flex items-center justify-between pt-4" {
|
||||||
|
div {
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5" {};
|
||||||
|
div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700" {};
|
||||||
|
};
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12" {};
|
||||||
|
}
|
||||||
|
div class="flex items-center justify-between pt-4" {
|
||||||
|
div {
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5" {};
|
||||||
|
div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700" {};
|
||||||
|
}
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12" {};
|
||||||
|
}
|
||||||
|
div class="flex items-center justify-between pt-4" {
|
||||||
|
div {
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5" {};
|
||||||
|
div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700" {};
|
||||||
|
};
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12" {};
|
||||||
|
}
|
||||||
|
div class="flex items-center justify-between pt-4" {
|
||||||
|
div {
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5" {};
|
||||||
|
div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700" {};
|
||||||
|
};
|
||||||
|
div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12" {};
|
||||||
|
}
|
||||||
|
span class="sr-only" { "Loading..." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
use maud::{PreEscaped, html};
|
|
||||||
|
|
||||||
use crate::request::{RequestContext, api::Pager};
|
use crate::request::{RequestContext, api::Pager};
|
||||||
|
use crate::ui::htmx::{Event, HTMXAttributes, SwapStrategy};
|
||||||
|
use crate::ui::prelude::*;
|
||||||
|
use maud::{PreEscaped, html};
|
||||||
|
|
||||||
/// Represents a search form with configurable options such as heading, placeholder, and CSS class.
|
/// Represents a search form with configurable options such as heading, placeholder, and CSS class.
|
||||||
pub struct Search {
|
pub struct Search {
|
||||||
|
@ -94,9 +95,12 @@ impl Search {
|
||||||
}
|
}
|
||||||
|
|
||||||
@if reslen as u64 == pager.items_per_page {
|
@if reslen as u64 == pager.items_per_page {
|
||||||
div hx-get=(format!("{}?query={}&page={}", self.post_url, query, page+1))
|
(Div()
|
||||||
hx-trigger="revealed"
|
.hx_get(
|
||||||
hx-swap="outerHTML" {};
|
&format!("{}?query={}&page={}", self.post_url, query, page+1)
|
||||||
|
).hx_trigger(
|
||||||
|
Event::on_revealed()
|
||||||
|
).hx_swap(SwapStrategy::outerHTML))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,7 +144,7 @@ impl Search {
|
||||||
/// The HTML string containing the entire search form and results UI.
|
/// The HTML string containing the entire search form and results UI.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build(&self, query: &str, first_page: PreEscaped<String>) -> PreEscaped<String> {
|
pub fn build(&self, query: &str, first_page: PreEscaped<String>) -> PreEscaped<String> {
|
||||||
let no_html = PreEscaped(String::new());
|
let no_html = Nothing();
|
||||||
html! {
|
html! {
|
||||||
(self.heading.as_ref().unwrap_or_else(|| &no_html))
|
(self.heading.as_ref().unwrap_or_else(|| &no_html))
|
||||||
input type="search" name="query"
|
input type="search" name="query"
|
436
src/ui/components/shell.rs
Normal file
436
src/ui/components/shell.rs
Normal file
|
@ -0,0 +1,436 @@
|
||||||
|
use maud::{PreEscaped, Render, html};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
request::{RequestContext, StringResponse},
|
||||||
|
ui::UIWidget,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Represents the HTML structure of a page shell, including the head, body class, and body content.
|
||||||
|
///
|
||||||
|
/// This structure is used to construct the overall HTML structure of a page, making it easier to generate consistent HTML pages dynamically.
|
||||||
|
pub struct Shell {
|
||||||
|
/// The HTML content for the `<head>` section of the page.
|
||||||
|
head: PreEscaped<String>,
|
||||||
|
/// An optional class attribute for the main container element.
|
||||||
|
main_class: String,
|
||||||
|
/// The HTML content for the static body portion.
|
||||||
|
body_content: PreEscaped<String>,
|
||||||
|
ui: bool,
|
||||||
|
bottom_nav: Option<PreEscaped<String>>,
|
||||||
|
sidebar: Option<SidebarWidget>,
|
||||||
|
navbar: Option<NavBarWidget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shell {
|
||||||
|
/// Constructs a new `Shell` instance with the given head content, body content, and body class.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `head` - The HTML content for the page's head.
|
||||||
|
/// * `body_content` - The HTML content for the body of the page.
|
||||||
|
/// * `body_class` - An optional class to apply to the `<body>` element.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `Shell` instance encapsulating the provided HTML content and attributes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new<T: UIWidget + 'static, C: UIWidget + 'static, B: UIWidget + 'static>(
|
||||||
|
head: T,
|
||||||
|
body_content: B,
|
||||||
|
body_class: C,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
head: head.render(),
|
||||||
|
main_class: body_class.extended_class().join(" "),
|
||||||
|
body_content: body_content.render(),
|
||||||
|
ui: false,
|
||||||
|
bottom_nav: None,
|
||||||
|
sidebar: None,
|
||||||
|
navbar: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_navbar(mut self, navbar: NavBarWidget) -> Self {
|
||||||
|
self.navbar = Some(navbar);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_bottom_navigation(mut self, bottom_nav: PreEscaped<String>) -> Self {
|
||||||
|
self.bottom_nav = Some(bottom_nav);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_sidebar(mut self, inner: PreEscaped<String>) -> Self {
|
||||||
|
self.sidebar = Some(Sidebar(inner));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_ui(mut self) -> Self {
|
||||||
|
self.ui = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the full HTML page using the shell structure, with additional content and a title.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `content` - The additional HTML content to render inside the main content div.
|
||||||
|
/// * `title` - The title of the page, rendered inside the `<title>` element.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `PreEscaped<String>` containing the full HTML page content.
|
||||||
|
#[must_use]
|
||||||
|
pub fn render(&self, content: PreEscaped<String>, title: &str) -> PreEscaped<String> {
|
||||||
|
let mut main_class = self.main_class.clone();
|
||||||
|
|
||||||
|
if self.bottom_nav.is_some() {
|
||||||
|
main_class.push_str(" pb-20");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.sidebar.is_some() {
|
||||||
|
main_class.push_str(" ml-[264px]");
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
(maud::DOCTYPE)
|
||||||
|
html {
|
||||||
|
head {
|
||||||
|
title { (title) };
|
||||||
|
@if self.ui {
|
||||||
|
script src="https://cdn.tailwindcss.com" {};
|
||||||
|
script src="/assets/htmx.min.js" {};
|
||||||
|
script src="/assets/flowbite.min.js" {};
|
||||||
|
link href="/assets/flowbite.min.css" rel="stylesheet" {};
|
||||||
|
meta name="viewport" content="width=device-width, initial-scale=1.0";
|
||||||
|
};
|
||||||
|
(self.head)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
body class=(self.main_class) {
|
||||||
|
(PreEscaped(self.navbar.as_ref().map(|x| {
|
||||||
|
if self.sidebar.is_some() {
|
||||||
|
x.clone().sticky().render().0.clone()
|
||||||
|
} else {
|
||||||
|
x.render().0.clone()
|
||||||
|
}
|
||||||
|
}).unwrap_or_default()));
|
||||||
|
div id="notification_area" class="fixed top-0 start-0 z-50 w-full p-4" {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
(PreEscaped(self.sidebar.as_ref().map(|x| x.render(self.navbar.is_some()).0).unwrap_or_default()))
|
||||||
|
|
||||||
|
(self.body_content);
|
||||||
|
div id="main_content" class=(main_class) {
|
||||||
|
(content)
|
||||||
|
};
|
||||||
|
|
||||||
|
(PreEscaped(self.bottom_nav.as_ref().map(|x| x.0.as_str()).unwrap_or_default()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a full page or an HTMX-compatible fragment based on the request context.
|
||||||
|
///
|
||||||
|
/// If the request is not an HTMX request, this function uses the provided shell to generate
|
||||||
|
/// a full HTML page. If it is an HTMX request, only the provided content is rendered.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `content` - The HTML content to render.
|
||||||
|
/// * `title` - The title of the page for full-page rendering.
|
||||||
|
/// * `ctx` - The `RequestContext` containing request metadata.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `StringResponse`
|
||||||
|
pub async fn render_page(
|
||||||
|
&self,
|
||||||
|
content: PreEscaped<String>,
|
||||||
|
title: &str,
|
||||||
|
ctx: RequestContext,
|
||||||
|
) -> StringResponse {
|
||||||
|
if ctx.is_htmx {
|
||||||
|
(
|
||||||
|
rocket::http::Status::Ok,
|
||||||
|
(rocket::http::ContentType::HTML, content.into_string()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
rocket::http::Status::Ok,
|
||||||
|
(
|
||||||
|
rocket::http::ContentType::HTML,
|
||||||
|
self.render(content, title).into_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOCOMMIT
|
||||||
|
|
||||||
|
// TODO : Integrate into shell
|
||||||
|
|
||||||
|
pub fn Drawer<T: UIWidget + 'static>(inner: T) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
// Toggle
|
||||||
|
div class="text-center" {
|
||||||
|
button class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800" type="button" data-drawer-target="drawer-example" data-drawer-show="drawer-example" aria-controls="drawer-example" { "Show drawer" };
|
||||||
|
};
|
||||||
|
|
||||||
|
div id="drawer-example" class="fixed top-0 left-0 z-40 h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white w-80 dark:bg-gray-800" tabindex="-1" aria-labelledby="drawer-label" {
|
||||||
|
h5 id="drawer-label" class="inline-flex items-center mb-4 text-base font-semibold text-gray-500 dark:text-gray-400" {
|
||||||
|
svg class="w-4 h-4 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20" {
|
||||||
|
path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" {};
|
||||||
|
}
|
||||||
|
"Info"
|
||||||
|
};
|
||||||
|
button type="button" data-drawer-hide="drawer-example" aria-controls="drawer-example" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 absolute top-2.5 end-2.5 flex items-center justify-center dark:hover:bg-gray-600 dark:hover:text-white" {
|
||||||
|
svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" {};
|
||||||
|
};
|
||||||
|
span class="sr-only" { "Close menu" };
|
||||||
|
};
|
||||||
|
|
||||||
|
(inner)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn BottomNavigation<T: UIWidget + 'static>(inner: &[T]) -> PreEscaped<String> {
|
||||||
|
let elements_len = inner.len();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="fixed bottom-0 left-0 z-50 w-full h-16 bg-white border-t border-gray-200 dark:bg-gray-700 dark:border-gray-600" {
|
||||||
|
div class=(format!("grid h-full max-w-lg grid-cols-{elements_len} mx-auto font-medium")) {
|
||||||
|
@for item in inner {
|
||||||
|
(item)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn BottomNavigationTile<T: UIWidget + 'static>(
|
||||||
|
icon: Option<T>,
|
||||||
|
text: &str,
|
||||||
|
) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
button type="button" class="inline-flex flex-col items-center justify-center px-5 hover:bg-gray-50 dark:hover:bg-gray-800 group" {
|
||||||
|
(icon.map(|x| x.render()).unwrap_or_default());
|
||||||
|
span class="text-sm text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-500" { (text) };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : NavBar integration with auth system
|
||||||
|
|
||||||
|
pub fn NavBar(title: &str) -> NavBarWidget {
|
||||||
|
NavBarWidget {
|
||||||
|
icon: None,
|
||||||
|
name: title.to_string(),
|
||||||
|
menu: None,
|
||||||
|
user: None,
|
||||||
|
no_dropdown: false,
|
||||||
|
centered: false,
|
||||||
|
sticky: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct NavBarWidget {
|
||||||
|
icon: Option<PreEscaped<String>>,
|
||||||
|
name: String,
|
||||||
|
menu: Option<PreEscaped<String>>,
|
||||||
|
user: Option<PreEscaped<String>>,
|
||||||
|
no_dropdown: bool,
|
||||||
|
centered: bool,
|
||||||
|
sticky: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavBarWidget {
|
||||||
|
pub fn icon<T: UIWidget + 'static>(mut self, icon: T) -> Self {
|
||||||
|
self.icon = Some(icon.render());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sticky(mut self) -> Self {
|
||||||
|
self.sticky = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn menu<T: UIWidget + 'static>(mut self, menu: T) -> Self {
|
||||||
|
self.menu = Some(menu.render());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extra<T: UIWidget + 'static>(mut self, extra: T) -> Self {
|
||||||
|
self.user = Some(extra.render());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn no_dropdown(mut self) -> Self {
|
||||||
|
self.no_dropdown = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center(mut self) -> Self {
|
||||||
|
self.centered = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for NavBarWidget {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for NavBarWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> maud::Markup {
|
||||||
|
let div_class = if self.centered {
|
||||||
|
"max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4"
|
||||||
|
} else {
|
||||||
|
"flex flex-wrap items-center justify-between mx-auto p-4"
|
||||||
|
};
|
||||||
|
|
||||||
|
let nav_class = if self.sticky {
|
||||||
|
"sticky top-0 bg-white border-gray-200 dark:bg-gray-900"
|
||||||
|
} else {
|
||||||
|
"bg-white border-gray-200 dark:bg-gray-900"
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
nav class=(nav_class) {
|
||||||
|
div class=(div_class) {
|
||||||
|
a href="/" class="flex items-center space-x-3 rtl:space-x-reverse" {
|
||||||
|
div class="h-8" {
|
||||||
|
(PreEscaped(self.icon.as_ref().map(|x| x.render().0).unwrap_or_default()))
|
||||||
|
};
|
||||||
|
span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white" { (self.name) };
|
||||||
|
};
|
||||||
|
div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse" {
|
||||||
|
(PreEscaped(self.user.as_ref().map(|x| x.render().0).unwrap_or_default()))
|
||||||
|
|
||||||
|
@if !self.no_dropdown {
|
||||||
|
button data-collapse-toggle="navbar-user" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-user" aria-expanded="false" {
|
||||||
|
span class="sr-only" { "Open main menu" };
|
||||||
|
svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user" {
|
||||||
|
ul class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700" {
|
||||||
|
(PreEscaped(self.menu.as_ref().map(|x| x.render().0).unwrap_or_default()))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SidebarWidget {
|
||||||
|
inner: PreEscaped<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SidebarWidget {
|
||||||
|
pub fn render(&self, has_navbar: bool) -> PreEscaped<String> {
|
||||||
|
let class = match has_navbar {
|
||||||
|
true => {
|
||||||
|
"fixed left-0 w-64 h-screen transition-transform -translate-x-full sm:translate-x-0 z-50"
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
"fixed top-0 left-0 w-64 h-screen transition-transform -translate-x-full sm:translate-x-0 z-50"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
aside id="default-sidebar" class=(class) aria-label="Sidebar" {
|
||||||
|
div class="h-full px-3 py-4 overflow-y-auto bg-gray-50 dark:bg-gray-800" {
|
||||||
|
ul class="space-y-2 font-medium" {
|
||||||
|
(self.inner)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Sidebar<T: UIWidget + 'static>(inner: T) -> SidebarWidget {
|
||||||
|
SidebarWidget {
|
||||||
|
inner: inner.render(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://flowbite.com/docs/components/speed-dial/
|
||||||
|
// TODO : speed dial (circle/square) (postioning) (buttons opt with text)
|
||||||
|
|
||||||
|
pub enum Position {
|
||||||
|
TopLeft,
|
||||||
|
TopRight,
|
||||||
|
BottomLeft,
|
||||||
|
BottomRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Alignment {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SpeedDialWidget {
|
||||||
|
buttons: Vec<Box<dyn UIWidget>>,
|
||||||
|
square: bool,
|
||||||
|
position: Position,
|
||||||
|
alignment: Alignment,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! page {
|
||||||
|
($shell:ident, $ctx:ident, $title:literal, $content:expr) => {{
|
||||||
|
use $crate::ui::UIWidget;
|
||||||
|
let content = $content.render_with_class("");
|
||||||
|
$shell.render_page(content, $title, $ctx).await
|
||||||
|
}};
|
||||||
|
($shell:ident, $ctx:ident, $title:ident, $content:expr) => {{
|
||||||
|
use $crate::ui::UIWidget;
|
||||||
|
let content = $content.render_with_class("");
|
||||||
|
$shell.render_page(content, &$title, $ctx).await
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://flowbite.com/docs/components/toast/
|
||||||
|
// TODO : impl toast
|
||||||
|
// icons + bg color
|
||||||
|
// positioning
|
||||||
|
// undo btn toast
|
||||||
|
|
||||||
|
pub fn Toast<T: UIWidget + 'static>(icon: T, text: &str) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div id="toast-default" class="flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert" {
|
||||||
|
div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-blue-500 bg-blue-100 rounded-lg dark:bg-blue-800 dark:text-blue-200" {
|
||||||
|
(icon)
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="ms-3 text-sm font-normal" { (text) };
|
||||||
|
|
||||||
|
button type="button" class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#toast-default" aria-label="Close" {
|
||||||
|
span class="sr-only" { "Close" };
|
||||||
|
svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
56
src/ui/components/timeline.rs
Normal file
56
src/ui/components/timeline.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// https://flowbite.com/docs/components/timeline/
|
||||||
|
// TODO : Timeline
|
||||||
|
// Timeline with icons
|
||||||
|
// Horizontal timeline
|
||||||
|
// activity timeline (custom)
|
||||||
|
|
||||||
|
use maud::{PreEscaped, html};
|
||||||
|
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
|
pub struct TimelineElement {
|
||||||
|
icon: Option<Box<dyn UIWidget>>,
|
||||||
|
title: Box<dyn UIWidget>,
|
||||||
|
time: String,
|
||||||
|
body: Box<dyn UIWidget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TimelineWidget {
|
||||||
|
horizontal: bool,
|
||||||
|
elements: Vec<TimelineElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn Timeline() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
ol class="relative border-s border-gray-200 dark:border-gray-700" {
|
||||||
|
|
||||||
|
li class="mb-10 ms-6" {
|
||||||
|
span class="absolute flex items-center justify-center w-6 h-6 bg-blue-100 rounded-full -start-3 dark:ring-gray-900 dark:bg-blue-900" {
|
||||||
|
svg class="w-2.5 h-2.5 text-blue-800 dark:text-blue-300" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20" {
|
||||||
|
path d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
h3 class="mb-1 text-lg font-semibold text-gray-900 dark:text-white" { "Flowbite Figma v1.3.0" };
|
||||||
|
time class="block mb-2 text-sm font-normal leading-none text-gray-400 dark:text-gray-500" { "Released on December 7th, 2021" };
|
||||||
|
p class="text-base font-normal text-gray-500 dark:text-gray-400" { "All of the pages and components are first designed in Figma and we keep a parity between the two versions even as we update the project." };
|
||||||
|
};
|
||||||
|
|
||||||
|
li class="mb-10 ms-4" {
|
||||||
|
div class="absolute w-3 h-3 bg-gray-200 rounded-full mt-1.5 -start-1.5 border border-white dark:border-gray-900 dark:bg-gray-700" {};
|
||||||
|
time class="mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500" { "March 2022" };
|
||||||
|
h3 class="text-lg font-semibold text-gray-900 dark:text-white" { "Marketing UI design in Figma" };
|
||||||
|
p class="text-base font-normal text-gray-500 dark:text-gray-400" { "All of the pages and components are first designed in Figma and we keep a parity between the two versions even as we update the project." };
|
||||||
|
};
|
||||||
|
|
||||||
|
li class="ms-4" {
|
||||||
|
div class="absolute w-3 h-3 bg-gray-200 rounded-full mt-1.5 -start-1.5 border border-white dark:border-gray-900 dark:bg-gray-700" {};
|
||||||
|
time class="mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500" { "April 2022" };
|
||||||
|
h3 class="text-lg font-semibold text-gray-900 dark:text-white" { "E-Commerce UI code in Tailwind CSS" };
|
||||||
|
p class="text-base font-normal text-gray-500 dark:text-gray-400" { "Get started with dozens of web components and interactive elements built on top of Tailwind CSS." };
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : https://flowbite.com/docs/components/timeline/#activity-log
|
208
src/ui/htmx/mod.rs
Normal file
208
src/ui/htmx/mod.rs
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
mod selector;
|
||||||
|
mod swap;
|
||||||
|
mod trigger;
|
||||||
|
|
||||||
|
pub use selector::Selector;
|
||||||
|
use swap::ModifiedSwapStrategy;
|
||||||
|
pub use swap::SwapStrategy;
|
||||||
|
pub use trigger::{Event, QueueOption, Trigger};
|
||||||
|
|
||||||
|
use super::AttrExtendable;
|
||||||
|
|
||||||
|
pub trait HTMXAttributes: AttrExtendable + std::marker::Sized {
|
||||||
|
/// Issues a `GET` request to the specified URL
|
||||||
|
#[must_use]
|
||||||
|
fn hx_get(self, url: &str) -> Self {
|
||||||
|
self.add_attr("hx-get", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issues a `POST` request to the specified URL
|
||||||
|
#[must_use]
|
||||||
|
fn hx_post(self, url: &str) -> Self {
|
||||||
|
self.add_attr("hx-post", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a URL into the browser location bar to create history
|
||||||
|
#[must_use]
|
||||||
|
fn hx_push_url(self) -> Self {
|
||||||
|
self.add_attr("hx-push-url", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select content to swap in from a response
|
||||||
|
#[must_use]
|
||||||
|
fn hx_select(self, element: &str) -> Self {
|
||||||
|
self.add_attr("hx-select", element)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select content to swap in from a response, somewhere other than the target (out of band).
|
||||||
|
/// Select `element` from response and replace `element` in the DOM.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_select_oob(self, element: &str) -> Self {
|
||||||
|
self.add_attr("hx-select-oob", element)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-boost attribute allows you to “boost” normal anchors and form tags to use AJAX instead.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_boost(self) -> Self {
|
||||||
|
self.add_attr("hx-boost", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-confirm attribute allows you to confirm an action before issuing a request.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_confirm(self, msg: &str) -> Self {
|
||||||
|
self.add_attr("hx-confirm", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-delete attribute will cause an element to issue a `DELETE` request to the specified URL and swap the HTML into the DOM using a swap strategy.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_delete(self, url: &str) -> Self {
|
||||||
|
self.add_attr("hx-delete", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-disable attribute will disable htmx processing for a given element and all its children.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_disable(self) -> Self {
|
||||||
|
self.add_attr("hx-disable", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-disabled-elt attribute allows you to specify elements that will have the disabled attribute added to them for the duration of the request.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_disabled_elt(self, element: Selector) -> Self {
|
||||||
|
self.add_attr("hx-disabled-elt", &element.to_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-disinherit attribute allows you to control automatic attribute inheritance.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_disinherit(self, attrs: &str) -> Self {
|
||||||
|
self.add_attr("hx-disinherit", attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-encoding attribute allows you to switch the request encoding from the usual `application/x-www-form-urlencoded` encoding to `multipart/form-data`, usually to support file uploads in an ajax request.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_encoding(self) -> Self {
|
||||||
|
self.add_attr("hx-encoding", "multipart/form-data")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-headers attribute allows you to add to the headers that will be submitted with an AJAX request.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_headers(self, headers: HashMap<String, String>) -> Self {
|
||||||
|
let json = serde_json::to_value(headers).unwrap();
|
||||||
|
let json_str = serde_json::to_string(&json).unwrap();
|
||||||
|
self.add_attr("hx-headers", &json_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-vals attribute allows you to add to the parameters that will be submitted with an AJAX request.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_vals(self, vals: HashMap<String, String>) -> Self {
|
||||||
|
let json = serde_json::to_value(vals).unwrap();
|
||||||
|
let json_str = serde_json::to_string(&json).unwrap();
|
||||||
|
self.add_attr("hx-vals", &json_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the hx-history attribute to false on any element in the current document, or any html fragment loaded into the current document by htmx, to prevent sensitive data being saved to the localStorage cache when htmx takes a snapshot of the page state.
|
||||||
|
///
|
||||||
|
/// History navigation will work as expected, but on restoration the URL will be requested from the server instead of the history cache.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_history(self) -> Self {
|
||||||
|
self.add_attr("hx-history", "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-history-elt attribute allows you to specify the element that will be used to snapshot and restore page state during navigation. By default, the body tag is used. This is typically good enough for most setups, but you may want to narrow it down to a child element. Just make sure that the element is always visible in your application, or htmx will not be able to restore history navigation properly.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_history_elt(self) -> Self {
|
||||||
|
self.add_attr("hx-history-elt", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-include attribute allows you to include additional element values in an AJAX request.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_include(self, element: Selector) -> Self {
|
||||||
|
self.add_attr("hx-include", &element.to_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-indicator attribute allows you to specify the element that will have the htmx-request class added to it for the duration of the request. This can be used to show spinners or progress indicators while the request is in flight.
|
||||||
|
///
|
||||||
|
/// Note: This attribute only supports CSS queries and `closest` match.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_indicator(self, indicator: Selector) -> Self {
|
||||||
|
self.add_attr("hx-indicator", &indicator.to_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-params attribute allows you to filter the parameters that will be submitted with an AJAX request.
|
||||||
|
///
|
||||||
|
/// The possible values of this attribute are:
|
||||||
|
/// `*` - Include all parameters (default)
|
||||||
|
/// `none` - Include no parameters
|
||||||
|
/// `not <param-list>` - Include all except the comma separated list of parameter names
|
||||||
|
/// `<param-list>` - Include all the comma separated list of parameter names
|
||||||
|
#[must_use]
|
||||||
|
fn hx_params(self, params: &str) -> Self {
|
||||||
|
self.add_attr("hx-params", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-patch attribute will cause an element to issue a PATCH to the specified URL and swap the HTML into the DOM using a swap strategy.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_patch(self, url: &str) -> Self {
|
||||||
|
self.add_attr("hx-patch", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-put attribute will cause an element to issue a PUT to the specified URL and swap the HTML into the DOM using a swap strategy
|
||||||
|
#[must_use]
|
||||||
|
fn hx_put(self, url: &str) -> Self {
|
||||||
|
self.add_attr("hx-put", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-replace-url attribute allows you to replace the current url of the browser location history.
|
||||||
|
///
|
||||||
|
/// The possible values of this attribute are:
|
||||||
|
/// `true`, which replaces the fetched URL in the browser navigation bar.
|
||||||
|
/// `false`, which disables replacing the fetched URL if it would otherwise be replaced due to inheritance.
|
||||||
|
/// A URL to be replaced into the location bar. This may be relative or absolute, as per `history.replaceState()`.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_replace_url(self, value: &str) -> Self {
|
||||||
|
self.add_attr("hx-replace-url", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-validate attribute will cause an element to validate itself by way of the HTML5 Validation API before it submits a request.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_validate(self) -> Self {
|
||||||
|
self.add_attr("hx-validte", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-preserve attribute allows you to keep an element unchanged during HTML replacement. Elements with hx-preserve set are preserved by id when htmx updates any ancestor element. You must set an unchanging id on elements for hx-preserve to work. The response requires an element with the same id, but its type and other attributes are ignored.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_preserve(self) -> Self {
|
||||||
|
self.add_attr("hx-preserve", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-prompt attribute allows you to show a prompt before issuing a request. The value of the prompt will be included in the request in the HX-Prompt header.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_prompt(self, msg: &str) -> Self {
|
||||||
|
self.add_attr("hx-prompt", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-swap attribute allows you to specify how the response will be swapped in relative to the target of an AJAX request.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_swap<T: Into<ModifiedSwapStrategy>>(self, swap: T) -> Self {
|
||||||
|
self.add_attr("hx-swap", &swap.into().to_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-swap-oob attribute allows you to specify that some content in a response should be swapped into the DOM somewhere other than the target, that is “Out of Band”. This allows you to piggy back updates to other element updates on a response.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_swap_oob(self) -> Self {
|
||||||
|
self.add_attr("hx-swap-oob", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-target attribute allows you to target a different element for swapping than the one issuing the AJAX request.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_target(self, element: Selector) -> Self {
|
||||||
|
self.add_attr("hx-target", &element.to_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-trigger attribute allows you to specify what triggers an AJAX request.
|
||||||
|
#[must_use]
|
||||||
|
fn hx_trigger<T: Into<Trigger>>(self, trigger: T) -> Self {
|
||||||
|
self.add_attr("hx-trigger", &trigger.into().to_value())
|
||||||
|
}
|
||||||
|
}
|
34
src/ui/htmx/selector.rs
Normal file
34
src/ui/htmx/selector.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
pub enum Selector {
|
||||||
|
/// A CSS query selector of the element
|
||||||
|
Query(String),
|
||||||
|
/// this element itself
|
||||||
|
This,
|
||||||
|
/// closest <CSS selector> which will find the closest ancestor element or itself, that matches the given CSS selector.
|
||||||
|
Closest(String),
|
||||||
|
/// find <CSS selector> which will find the first child descendant element that matches the given CSS selector
|
||||||
|
Find(String),
|
||||||
|
/// next which resolves to element.nextElementSibling
|
||||||
|
Next,
|
||||||
|
/// next <CSS selector> which will scan the DOM forward for the first element that matches the given CSS selector.
|
||||||
|
NextQuery(String),
|
||||||
|
/// previous which resolves to element.previousElementSibling
|
||||||
|
Previous,
|
||||||
|
/// previous <CSS selector> which will scan the DOM backwards for the first element that matches the given CSS selector.
|
||||||
|
PreviousQuery(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Selector {
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_value(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Query(query) => query.clone(),
|
||||||
|
Self::This => "this".to_owned(),
|
||||||
|
Self::Closest(css) => format!("closest {css}"),
|
||||||
|
Self::Find(css) => format!("find {css}"),
|
||||||
|
Self::Next => "next".to_owned(),
|
||||||
|
Self::NextQuery(css) => format!("next {css}"),
|
||||||
|
Self::Previous => "previous".to_owned(),
|
||||||
|
Self::PreviousQuery(css) => format!("previous {css}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
166
src/ui/htmx/swap.rs
Normal file
166
src/ui/htmx/swap.rs
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum SwapStrategy {
|
||||||
|
/// Replace the inner html of the target element
|
||||||
|
innerHTML,
|
||||||
|
/// Replace the entire target element with the response
|
||||||
|
outerHTML,
|
||||||
|
/// Replace the text content of the target element, without parsing the response as HTML
|
||||||
|
textContent,
|
||||||
|
/// Insert the response before the target element
|
||||||
|
beforebegin,
|
||||||
|
/// Insert the response before the first child of the target element
|
||||||
|
afterbegin,
|
||||||
|
/// Insert the response after the last child of the target element
|
||||||
|
beforeend,
|
||||||
|
/// Insert the response after the target element
|
||||||
|
afterend,
|
||||||
|
/// Deletes the target element regardless of the response
|
||||||
|
delete,
|
||||||
|
/// Does not append content from response (out of band items will still be processed).
|
||||||
|
none,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SwapStrategy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::innerHTML
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwapStrategy {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::innerHTML => "innerHTML",
|
||||||
|
Self::outerHTML => "outerHTML",
|
||||||
|
Self::textContent => "textContent",
|
||||||
|
Self::beforebegin => "beforebegin",
|
||||||
|
Self::afterbegin => "afterbegin",
|
||||||
|
Self::beforeend => "beforeend",
|
||||||
|
Self::afterend => "afterend",
|
||||||
|
Self::delete => "delete",
|
||||||
|
Self::none => "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If you want to use the new View Transitions API when a swap occurs, you can use the transition:true option for your swap.
|
||||||
|
#[must_use]
|
||||||
|
pub fn transition(self) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = "transition".to_owned();
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You can modify the amount of time that htmx will wait after receiving a response to swap the content by including a swap modifier.
|
||||||
|
#[must_use]
|
||||||
|
pub fn swap(self, duration: &str) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = format!("swap:{duration}");
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You can modify the time between the swap and the settle logic by including a settle modifier.
|
||||||
|
#[must_use]
|
||||||
|
pub fn settle(self, duration: &str) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = format!("settle:{duration}");
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// By default, htmx will update the title of the page if it finds a `<title>` tag in the response content. You can turn off this behavior.
|
||||||
|
#[must_use]
|
||||||
|
pub fn ignore_title(self) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = "ignoreTitle:true";
|
||||||
|
ModifiedSwapStrategy::new(self, modifier.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// htmx preserves focus between requests for inputs that have a defined id attribute. By default htmx prevents auto-scrolling to focused inputs between requests which can be unwanted behavior on longer requests when the user has already scrolled away.
|
||||||
|
#[must_use]
|
||||||
|
pub fn focus_scroll(self, enable: bool) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = format!("focus-scroll:{enable}");
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure visibility
|
||||||
|
#[must_use]
|
||||||
|
pub fn show(self, e: &str) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = format!("show:{e}");
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll to this location after load
|
||||||
|
#[must_use]
|
||||||
|
pub fn scroll(self, e: &str) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = format!("scroll:{e}");
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModifiedSwapStrategy {
|
||||||
|
pub strategy: SwapStrategy,
|
||||||
|
pub modifiers: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SwapStrategy> for ModifiedSwapStrategy {
|
||||||
|
fn from(value: SwapStrategy) -> Self {
|
||||||
|
Self::new(value, String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModifiedSwapStrategy {
|
||||||
|
fn new(strategy: SwapStrategy, modifier: String) -> Self {
|
||||||
|
Self {
|
||||||
|
strategy,
|
||||||
|
modifiers: vec![modifier],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If you want to use the new View Transitions API when a swap occurs, you can use the transition:true option for your swap.
|
||||||
|
pub fn transition(mut self) -> Self {
|
||||||
|
let modifier = "transition:true".to_owned();
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You can modify the amount of time that htmx will wait after receiving a response to swap the content by including a swap modifier.
|
||||||
|
pub fn swap(mut self, duration: &str) -> Self {
|
||||||
|
let modifier = format!("swap:{duration}");
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You can modify the time between the swap and the settle logic by including a settle modifier.
|
||||||
|
pub fn settle(mut self, duration: &str) -> Self {
|
||||||
|
let modifier = format!("settle:{duration}");
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// By default, htmx will update the title of the page if it finds a `<title>` tag in the response content. You can turn off this behavior.
|
||||||
|
pub fn ignore_title(mut self) -> Self {
|
||||||
|
let modifier = "ignoreTitle:true";
|
||||||
|
self.modifiers.push(modifier.to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// htmx preserves focus between requests for inputs that have a defined id attribute. By default htmx prevents auto-scrolling to focused inputs between requests which can be unwanted behavior on longer requests when the user has already scrolled away.
|
||||||
|
pub fn focus_scroll(mut self, enable: bool) -> Self {
|
||||||
|
let modifier = format!("focus-scroll:{enable}");
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure visibility
|
||||||
|
pub fn show(mut self, e: &str) -> Self {
|
||||||
|
let modifier = format!("show:{e}");
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll to this location after load
|
||||||
|
pub fn scroll(mut self, e: &str) -> Self {
|
||||||
|
let modifier = format!("scroll:{e}");
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_value(&self) -> String {
|
||||||
|
format!("{} {}", self.strategy.to_value(), self.modifiers.join(" "))
|
||||||
|
}
|
||||||
|
}
|
202
src/ui/htmx/trigger.rs
Normal file
202
src/ui/htmx/trigger.rs
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
use super::Selector;
|
||||||
|
|
||||||
|
pub struct Event {
|
||||||
|
modifiers: Vec<String>,
|
||||||
|
kind: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event {
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_value(&self) -> String {
|
||||||
|
if self.kind.starts_with("every") {
|
||||||
|
return self.kind.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{} {}", self.kind, self.modifiers.join(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a second event trigger
|
||||||
|
#[must_use]
|
||||||
|
pub fn and(self, event: Self) -> Trigger {
|
||||||
|
Trigger {
|
||||||
|
triggers: vec![self, event],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Periodically poll
|
||||||
|
///
|
||||||
|
/// Value can be something like `1s [someConditional]`
|
||||||
|
#[must_use]
|
||||||
|
pub fn poll(value: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
modifiers: vec![],
|
||||||
|
kind: format!("every {value}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standard events refer to web API events (e.g. `click`, `keydown`, `mouseup`, `load`).
|
||||||
|
#[allow(clippy::self_named_constructors)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn event(event: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
modifiers: vec![],
|
||||||
|
kind: event.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// triggered on load (useful for lazy-loading something)
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_load() -> Self {
|
||||||
|
Self {
|
||||||
|
modifiers: vec![],
|
||||||
|
kind: "load".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// triggered when an element is scrolled into the viewport (also useful for lazy-loading). If you are using overflow in css like `overflow-y: scroll` you should use `intersect once` instead of `revealed`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_revealed() -> Self {
|
||||||
|
Self {
|
||||||
|
modifiers: vec![],
|
||||||
|
kind: "revealed".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fires once when an element first intersects the viewport. This supports two additional options:
|
||||||
|
/// `root:<selector>` - a CSS selector of the root element for intersection
|
||||||
|
/// `threshold:<float>` - a floating point number between 0.0 and 1.0, indicating what amount of intersection to fire the event on
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_intersect(value: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
modifiers: vec![],
|
||||||
|
kind: format!("intersect:{value}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the event will only trigger once (e.g. the first click)
|
||||||
|
#[must_use]
|
||||||
|
pub fn once(mut self) -> Self {
|
||||||
|
self.modifiers.push("once".to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the event will only change if the value of the element has changed.
|
||||||
|
#[must_use]
|
||||||
|
pub fn changed(mut self) -> Self {
|
||||||
|
self.modifiers.push("changed".to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a delay will occur before an event triggers a request. If the event is seen again it will reset the delay.
|
||||||
|
#[must_use]
|
||||||
|
pub fn delay(mut self, delay: &str) -> Self {
|
||||||
|
self.modifiers.push(format!("delay:{delay}"));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a throttle will occur after an event triggers a request. If the event is seen again before the delay completes, it is ignored, the element will trigger at the end of the delay.
|
||||||
|
#[must_use]
|
||||||
|
pub fn throttle(mut self, delay: &str) -> Self {
|
||||||
|
self.modifiers.push(format!("throttle:{delay}"));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// allows the event that triggers a request to come from another element in the document (e.g. listening to a key event on the body, to support hot keys)
|
||||||
|
#[must_use]
|
||||||
|
pub fn from(mut self, element: Selector) -> Self {
|
||||||
|
self.modifiers.push(format!("from:{}", element.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// allows you to filter via a CSS selector on the target of the event. This can be useful when you want to listen for triggers from elements that might not be in the DOM at the point of initialization, by, for example, listening on the body, but with a target filter for a child element
|
||||||
|
#[must_use]
|
||||||
|
pub fn target(mut self, selector: &str) -> Self {
|
||||||
|
self.modifiers.push(format!("target:{selector}"));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// if this option is included the event will not trigger any other htmx requests on parents (or on elements listening on parents)
|
||||||
|
#[must_use]
|
||||||
|
pub fn consume(mut self) -> Self {
|
||||||
|
self.modifiers.push("consume".to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// determines how events are queued if an event occurs while a request for another event is in flight.
|
||||||
|
#[must_use]
|
||||||
|
pub fn queue(mut self, opt: QueueOption) -> Self {
|
||||||
|
self.modifiers.push(format!("queue:{}", opt.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum QueueOption {
|
||||||
|
/// queue the first event
|
||||||
|
first,
|
||||||
|
/// queue the last event (default)
|
||||||
|
#[default]
|
||||||
|
last,
|
||||||
|
/// queue all events (issue a request for each event)
|
||||||
|
all,
|
||||||
|
/// do not queue new events
|
||||||
|
none,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueueOption {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::first => "first",
|
||||||
|
Self::last => "last",
|
||||||
|
Self::all => "all",
|
||||||
|
Self::none => "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Trigger {
|
||||||
|
triggers: Vec<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Event> for Trigger {
|
||||||
|
fn from(value: Event) -> Self {
|
||||||
|
Self {
|
||||||
|
triggers: vec![value],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Trigger {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { triggers: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn push(mut self, event: Event) -> Self {
|
||||||
|
self.triggers.push(event);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn and(self, event: Event) -> Self {
|
||||||
|
self.push(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_value(&self) -> String {
|
||||||
|
self.triggers
|
||||||
|
.iter()
|
||||||
|
.map(Event::to_value)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Trigger {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
165
src/ui/mod.rs
Normal file
165
src/ui/mod.rs
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
|
use prelude::Div;
|
||||||
|
|
||||||
|
// UI
|
||||||
|
|
||||||
|
// Basic Primitives
|
||||||
|
pub mod color;
|
||||||
|
pub mod htmx;
|
||||||
|
pub mod primitives;
|
||||||
|
pub mod wrapper;
|
||||||
|
|
||||||
|
// Complex Components
|
||||||
|
pub mod components;
|
||||||
|
|
||||||
|
// Preludes
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::color::*;
|
||||||
|
pub use super::primitives::animation::{Animated, Animation, Delay, Duration, Scope, Timing};
|
||||||
|
pub use super::primitives::background::{
|
||||||
|
Background, BackgroundClip, BackgroundGradient, BackgroundOrigin, BackgroundPosition,
|
||||||
|
BackgroundRepeat, BackgroundScrollAttachment, BackgroundSize,
|
||||||
|
};
|
||||||
|
pub use super::primitives::border::{
|
||||||
|
Border, BorderSide, BorderSize, BorderStyle, Outline, OutlineStyle, Ring,
|
||||||
|
};
|
||||||
|
pub use super::primitives::container::Container;
|
||||||
|
pub use super::primitives::cursor::{Action, Cursor, TouchAction};
|
||||||
|
pub use super::primitives::display::{
|
||||||
|
AlignSelf, Aspect, BoxDecorationBreak, BoxSizing, BreakAfter, BreakBefore, BreakInside,
|
||||||
|
BreakInsideValue, BreakValue, Clear, Display, Float, JustifySelf, ObjectFit, Overflow,
|
||||||
|
PlaceSelf, ZIndex,
|
||||||
|
};
|
||||||
|
pub use super::primitives::div::Div;
|
||||||
|
pub use super::primitives::filter::{
|
||||||
|
BackgroundBlendMode, BlendMode, Blur, Brightness, Contrast, Grayscale, HueRotate, Invert,
|
||||||
|
MixBlendMode, Opacity, Saturate, Sepia,
|
||||||
|
};
|
||||||
|
pub use super::primitives::flex::{
|
||||||
|
AlignContent, AlignItems, Direction, DivideStyle, DivideWidth, Flex, FlexBasis, FlexGrow,
|
||||||
|
Justify, JustifyItems, Order, Strategy, Wrap,
|
||||||
|
};
|
||||||
|
pub use super::primitives::grid::{
|
||||||
|
Columns, Grid, GridAmount, GridAutoFlow, GridAutoSize, GridElementColumn, GridElementRow,
|
||||||
|
GridElementValue,
|
||||||
|
};
|
||||||
|
pub use super::primitives::height::{Height, MaxHeight, MinHeight};
|
||||||
|
pub use super::primitives::image::{Image, Source, Video};
|
||||||
|
pub use super::primitives::width::{MaxWidth, MinWidth, Width};
|
||||||
|
pub use super::primitives::{Context, NoBrowserAppearance, Nothing, Side, Size, script};
|
||||||
|
// ENSURE: prelude
|
||||||
|
pub use super::primitives::input::*;
|
||||||
|
pub use super::primitives::link::Link;
|
||||||
|
pub use super::primitives::list::{OrderedList, UnorderedList};
|
||||||
|
pub use super::primitives::margin::Margin;
|
||||||
|
pub use super::primitives::padding::Padding;
|
||||||
|
pub use super::primitives::position::{
|
||||||
|
ObjectPosition, Position, PositionKind, Resize, Resizeable,
|
||||||
|
};
|
||||||
|
pub use super::primitives::rounded::Rounded;
|
||||||
|
pub use super::primitives::scroll::{Overscroll, Scroll, SnapAlign, SnapType};
|
||||||
|
pub use super::primitives::shadow::{DropShadow, Shadow};
|
||||||
|
pub use super::primitives::sized::Sized;
|
||||||
|
pub use super::primitives::space::{Fraction, ScreenValue, SpaceBetween};
|
||||||
|
pub use super::primitives::svg::SVG;
|
||||||
|
pub use super::primitives::table::{Caption, Header, Table, TableData, TableHead, TableRow};
|
||||||
|
pub use super::primitives::text::{
|
||||||
|
AccentColor, Code, DecorationKind, DecorationStyle, DecorationThickness, LetterSpacing,
|
||||||
|
LineClamp, LineHeight, ListStyle, NumberStyle, Paragraph, Span, Text, TextAlignment,
|
||||||
|
TextContent, TextCursorColor, TextDecoration, TextHyphens, TextOverflow, TextSelection,
|
||||||
|
TextTransform, TextWhitespace, TextWordBreak, TextWrap, UnderlineOffset,
|
||||||
|
VerticalTextAlignment,
|
||||||
|
};
|
||||||
|
pub use super::primitives::transform::{
|
||||||
|
RenderTransformCPU, RenderTransformGPU, Rotate, Scale, Skew, SkewValue, Transform,
|
||||||
|
TransformOrigin,
|
||||||
|
};
|
||||||
|
pub use super::primitives::visibility::Visibility;
|
||||||
|
pub use super::wrapper::{
|
||||||
|
_2XLScreen, Hover, LargeScreen, MediumScreen, Screen, SmallScreen, XLScreen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic UI Widget
|
||||||
|
pub trait UIWidget: Render {
|
||||||
|
/// Indicating if the widget supports inheriting classes
|
||||||
|
fn can_inherit(&self) -> bool;
|
||||||
|
|
||||||
|
/// Returning the base classes for this widget
|
||||||
|
///
|
||||||
|
/// Base here means all classes defining the current widget
|
||||||
|
fn base_class(&self) -> Vec<String>;
|
||||||
|
|
||||||
|
/// Return own base classes and all classes below the tree
|
||||||
|
fn extended_class(&self) -> Vec<String>;
|
||||||
|
|
||||||
|
/// Render the widget with additional classes
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation for raw HTML with html! macro
|
||||||
|
impl UIWidget for PreEscaped<String> {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
self.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for String {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
html!((self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for &str {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
html!((self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for an element which can add new `attrs`
|
||||||
|
pub trait AttrExtendable: Sized {
|
||||||
|
#[must_use]
|
||||||
|
fn add_attr(self, key: &str, val: &str) -> Self;
|
||||||
|
|
||||||
|
/// Set the `id` attribute of an element.
|
||||||
|
#[must_use]
|
||||||
|
fn id(self, id: &str) -> Self {
|
||||||
|
self.add_attr("id", id)
|
||||||
|
}
|
||||||
|
}
|
205
src/ui/primitives/animation.rs
Normal file
205
src/ui/primitives/animation.rs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Animated<T: UIWidget + 'static>(inner: T) -> AnimatedWidget {
|
||||||
|
AnimatedWidget {
|
||||||
|
inner: Box::new(inner),
|
||||||
|
scope: Scope::Normal,
|
||||||
|
timing: None,
|
||||||
|
delay: None,
|
||||||
|
duration: None,
|
||||||
|
animation: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AnimatedWidget {
|
||||||
|
inner: Box<dyn UIWidget>,
|
||||||
|
scope: Scope,
|
||||||
|
timing: Option<Timing>,
|
||||||
|
delay: Option<Delay>,
|
||||||
|
duration: Option<Duration>,
|
||||||
|
animation: Option<Animation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! setter {
|
||||||
|
($fnname:ident, $varname:ident, $vartype:ident, $internal_var:ident) => {
|
||||||
|
#[must_use]
|
||||||
|
pub fn $fnname(mut self, $varname: $vartype) -> Self {
|
||||||
|
self.$internal_var = $varname;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! setter_opt {
|
||||||
|
($fnname:ident, $varname:ident, $vartype:ident, $internal_var:ident) => {
|
||||||
|
#[must_use]
|
||||||
|
pub fn $fnname(mut self, $varname: $vartype) -> Self {
|
||||||
|
self.$internal_var = Some($varname);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimatedWidget {
|
||||||
|
setter!(scope, scope, Scope, scope);
|
||||||
|
setter_opt!(timing, timing, Timing, timing);
|
||||||
|
setter_opt!(delay, delay, Delay, delay);
|
||||||
|
setter_opt!(duration, duration, Duration, duration);
|
||||||
|
setter_opt!(animate, animation, Animation, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AnimatedWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for AnimatedWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = vec![self.scope.to_value().to_string()];
|
||||||
|
|
||||||
|
if let Some(timing) = &self.timing {
|
||||||
|
ret.push(timing.to_value().to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(delay) = &self.delay {
|
||||||
|
ret.push(delay.to_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(duration) = &self.duration {
|
||||||
|
ret.push(duration.to_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(anim) = &self.animation {
|
||||||
|
ret.push(anim.to_value().to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.inner.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.inner.as_ref().can_inherit() {
|
||||||
|
self.inner
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.inner.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Scope {
|
||||||
|
None,
|
||||||
|
All,
|
||||||
|
Normal,
|
||||||
|
Colors,
|
||||||
|
Opacity,
|
||||||
|
Shadow,
|
||||||
|
Transform,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scope {
|
||||||
|
pub fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Scope::None => "transition-none",
|
||||||
|
Scope::All => "transition-all",
|
||||||
|
Scope::Normal => "transition",
|
||||||
|
Scope::Colors => "transition-colors",
|
||||||
|
Scope::Opacity => "transition-opacity",
|
||||||
|
Scope::Shadow => "transition-shadow",
|
||||||
|
Scope::Transform => "transition-transform",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! num_opt {
|
||||||
|
($name:ident, $id:literal) => {
|
||||||
|
pub enum $name {
|
||||||
|
Custom(String),
|
||||||
|
_0,
|
||||||
|
_75,
|
||||||
|
_100,
|
||||||
|
_150,
|
||||||
|
_200,
|
||||||
|
_300,
|
||||||
|
_500,
|
||||||
|
_700,
|
||||||
|
_1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
pub fn to_value(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Custom(s) => format!("{}-[{s}]", $id),
|
||||||
|
Self::_0 => concat!($id, "-0").to_string(),
|
||||||
|
Self::_75 => concat!($id, "-75").to_string(),
|
||||||
|
Self::_100 => concat!($id, "-100").to_string(),
|
||||||
|
Self::_150 => concat!($id, "-150").to_string(),
|
||||||
|
Self::_200 => concat!($id, "-200").to_string(),
|
||||||
|
Self::_300 => concat!($id, "-300").to_string(),
|
||||||
|
Self::_500 => concat!($id, "-500").to_string(),
|
||||||
|
Self::_700 => concat!($id, "-700").to_string(),
|
||||||
|
Self::_1000 => concat!($id, "-1000").to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
num_opt!(Duration, "duration");
|
||||||
|
num_opt!(Delay, "delay");
|
||||||
|
|
||||||
|
pub enum Timing {
|
||||||
|
EaseLinear,
|
||||||
|
EaseIn,
|
||||||
|
EaseOut,
|
||||||
|
EaseInOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timing {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Timing::EaseLinear => "ease-linear",
|
||||||
|
Timing::EaseIn => "ease-in",
|
||||||
|
Timing::EaseOut => "ease-out",
|
||||||
|
Timing::EaseInOut => "ease-in-out",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Animation {
|
||||||
|
None,
|
||||||
|
Spin,
|
||||||
|
Ping,
|
||||||
|
Pulse,
|
||||||
|
Bounce,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation {
|
||||||
|
pub fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Animation::None => "animate-none",
|
||||||
|
Animation::Spin => "animate-spin",
|
||||||
|
Animation::Ping => "animate-ping",
|
||||||
|
Animation::Pulse => "animate-pulse",
|
||||||
|
Animation::Bounce => "animate-bounce",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
321
src/ui/primitives/background.rs
Normal file
321
src/ui/primitives/background.rs
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::{
|
||||||
|
UIWidget,
|
||||||
|
color::{Gradient, UIColor},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Background<T: UIWidget + 'static>(inner: T) -> BackgroundWidget {
|
||||||
|
BackgroundWidget(
|
||||||
|
Box::new(inner),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BackgroundWidget(
|
||||||
|
// Inner
|
||||||
|
Box<dyn UIWidget>,
|
||||||
|
// Background Color
|
||||||
|
Option<Box<dyn UIColor>>,
|
||||||
|
// Background Attachment
|
||||||
|
Option<BackgroundScrollAttachment>,
|
||||||
|
Option<BackgroundClip>,
|
||||||
|
Option<BackgroundOrigin>,
|
||||||
|
Option<BackgroundRepeat>,
|
||||||
|
Option<BackgroundSize>,
|
||||||
|
// Background Image URL
|
||||||
|
Option<String>,
|
||||||
|
// Gradient
|
||||||
|
Option<BackgroundGradient>,
|
||||||
|
Option<Gradient>,
|
||||||
|
Option<BackgroundPosition>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl BackgroundWidget {
|
||||||
|
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.1 = Some(Box::new(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn image(mut self, url: &str) -> Self {
|
||||||
|
self.7 = Some(url.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn none(mut self) -> Self {
|
||||||
|
self.8 = Some(BackgroundGradient::None);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gradient(mut self, direction: BackgroundGradient, gradient: Gradient) -> Self {
|
||||||
|
self.8 = Some(direction);
|
||||||
|
self.9 = Some(gradient);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn position(mut self, position: BackgroundPosition) -> Self {
|
||||||
|
self.10 = Some(position);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll(mut self, attachment: BackgroundScrollAttachment) -> Self {
|
||||||
|
self.2 = Some(attachment);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clip(mut self, clip: BackgroundClip) -> Self {
|
||||||
|
self.3 = Some(clip);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn origin(mut self, origin: BackgroundOrigin) -> Self {
|
||||||
|
self.4 = Some(origin);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn repeat(mut self, repeat: BackgroundRepeat) -> Self {
|
||||||
|
self.5 = Some(repeat);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(mut self, size: BackgroundSize) -> Self {
|
||||||
|
self.6 = Some(size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for BackgroundWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for BackgroundWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
|
||||||
|
if let Some(color) = &self.1 {
|
||||||
|
ret.push(format!("bg-{}", color.color_class()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(attachment) = &self.2 {
|
||||||
|
ret.push(attachment.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(clip) = &self.3 {
|
||||||
|
ret.push(clip.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(origin) = &self.4 {
|
||||||
|
ret.push(origin.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(repeat) = &self.5 {
|
||||||
|
ret.push(repeat.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(size) = &self.6 {
|
||||||
|
ret.push(size.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(image) = &self.7 {
|
||||||
|
ret.push(format!("bg-[url('{image}')]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(gradient) = &self.8 {
|
||||||
|
ret.push(gradient.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(gradient) = &self.9 {
|
||||||
|
ret.extend_from_slice(&gradient.color_class());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(position) = &self.10 {
|
||||||
|
ret.push(position.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controlling how a background image behaves when scrolling.
|
||||||
|
pub enum BackgroundScrollAttachment {
|
||||||
|
/// Fix the background image relative to the viewport.
|
||||||
|
Fixed,
|
||||||
|
/// Scroll the background image with the container and the viewport.
|
||||||
|
Local,
|
||||||
|
/// Scroll the background image with the viewport, but not with the container.
|
||||||
|
Scroll,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundScrollAttachment {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BackgroundScrollAttachment::Fixed => "bg-fixed",
|
||||||
|
BackgroundScrollAttachment::Local => "bg-local",
|
||||||
|
BackgroundScrollAttachment::Scroll => "bg-scroll",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BackgroundClip {
|
||||||
|
Border,
|
||||||
|
Padding,
|
||||||
|
Content,
|
||||||
|
Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundClip {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BackgroundClip::Border => "bg-clip-border",
|
||||||
|
BackgroundClip::Padding => "bg-clip-padding",
|
||||||
|
BackgroundClip::Content => "bg-clip-content",
|
||||||
|
BackgroundClip::Text => "bg-clip-text",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BackgroundOrigin {
|
||||||
|
Border,
|
||||||
|
Padding,
|
||||||
|
Content,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundOrigin {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BackgroundOrigin::Border => "bg-origin-border",
|
||||||
|
BackgroundOrigin::Padding => "bg-origin-padding",
|
||||||
|
BackgroundOrigin::Content => "bg-origin-content",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BackgroundRepeat {
|
||||||
|
Repeat,
|
||||||
|
NoRepeat,
|
||||||
|
RepeatX,
|
||||||
|
RepeatY,
|
||||||
|
Round,
|
||||||
|
Space,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundRepeat {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BackgroundRepeat::Repeat => "bg-repeat",
|
||||||
|
BackgroundRepeat::NoRepeat => "bg-no-repeat",
|
||||||
|
BackgroundRepeat::RepeatX => "bg-repeat-x",
|
||||||
|
BackgroundRepeat::RepeatY => "bg-repeat-y",
|
||||||
|
BackgroundRepeat::Round => "bg-repeat-round",
|
||||||
|
BackgroundRepeat::Space => "bg-repeat-space",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BackgroundSize {
|
||||||
|
Auto,
|
||||||
|
Cover,
|
||||||
|
Contain,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundSize {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BackgroundSize::Auto => "bg-auto",
|
||||||
|
BackgroundSize::Cover => "bg-cover",
|
||||||
|
BackgroundSize::Contain => "bg-contain",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BackgroundGradient {
|
||||||
|
None,
|
||||||
|
ToTop,
|
||||||
|
ToTopRight,
|
||||||
|
ToRight,
|
||||||
|
ToBottomRight,
|
||||||
|
ToBottom,
|
||||||
|
ToBottomLeft,
|
||||||
|
ToLeft,
|
||||||
|
ToTopLeft,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundGradient {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BackgroundGradient::None => "bg-none",
|
||||||
|
BackgroundGradient::ToTop => "bg-gradient-to-t",
|
||||||
|
BackgroundGradient::ToTopRight => "bg-gradient-to-tr",
|
||||||
|
BackgroundGradient::ToRight => "bg-gradient-to-r",
|
||||||
|
BackgroundGradient::ToBottomRight => "bg-gradient-to-br",
|
||||||
|
BackgroundGradient::ToBottom => "bg-gradient-to-b",
|
||||||
|
BackgroundGradient::ToBottomLeft => "bg-gradient-to-bl",
|
||||||
|
BackgroundGradient::ToLeft => "bg-gradient-to-l",
|
||||||
|
BackgroundGradient::ToTopLeft => "bg-gradient-to-tl",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BackgroundPosition {
|
||||||
|
Bottom,
|
||||||
|
Center,
|
||||||
|
Left,
|
||||||
|
LeftBottom,
|
||||||
|
LeftTop,
|
||||||
|
Right,
|
||||||
|
RightBottom,
|
||||||
|
RightTop,
|
||||||
|
Top,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundPosition {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BackgroundPosition::Bottom => "bg-bottom",
|
||||||
|
BackgroundPosition::Center => "bg-center",
|
||||||
|
BackgroundPosition::Left => "bg-left",
|
||||||
|
BackgroundPosition::LeftBottom => "bg-left-bottom",
|
||||||
|
BackgroundPosition::LeftTop => "bg-left-top",
|
||||||
|
BackgroundPosition::Right => "bg-right",
|
||||||
|
BackgroundPosition::RightBottom => "bg-right-bottom",
|
||||||
|
BackgroundPosition::RightTop => "bg-right-top",
|
||||||
|
BackgroundPosition::Top => "bg-top",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
397
src/ui/primitives/border.rs
Normal file
397
src/ui/primitives/border.rs
Normal file
|
@ -0,0 +1,397 @@
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::{UIWidget, color::UIColor};
|
||||||
|
|
||||||
|
pub enum BorderSize {
|
||||||
|
_0,
|
||||||
|
_2,
|
||||||
|
_4,
|
||||||
|
_8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorderSize {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BorderSize::_0 => "0",
|
||||||
|
BorderSize::_2 => "2",
|
||||||
|
BorderSize::_4 => "4",
|
||||||
|
BorderSize::_8 => "8",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BorderSide {
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Top,
|
||||||
|
Right,
|
||||||
|
Bottom,
|
||||||
|
Left,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorderSide {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BorderSide::X => "x",
|
||||||
|
BorderSide::Y => "y",
|
||||||
|
BorderSide::Start => "s",
|
||||||
|
BorderSide::End => "e",
|
||||||
|
BorderSide::Top => "t",
|
||||||
|
BorderSide::Right => "r",
|
||||||
|
BorderSide::Bottom => "b",
|
||||||
|
BorderSide::Left => "l",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BorderStyle {
|
||||||
|
Solid,
|
||||||
|
Dashed,
|
||||||
|
Dotted,
|
||||||
|
Double,
|
||||||
|
Hidden,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorderStyle {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BorderStyle::Solid => "border-solid",
|
||||||
|
BorderStyle::Dashed => "border-dashed",
|
||||||
|
BorderStyle::Dotted => "border-dotted",
|
||||||
|
BorderStyle::Double => "border-double",
|
||||||
|
BorderStyle::Hidden => "border-hidden",
|
||||||
|
BorderStyle::None => "border-none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Border<T: UIWidget + 'static>(inner: T) -> BorderWidget {
|
||||||
|
BorderWidget(Box::new(inner), None, None, None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BorderWidget(
|
||||||
|
Box<dyn UIWidget>,
|
||||||
|
Option<BorderSize>,
|
||||||
|
Option<BorderSide>,
|
||||||
|
Option<Box<dyn UIColor>>,
|
||||||
|
Option<BorderStyle>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl BorderWidget {
|
||||||
|
#[must_use]
|
||||||
|
pub fn size(mut self, size: BorderSize) -> Self {
|
||||||
|
self.1 = Some(size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn side(mut self, side: BorderSide) -> Self {
|
||||||
|
self.2 = Some(side);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn style(mut self, style: BorderStyle) -> Self {
|
||||||
|
self.4 = Some(style);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.3 = Some(Box::new(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn border_class(&self) -> String {
|
||||||
|
if let Some(side) = &self.2 {
|
||||||
|
if let Some(size) = &self.1 {
|
||||||
|
return format!("border-{}-{}", side.to_value(), size.to_value());
|
||||||
|
}
|
||||||
|
} else if let Some(size) = &self.1 {
|
||||||
|
return format!("border-{}", size.to_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
"border".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for BorderWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for BorderWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = vec![self.border_class()];
|
||||||
|
|
||||||
|
if let Some(color) = &self.3 {
|
||||||
|
ret.push(format!("border-{}", color.color_class()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(style) = &self.4 {
|
||||||
|
ret.push(style.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum OutlineStyle {
|
||||||
|
Solid,
|
||||||
|
Dashed,
|
||||||
|
Dotted,
|
||||||
|
Double,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutlineStyle {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
OutlineStyle::Solid => "outline",
|
||||||
|
OutlineStyle::Dashed => "outline-dashed",
|
||||||
|
OutlineStyle::Dotted => "outline-dotted",
|
||||||
|
OutlineStyle::Double => "outline-double",
|
||||||
|
OutlineStyle::None => "outline-none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Outline<T: UIWidget + 'static>(width: u32, inner: T) -> OutlineWidget {
|
||||||
|
OutlineWidget(Box::new(inner), width, None, None, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OutlineWidget(
|
||||||
|
Box<dyn UIWidget>,
|
||||||
|
u32,
|
||||||
|
Option<Box<dyn UIColor>>,
|
||||||
|
Option<OutlineStyle>,
|
||||||
|
u32,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl OutlineWidget {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn offset(mut self, offset: u32) -> Self {
|
||||||
|
self.4 = offset;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn style(mut self, style: OutlineStyle) -> Self {
|
||||||
|
self.3 = Some(style);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.2 = Some(Box::new(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for OutlineWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for OutlineWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let class = match self.1 {
|
||||||
|
0 => "outline-0",
|
||||||
|
1 => "outline-1",
|
||||||
|
2 => "outline-2",
|
||||||
|
4 => "outline-4",
|
||||||
|
8 => "outline-8",
|
||||||
|
_ => &format!("outline-[{}px]", self.1),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ret = vec![class.to_string()];
|
||||||
|
|
||||||
|
if let Some(color) = &self.2 {
|
||||||
|
ret.push(format!("outline-{}", color.color_class()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(style) = &self.3 {
|
||||||
|
ret.push(style.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push(match self.4 {
|
||||||
|
0 => "outline-offset-0".to_string(),
|
||||||
|
1 => "outline-offset-1".to_string(),
|
||||||
|
2 => "outline-offset-2".to_string(),
|
||||||
|
4 => "outline-offset-4".to_string(),
|
||||||
|
8 => "outline-offset-8".to_string(),
|
||||||
|
_ => format!("outline-offset-[{}px]", self.4),
|
||||||
|
});
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Ring<T: UIWidget + 'static>(width: u32, inner: T) -> RingWidget {
|
||||||
|
RingWidget(Box::new(inner), width, None, false, 0, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RingWidget(
|
||||||
|
// Inner
|
||||||
|
Box<dyn UIWidget>,
|
||||||
|
// Size
|
||||||
|
u32,
|
||||||
|
// Color
|
||||||
|
Option<Box<dyn UIColor>>,
|
||||||
|
// Inset
|
||||||
|
bool,
|
||||||
|
// Offset Width
|
||||||
|
u32,
|
||||||
|
// Offset Color
|
||||||
|
Option<Box<dyn UIColor>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl RingWidget {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn inset(mut self) -> Self {
|
||||||
|
self.3 = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn offset_width(mut self, offset: u32) -> Self {
|
||||||
|
self.4 = offset;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn offset_color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.5 = Some(Box::new(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.2 = Some(Box::new(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for RingWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for RingWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let class = match self.1 {
|
||||||
|
0 => "ring-0",
|
||||||
|
1 => "ring-1",
|
||||||
|
2 => "ring-2",
|
||||||
|
4 => "ring-4",
|
||||||
|
8 => "ring-8",
|
||||||
|
_ => &format!("ring-[{}px]", self.1),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ret = vec![class.to_string()];
|
||||||
|
|
||||||
|
if let Some(color) = &self.2 {
|
||||||
|
ret.push(format!("ring-{}", color.color_class()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.3 {
|
||||||
|
ret.push("ring-inset".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push(match self.4 {
|
||||||
|
0 => "ring-offset-0".to_string(),
|
||||||
|
1 => "ring-offset-1".to_string(),
|
||||||
|
2 => "ring-offset-2".to_string(),
|
||||||
|
4 => "ring-offset-4".to_string(),
|
||||||
|
8 => "ring-offset-8".to_string(),
|
||||||
|
_ => format!("ring-offset-[{}px]", self.4),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(color) = &self.5 {
|
||||||
|
ret.push(format!("ring-offset-{}", color.color_class()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
src/ui/primitives/container.rs
Normal file
47
src/ui/primitives/container.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// A component for fixing an element's width to the current breakpoint.
|
||||||
|
pub fn Container<T: UIWidget + 'static>(inner: T) -> ContainerWidget {
|
||||||
|
ContainerWidget(Box::new(inner))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContainerWidget(Box<dyn UIWidget>);
|
||||||
|
|
||||||
|
impl Render for ContainerWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for ContainerWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec!["container".to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("container {class}"))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("container {class}")) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
197
src/ui/primitives/cursor.rs
Normal file
197
src/ui/primitives/cursor.rs
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
pub enum Cursor {
|
||||||
|
Auto,
|
||||||
|
Default,
|
||||||
|
Pointer,
|
||||||
|
Wait,
|
||||||
|
Text,
|
||||||
|
Move,
|
||||||
|
Help,
|
||||||
|
NotAllowed,
|
||||||
|
None,
|
||||||
|
ContextMenu,
|
||||||
|
Progress,
|
||||||
|
Cell,
|
||||||
|
Crosshair,
|
||||||
|
VerticalText,
|
||||||
|
Alias,
|
||||||
|
Copy,
|
||||||
|
NoDrop,
|
||||||
|
Grab,
|
||||||
|
Grabbing,
|
||||||
|
AllScroll,
|
||||||
|
ColResize,
|
||||||
|
RowResize,
|
||||||
|
NorthResize,
|
||||||
|
EastResize,
|
||||||
|
SouthResize,
|
||||||
|
WestResize,
|
||||||
|
NorthEastResize,
|
||||||
|
NorthWestResize,
|
||||||
|
SouthEastResize,
|
||||||
|
SouthWestResize,
|
||||||
|
EastWestResize,
|
||||||
|
NorthSouthResize,
|
||||||
|
NorthEastSouthWestResize,
|
||||||
|
NorthWestSouthEastResize,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cursor {
|
||||||
|
pub fn on<T: UIWidget + 'static>(self, inner: T) -> CursorWidget {
|
||||||
|
CursorWidget(self, Box::new(inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CursorWidget(Cursor, Box<dyn UIWidget>);
|
||||||
|
|
||||||
|
impl Render for CursorWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for CursorWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let class = match self.0 {
|
||||||
|
Cursor::Auto => "cursor-auto",
|
||||||
|
Cursor::Default => "cursor-default",
|
||||||
|
Cursor::Pointer => "cursor-pointer",
|
||||||
|
Cursor::Wait => "cursor-wait",
|
||||||
|
Cursor::Text => "cursor-text",
|
||||||
|
Cursor::Move => "cursor-move",
|
||||||
|
Cursor::Help => "cursor-help",
|
||||||
|
Cursor::NotAllowed => "cursor-not-allowed",
|
||||||
|
Cursor::None => "cursor-none",
|
||||||
|
Cursor::ContextMenu => "cursor-context-menu",
|
||||||
|
Cursor::Progress => "cursor-progress",
|
||||||
|
Cursor::Cell => "cursor-cell",
|
||||||
|
Cursor::Crosshair => "cursor-crosshair",
|
||||||
|
Cursor::VerticalText => "cursor-vertical-text",
|
||||||
|
Cursor::Alias => "cursor-alias",
|
||||||
|
Cursor::Copy => "cursor-copy",
|
||||||
|
Cursor::NoDrop => "cursor-no-drop",
|
||||||
|
Cursor::Grab => "cursor-grab",
|
||||||
|
Cursor::Grabbing => "cursor-grabbing",
|
||||||
|
Cursor::AllScroll => "cursor-all-scroll",
|
||||||
|
Cursor::ColResize => "cursor-col-resize",
|
||||||
|
Cursor::RowResize => "cursor-row-resize",
|
||||||
|
Cursor::NorthResize => "cursor-n-resize",
|
||||||
|
Cursor::EastResize => "cursor-e-resize",
|
||||||
|
Cursor::SouthResize => "cursor-s-resize",
|
||||||
|
Cursor::WestResize => "cursor-w-resize",
|
||||||
|
Cursor::NorthEastResize => "cursor-ne-resize",
|
||||||
|
Cursor::NorthWestResize => "cursor-nw-resize",
|
||||||
|
Cursor::SouthEastResize => "cursor-se-resize",
|
||||||
|
Cursor::SouthWestResize => "cursor-sw-resize",
|
||||||
|
Cursor::EastWestResize => "cursor-ew-resize",
|
||||||
|
Cursor::NorthSouthResize => "cursor-ns-resize",
|
||||||
|
Cursor::NorthEastSouthWestResize => "cursor-nesw-resize",
|
||||||
|
Cursor::NorthWestSouthEastResize => "cursor-nwse-resize",
|
||||||
|
Cursor::ZoomIn => "cursor-zoom-in",
|
||||||
|
Cursor::ZoomOut => "cursor-zoom-out",
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![class.to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.1.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let inner = &self.1;
|
||||||
|
|
||||||
|
if inner.can_inherit() {
|
||||||
|
inner.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn TouchAction<T: UIWidget + 'static>(action: Action, inner: T) -> TouchActionWidget {
|
||||||
|
TouchActionWidget(Box::new(inner), action)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TouchActionWidget(Box<dyn UIWidget>, Action);
|
||||||
|
|
||||||
|
impl Render for TouchActionWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for TouchActionWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![self.1.to_value().to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Action {
|
||||||
|
Auto,
|
||||||
|
None,
|
||||||
|
PanX,
|
||||||
|
PanLeft,
|
||||||
|
PanRight,
|
||||||
|
PanY,
|
||||||
|
PanUp,
|
||||||
|
PanDown,
|
||||||
|
PinchZoom,
|
||||||
|
Manipulation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Action::Auto => "touch-auto",
|
||||||
|
Action::None => "touch-none",
|
||||||
|
Action::PanX => "touch-pan-x",
|
||||||
|
Action::PanLeft => "touch-pan-left",
|
||||||
|
Action::PanRight => "touch-pan-right",
|
||||||
|
Action::PanY => "touch-pan-y",
|
||||||
|
Action::PanUp => "touch-pan-up",
|
||||||
|
Action::PanDown => "touch-pan-down",
|
||||||
|
Action::PinchZoom => "touch-pinch-zoom",
|
||||||
|
Action::Manipulation => "touch-manipulation",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
334
src/ui/primitives/display.rs
Normal file
334
src/ui/primitives/display.rs
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
macro_rules! string_class_widget {
|
||||||
|
($name:ident) => {
|
||||||
|
pub struct $name(Box<dyn UIWidget>, String);
|
||||||
|
|
||||||
|
impl Render for $name {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for $name {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![self.1.clone()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! constructor {
|
||||||
|
($name:ident, $class:literal) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn $name<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), $class.to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BreakValue {
|
||||||
|
Auto,
|
||||||
|
Avoid,
|
||||||
|
All,
|
||||||
|
AvoidPage,
|
||||||
|
Page,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Column,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BreakValue {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BreakValue::Auto => "auto",
|
||||||
|
BreakValue::Avoid => "avoid",
|
||||||
|
BreakValue::All => "all",
|
||||||
|
BreakValue::AvoidPage => "break-page",
|
||||||
|
BreakValue::Page => "page",
|
||||||
|
BreakValue::Left => "left",
|
||||||
|
BreakValue::Right => "right",
|
||||||
|
BreakValue::Column => "column",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn BreakAfter<T: UIWidget + 'static>(value: BreakValue, inner: T) -> BreakWidget {
|
||||||
|
BreakWidget(Box::new(inner), false, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn BreakBefore<T: UIWidget + 'static>(value: BreakValue, inner: T) -> BreakWidget {
|
||||||
|
BreakWidget(Box::new(inner), true, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BreakWidget(Box<dyn UIWidget>, bool, BreakValue);
|
||||||
|
|
||||||
|
impl Render for BreakWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for BreakWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
if self.1 {
|
||||||
|
vec![format!("break-before-{}", self.2.to_value())]
|
||||||
|
} else {
|
||||||
|
vec![format!("break-after-{}", self.2.to_value())]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BreakInsideValue {
|
||||||
|
Auto,
|
||||||
|
Avoid,
|
||||||
|
AvoidPage,
|
||||||
|
AvoidColumn,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BreakInsideValue {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BreakInsideValue::Auto => "break-inside-auto",
|
||||||
|
BreakInsideValue::Avoid => "break-inside-avoid",
|
||||||
|
BreakInsideValue::AvoidPage => "break-inside-avoid-page",
|
||||||
|
BreakInsideValue::AvoidColumn => "break-inside-avoid-column",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn BreakInside<T: UIWidget + 'static>(value: BreakValue, inner: T) -> BreakWidget {
|
||||||
|
BreakWidget(Box::new(inner), true, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BreakInsideWidget(Box<dyn UIWidget>, BreakValue);
|
||||||
|
|
||||||
|
impl Render for BreakInsideWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for BreakInsideWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![self.1.to_value().to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(BoxDecorationBreak);
|
||||||
|
|
||||||
|
impl BoxDecorationBreak {
|
||||||
|
constructor!(Clone, "box-decoration-clone");
|
||||||
|
constructor!(Slice, "box-decoration-slice");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(BoxSizing);
|
||||||
|
|
||||||
|
impl BoxSizing {
|
||||||
|
constructor!(Border, "box-border");
|
||||||
|
constructor!(Content, "box-content");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(Display);
|
||||||
|
|
||||||
|
impl Display {
|
||||||
|
constructor!(Block, "block");
|
||||||
|
constructor!(InlineBlock, "inline-block");
|
||||||
|
constructor!(Inline, "inline");
|
||||||
|
constructor!(Flex, "flex");
|
||||||
|
constructor!(InlineFlex, "inline-flex");
|
||||||
|
constructor!(Table, "table");
|
||||||
|
constructor!(InlineTable, "inline-table");
|
||||||
|
constructor!(TableCaption, "table-caption");
|
||||||
|
constructor!(TableCell, "table-cell");
|
||||||
|
constructor!(TableColumn, "table-column");
|
||||||
|
constructor!(TableColumnGroup, "table-column-group");
|
||||||
|
constructor!(TableFooterGroup, "table-footer-group");
|
||||||
|
constructor!(TableHeaderGroup, "table-header-group");
|
||||||
|
constructor!(TableRowGroup, "table-row-group");
|
||||||
|
constructor!(TableRow, "table-row");
|
||||||
|
constructor!(FlowRoot, "flow-root");
|
||||||
|
constructor!(Grid, "grid");
|
||||||
|
constructor!(InlineGrid, "inline-grid");
|
||||||
|
constructor!(Contents, "contents");
|
||||||
|
constructor!(ListItem, "list-item");
|
||||||
|
constructor!(Hidden, "hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(Float);
|
||||||
|
|
||||||
|
impl Float {
|
||||||
|
constructor!(Start, "float-start");
|
||||||
|
constructor!(End, "float-end");
|
||||||
|
constructor!(Left, "float-left");
|
||||||
|
constructor!(Right, "float-right");
|
||||||
|
constructor!(None, "float-none");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(Clear);
|
||||||
|
|
||||||
|
impl Clear {
|
||||||
|
constructor!(Start, "clear-start");
|
||||||
|
constructor!(End, "clear-end");
|
||||||
|
constructor!(Left, "clear-left");
|
||||||
|
constructor!(Right, "clear-right");
|
||||||
|
constructor!(Both, "clear-both");
|
||||||
|
constructor!(None, "clear-none");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(ObjectFit);
|
||||||
|
|
||||||
|
impl ObjectFit {
|
||||||
|
constructor!(Contain, "object-contain");
|
||||||
|
constructor!(Cover, "object-cover");
|
||||||
|
constructor!(Fill, "object-fill");
|
||||||
|
constructor!(None, "object-none");
|
||||||
|
constructor!(ScaleDown, "object-scale-down");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(Overflow);
|
||||||
|
|
||||||
|
impl Overflow {
|
||||||
|
constructor!(Auto, "overflow-auto");
|
||||||
|
constructor!(Hidden, "overflow-hidden");
|
||||||
|
constructor!(Clip, "overflow-clip");
|
||||||
|
constructor!(Visible, "overflow-visible");
|
||||||
|
constructor!(Scroll, "overflow-scroll");
|
||||||
|
constructor!(XAuto, "overflow-x-auto");
|
||||||
|
constructor!(YAuto, "overflow-y-auto");
|
||||||
|
constructor!(XHidden, "overflow-x-hidden");
|
||||||
|
constructor!(YHidden, "overflow-y-hidden");
|
||||||
|
constructor!(XClip, "overflow-x-clip");
|
||||||
|
constructor!(YClip, "overflow-y-clip");
|
||||||
|
constructor!(XVisible, "overflow-x-visible");
|
||||||
|
constructor!(YVisible, "overflow-y-visible");
|
||||||
|
constructor!(XScroll, "overflow-x-scroll");
|
||||||
|
constructor!(YScroll, "overflow-y-scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(JustifySelf);
|
||||||
|
|
||||||
|
impl JustifySelf {
|
||||||
|
constructor!(Auto, "justify-self-auto");
|
||||||
|
constructor!(Start, "justify-self-start");
|
||||||
|
constructor!(End, "justify-self-end");
|
||||||
|
constructor!(Center, "justify-self-center");
|
||||||
|
constructor!(Stretch, "justify-self-stretch");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(PlaceSelf);
|
||||||
|
|
||||||
|
impl PlaceSelf {
|
||||||
|
constructor!(Auto, "place-self-auto");
|
||||||
|
constructor!(Start, "place-self-start");
|
||||||
|
constructor!(End, "place-self-end");
|
||||||
|
constructor!(Center, "place-self-center");
|
||||||
|
constructor!(Stretch, "place-self-stretch");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(AlignSelf);
|
||||||
|
|
||||||
|
impl AlignSelf {
|
||||||
|
constructor!(Auto, "self-auto");
|
||||||
|
constructor!(Start, "self-start");
|
||||||
|
constructor!(End, "self-end");
|
||||||
|
constructor!(Center, "self-center");
|
||||||
|
constructor!(Stretch, "self-stretch");
|
||||||
|
constructor!(Baseline, "self-baseline");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(Aspect);
|
||||||
|
|
||||||
|
impl Aspect {
|
||||||
|
constructor!(Auto, "aspect-auto");
|
||||||
|
constructor!(Square, "aspect-square");
|
||||||
|
constructor!(Video, "aspect-video");
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(ZIndex);
|
||||||
|
|
||||||
|
impl ZIndex {
|
||||||
|
constructor!(Auto, "z-auto");
|
||||||
|
constructor!(Zero, "z-0");
|
||||||
|
constructor!(One, "z-10");
|
||||||
|
constructor!(Two, "z-20");
|
||||||
|
constructor!(Three, "z-30");
|
||||||
|
constructor!(Four, "z-40");
|
||||||
|
constructor!(Five, "z-50");
|
||||||
|
}
|
143
src/ui/primitives/div.rs
Normal file
143
src/ui/primitives/div.rs
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::{AttrExtendable, UIWidget, htmx::HTMXAttributes};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// `<div>` element
|
||||||
|
///
|
||||||
|
/// Useful for grouping values together
|
||||||
|
#[must_use]
|
||||||
|
pub fn Div() -> DivWidget {
|
||||||
|
DivWidget(Vec::new(), false, HashMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DivWidget(Vec<Box<dyn UIWidget>>, bool, HashMap<String, String>);
|
||||||
|
|
||||||
|
impl AttrExtendable for DivWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.2.insert(key.to_string(), val.replace('\'', "\\'"));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(self, id: &str) -> Self {
|
||||||
|
self.add_attr("id", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DivWidget {
|
||||||
|
/// Add an element to the `<div>`
|
||||||
|
#[must_use]
|
||||||
|
pub fn push<T: UIWidget + 'static>(mut self, element: T) -> Self {
|
||||||
|
self.0.push(Box::new(element));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an optional element to the `<div>`
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// use based::ui::basic::*;
|
||||||
|
///
|
||||||
|
/// let div = Div().push(Some("hello"), |value| Text(value));
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub fn push_some<T: UIWidget + 'static, X, U: Fn(X) -> T>(
|
||||||
|
mut self,
|
||||||
|
option: Option<X>,
|
||||||
|
then: U,
|
||||||
|
) -> Self {
|
||||||
|
if let Some(val) = option {
|
||||||
|
self.0.push(Box::new(then(val)));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn push_if<T: UIWidget + 'static, U: Fn() -> T>(
|
||||||
|
mut self,
|
||||||
|
condition: bool,
|
||||||
|
then: U,
|
||||||
|
) -> Self {
|
||||||
|
if condition {
|
||||||
|
self.0.push(Box::new(then()));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn push_for_each<T, X, F>(mut self, items: &[X], mut action: F) -> Self
|
||||||
|
where
|
||||||
|
T: UIWidget + 'static,
|
||||||
|
F: FnMut(&X) -> T,
|
||||||
|
{
|
||||||
|
for item in items {
|
||||||
|
self.0.push(Box::new(action(item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the `<div>`s innerHTML
|
||||||
|
///
|
||||||
|
/// This will render `<content>` instead of `<div> <content> </div>`
|
||||||
|
#[must_use]
|
||||||
|
pub const fn vanish(mut self) -> Self {
|
||||||
|
self.1 = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for DivWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DivWidget {
|
||||||
|
pub fn extended_class_(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
for e in &self.0 {
|
||||||
|
c.extend_from_slice(&e.extended_class());
|
||||||
|
}
|
||||||
|
c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for DivWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
!self.1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let inner = html! {
|
||||||
|
@for e in &self.0 {
|
||||||
|
(e.as_ref())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if self.1 {
|
||||||
|
inner
|
||||||
|
} else {
|
||||||
|
let attrs = self
|
||||||
|
.2
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{k}='{v}'"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
PreEscaped(format!("<div class='{class}' {attrs}> {} </div>", inner.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HTMXAttributes for DivWidget {}
|
479
src/ui/primitives/filter.rs
Normal file
479
src/ui/primitives/filter.rs
Normal file
|
@ -0,0 +1,479 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use super::Size;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Blur<T: UIWidget + 'static>(amount: Size, inner: T) -> BlurWidget {
|
||||||
|
BlurWidget(Box::new(inner), amount, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BlurWidget(Box<dyn UIWidget>, Size, bool);
|
||||||
|
|
||||||
|
impl BlurWidget {
|
||||||
|
pub fn backdrop(mut self) -> Self {
|
||||||
|
self.2 = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for BlurWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for BlurWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let class = match &self.1 {
|
||||||
|
Size::Custom(s) => &format!(" blur-[{s}]"),
|
||||||
|
Size::None => "blur-none",
|
||||||
|
Size::Small => "blur-sm",
|
||||||
|
Size::Regular => "blur",
|
||||||
|
Size::Medium => "blur-md",
|
||||||
|
Size::Large => "blur-lg",
|
||||||
|
Size::XL => "blur-xl",
|
||||||
|
Size::_2XL => "blur-2xl",
|
||||||
|
Size::_3XL => "blur-3xl",
|
||||||
|
Size::Full => "blur-3xl",
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.2 {
|
||||||
|
return vec![format!("backdrop-{class}")];
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![class.to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! build_value_widget {
|
||||||
|
($constr:ident, $widget:ident, $class:literal) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn $constr<T: UIWidget + 'static>(value: f64, inner: T) -> $widget {
|
||||||
|
$widget(Box::new(inner), value, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct $widget(Box<dyn UIWidget>, f64, bool);
|
||||||
|
|
||||||
|
impl $widget {
|
||||||
|
pub fn backdrop(mut self) -> Self {
|
||||||
|
self.2 = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for $widget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for $widget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = $class.to_string();
|
||||||
|
ret.push_str(&format!("-[{:.2}]", self.1));
|
||||||
|
|
||||||
|
if self.2 {
|
||||||
|
return vec![format!("backdrop-{ret}")];
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![ret]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
build_value_widget!(Brightness, BrightnessWidget, "brightness");
|
||||||
|
build_value_widget!(Contrast, ConstrastWidget, "contrast");
|
||||||
|
build_value_widget!(Saturate, SaturationWidget, "saturate");
|
||||||
|
|
||||||
|
macro_rules! build_on_off_widget {
|
||||||
|
($constr:ident, $widget:ident, $class:literal) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn $constr<T: UIWidget + 'static>(inner: T) -> $widget {
|
||||||
|
$widget(Box::new(inner), true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct $widget(Box<dyn UIWidget>, bool, bool);
|
||||||
|
|
||||||
|
impl $widget {
|
||||||
|
pub fn none(mut self) -> Self {
|
||||||
|
self.1 = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backdrop(mut self) -> Self {
|
||||||
|
self.2 = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for $widget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for $widget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let class = if self.1 {
|
||||||
|
$class.to_string()
|
||||||
|
} else {
|
||||||
|
concat!($class, "-0").to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.2 {
|
||||||
|
return vec![format!("backdrop-{class}")];
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![class]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
build_on_off_widget!(Grayscale, GrayscaleWidget, "grayscale");
|
||||||
|
build_on_off_widget!(Invert, InvertWidget, "invert");
|
||||||
|
build_on_off_widget!(Sepia, SepiaWidget, "sepia");
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn HueRotate<T: UIWidget + 'static>(deg: u32, inner: T) -> HueRotateWidget {
|
||||||
|
HueRotateWidget(Box::new(inner), deg, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HueRotateWidget(Box<dyn UIWidget>, u32, bool);
|
||||||
|
|
||||||
|
impl HueRotateWidget {
|
||||||
|
pub fn backdrop(mut self) -> Self {
|
||||||
|
self.2 = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for HueRotateWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for HueRotateWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let class = format!("hue-rotate-[{:.2}deg]", self.1);
|
||||||
|
|
||||||
|
if self.2 {
|
||||||
|
return vec![format!("backdrop-{class}")];
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![class]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Opacity<T: UIWidget + 'static>(value: f64, inner: T) -> OpacityWidget {
|
||||||
|
OpacityWidget(Box::new(inner), value, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OpacityWidget(Box<dyn UIWidget>, f64, bool);
|
||||||
|
|
||||||
|
impl OpacityWidget {
|
||||||
|
pub fn backdrop(mut self) -> Self {
|
||||||
|
self.2 = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for OpacityWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for OpacityWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let class = match self.1 {
|
||||||
|
0.0 => "opacity-0",
|
||||||
|
0.05 => "opacity-5",
|
||||||
|
0.1 => "opacity-10",
|
||||||
|
0.15 => "opacity-15",
|
||||||
|
0.2 => "opacity-20",
|
||||||
|
0.25 => "opacity-25",
|
||||||
|
0.3 => "opacity-30",
|
||||||
|
0.35 => "opacity-35",
|
||||||
|
0.4 => "opacity-40",
|
||||||
|
0.45 => "opacity-45",
|
||||||
|
0.5 => "opacity-50",
|
||||||
|
0.55 => "opacity-55",
|
||||||
|
0.6 => "opacity-60",
|
||||||
|
0.65 => "opacity-65",
|
||||||
|
0.7 => "opacity-70",
|
||||||
|
0.75 => "opacity-75",
|
||||||
|
0.8 => "opacity-80",
|
||||||
|
0.85 => "opacity-85",
|
||||||
|
0.9 => "opacity-90",
|
||||||
|
0.95 => "opacity-95",
|
||||||
|
1.0 => "opacity-100",
|
||||||
|
_ => &format!("opacity-[{:.2}]", self.1),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.2 {
|
||||||
|
return vec![format!("backdrop-{class}")];
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![class.to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BlendMode {
|
||||||
|
Normal,
|
||||||
|
Multiply,
|
||||||
|
Screen,
|
||||||
|
Overlay,
|
||||||
|
Darken,
|
||||||
|
Lighten,
|
||||||
|
ColorDodge,
|
||||||
|
ColorBurn,
|
||||||
|
HardLight,
|
||||||
|
SoftLight,
|
||||||
|
Difference,
|
||||||
|
Exclusion,
|
||||||
|
Hue,
|
||||||
|
Saturation,
|
||||||
|
Color,
|
||||||
|
Luminosity,
|
||||||
|
PlusDarker,
|
||||||
|
PlusLighter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlendMode {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BlendMode::Normal => "normal",
|
||||||
|
BlendMode::Multiply => "multiply",
|
||||||
|
BlendMode::Screen => "screen",
|
||||||
|
BlendMode::Overlay => "overlay",
|
||||||
|
BlendMode::Darken => "darken",
|
||||||
|
BlendMode::Lighten => "lighten",
|
||||||
|
BlendMode::ColorDodge => "color-dodge",
|
||||||
|
BlendMode::ColorBurn => "color-burn",
|
||||||
|
BlendMode::HardLight => "hard-light",
|
||||||
|
BlendMode::SoftLight => "soft-light",
|
||||||
|
BlendMode::Difference => "difference",
|
||||||
|
BlendMode::Exclusion => "exclusion",
|
||||||
|
BlendMode::Hue => "hue",
|
||||||
|
BlendMode::Saturation => "saturation",
|
||||||
|
BlendMode::Color => "color",
|
||||||
|
BlendMode::Luminosity => "luminosity",
|
||||||
|
BlendMode::PlusDarker => "plus-darker",
|
||||||
|
BlendMode::PlusLighter => "plus-lighter",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn MixBlendMode<T: UIWidget + 'static>(mode: BlendMode, inner: T) -> MixBlendModeWidget {
|
||||||
|
MixBlendModeWidget(Box::new(inner), mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MixBlendModeWidget(Box<dyn UIWidget>, BlendMode);
|
||||||
|
|
||||||
|
impl Render for MixBlendModeWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for MixBlendModeWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![format!("mix-blend-{}", self.1.to_value())]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn BackgroundBlendMode<T: UIWidget + 'static>(
|
||||||
|
mode: BlendMode,
|
||||||
|
inner: T,
|
||||||
|
) -> BackgroundBlendModeWidget {
|
||||||
|
BackgroundBlendModeWidget(Box::new(inner), mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BackgroundBlendModeWidget(Box<dyn UIWidget>, BlendMode);
|
||||||
|
|
||||||
|
impl Render for BackgroundBlendModeWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for BackgroundBlendModeWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![format!("bg-blend-{}", self.1.to_value())]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
611
src/ui/primitives/flex.rs
Normal file
611
src/ui/primitives/flex.rs
Normal file
|
@ -0,0 +1,611 @@
|
||||||
|
use crate::ui::{UIWidget, color::UIColor};
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
div::Div,
|
||||||
|
space::{Fraction, ScreenValue},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Column<T: UIWidget + 'static>(inner: Vec<T>) -> FlexWidget {
|
||||||
|
let mut div = Div().vanish();
|
||||||
|
|
||||||
|
for e in inner {
|
||||||
|
div = div.push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Flex(div).direction(Direction::Column)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Row<T: UIWidget + 'static>(inner: Vec<T>) -> FlexWidget {
|
||||||
|
let mut div = Div().vanish();
|
||||||
|
|
||||||
|
for e in inner {
|
||||||
|
div = div.push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Flex(div).direction(Direction::Row)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Flex<T: UIWidget + 'static>(inner: T) -> FlexWidget {
|
||||||
|
FlexWidget(Box::new(inner), vec![], false, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Justify {
|
||||||
|
Normal,
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Center,
|
||||||
|
Between,
|
||||||
|
Around,
|
||||||
|
Evenly,
|
||||||
|
Stretch,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FlexWidget(Box<dyn UIWidget>, Vec<String>, bool, Option<Direction>);
|
||||||
|
|
||||||
|
impl Render for FlexWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlexWidget {
|
||||||
|
#[must_use]
|
||||||
|
pub fn full_center(mut self) -> Self {
|
||||||
|
self.1.push("items-center".to_owned());
|
||||||
|
self.1.push("justify-center".to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn group(mut self) -> Self {
|
||||||
|
self.2 = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn divide_style(mut self, style: DivideStyle) -> Self {
|
||||||
|
self.1.push(style.to_value().to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn divide_color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.1.push(format!("divide-{}", color.color_class()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn divide_x(mut self, width: DivideWidth) -> Self {
|
||||||
|
let reversed = self
|
||||||
|
.3
|
||||||
|
.as_ref()
|
||||||
|
.map(|x| match x {
|
||||||
|
Direction::Row => false,
|
||||||
|
Direction::RowReverse => true,
|
||||||
|
Direction::Column => false,
|
||||||
|
Direction::ColumnReverse => true,
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
self.1.push(format!("divide-x-{}", width.to_value()));
|
||||||
|
|
||||||
|
if reversed {
|
||||||
|
self.1.push("divide-x-reverse".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn divide_y(mut self, width: DivideWidth) -> Self {
|
||||||
|
let reversed = self
|
||||||
|
.3
|
||||||
|
.as_ref()
|
||||||
|
.map(|x| match x {
|
||||||
|
Direction::Row => false,
|
||||||
|
Direction::RowReverse => true,
|
||||||
|
Direction::Column => false,
|
||||||
|
Direction::ColumnReverse => true,
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
self.1.push(format!("divide-y-{}", width.to_value()));
|
||||||
|
|
||||||
|
if reversed {
|
||||||
|
self.1.push("divide-y-reverse".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn direction(mut self, direction: Direction) -> Self {
|
||||||
|
self.3 = Some(direction);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn wrap(mut self, wrap: Wrap) -> Self {
|
||||||
|
self.1.push(format!("flex-{}", wrap.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn justify_items(mut self, justify: JustifyItems) -> Self {
|
||||||
|
self.1.push(justify.to_value().to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn align_content(mut self, align: AlignContent) -> Self {
|
||||||
|
self.1.push(align.to_value().to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn align_items(mut self, align: AlignItems) -> Self {
|
||||||
|
self.1.push(align.to_value().to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn justify(mut self, value: Justify) -> Self {
|
||||||
|
let class = match value {
|
||||||
|
Justify::Center => "justify-center".to_string(),
|
||||||
|
Justify::Between => "justify-between".to_string(),
|
||||||
|
Justify::Normal => "justify-normal".to_string(),
|
||||||
|
Justify::Start => "justify-start".to_string(),
|
||||||
|
Justify::End => "justify-end".to_string(),
|
||||||
|
Justify::Around => "justify-around".to_string(),
|
||||||
|
Justify::Evenly => "justify-evenly".to_string(),
|
||||||
|
Justify::Stretch => "justify-stretch".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.1.push(class);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn items_center(mut self) -> Self {
|
||||||
|
self.1.push("items-center".to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn gap(mut self, amount: ScreenValue) -> Self {
|
||||||
|
self.1.push(format!("gap-{}", amount.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn gap_x(mut self, amount: ScreenValue) -> Self {
|
||||||
|
self.1.push(format!("gap-x-{}", amount.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn gap_y(mut self, amount: ScreenValue) -> Self {
|
||||||
|
self.1.push(format!("gap-y-{}", amount.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DivideWidth {
|
||||||
|
Custom(u64),
|
||||||
|
_0,
|
||||||
|
_2,
|
||||||
|
_4,
|
||||||
|
_8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DivideWidth {
|
||||||
|
pub fn to_value(&self) -> String {
|
||||||
|
match self {
|
||||||
|
DivideWidth::Custom(s) => format!("[{s}px]"),
|
||||||
|
DivideWidth::_0 => "0".to_string(),
|
||||||
|
DivideWidth::_2 => "2".to_string(),
|
||||||
|
DivideWidth::_4 => "4".to_string(),
|
||||||
|
DivideWidth::_8 => "8".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Direction {
|
||||||
|
Row,
|
||||||
|
RowReverse,
|
||||||
|
Column,
|
||||||
|
ColumnReverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Direction {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Direction::Row => "row",
|
||||||
|
Direction::RowReverse => "row-reverse",
|
||||||
|
Direction::Column => "col",
|
||||||
|
Direction::ColumnReverse => "col-reverse",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Wrap {
|
||||||
|
Wrap,
|
||||||
|
Reverse,
|
||||||
|
NoWrap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wrap {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Wrap::Wrap => "wrap",
|
||||||
|
Wrap::Reverse => "wrap-reverse",
|
||||||
|
Wrap::NoWrap => "nowrap",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for FlexWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut res = vec!["flex".to_string()];
|
||||||
|
|
||||||
|
if let Some(direction) = &self.3 {
|
||||||
|
res.push(format!("flex-{}", direction.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.extend_from_slice(&self.1);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() && !self.2 {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Either<R, L> {
|
||||||
|
Right(R),
|
||||||
|
Left(L),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, L> Either<R, L> {
|
||||||
|
pub fn map<X, Y, U>(self, lf: X, rf: Y) -> U
|
||||||
|
where
|
||||||
|
X: FnOnce(L) -> U,
|
||||||
|
Y: FnOnce(R) -> U,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Either::Right(r) => rf(r),
|
||||||
|
Either::Left(l) => lf(l),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ScreenValue> for Either<ScreenValue, Fraction> {
|
||||||
|
fn from(value: ScreenValue) -> Self {
|
||||||
|
Self::Right(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Fraction> for Either<ScreenValue, Fraction> {
|
||||||
|
fn from(value: Fraction) -> Self {
|
||||||
|
Self::Left(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn FlexBasis<T: UIWidget + 'static>(
|
||||||
|
inner: T,
|
||||||
|
value: Either<ScreenValue, Fraction>,
|
||||||
|
) -> FlexBasisWidget {
|
||||||
|
FlexBasisWidget(Box::new(inner), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FlexBasisWidget(Box<dyn UIWidget>, Either<ScreenValue, Fraction>);
|
||||||
|
|
||||||
|
impl Render for FlexBasisWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for FlexBasisWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![format!(
|
||||||
|
"basis-{}",
|
||||||
|
self.1
|
||||||
|
.clone()
|
||||||
|
.map(|x| x.to_value().to_string(), |x| x.to_value().to_string())
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn FlexGrow<T: UIWidget + 'static>(strategy: Strategy, inner: T) -> FlexGrowWidget {
|
||||||
|
FlexGrowWidget(strategy, Box::new(inner))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FlexGrowWidget(Strategy, Box<dyn UIWidget>);
|
||||||
|
|
||||||
|
impl Render for FlexGrowWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for FlexGrowWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![self.0.to_value().to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.1.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
if self.1.as_ref().can_inherit() {
|
||||||
|
self.1
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{}", self.base_class().join(" "))) {
|
||||||
|
(self.1.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Strategy {
|
||||||
|
/// Allow a flex item to shrink but not grow, taking into account its initial size.
|
||||||
|
Initial,
|
||||||
|
/// Allow a flex item to grow and shrink as needed, ignoring its initial size.
|
||||||
|
Expand,
|
||||||
|
/// Allow a flex item to grow and shrink, taking into account its initial size.
|
||||||
|
Auto,
|
||||||
|
/// Prevent a flex item from growing or shrinking.
|
||||||
|
None,
|
||||||
|
/// Allow a flex item to grow to fill any available space.
|
||||||
|
Grow,
|
||||||
|
/// Prevent a flex item from growing.
|
||||||
|
NoGrow,
|
||||||
|
/// Allow a flex item to shrink if needed.
|
||||||
|
Shrink,
|
||||||
|
/// Prevent a flex item from shrinking.
|
||||||
|
NoShrink,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Strategy {
|
||||||
|
pub fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Initial => "flex-initial",
|
||||||
|
Self::Expand => "flex-1",
|
||||||
|
Self::Auto => "flex-auto",
|
||||||
|
Self::None => "flex-none",
|
||||||
|
Self::Grow => "grow",
|
||||||
|
Self::NoGrow => "grow-0",
|
||||||
|
Self::Shrink => "shrink",
|
||||||
|
Self::NoShrink => "shrink-0",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Order {
|
||||||
|
_1,
|
||||||
|
_2,
|
||||||
|
_3,
|
||||||
|
_4,
|
||||||
|
_5,
|
||||||
|
_6,
|
||||||
|
_7,
|
||||||
|
_8,
|
||||||
|
_9,
|
||||||
|
_10,
|
||||||
|
_11,
|
||||||
|
_12,
|
||||||
|
First,
|
||||||
|
Last,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Order {
|
||||||
|
pub fn on<T: UIWidget + 'static>(self, inner: T) -> OrderWidget {
|
||||||
|
OrderWidget(self, Box::new(inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OrderWidget(Order, Box<dyn UIWidget>);
|
||||||
|
|
||||||
|
impl Render for OrderWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for OrderWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let class = match self.0 {
|
||||||
|
Order::_1 => "order-1",
|
||||||
|
Order::_2 => "order-2",
|
||||||
|
Order::_3 => "order-3",
|
||||||
|
Order::_4 => "order-4",
|
||||||
|
Order::_5 => "order-5",
|
||||||
|
Order::_6 => "order-6",
|
||||||
|
Order::_7 => "order-7",
|
||||||
|
Order::_8 => "order-8",
|
||||||
|
Order::_9 => "order-9",
|
||||||
|
Order::_10 => "order-10",
|
||||||
|
Order::_11 => "order-11",
|
||||||
|
Order::_12 => "order-12",
|
||||||
|
Order::First => "order-first",
|
||||||
|
Order::Last => "order-last",
|
||||||
|
Order::None => "order-none",
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![class.to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.1.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let inner = &self.1;
|
||||||
|
|
||||||
|
if inner.can_inherit() {
|
||||||
|
inner.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DivideStyle {
|
||||||
|
Solid,
|
||||||
|
Dashed,
|
||||||
|
Dotted,
|
||||||
|
Double,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DivideStyle {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
DivideStyle::Solid => "divide-solid",
|
||||||
|
DivideStyle::Dashed => "divide-dashed",
|
||||||
|
DivideStyle::Dotted => "divide-dotted",
|
||||||
|
DivideStyle::Double => "divide-double",
|
||||||
|
DivideStyle::None => "divide-none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum JustifyItems {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Center,
|
||||||
|
Stretch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JustifyItems {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
JustifyItems::Start => "justify-items-start",
|
||||||
|
JustifyItems::End => "justify-items-end",
|
||||||
|
JustifyItems::Center => "justify-items-center",
|
||||||
|
JustifyItems::Stretch => "justify-items-stretch",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AlignContent {
|
||||||
|
Normal,
|
||||||
|
Center,
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Between,
|
||||||
|
Around,
|
||||||
|
Evenly,
|
||||||
|
Baseline,
|
||||||
|
Stretch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlignContent {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
AlignContent::Normal => "content-normal",
|
||||||
|
AlignContent::Center => "content-center",
|
||||||
|
AlignContent::Start => "content-start",
|
||||||
|
AlignContent::End => "content-end",
|
||||||
|
AlignContent::Between => "content-between",
|
||||||
|
AlignContent::Around => "content-around",
|
||||||
|
AlignContent::Evenly => "content-evenly",
|
||||||
|
AlignContent::Baseline => "content-baseline",
|
||||||
|
AlignContent::Stretch => "content-stretch",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AlignItems {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Center,
|
||||||
|
Baseline,
|
||||||
|
Stretch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlignItems {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
AlignItems::Start => "items-start",
|
||||||
|
AlignItems::End => "items-end",
|
||||||
|
AlignItems::Center => "items-center",
|
||||||
|
AlignItems::Baseline => "items-baseline",
|
||||||
|
AlignItems::Stretch => "items-stretch",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
461
src/ui/primitives/grid.rs
Normal file
461
src/ui/primitives/grid.rs
Normal file
|
@ -0,0 +1,461 @@
|
||||||
|
use crate::ui::{UIWidget, color::UIColor};
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
flex::{AlignContent, AlignItems, DivideStyle, DivideWidth, Justify, JustifyItems},
|
||||||
|
space::ScreenValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Grid<T: UIWidget + 'static>(inner: T) -> GridWidget {
|
||||||
|
GridWidget(Box::new(inner), vec![], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GridWidget(Box<dyn UIWidget>, Vec<String>, bool);
|
||||||
|
|
||||||
|
impl Render for GridWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GridAmount {
|
||||||
|
_1,
|
||||||
|
_2,
|
||||||
|
_3,
|
||||||
|
_4,
|
||||||
|
_5,
|
||||||
|
_6,
|
||||||
|
_7,
|
||||||
|
_8,
|
||||||
|
_9,
|
||||||
|
_10,
|
||||||
|
_11,
|
||||||
|
_12,
|
||||||
|
None,
|
||||||
|
Subgrid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GridAmount {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
GridAmount::_1 => "1",
|
||||||
|
GridAmount::_2 => "2",
|
||||||
|
GridAmount::_3 => "3",
|
||||||
|
GridAmount::_4 => "4",
|
||||||
|
GridAmount::_5 => "5",
|
||||||
|
GridAmount::_6 => "6",
|
||||||
|
GridAmount::_7 => "7",
|
||||||
|
GridAmount::_8 => "8",
|
||||||
|
GridAmount::_9 => "9",
|
||||||
|
GridAmount::_10 => "10",
|
||||||
|
GridAmount::_11 => "11",
|
||||||
|
GridAmount::_12 => "12",
|
||||||
|
GridAmount::None => "none",
|
||||||
|
GridAmount::Subgrid => "subgrid",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GridWidget {
|
||||||
|
#[must_use]
|
||||||
|
pub fn columns(mut self, amount: GridAmount) -> Self {
|
||||||
|
self.1.push(format!("grid-cols-{}", amount.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn rows(mut self, amount: GridAmount) -> Self {
|
||||||
|
self.1.push(format!("grid-rows-{}", amount.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn auto_flow(mut self, flow: GridAutoFlow) -> Self {
|
||||||
|
self.1.push(flow.to_value().to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn auto_columns(mut self, size: GridAutoSize) -> Self {
|
||||||
|
self.1.push(format!("auto-cols-{}", size.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn auto_rows(mut self, size: GridAutoSize) -> Self {
|
||||||
|
self.1.push(format!("auto-rows-{}", size.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn full_center(mut self) -> Self {
|
||||||
|
self.1.push("items-center".to_owned());
|
||||||
|
self.1.push("justify-center".to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn group(mut self) -> Self {
|
||||||
|
self.2 = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn divide_style(mut self, style: DivideStyle) -> Self {
|
||||||
|
self.1.push(style.to_value().to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn divide_color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.1.push(format!("divide-{}", color.color_class()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn divide_x(mut self, width: DivideWidth) -> Self {
|
||||||
|
self.1.push(format!("divide-x-{}", width.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn divide_y(mut self, width: DivideWidth) -> Self {
|
||||||
|
self.1.push(format!("divide-y-{}", width.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn justify_items(mut self, justify: JustifyItems) -> Self {
|
||||||
|
self.1.push(justify.to_value().to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn align_content(mut self, align: AlignContent) -> Self {
|
||||||
|
self.1.push(align.to_value().to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn align_items(mut self, align: AlignItems) -> Self {
|
||||||
|
self.1.push(align.to_value().to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn justify(mut self, value: Justify) -> Self {
|
||||||
|
let class = match value {
|
||||||
|
Justify::Center => "justify-center".to_string(),
|
||||||
|
Justify::Between => "justify-between".to_string(),
|
||||||
|
Justify::Normal => "justify-normal".to_string(),
|
||||||
|
Justify::Start => "justify-start".to_string(),
|
||||||
|
Justify::End => "justify-end".to_string(),
|
||||||
|
Justify::Around => "justify-around".to_string(),
|
||||||
|
Justify::Evenly => "justify-evenly".to_string(),
|
||||||
|
Justify::Stretch => "justify-stretch".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.1.push(class);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn items_center(mut self) -> Self {
|
||||||
|
self.1.push("items-center".to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn gap(mut self, amount: ScreenValue) -> Self {
|
||||||
|
self.1.push(format!("gap-{}", amount.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn gap_x(mut self, amount: ScreenValue) -> Self {
|
||||||
|
self.1.push(format!("gap-x-{}", amount.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn gap_y(mut self, amount: ScreenValue) -> Self {
|
||||||
|
self.1.push(format!("gap-y-{}", amount.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for GridWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut res = vec!["grid".to_string()];
|
||||||
|
res.extend_from_slice(&self.1);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() && !self.2 {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GridAutoFlow {
|
||||||
|
Row,
|
||||||
|
Column,
|
||||||
|
Dense,
|
||||||
|
RowDense,
|
||||||
|
ColumnDense,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GridAutoFlow {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
GridAutoFlow::Row => "grid-flow-row",
|
||||||
|
GridAutoFlow::Column => "grid-flow-col",
|
||||||
|
GridAutoFlow::Dense => "grid-flow-dense",
|
||||||
|
GridAutoFlow::RowDense => "grid-flow-row-dense",
|
||||||
|
GridAutoFlow::ColumnDense => "grid-flow-col-dense",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GridAutoSize {
|
||||||
|
Auto,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
Fr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GridAutoSize {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
GridAutoSize::Auto => "auto",
|
||||||
|
GridAutoSize::Min => "min",
|
||||||
|
GridAutoSize::Max => "max",
|
||||||
|
GridAutoSize::Fr => "fr",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn GridElementColumn<T: UIWidget + 'static>(inner: T) -> GridElement {
|
||||||
|
GridElement(Box::new(inner), Vec::new(), "col".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn GridElementRow<T: UIWidget + 'static>(inner: T) -> GridElement {
|
||||||
|
GridElement(Box::new(inner), Vec::new(), "row".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GridElement(Box<dyn UIWidget>, Vec<String>, String);
|
||||||
|
|
||||||
|
impl GridElement {
|
||||||
|
pub fn auto(mut self) -> Self {
|
||||||
|
self.1.push(format!("{}-auto", self.2));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn span(mut self, value: GridElementValue) -> Self {
|
||||||
|
self.1.push(format!("{}-span-{}", self.2, match value {
|
||||||
|
GridElementValue::_1 => "1",
|
||||||
|
GridElementValue::_2 => "2",
|
||||||
|
GridElementValue::_3 => "3",
|
||||||
|
GridElementValue::_4 => "4",
|
||||||
|
GridElementValue::_5 => "5",
|
||||||
|
GridElementValue::_6 => "6",
|
||||||
|
GridElementValue::_7 => "7",
|
||||||
|
GridElementValue::_8 => "8",
|
||||||
|
GridElementValue::_9 => "9",
|
||||||
|
GridElementValue::_10 => "10",
|
||||||
|
GridElementValue::_11 => "11",
|
||||||
|
GridElementValue::_12 => "12",
|
||||||
|
GridElementValue::Auto => "full",
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(mut self, value: GridElementValue) -> Self {
|
||||||
|
self.1
|
||||||
|
.push(format!("{}-start-{}", self.2, value.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(mut self, value: GridElementValue) -> Self {
|
||||||
|
self.1.push(format!("{}-end-{}", self.2, value.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for GridElement {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for GridElement {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut res = vec!["grid".to_string()];
|
||||||
|
res.extend_from_slice(&self.1);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GridElementValue {
|
||||||
|
_1,
|
||||||
|
_2,
|
||||||
|
_3,
|
||||||
|
_4,
|
||||||
|
_5,
|
||||||
|
_6,
|
||||||
|
_7,
|
||||||
|
_8,
|
||||||
|
_9,
|
||||||
|
_10,
|
||||||
|
_11,
|
||||||
|
_12,
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GridElementValue {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
GridElementValue::_1 => "1",
|
||||||
|
GridElementValue::_2 => "2",
|
||||||
|
GridElementValue::_3 => "3",
|
||||||
|
GridElementValue::_4 => "4",
|
||||||
|
GridElementValue::_5 => "5",
|
||||||
|
GridElementValue::_6 => "6",
|
||||||
|
GridElementValue::_7 => "7",
|
||||||
|
GridElementValue::_8 => "8",
|
||||||
|
GridElementValue::_9 => "9",
|
||||||
|
GridElementValue::_10 => "10",
|
||||||
|
GridElementValue::_11 => "11",
|
||||||
|
GridElementValue::_12 => "12",
|
||||||
|
GridElementValue::Auto => "auto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! string_class_widget {
|
||||||
|
($name:ident) => {
|
||||||
|
pub struct $name(Box<dyn UIWidget>, String);
|
||||||
|
|
||||||
|
impl Render for $name {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for $name {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![self.1.clone()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! constructor {
|
||||||
|
($name:ident, $class:literal) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn $name<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), $class.to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
string_class_widget!(Columns);
|
||||||
|
|
||||||
|
impl Columns {
|
||||||
|
constructor!(_1, "columns-1");
|
||||||
|
constructor!(_2, "columns-2");
|
||||||
|
constructor!(_3, "columns-3");
|
||||||
|
constructor!(_4, "columns-4");
|
||||||
|
constructor!(_5, "columns-5");
|
||||||
|
constructor!(_6, "columns-6");
|
||||||
|
constructor!(_7, "columns-7");
|
||||||
|
constructor!(_8, "columns-8");
|
||||||
|
constructor!(_9, "columns-9");
|
||||||
|
constructor!(_10, "columns-10");
|
||||||
|
constructor!(_11, "columns-11");
|
||||||
|
constructor!(_12, "columns-12");
|
||||||
|
constructor!(Auto, "columns-auto");
|
||||||
|
constructor!(_3XS, "columns-3xs");
|
||||||
|
constructor!(_2XS, "columns-2xs");
|
||||||
|
constructor!(XS, "columns-xs");
|
||||||
|
constructor!(Small, "columns-sm");
|
||||||
|
constructor!(Medium, "columns-md");
|
||||||
|
constructor!(Large, "columns-lg");
|
||||||
|
constructor!(XL, "columns-xl");
|
||||||
|
constructor!(_2XL, "columns-2xl");
|
||||||
|
constructor!(_3XL, "columns-3xl");
|
||||||
|
constructor!(_4XL, "columns-4xl");
|
||||||
|
constructor!(_5XL, "columns-5xl");
|
||||||
|
constructor!(_6XL, "columns-6xl");
|
||||||
|
constructor!(_7XL, "columns-7xl");
|
||||||
|
}
|
91
src/ui/primitives/height.rs
Normal file
91
src/ui/primitives/height.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
flex::Either,
|
||||||
|
space::{Fraction, ScreenValue},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Height<T: UIWidget + 'static, S: Into<Either<ScreenValue, Fraction>>>(
|
||||||
|
size: S,
|
||||||
|
inner: T,
|
||||||
|
) -> HeightWidget {
|
||||||
|
HeightWidget(Box::new(inner), size.into(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn MinHeight<T: UIWidget + 'static, S: Into<Either<ScreenValue, Fraction>>>(
|
||||||
|
size: S,
|
||||||
|
inner: T,
|
||||||
|
) -> HeightWidget {
|
||||||
|
HeightWidget(Box::new(inner), size.into(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn MaxHeight<T: UIWidget + 'static, S: Into<Either<ScreenValue, Fraction>>>(
|
||||||
|
size: S,
|
||||||
|
inner: T,
|
||||||
|
) -> HeightWidget {
|
||||||
|
HeightWidget(Box::new(inner), size.into(), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HeightWidget(Box<dyn UIWidget>, Either<ScreenValue, Fraction>, u8);
|
||||||
|
|
||||||
|
impl Render for HeightWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for HeightWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
match self.2 {
|
||||||
|
1 => {
|
||||||
|
return vec![format!(
|
||||||
|
"min-h-{}",
|
||||||
|
self.1
|
||||||
|
.map(|x| x.to_value().to_string(), |x| x.to_value().to_string())
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
return vec![format!(
|
||||||
|
"max-h-{}",
|
||||||
|
self.1
|
||||||
|
.map(|x| x.to_value().to_string(), |x| x.to_value().to_string())
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![format!(
|
||||||
|
"h-{}",
|
||||||
|
self.1
|
||||||
|
.map(|x| x.to_value().to_string(), |x| x.to_value().to_string())
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
279
src/ui/primitives/image.rs
Normal file
279
src/ui/primitives/image.rs
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn Image(src: &str) -> ImageWidget {
|
||||||
|
ImageWidget {
|
||||||
|
src: src.to_owned(),
|
||||||
|
alt: String::new(),
|
||||||
|
width: None,
|
||||||
|
height: None,
|
||||||
|
caption: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ImageWidget {
|
||||||
|
src: String,
|
||||||
|
alt: String,
|
||||||
|
width: Option<u32>,
|
||||||
|
height: Option<u32>,
|
||||||
|
caption: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ImageWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageWidget {
|
||||||
|
#[must_use]
|
||||||
|
pub fn alt(mut self, alt: &str) -> Self {
|
||||||
|
self.alt = alt.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn width(mut self, width: u32) -> Self {
|
||||||
|
self.width = Some(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn height(mut self, height: u32) -> Self {
|
||||||
|
self.height = Some(height);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn caption(mut self, caption: &str) -> Self {
|
||||||
|
self.caption = Some(caption.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_img(&self, class: &str) -> PreEscaped<String> {
|
||||||
|
let mut str = "<img".to_string();
|
||||||
|
|
||||||
|
str.push_str(&format!(" src=\"{}\"", self.src));
|
||||||
|
|
||||||
|
if !self.alt.is_empty() {
|
||||||
|
str.push_str(&format!(" alt=\"{}\"", self.alt));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(width) = self.width {
|
||||||
|
str.push_str(&format!(" width=\"{width}\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(height) = self.height {
|
||||||
|
str.push_str(&format!(" height=\"{height}\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
str.push_str(&format!(" class=\"{class}\">"));
|
||||||
|
|
||||||
|
PreEscaped(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for ImageWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
self.base_class()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if let Some(caption) = &self.caption {
|
||||||
|
return html! {
|
||||||
|
figure class="w-fit" {
|
||||||
|
(self.build_img(class))
|
||||||
|
figcaption class="mt-2 text-sm text-center text-gray-500 dark:text-gray-400" { (caption) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.build_img(class)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn Video() -> VideoWidget {
|
||||||
|
VideoWidget {
|
||||||
|
src: Vec::new(),
|
||||||
|
controls: false,
|
||||||
|
autoplay: false,
|
||||||
|
looping: false,
|
||||||
|
muted: false,
|
||||||
|
poster: None,
|
||||||
|
width: None,
|
||||||
|
height: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VideoWidget {
|
||||||
|
src: Vec<SourceWidget>,
|
||||||
|
controls: bool,
|
||||||
|
autoplay: bool,
|
||||||
|
looping: bool,
|
||||||
|
muted: bool,
|
||||||
|
poster: Option<String>,
|
||||||
|
width: Option<u32>,
|
||||||
|
height: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for VideoWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoWidget {
|
||||||
|
pub fn add_src(mut self, src: SourceWidget) -> Self {
|
||||||
|
self.src.push(src);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn controls(mut self) -> Self {
|
||||||
|
self.controls = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn autoplay(mut self) -> Self {
|
||||||
|
self.autoplay = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn looping(mut self) -> Self {
|
||||||
|
self.looping = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn muted(mut self) -> Self {
|
||||||
|
self.muted = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poster(mut self, poster: &str) -> Self {
|
||||||
|
self.poster = Some(poster.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(mut self, w: u32) -> Self {
|
||||||
|
self.width = Some(w);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(mut self, h: u32) -> Self {
|
||||||
|
self.height = Some(h);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for VideoWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
self.base_class()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let mut ret = "<video".to_string();
|
||||||
|
|
||||||
|
if self.controls {
|
||||||
|
ret.push_str(" controls");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.autoplay {
|
||||||
|
ret.push_str(" autoplay");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.looping {
|
||||||
|
ret.push_str(" loop");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.muted {
|
||||||
|
ret.push_str(" muted");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(poster) = &self.poster {
|
||||||
|
ret.push_str(&format!(" poster=\"{}\"", poster.replace("\"", "\\\"")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(w) = &self.width {
|
||||||
|
ret.push_str(&format!(" width=\"{}\"", w));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(h) = &self.height {
|
||||||
|
ret.push_str(&format!(" height=\"{}\"", h));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push_str(&format!(" class=\"{class}\""));
|
||||||
|
|
||||||
|
ret.push_str("> ");
|
||||||
|
|
||||||
|
for src in &self.src {
|
||||||
|
ret.push_str(&src.render().0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push_str("\nYour browser does not support the video tag.\n</video>");
|
||||||
|
|
||||||
|
PreEscaped(ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn Source(src: &str, mime: Option<String>) -> SourceWidget {
|
||||||
|
SourceWidget {
|
||||||
|
src: src.to_owned(),
|
||||||
|
mime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SourceWidget {
|
||||||
|
src: String,
|
||||||
|
mime: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SourceWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for SourceWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
self.base_class()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
@if let Some(mime) = &self.mime {
|
||||||
|
source src=(self.src) type=(mime);
|
||||||
|
} @else {
|
||||||
|
source src=(self.src);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
91
src/ui/primitives/input/form.rs
Normal file
91
src/ui/primitives/input/form.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use std::{collections::HashMap, fmt::Write};
|
||||||
|
|
||||||
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth::{User, csrf::CSRF},
|
||||||
|
ui::{AttrExtendable, UIWidget, prelude::HiddenInput},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum FormMethod {
|
||||||
|
GET,
|
||||||
|
POST,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Form {
|
||||||
|
action: String,
|
||||||
|
method: Option<FormMethod>,
|
||||||
|
multipart: bool,
|
||||||
|
items: Vec<Box<dyn UIWidget>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Form {
|
||||||
|
pub fn new(action: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
action: action.to_string(),
|
||||||
|
method: None,
|
||||||
|
multipart: false,
|
||||||
|
items: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_csrf(self, u: &User) -> Self {
|
||||||
|
self.add_input(
|
||||||
|
HiddenInput("csrf", &u.get_csrf().await.to_string()).add_attr("class", "csrf"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multipart(mut self) -> Self {
|
||||||
|
self.multipart = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn method(mut self, m: FormMethod) -> Self {
|
||||||
|
self.method = Some(m);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_input<T: UIWidget + 'static>(mut self, input: T) -> Self {
|
||||||
|
self.items.push(Box::new(input));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Form {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for Form {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let method = self
|
||||||
|
.method
|
||||||
|
.as_ref()
|
||||||
|
.map(|x| match x {
|
||||||
|
FormMethod::GET => "get",
|
||||||
|
FormMethod::POST => "post",
|
||||||
|
})
|
||||||
|
.unwrap_or("post");
|
||||||
|
|
||||||
|
html! {
|
||||||
|
form action=(self.action) method=(method) enctype=(if self.multipart { "multipart/form-data" } else { "application/x-www-form-urlencoded" }) class=(class) {
|
||||||
|
@for item in &self.items {
|
||||||
|
(item)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
751
src/ui/primitives/input/mod.rs
Normal file
751
src/ui/primitives/input/mod.rs
Normal file
|
@ -0,0 +1,751 @@
|
||||||
|
use std::{collections::HashMap, fmt::Write};
|
||||||
|
|
||||||
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
|
|
||||||
|
pub mod form;
|
||||||
|
pub mod toggle;
|
||||||
|
pub use form::*;
|
||||||
|
pub use toggle::*;
|
||||||
|
|
||||||
|
use crate::ui::{
|
||||||
|
AttrExtendable, UIWidget,
|
||||||
|
color::{ColorCircle, UIColor},
|
||||||
|
htmx::HTMXAttributes,
|
||||||
|
prelude::{Nothing, script},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn TextInput(name: &str) -> TextInputWidget {
|
||||||
|
TextInputWidget {
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
password: false,
|
||||||
|
icon: None,
|
||||||
|
icon_border: false,
|
||||||
|
}
|
||||||
|
.name(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextInputWidget {
|
||||||
|
attrs: HashMap<String, String>,
|
||||||
|
password: bool,
|
||||||
|
icon: Option<Box<dyn UIWidget>>,
|
||||||
|
icon_border: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextInputWidget {
|
||||||
|
pub fn placeholder(self, placeholder: &str) -> Self {
|
||||||
|
self.add_attr("placeholder", placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pattern(self, pattern: &str) -> Self {
|
||||||
|
self.add_attr("pattern", pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn maxlength(self, maxlength: &str) -> Self {
|
||||||
|
self.add_attr("maxlength", maxlength)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn minlength(self, minlength: &str) -> Self {
|
||||||
|
self.add_attr("minlength", minlength)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_border(mut self) -> Self {
|
||||||
|
self.icon_border = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon<T: UIWidget + 'static>(mut self, icon: T) -> Self {
|
||||||
|
self.icon = Some(Box::new(icon));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(mut self) -> Self {
|
||||||
|
self.password = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAttr for TextInputWidget {}
|
||||||
|
|
||||||
|
impl AttrExtendable for TextInputWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.attrs.insert(key.into(), val.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TextInputWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for TextInputWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let mut attrs = self.attrs.clone();
|
||||||
|
|
||||||
|
if self.password {
|
||||||
|
attrs.insert("type".into(), "password".into());
|
||||||
|
} else {
|
||||||
|
attrs.insert("type".into(), "text".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.icon_border {
|
||||||
|
attrs.insert("class".into(),
|
||||||
|
"rounded-none rounded-e-lg bg-gray-50 border text-gray-900 focus:ring-blue-500 focus:border-blue-500 block flex-1 min-w-0 w-full text-sm border-gray-300 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500".into()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
attrs.insert("class".into(),
|
||||||
|
format!("bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full {} p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500", if self.icon.is_some() { "ps-10" } else { "" })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = build_element("input", &attrs, Nothing());
|
||||||
|
|
||||||
|
html! {
|
||||||
|
@if self.icon_border {
|
||||||
|
div class="flex" {
|
||||||
|
@if let Some(icon) = &self.icon {
|
||||||
|
span class="inline-flex items-center px-3 text-sm text-gray-900 bg-gray-200 border rounded-e-0 border-gray-300 border-e-0 rounded-s-md dark:bg-gray-600 dark:text-gray-400 dark:border-gray-600" {
|
||||||
|
(icon)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
(input)
|
||||||
|
|
||||||
|
};
|
||||||
|
} @else {
|
||||||
|
div class="relative mb-6" {
|
||||||
|
@if let Some(icon) = &self.icon {
|
||||||
|
div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none" {
|
||||||
|
(icon)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
(input)
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn FileInput(name: &str) -> FileInputWidget {
|
||||||
|
FileInputWidget {
|
||||||
|
dropzone: None,
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
}
|
||||||
|
.name(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileInputWidget {
|
||||||
|
dropzone: Option<PreEscaped<String>>,
|
||||||
|
attrs: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileInputWidget {
|
||||||
|
pub fn multiple(self) -> Self {
|
||||||
|
self.add_attr("multiple", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accept(self, mime: &str) -> Self {
|
||||||
|
self.add_attr("accept", mime)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dropzone<T: UIWidget + 'static>(mut self, inner: T) -> Self {
|
||||||
|
self.dropzone = Some(inner.render());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAttr for FileInputWidget {}
|
||||||
|
|
||||||
|
impl AttrExtendable for FileInputWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.attrs.insert(key.into(), val.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for FileInputWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for FileInputWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let class = format!("
|
||||||
|
{class} block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400
|
||||||
|
");
|
||||||
|
|
||||||
|
let mut attrs = self.attrs.clone();
|
||||||
|
|
||||||
|
if self.dropzone.is_some() {
|
||||||
|
attrs.insert("class".to_string(), "hidden".to_string());
|
||||||
|
} else {
|
||||||
|
attrs.insert("class".to_string(), class);
|
||||||
|
}
|
||||||
|
attrs.insert("type".to_string(), "file".to_string());
|
||||||
|
|
||||||
|
let input_name = attrs.get("name").map(|x| x.as_str()).unwrap_or_default();
|
||||||
|
|
||||||
|
if let Some(dropzone) = &self.dropzone {
|
||||||
|
return html! {
|
||||||
|
div class="flex items-center justify-center w-full" {
|
||||||
|
label for=(input_name) class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600" {
|
||||||
|
div class="flex flex-col items-center justify-center pt-5 pb-6" {
|
||||||
|
svg class="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2" {};
|
||||||
|
};
|
||||||
|
p class="mb-2 text-sm text-gray-500 dark:text-gray-400" { span class="font-semibold" { "Click to upload" } };
|
||||||
|
(dropzone)
|
||||||
|
};
|
||||||
|
(build_element("input", &attrs, Nothing()))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
build_element("input", &attrs, Nothing())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn PinInput(digits: u8) -> PreEscaped<String> {
|
||||||
|
let js = r#"
|
||||||
|
function pin_move_next(event) {
|
||||||
|
const input = event.target;
|
||||||
|
if (input.value.length === 1) {
|
||||||
|
const nextInput = document.querySelector(`#${input.dataset.focusInputNext}`);
|
||||||
|
if (nextInput) nextInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function pin_move_back(event) {
|
||||||
|
const input = event.target;
|
||||||
|
if (event.key === "Backspace" && input.value === "") {
|
||||||
|
const prevInput = document.querySelector(`[data-focus-input-next='${input.id}']`);
|
||||||
|
if (prevInput) {
|
||||||
|
prevInput.focus();
|
||||||
|
prevInput.setSelectionRange(1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function enforceNumericInput(event) {
|
||||||
|
if (!/^[0-9]$/.test(event.key)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="flex mb-2 space-x-2 rtl:space-x-reverse" {
|
||||||
|
(script(js))
|
||||||
|
|
||||||
|
@for i in 1..=digits {
|
||||||
|
div {
|
||||||
|
label for=(format!("pin-{i}")) class="sr-only" { (format!("Pin {i}")) };
|
||||||
|
|
||||||
|
@if i == digits {
|
||||||
|
input type="text"
|
||||||
|
maxlength="1"
|
||||||
|
data-focus-input-init
|
||||||
|
pattern="[0-9]"
|
||||||
|
onkeypress="enforceNumericInput(event)"
|
||||||
|
id=(format!("pin-{i}"))
|
||||||
|
oninput="pin_move_next(event)" onkeydown="pin_move_back(event)"
|
||||||
|
class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required {};
|
||||||
|
} @else {
|
||||||
|
input type="text"
|
||||||
|
maxlength="1"
|
||||||
|
pattern="[0-9]"
|
||||||
|
data-focus-input-init
|
||||||
|
onkeypress="enforceNumericInput(event)"
|
||||||
|
data-focus-input-next=(format!("pin-{}", i+1))
|
||||||
|
id=(format!("pin-{i}"))
|
||||||
|
oninput="pin_move_next(event)" onkeydown="pin_move_back(event)"
|
||||||
|
class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NumberInputWidget {
|
||||||
|
inner: Option<Box<dyn UIWidget>>,
|
||||||
|
attr: HashMap<String, String>,
|
||||||
|
buttons: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn NumberInput() -> NumberInputWidget {
|
||||||
|
NumberInputWidget {
|
||||||
|
inner: None,
|
||||||
|
attr: HashMap::new(),
|
||||||
|
buttons: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NumberInputWidget {
|
||||||
|
pub fn with_buttons(mut self) -> Self {
|
||||||
|
self.buttons = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min(self, min: u32) -> Self {
|
||||||
|
self.add_attr("min", &min.to_string())
|
||||||
|
.add_attr("data-input-counter-min", &min.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max(self, max: u32) -> Self {
|
||||||
|
self.add_attr("max", &max.to_string())
|
||||||
|
.add_attr("data-input-counter-max", &max.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(self, step: u32) -> Self {
|
||||||
|
self.add_attr("step", &step.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn help<T: UIWidget + 'static>(mut self, help: T) -> Self {
|
||||||
|
self.inner = Some(Box::new(help));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrExtendable for NumberInputWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.attr.insert(key.into(), val.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAttr for NumberInputWidget {}
|
||||||
|
|
||||||
|
impl Render for NumberInputWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for NumberInputWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
let mut attrs = self.attr.clone();
|
||||||
|
|
||||||
|
attrs.insert("type".to_string(), "text".to_string());
|
||||||
|
attrs.insert("data-input-counter".into(), "".into());
|
||||||
|
attrs.insert("class".into(), format!(
|
||||||
|
"bg-gray-50 border-x-0 border-gray-300 h-11 font-medium text-center text-gray-900 text-sm focus:ring-blue-500 focus:border-blue-500 block w-full dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 {}", if self.inner.is_some() { "pb-6" } else { ""}));
|
||||||
|
|
||||||
|
let input_name = attrs.get("id").map(|x| x.as_str()).unwrap_or_default();
|
||||||
|
|
||||||
|
let input = build_element("input", &attrs, Nothing());
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="relative flex items-center max-w-[11rem]" {
|
||||||
|
@if self.buttons {
|
||||||
|
button
|
||||||
|
type="button"
|
||||||
|
id="decrement-button"
|
||||||
|
onclick=(format!("document.getElementById(\"{input_name}\").value = (+document.getElementById(\"{input_name}\").value || 0) + ((-document.getElementById(\"{input_name}\").step || 1) + 1);"))
|
||||||
|
data-input-counter-decrement=(input_name)
|
||||||
|
class="bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:border-gray-600 hover:bg-gray-200 border border-gray-300 rounded-s-lg p-3 h-11 focus:ring-gray-100 dark:focus:ring-gray-700 focus:ring-2 focus:outline-none" {
|
||||||
|
svg class="w-3 h-3 text-gray-900 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 2" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h16" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
(input)
|
||||||
|
|
||||||
|
@if let Some(inner) = &self.inner {
|
||||||
|
div class="absolute bottom-1 start-1/2 -translate-x-1/2 rtl:translate-x-1/2 flex items-center text-xs text-gray-400 space-x-1 rtl:space-x-reverse" {
|
||||||
|
(inner)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
@if self.buttons {
|
||||||
|
button type="button" id="increment-button"
|
||||||
|
onclick=(format!("document.getElementById(\"{input_name}\").value = (+document.getElementById(\"{input_name}\").value || 0) + ((+document.getElementById(\"{input_name}\").step || 1) - 1);"))
|
||||||
|
data-input-counter-increment=(input_name) class="bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:border-gray-600 hover:bg-gray-200 border border-gray-300 rounded-e-lg p-3 h-11 focus:ring-gray-100 dark:focus:ring-gray-700 focus:ring-2 focus:outline-none" {
|
||||||
|
svg class="w-3 h-3 text-gray-900 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 18" {
|
||||||
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 1v16M1 9h16" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : Test impl
|
||||||
|
pub fn TimePicker() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div class="relative" {
|
||||||
|
div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none" {
|
||||||
|
svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" {
|
||||||
|
path fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l3 3a1 1 0 0 0 1.414-1.414L13 11.586V8Z" clip-rule="evenodd" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
input type="time" id="time" class="bg-gray-50 border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" min="09:00" max="18:00" value="00:00" required {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn TextArea() -> TextAreaWidget {
|
||||||
|
TextAreaWidget {
|
||||||
|
content: String::new(),
|
||||||
|
attr: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextAreaWidget {
|
||||||
|
content: String,
|
||||||
|
attr: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for TextAreaWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let mut attrs = self.attr.clone();
|
||||||
|
attrs.insert("class".into(), format!(
|
||||||
|
"{class} block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
));
|
||||||
|
|
||||||
|
build_element("textarea", &attrs, PreEscaped(self.content.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TextAreaWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAttr for TextAreaWidget {}
|
||||||
|
|
||||||
|
impl AttrExtendable for TextAreaWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.attr.insert(key.into(), val.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextAreaWidget {
|
||||||
|
pub fn content(mut self, content: String) -> Self {
|
||||||
|
self.content = content;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn placeholder(self, placeholder: &str) -> Self {
|
||||||
|
self.add_attr("placeholder", placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rows(self, rows: u32) -> Self {
|
||||||
|
self.add_attr("rows", &rows.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pattern(self, pattern: &str) -> Self {
|
||||||
|
self.add_attr("pattern", pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn maxlength(self, maxlength: &str) -> Self {
|
||||||
|
self.add_attr("maxlength", maxlength)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn minlength(self, minlength: &str) -> Self {
|
||||||
|
self.add_attr("minlength", minlength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn Select(
|
||||||
|
label: Option<String>,
|
||||||
|
id: &str,
|
||||||
|
default: &str,
|
||||||
|
options: Vec<(String, String)>,
|
||||||
|
) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
@if let Some(label) = label {
|
||||||
|
label for=(id) class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" { (label) };
|
||||||
|
};
|
||||||
|
|
||||||
|
select id=(id) class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" {
|
||||||
|
@for (value, label) in &options {
|
||||||
|
@if value == default {
|
||||||
|
@if value.is_empty() {
|
||||||
|
option selected { (label) };
|
||||||
|
} @else {
|
||||||
|
option value=(value) selected { (label) };
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
@if value.is_empty() {
|
||||||
|
option { (label) };
|
||||||
|
} @else {
|
||||||
|
option value=(value) { (label) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait InputAttr: AttrExtendable + Sized {
|
||||||
|
fn name(self, name: &str) -> Self {
|
||||||
|
self.add_attr("name", name).add_attr("id", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value(self, value: &str) -> Self {
|
||||||
|
self.add_attr("value", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readonly(self) -> Self {
|
||||||
|
self.add_attr("readonly", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disabled(self) -> Self {
|
||||||
|
self.add_attr("disabled", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required(self) -> Self {
|
||||||
|
self.add_attr("required", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn autofocus(self) -> Self {
|
||||||
|
self.add_attr("autofocus", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn autocomplete(self, value: bool) -> Self {
|
||||||
|
self.add_attr("autocomplete", &value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : other inputs
|
||||||
|
// color input
|
||||||
|
// date
|
||||||
|
// datetime
|
||||||
|
// time
|
||||||
|
// url
|
||||||
|
// email
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn HiddenInput(name: &str, value: &str) -> HiddenInputWidget {
|
||||||
|
HiddenInputWidget {
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
}
|
||||||
|
.name(name)
|
||||||
|
.value(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HiddenInputWidget {
|
||||||
|
attrs: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrExtendable for HiddenInputWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.attrs.insert(key.to_string(), val.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAttr for HiddenInputWidget {
|
||||||
|
fn name(self, name: &str) -> Self {
|
||||||
|
self.add_attr("name", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for HiddenInputWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for HiddenInputWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
let mut attrs = self.attrs.clone();
|
||||||
|
attrs.insert("type".into(), "hidden".into());
|
||||||
|
build_element("input", &attrs, Nothing())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn FormResetButton<T: UIWidget + 'static>(inner: T) -> ButtonWidget {
|
||||||
|
let btn = ButtonWidget {
|
||||||
|
inner: Box::new(inner),
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
color: None,
|
||||||
|
hover_color: None,
|
||||||
|
};
|
||||||
|
btn.add_attr("type", "reset")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn FormSubmitButton<T: UIWidget + 'static>(inner: T) -> ButtonWidget {
|
||||||
|
let btn = ButtonWidget {
|
||||||
|
inner: Box::new(inner),
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
color: None,
|
||||||
|
hover_color: None,
|
||||||
|
};
|
||||||
|
btn.add_attr("type", "submit")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ButtonWidget {
|
||||||
|
inner: Box<dyn UIWidget>,
|
||||||
|
attrs: HashMap<String, String>,
|
||||||
|
color: Option<Box<dyn UIColor>>,
|
||||||
|
hover_color: Option<Box<dyn UIColor>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonWidget {
|
||||||
|
pub fn color<C: UIColor + ColorCircle + 'static>(mut self, color: C) -> Self {
|
||||||
|
let hover = color.next();
|
||||||
|
self.color = Some(Box::new(color));
|
||||||
|
self.hover_color = Some(Box::new(hover));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HTMXAttributes for ButtonWidget {}
|
||||||
|
impl InputAttr for ButtonWidget {}
|
||||||
|
|
||||||
|
impl AttrExtendable for ButtonWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.attrs.insert(key.into(), val.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ButtonWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for ButtonWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let mut attrs = self.attrs.clone();
|
||||||
|
|
||||||
|
let color = if let Some(c) = &self.color {
|
||||||
|
c.color_class()
|
||||||
|
} else {
|
||||||
|
"blue-700"
|
||||||
|
};
|
||||||
|
|
||||||
|
let hover_color = if let Some(c) = &self.hover_color {
|
||||||
|
c.color_class()
|
||||||
|
} else {
|
||||||
|
"blue-800"
|
||||||
|
};
|
||||||
|
|
||||||
|
attrs.insert("class".to_string(), format!(
|
||||||
|
"{class} px-5 py-2.5 text-sm font-medium text-white inline-flex items-center bg-{color} hover:bg-{hover_color} rounded-lg text-center"
|
||||||
|
));
|
||||||
|
|
||||||
|
build_element("button", &attrs, self.inner.render())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Button<T: UIWidget + 'static>(inner: T) -> ButtonWidget {
|
||||||
|
ButtonWidget {
|
||||||
|
inner: Box::new(inner),
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
color: None,
|
||||||
|
hover_color: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn ButtonGroup(buttons: Vec<ButtonWidget>) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
div class="inline-flex rounded-md shadow-xs" role="group" {
|
||||||
|
@for (index, element) in buttons.iter().enumerate() {
|
||||||
|
@if index == 0 {
|
||||||
|
button type="button" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white" {
|
||||||
|
(element.inner)
|
||||||
|
}
|
||||||
|
} @else if index == (buttons.len()-1) {
|
||||||
|
button type="button" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white" {
|
||||||
|
(element.inner)
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
button type="button" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border-t border-b border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white" {
|
||||||
|
(element.inner)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
394
src/ui/primitives/input/toggle.rs
Normal file
394
src/ui/primitives/input/toggle.rs
Normal file
|
@ -0,0 +1,394 @@
|
||||||
|
use std::{collections::HashMap, fmt::Write};
|
||||||
|
|
||||||
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::{
|
||||||
|
AttrExtendable, UIWidget,
|
||||||
|
color::{Blue, UIColor},
|
||||||
|
prelude::Nothing,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::InputAttr;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Range() -> RangeWidget {
|
||||||
|
RangeWidget {
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RangeWidget {
|
||||||
|
attrs: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrExtendable for RangeWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.attrs.insert(key.into(), val.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RangeWidget {
|
||||||
|
pub fn min(self, min: u32) -> Self {
|
||||||
|
self.add_attr("min", &min.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max(self, max: u32) -> Self {
|
||||||
|
self.add_attr("max", &max.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(self, step: u32) -> Self {
|
||||||
|
self.add_attr("step", &step.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAttr for RangeWidget {}
|
||||||
|
|
||||||
|
impl Render for RangeWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for RangeWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"w-full".into(),
|
||||||
|
"h-2".into(),
|
||||||
|
"bg-gray-200".into(),
|
||||||
|
"rounded-lg".into(),
|
||||||
|
"appearence-none".into(),
|
||||||
|
"cursor-pointer".into(),
|
||||||
|
"dark:bg-gray-700".into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
self.base_class()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let mut attrs = self.attrs.clone();
|
||||||
|
attrs.insert(
|
||||||
|
"class".into(),
|
||||||
|
format!("{class} {}", self.base_class().join(" ")),
|
||||||
|
);
|
||||||
|
attrs.insert("type".into(), "range".into());
|
||||||
|
build_element("input", &attrs, PreEscaped(String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_element(
|
||||||
|
element: &str,
|
||||||
|
attrs: &HashMap<String, String>,
|
||||||
|
inner: PreEscaped<String>,
|
||||||
|
) -> PreEscaped<String> {
|
||||||
|
let mut ret = String::with_capacity(256);
|
||||||
|
write!(&mut ret, "<{element}").unwrap();
|
||||||
|
|
||||||
|
for (key, value) in attrs {
|
||||||
|
if value.is_empty() {
|
||||||
|
write!(&mut ret, " {key}").unwrap();
|
||||||
|
} else {
|
||||||
|
write!(&mut ret, " {key}='{}'", value.replace("'", "\\'")).unwrap();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if inner.0.is_empty() {
|
||||||
|
if element == "textarea" {
|
||||||
|
write!(&mut ret, "></textarea>").unwrap();
|
||||||
|
} else {
|
||||||
|
write!(&mut ret, ">").unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write!(&mut ret, ">{}</{element}>", inner.0).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
PreEscaped(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Toggle(title: &str) -> ToggleWidget {
|
||||||
|
ToggleWidget {
|
||||||
|
color: Box::new(Blue::_600),
|
||||||
|
checked: false,
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
title: title.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ToggleWidget {
|
||||||
|
color: Box<dyn UIColor>,
|
||||||
|
checked: bool,
|
||||||
|
attrs: HashMap<String, String>,
|
||||||
|
title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToggleWidget {
|
||||||
|
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.color = Box::new(color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checked(mut self, checked: bool) -> Self {
|
||||||
|
self.checked = checked;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrExtendable for ToggleWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.attrs.insert(key.to_string(), val.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAttr for ToggleWidget {}
|
||||||
|
|
||||||
|
impl Render for ToggleWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for ToggleWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
let color = self.color.color_class();
|
||||||
|
let class = format!(
|
||||||
|
"relative w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-{color} dark:peer-focus:ring-blue-800 dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-{color} dark:peer-checked:bg-{color}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut attrs = self.attrs.clone();
|
||||||
|
|
||||||
|
attrs.insert("class".into(), "sr-only peer".into());
|
||||||
|
attrs.insert("type".into(), "checkbox".into());
|
||||||
|
|
||||||
|
if self.checked {
|
||||||
|
attrs.insert("checked".into(), "".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = build_element("input", &attrs, Nothing());
|
||||||
|
|
||||||
|
html! {
|
||||||
|
label class="inline-flex items-center cursor-pointer" {
|
||||||
|
(input)
|
||||||
|
div class=(class) {};
|
||||||
|
span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300" { (self.title) };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn CustomRadio<T: UIWidget + 'static>(inner: T, group_name: &str) -> RadioWidget {
|
||||||
|
RadioWidget {
|
||||||
|
color: Box::new(Blue::_600),
|
||||||
|
checked: false,
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
title: inner.render().0,
|
||||||
|
custom: true,
|
||||||
|
}
|
||||||
|
.name(group_name)
|
||||||
|
.id(&format!("radio-{}", uuid::Uuid::new_v4().to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Radio(title: &str, group_name: &str) -> RadioWidget {
|
||||||
|
RadioWidget {
|
||||||
|
color: Box::new(Blue::_600),
|
||||||
|
checked: false,
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
title: title.to_string(),
|
||||||
|
custom: false,
|
||||||
|
}
|
||||||
|
.name(group_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RadioWidget {
|
||||||
|
color: Box<dyn UIColor>,
|
||||||
|
checked: bool,
|
||||||
|
attrs: HashMap<String, String>,
|
||||||
|
title: String,
|
||||||
|
custom: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RadioWidget {
|
||||||
|
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.color = Box::new(color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checked(mut self, checked: bool) -> Self {
|
||||||
|
self.checked = checked;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrExtendable for RadioWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.attrs.insert(key.to_string(), val.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAttr for RadioWidget {
|
||||||
|
fn name(self, name: &str) -> Self {
|
||||||
|
self.add_attr("name", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for RadioWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for RadioWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
let mut attrs = self.attrs.clone();
|
||||||
|
|
||||||
|
let color = self.color.color_class();
|
||||||
|
|
||||||
|
if self.custom {
|
||||||
|
attrs.insert("class".into(), "hidden peer".into());
|
||||||
|
} else {
|
||||||
|
attrs.insert("class".into(), format!("w-4 h-4 text-{color} bg-gray-100 border-gray-300 focus:ring-{color} dark:focus:ring-{color} dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"));
|
||||||
|
}
|
||||||
|
attrs.insert("type".into(), "radio".into());
|
||||||
|
|
||||||
|
let input = build_element("input", &attrs, Nothing());
|
||||||
|
|
||||||
|
let input_id = attrs.get("id").map(|x| x.as_str()).unwrap_or_default();
|
||||||
|
|
||||||
|
let label_class = if self.custom {
|
||||||
|
format!(
|
||||||
|
"inline-flex items-center justify-between w-full p-5 text-gray-500 bg-white border border-gray-200 rounded-lg cursor-pointer dark:hover:text-gray-300 dark:border-gray-700 dark:peer-checked:text-{color} peer-checked:border-{color} dark:peer-checked:border-{color} peer-checked:text-{color} hover:text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"ms-2 text-sm font-medium text-gray-900 dark:text-gray-300".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="flex items-center me-4" {
|
||||||
|
(input)
|
||||||
|
|
||||||
|
label for=(input_id) class=(label_class) {
|
||||||
|
(PreEscaped(self.title.clone()))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Checkbox(title: &str) -> CheckboxWidget {
|
||||||
|
CheckboxWidget {
|
||||||
|
color: Box::new(Blue::_600),
|
||||||
|
checked: false,
|
||||||
|
attrs: HashMap::new(),
|
||||||
|
title: title.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CheckboxWidget {
|
||||||
|
color: Box<dyn UIColor>,
|
||||||
|
checked: bool,
|
||||||
|
attrs: HashMap<String, String>,
|
||||||
|
title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CheckboxWidget {
|
||||||
|
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.color = Box::new(color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checked(mut self, checked: bool) -> Self {
|
||||||
|
self.checked = checked;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrExtendable for CheckboxWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.attrs.insert(key.to_string(), val.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAttr for CheckboxWidget {}
|
||||||
|
|
||||||
|
impl Render for CheckboxWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for CheckboxWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
let color = self.color.color_class();
|
||||||
|
let class = format!(
|
||||||
|
"w-4 h-4 text-{color} bg-gray-100 border-gray-300 rounded-sm focus:ring-{color} dark:focus:ring-{color} dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut attrs = self.attrs.clone();
|
||||||
|
|
||||||
|
attrs.insert("class".into(), class);
|
||||||
|
attrs.insert("type".into(), "checkbox".into());
|
||||||
|
|
||||||
|
if self.checked {
|
||||||
|
attrs.insert("checked".into(), "".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = build_element("input", &attrs, Nothing());
|
||||||
|
|
||||||
|
let input_id = attrs.get("id").map(|x| x.as_str()).unwrap_or_default();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="flex items-center me-4" {
|
||||||
|
(input)
|
||||||
|
label for=(input_id) class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300" { (self.title) };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
src/ui/primitives/link.rs
Normal file
84
src/ui/primitives/link.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use maud::{Markup, PreEscaped, Render};
|
||||||
|
|
||||||
|
use crate::ui::{
|
||||||
|
AttrExtendable, UIWidget,
|
||||||
|
htmx::{HTMXAttributes, Selector, SwapStrategy},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// A component for fixing an element's width to the current breakpoint.
|
||||||
|
pub fn Link<T: UIWidget + 'static>(reference: &str, inner: T) -> LinkWidget {
|
||||||
|
LinkWidget(Box::new(inner), reference.to_owned(), HashMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LinkWidget(Box<dyn UIWidget>, String, HashMap<String, String>);
|
||||||
|
|
||||||
|
impl AttrExtendable for LinkWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.2.insert(key.to_string(), val.replace('\'', "\\'"));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(self, id: &str) -> Self {
|
||||||
|
self.add_attr("id", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HTMXAttributes for LinkWidget {}
|
||||||
|
|
||||||
|
impl Render for LinkWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class(&self.extended_class().join(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for LinkWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let attrs = self
|
||||||
|
.2
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{k}='{v}'"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
PreEscaped(format!(
|
||||||
|
"<a href='{}' class='{class}' {attrs}> {} </a>",
|
||||||
|
self.1,
|
||||||
|
self.0.render().0
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinkWidget {
|
||||||
|
/// Enable HTMX link capabilities
|
||||||
|
#[must_use]
|
||||||
|
pub fn use_htmx(self) -> Self {
|
||||||
|
let url = self.1.clone();
|
||||||
|
self.hx_get(&url)
|
||||||
|
.hx_target(Selector::Query("#main_content".to_string()))
|
||||||
|
.hx_push_url()
|
||||||
|
.hx_swap(
|
||||||
|
SwapStrategy::innerHTML
|
||||||
|
.focus_scroll(true)
|
||||||
|
.show("window:top"),
|
||||||
|
)
|
||||||
|
.hx_boost()
|
||||||
|
.hx_disabled_elt(Selector::This)
|
||||||
|
}
|
||||||
|
}
|
109
src/ui/primitives/list.rs
Normal file
109
src/ui/primitives/list.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn OrderedList() -> ListWidget {
|
||||||
|
ListWidget(Vec::new(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn UnorderedList() -> ListWidget {
|
||||||
|
ListWidget(Vec::new(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ListWidget(Vec<Box<dyn UIWidget>>, bool);
|
||||||
|
|
||||||
|
impl ListWidget {
|
||||||
|
#[must_use]
|
||||||
|
pub fn push<T: UIWidget + 'static>(mut self, element: T) -> Self {
|
||||||
|
self.0.push(Box::new(element));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn push_some<T: UIWidget + 'static, X, U: Fn(X) -> T>(
|
||||||
|
mut self,
|
||||||
|
option: Option<X>,
|
||||||
|
then: U,
|
||||||
|
) -> Self {
|
||||||
|
if let Some(val) = option {
|
||||||
|
self.0.push(Box::new(then(val)));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn push_if<T: UIWidget + 'static, U: Fn() -> T>(
|
||||||
|
mut self,
|
||||||
|
condition: bool,
|
||||||
|
then: U,
|
||||||
|
) -> Self {
|
||||||
|
if condition {
|
||||||
|
self.0.push(Box::new(then()));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn push_for_each<T, X, F>(mut self, items: &[X], mut action: F) -> Self
|
||||||
|
where
|
||||||
|
T: UIWidget + 'static,
|
||||||
|
F: FnMut(&X) -> T,
|
||||||
|
{
|
||||||
|
for item in items {
|
||||||
|
self.0.push(Box::new(action(item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ListWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for ListWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let inner = html! {
|
||||||
|
@for e in &self.0 {
|
||||||
|
li { (e.as_ref()) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.1 {
|
||||||
|
html! {
|
||||||
|
ol class=(class) {
|
||||||
|
(inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
ul class=(class) {
|
||||||
|
(inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://flowbite.com/docs/typography/lists/
|
||||||
|
// TODO : List
|
||||||
|
// list with icons
|
||||||
|
// horizontal list
|
||||||
|
// data backed list + reorderable + add + remove + crud
|
150
src/ui/primitives/margin.rs
Normal file
150
src/ui/primitives/margin.rs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
use super::space::ScreenValue;
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Margin<T: UIWidget + 'static>(inner: T) -> Margin {
|
||||||
|
Margin {
|
||||||
|
inner: Box::new(inner),
|
||||||
|
all: None,
|
||||||
|
x: None,
|
||||||
|
y: None,
|
||||||
|
start: None,
|
||||||
|
end: None,
|
||||||
|
top: None,
|
||||||
|
right: None,
|
||||||
|
bottom: None,
|
||||||
|
left: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Margin {
|
||||||
|
pub inner: Box<dyn UIWidget>,
|
||||||
|
pub all: Option<ScreenValue>,
|
||||||
|
pub x: Option<ScreenValue>,
|
||||||
|
pub y: Option<ScreenValue>,
|
||||||
|
pub start: Option<ScreenValue>,
|
||||||
|
pub end: Option<ScreenValue>,
|
||||||
|
pub top: Option<ScreenValue>,
|
||||||
|
pub right: Option<ScreenValue>,
|
||||||
|
pub bottom: Option<ScreenValue>,
|
||||||
|
pub left: Option<ScreenValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Margin {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn all(mut self, all: ScreenValue) -> Self {
|
||||||
|
self.all = Some(all);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn top(mut self, top: ScreenValue) -> Self {
|
||||||
|
self.top = Some(top);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn right(mut self, right: ScreenValue) -> Self {
|
||||||
|
self.right = Some(right);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn bottom(mut self, bottom: ScreenValue) -> Self {
|
||||||
|
self.bottom = Some(bottom);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn left(mut self, left: ScreenValue) -> Self {
|
||||||
|
self.left = Some(left);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn y(mut self, y: ScreenValue) -> Self {
|
||||||
|
self.y = Some(y);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn x(mut self, x: ScreenValue) -> Self {
|
||||||
|
self.x = Some(x);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Margin {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for Margin {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut our_class = Vec::new();
|
||||||
|
|
||||||
|
if let Some(all) = &self.all {
|
||||||
|
our_class.push(format!("m-{}", all.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = &self.x {
|
||||||
|
our_class.push(format!("mx-{}", x.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(y) = &self.y {
|
||||||
|
our_class.push(format!("my-{}", y.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(start) = &self.start {
|
||||||
|
our_class.push(format!("ms-{}", start.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(end) = &self.end {
|
||||||
|
our_class.push(format!("me-{}", end.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(top) = &self.top {
|
||||||
|
our_class.push(format!("mt-{}", top.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(right) = &self.right {
|
||||||
|
our_class.push(format!("mr-{}", right.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bottom) = &self.bottom {
|
||||||
|
our_class.push(format!("mb-{}", bottom.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(left) = &self.left {
|
||||||
|
our_class.push(format!("ml-{}", left.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
our_class
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.inner.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.inner.as_ref().can_inherit() {
|
||||||
|
self.inner
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.inner.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
194
src/ui/primitives/mod.rs
Normal file
194
src/ui/primitives/mod.rs
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
use super::UIWidget;
|
||||||
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
|
|
||||||
|
pub mod animation;
|
||||||
|
pub mod background;
|
||||||
|
pub mod border;
|
||||||
|
pub mod container;
|
||||||
|
pub mod cursor;
|
||||||
|
pub mod display;
|
||||||
|
pub mod div;
|
||||||
|
pub mod filter;
|
||||||
|
pub mod flex;
|
||||||
|
pub mod grid;
|
||||||
|
pub mod height;
|
||||||
|
pub mod image;
|
||||||
|
pub mod input;
|
||||||
|
pub mod link;
|
||||||
|
pub mod list;
|
||||||
|
pub mod margin;
|
||||||
|
pub mod padding;
|
||||||
|
pub mod position;
|
||||||
|
pub mod rounded;
|
||||||
|
pub mod scroll;
|
||||||
|
pub mod shadow;
|
||||||
|
pub mod sized;
|
||||||
|
pub mod space;
|
||||||
|
pub mod svg;
|
||||||
|
pub mod table;
|
||||||
|
pub mod text;
|
||||||
|
pub mod transform;
|
||||||
|
pub mod visibility;
|
||||||
|
pub mod width;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn Nothing() -> PreEscaped<String> {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[must_use]
|
||||||
|
/// Create a new inheritance context
|
||||||
|
///
|
||||||
|
/// This acts as a hard barrier for inheritance.
|
||||||
|
/// This allows you to embed Widgets without them interacting with the rest of the tree.
|
||||||
|
pub fn Context<T: UIWidget>(inner: T) -> PreEscaped<String> {
|
||||||
|
html! { (inner) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a `<script>` element containing the provided JavaScript code.
|
||||||
|
///
|
||||||
|
/// This function wraps the provided JavaScript code in a `<script>` tag,
|
||||||
|
/// allowing for easy inclusion of custom scripts in the rendered HTML.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `script` - The JavaScript code to include.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `PreEscaped<String>` containing the rendered `<script>` element.
|
||||||
|
#[must_use]
|
||||||
|
pub fn script(script: &str) -> PreEscaped<String> {
|
||||||
|
html!(
|
||||||
|
script {
|
||||||
|
(PreEscaped(script))
|
||||||
|
};
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Size {
|
||||||
|
Custom(String),
|
||||||
|
None,
|
||||||
|
Small,
|
||||||
|
Regular,
|
||||||
|
Medium,
|
||||||
|
Large,
|
||||||
|
XL,
|
||||||
|
_2XL,
|
||||||
|
_3XL,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Size {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Custom(str) => str.as_str(),
|
||||||
|
Self::None => "none",
|
||||||
|
Self::Small => "sm",
|
||||||
|
Self::Regular => "",
|
||||||
|
Self::Medium => "md",
|
||||||
|
Self::Large => "lg",
|
||||||
|
Self::XL => "xl",
|
||||||
|
Self::_2XL => "2xl",
|
||||||
|
Self::_3XL => "3xl",
|
||||||
|
Self::Full => "full",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Side {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Top,
|
||||||
|
Right,
|
||||||
|
Bottom,
|
||||||
|
Left,
|
||||||
|
StartStart,
|
||||||
|
StartEnd,
|
||||||
|
EndEnd,
|
||||||
|
EndStart,
|
||||||
|
TopLeft,
|
||||||
|
TopRight,
|
||||||
|
BottomRight,
|
||||||
|
BottomLeft,
|
||||||
|
Center,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Side {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Start => "s",
|
||||||
|
Self::End => "e",
|
||||||
|
Self::Top => "t",
|
||||||
|
Self::Right => "r",
|
||||||
|
Self::Bottom => "b",
|
||||||
|
Self::Left => "l",
|
||||||
|
Self::StartStart => "ss",
|
||||||
|
Self::StartEnd => "se",
|
||||||
|
Self::EndEnd => "ee",
|
||||||
|
Self::EndStart => "es",
|
||||||
|
Self::TopLeft => "tl",
|
||||||
|
Self::TopRight => "tr",
|
||||||
|
Self::BottomRight => "br",
|
||||||
|
Self::BottomLeft => "bl",
|
||||||
|
Self::Center => "center",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn NoBrowserAppearance<T: UIWidget + 'static>(inner: T) -> NoBrowserAppearanceWidget {
|
||||||
|
NoBrowserAppearanceWidget(Box::new(inner))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NoBrowserAppearanceWidget(Box<dyn UIWidget>);
|
||||||
|
|
||||||
|
impl Render for NoBrowserAppearanceWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for NoBrowserAppearanceWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec!["appearance-none".to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("appearance-none {class}"))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("appearance-none {class}")) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Optional<T: UIWidget + 'static, X, U: Fn(X) -> T>(
|
||||||
|
option: Option<X>,
|
||||||
|
then: U,
|
||||||
|
) -> PreEscaped<String> {
|
||||||
|
if let Some(val) = option {
|
||||||
|
return then(val).render();
|
||||||
|
}
|
||||||
|
|
||||||
|
Nothing()
|
||||||
|
}
|
150
src/ui/primitives/padding.rs
Normal file
150
src/ui/primitives/padding.rs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
use super::space::ScreenValue;
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Padding<T: UIWidget + 'static>(inner: T) -> PaddingWidget {
|
||||||
|
PaddingWidget {
|
||||||
|
inner: Box::new(inner),
|
||||||
|
all: None,
|
||||||
|
x: None,
|
||||||
|
y: None,
|
||||||
|
start: None,
|
||||||
|
end: None,
|
||||||
|
top: None,
|
||||||
|
right: None,
|
||||||
|
bottom: None,
|
||||||
|
left: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PaddingWidget {
|
||||||
|
pub inner: Box<dyn UIWidget>,
|
||||||
|
pub all: Option<ScreenValue>,
|
||||||
|
pub x: Option<ScreenValue>,
|
||||||
|
pub y: Option<ScreenValue>,
|
||||||
|
pub start: Option<ScreenValue>,
|
||||||
|
pub end: Option<ScreenValue>,
|
||||||
|
pub top: Option<ScreenValue>,
|
||||||
|
pub right: Option<ScreenValue>,
|
||||||
|
pub bottom: Option<ScreenValue>,
|
||||||
|
pub left: Option<ScreenValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaddingWidget {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn all(mut self, all: ScreenValue) -> Self {
|
||||||
|
self.all = Some(all);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn top(mut self, top: ScreenValue) -> Self {
|
||||||
|
self.top = Some(top);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn right(mut self, right: ScreenValue) -> Self {
|
||||||
|
self.right = Some(right);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn bottom(mut self, bottom: ScreenValue) -> Self {
|
||||||
|
self.bottom = Some(bottom);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn left(mut self, left: ScreenValue) -> Self {
|
||||||
|
self.left = Some(left);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn y(mut self, y: ScreenValue) -> Self {
|
||||||
|
self.y = Some(y);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn x(mut self, x: ScreenValue) -> Self {
|
||||||
|
self.x = Some(x);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PaddingWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for PaddingWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut our_class = Vec::new();
|
||||||
|
|
||||||
|
if let Some(all) = &self.all {
|
||||||
|
our_class.push(format!("p-{}", all.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = &self.x {
|
||||||
|
our_class.push(format!("px-{}", x.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(y) = &self.y {
|
||||||
|
our_class.push(format!("py-{}", y.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(start) = &self.start {
|
||||||
|
our_class.push(format!("ps-{}", start.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(end) = &self.end {
|
||||||
|
our_class.push(format!("pe-{}", end.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(top) = &self.top {
|
||||||
|
our_class.push(format!("pt-{}", top.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(right) = &self.right {
|
||||||
|
our_class.push(format!("pr-{}", right.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bottom) = &self.bottom {
|
||||||
|
our_class.push(format!("pb-{}", bottom.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(left) = &self.left {
|
||||||
|
our_class.push(format!("pl-{}", left.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
our_class
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.inner.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.inner.as_ref().can_inherit() {
|
||||||
|
self.inner
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.inner.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
341
src/ui/primitives/position.rs
Normal file
341
src/ui/primitives/position.rs
Normal file
|
@ -0,0 +1,341 @@
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
|
use super::Side;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Position<T: UIWidget + 'static>(kind: PositionKind, inner: T) -> Positioned {
|
||||||
|
Positioned {
|
||||||
|
inner: Box::new(inner),
|
||||||
|
kind,
|
||||||
|
inset: None,
|
||||||
|
inset_x: None,
|
||||||
|
inset_y: None,
|
||||||
|
start: None,
|
||||||
|
end: None,
|
||||||
|
top: None,
|
||||||
|
right: None,
|
||||||
|
bottom: None,
|
||||||
|
left: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Positioned {
|
||||||
|
inner: Box<dyn UIWidget>,
|
||||||
|
kind: PositionKind,
|
||||||
|
inset: Option<i64>,
|
||||||
|
inset_x: Option<i64>,
|
||||||
|
inset_y: Option<i64>,
|
||||||
|
start: Option<i64>,
|
||||||
|
end: Option<i64>,
|
||||||
|
top: Option<i64>,
|
||||||
|
right: Option<i64>,
|
||||||
|
bottom: Option<i64>,
|
||||||
|
left: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Positioned {
|
||||||
|
pub fn inset(mut self, value: i64) -> Self {
|
||||||
|
self.inset = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn inset_x(mut self, value: i64) -> Self {
|
||||||
|
self.inset_x = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inset_y(mut self, value: i64) -> Self {
|
||||||
|
self.inset_y = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(mut self, value: i64) -> Self {
|
||||||
|
self.start = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(mut self, value: i64) -> Self {
|
||||||
|
self.end = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn top(mut self, value: i64) -> Self {
|
||||||
|
self.top = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right(mut self, value: i64) -> Self {
|
||||||
|
self.right = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bottom(mut self, value: i64) -> Self {
|
||||||
|
self.bottom = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn left(mut self, value: i64) -> Self {
|
||||||
|
self.left = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Positioned {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for Positioned {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = vec![self.kind.to_value().to_string()];
|
||||||
|
|
||||||
|
if let Some(inset) = &self.inset {
|
||||||
|
if inset.is_negative() {
|
||||||
|
ret.push(format!("-inset-[{inset}px]"));
|
||||||
|
} else {
|
||||||
|
ret.push(format!("inset-[{inset}px]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(inset) = &self.inset_x {
|
||||||
|
if inset.is_negative() {
|
||||||
|
ret.push(format!("-inset-x-[{inset}px]"));
|
||||||
|
} else {
|
||||||
|
ret.push(format!("inset-x-[{inset}px]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(inset) = &self.inset_y {
|
||||||
|
if inset.is_negative() {
|
||||||
|
ret.push(format!("-inset-y-[{inset}px]"));
|
||||||
|
} else {
|
||||||
|
ret.push(format!("inset-y-[{inset}px]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(start) = &self.start {
|
||||||
|
if start.is_negative() {
|
||||||
|
ret.push(format!("-start-[{start}px]"));
|
||||||
|
} else {
|
||||||
|
ret.push(format!("start-[{start}px]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(end) = &self.end {
|
||||||
|
if end.is_negative() {
|
||||||
|
ret.push(format!("-end-[{end}px]"));
|
||||||
|
} else {
|
||||||
|
ret.push(format!("end-[{end}px]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = &self.top {
|
||||||
|
if value.is_negative() {
|
||||||
|
ret.push(format!("-top-[{value}px]"));
|
||||||
|
} else {
|
||||||
|
ret.push(format!("top-[{value}px]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = &self.right {
|
||||||
|
if value.is_negative() {
|
||||||
|
ret.push(format!("-right-[{value}px]"));
|
||||||
|
} else {
|
||||||
|
ret.push(format!("right-[{value}px]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = &self.bottom {
|
||||||
|
if value.is_negative() {
|
||||||
|
ret.push(format!("-bottom-[{value}px]"));
|
||||||
|
} else {
|
||||||
|
ret.push(format!("bottom-[{value}px]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = &self.left {
|
||||||
|
if value.is_negative() {
|
||||||
|
ret.push(format!("-left-[{value}px]"));
|
||||||
|
} else {
|
||||||
|
ret.push(format!("left-[{value}px]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.inner.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.inner.as_ref().can_inherit() {
|
||||||
|
self.inner
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.inner.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PositionKind {
|
||||||
|
Static,
|
||||||
|
Fixed,
|
||||||
|
Absolute,
|
||||||
|
Relative,
|
||||||
|
Sticky,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PositionKind {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
PositionKind::Static => "static",
|
||||||
|
PositionKind::Fixed => "fixed",
|
||||||
|
PositionKind::Absolute => "absolute",
|
||||||
|
PositionKind::Relative => "relative",
|
||||||
|
PositionKind::Sticky => "sticky",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn ObjectPosition<T: UIWidget + 'static>(side: Side, inner: T) -> ObjectPositioned {
|
||||||
|
ObjectPositioned {
|
||||||
|
inner: Box::new(inner),
|
||||||
|
side,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ObjectPositioned {
|
||||||
|
inner: Box<dyn UIWidget>,
|
||||||
|
side: Side,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ObjectPositioned {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for ObjectPositioned {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
match self.side {
|
||||||
|
Side::Start => "object-top",
|
||||||
|
Side::End => "object-bottom",
|
||||||
|
Side::Top => "object-top",
|
||||||
|
Side::Right => "object-right",
|
||||||
|
Side::Bottom => "object-bottom",
|
||||||
|
Side::Left => "object-left",
|
||||||
|
Side::StartStart => "object-left-top",
|
||||||
|
Side::StartEnd => "object-right-top",
|
||||||
|
Side::EndEnd => "object-right-bottom",
|
||||||
|
Side::EndStart => "object-left-bottom",
|
||||||
|
Side::TopLeft => "object-left-top",
|
||||||
|
Side::TopRight => "object-right-top",
|
||||||
|
Side::BottomRight => "object-right-bottom",
|
||||||
|
Side::BottomLeft => "object-left-bottom",
|
||||||
|
Side::Center => "object-center",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.inner.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.inner.as_ref().can_inherit() {
|
||||||
|
self.inner
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.inner.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Resize {
|
||||||
|
None,
|
||||||
|
Y,
|
||||||
|
X,
|
||||||
|
Both,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resize {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Resize::None => "resize-none",
|
||||||
|
Resize::Y => "resize-y",
|
||||||
|
Resize::X => "resize-x",
|
||||||
|
Resize::Both => "resize",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Resizeable<T: UIWidget + 'static>(mode: Resize, inner: T) -> ResizeableWidget {
|
||||||
|
ResizeableWidget(Box::new(inner), mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResizeableWidget(Box<dyn UIWidget>, Resize);
|
||||||
|
|
||||||
|
impl Render for ResizeableWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for ResizeableWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![self.1.to_value().to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
src/ui/primitives/rounded.rs
Normal file
68
src/ui/primitives/rounded.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use super::{Side, Size};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Rounded<T: UIWidget + 'static>(inner: T) -> RoundedWidget {
|
||||||
|
RoundedWidget(Box::new(inner), None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RoundedWidget(Box<dyn UIWidget>, Option<Size>, Option<Side>);
|
||||||
|
|
||||||
|
impl RoundedWidget {
|
||||||
|
#[must_use]
|
||||||
|
pub fn size(mut self, size: Size) -> Self {
|
||||||
|
self.1 = Some(size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn side(mut self, side: Side) -> Self {
|
||||||
|
self.2 = Some(side);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for RoundedWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for RoundedWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
if let Some(side) = &self.2 {
|
||||||
|
if let Some(size) = &self.1 {
|
||||||
|
return vec![format!("rounded-{}-{}", side.to_value(), size.to_value())];
|
||||||
|
}
|
||||||
|
} else if let Some(size) = &self.1 {
|
||||||
|
return vec![format!("rounded-{}", size.to_value())];
|
||||||
|
}
|
||||||
|
vec!["rounded".to_owned()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
234
src/ui/primitives/scroll.rs
Normal file
234
src/ui/primitives/scroll.rs
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
use super::{margin::Margin, padding::PaddingWidget};
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Scroll<T: UIWidget + 'static>(inner: T) -> ScrollWidget {
|
||||||
|
ScrollWidget(Box::new(inner), true, None, None, None, None, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScrollWidget(
|
||||||
|
Box<dyn UIWidget>,
|
||||||
|
bool,
|
||||||
|
Option<Margin>,
|
||||||
|
Option<PaddingWidget>,
|
||||||
|
Option<Overscroll>,
|
||||||
|
Option<SnapType>,
|
||||||
|
bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl ScrollWidget {
|
||||||
|
pub fn smooth(mut self, value: bool) -> Self {
|
||||||
|
self.1 = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_margin(mut self, margin: Margin) -> Self {
|
||||||
|
self.2 = Some(margin);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_padding(mut self, padding: PaddingWidget) -> Self {
|
||||||
|
self.3 = Some(padding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn overscroll(mut self, behaviour: Overscroll) -> Self {
|
||||||
|
self.4 = Some(behaviour);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snap(mut self, kind: SnapType) -> Self {
|
||||||
|
self.5 = Some(kind);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skip_snap(mut self) -> Self {
|
||||||
|
self.6 = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ScrollWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for ScrollWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
|
||||||
|
if self.1 {
|
||||||
|
ret.push("scroll-smooth".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(margin) = &self.2 {
|
||||||
|
let classes = margin
|
||||||
|
.base_class()
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| format!("scroll-{x}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ret.extend_from_slice(&classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(padding) = &self.3 {
|
||||||
|
let classes = padding
|
||||||
|
.base_class()
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| format!("scroll-{x}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ret.extend_from_slice(&classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(overscroll) = &self.4 {
|
||||||
|
ret.push(overscroll.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(snap) = &self.5 {
|
||||||
|
ret.push(snap.to_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.6 {
|
||||||
|
ret.push("snap-normal".to_string());
|
||||||
|
} else {
|
||||||
|
ret.push("snap-always".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Overscroll {
|
||||||
|
Auto,
|
||||||
|
Contain,
|
||||||
|
None,
|
||||||
|
YAuto,
|
||||||
|
YContain,
|
||||||
|
YNone,
|
||||||
|
XAuto,
|
||||||
|
XContain,
|
||||||
|
XNone,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Overscroll {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Overscroll::Auto => "overscroll-auto",
|
||||||
|
Overscroll::Contain => "overscroll-contain",
|
||||||
|
Overscroll::None => "overscroll-none",
|
||||||
|
Overscroll::YAuto => "overscroll-y-auto",
|
||||||
|
Overscroll::YContain => "overscroll-y-contain",
|
||||||
|
Overscroll::YNone => "overscroll-y-none",
|
||||||
|
Overscroll::XAuto => "overscroll-x-auto",
|
||||||
|
Overscroll::XContain => "overscroll-x-contain",
|
||||||
|
Overscroll::XNone => "overscroll-x-none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SnapType {
|
||||||
|
None,
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
Both,
|
||||||
|
Mandatory,
|
||||||
|
Proximity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SnapType {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
SnapType::None => "snap-none",
|
||||||
|
SnapType::X => "snap-x",
|
||||||
|
SnapType::Y => "snap-y",
|
||||||
|
SnapType::Both => "snap-both",
|
||||||
|
SnapType::Mandatory => "snap-mandatory",
|
||||||
|
SnapType::Proximity => "snap-proximity",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SnapAlign(Box<dyn UIWidget>, String);
|
||||||
|
|
||||||
|
impl SnapAlign {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Start<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "snap-start".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn End<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "snap-end".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Center<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "snap-center".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn None<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "snap-align-none".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SnapAlign {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for SnapAlign {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![self.1.clone()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
163
src/ui/primitives/shadow.rs
Normal file
163
src/ui/primitives/shadow.rs
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
use crate::ui::{UIWidget, color::UIColor};
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
pub struct Shadow(Box<dyn UIWidget>, String, Option<Box<dyn UIColor>>);
|
||||||
|
|
||||||
|
impl Shadow {
|
||||||
|
pub fn small<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "sm".to_owned(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn regular<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), String::new(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn medium<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "md".to_owned(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn large<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "lg".to_owned(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xl<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "xl".to_owned(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _2xl<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "2xl".to_owned(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inner<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "inner".to_owned(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn none<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "none".to_owned(), None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shadow {
|
||||||
|
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.2 = Some(Box::new(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Shadow {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for Shadow {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = if self.1.is_empty() {
|
||||||
|
vec!["shadow".to_string()]
|
||||||
|
} else {
|
||||||
|
vec![format!("shadow-{}", self.1)]
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(color) = &self.2 {
|
||||||
|
ret.push(format!("shadow-{}", color.color_class()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DropShadow(Box<dyn UIWidget>, String);
|
||||||
|
|
||||||
|
impl DropShadow {
|
||||||
|
pub fn small<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "sm".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn regular<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), String::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn medium<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "md".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn large<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "lg".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn none<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "none".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xl<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "xl".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _2xl<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "2xl".to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for DropShadow {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for DropShadow {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
if self.1.is_empty() {
|
||||||
|
vec!["drop-shadow".to_string()]
|
||||||
|
} else {
|
||||||
|
vec![format!("drop-shadow-{}", self.1)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
src/ui/primitives/sized.rs
Normal file
54
src/ui/primitives/sized.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use super::space::ScreenValue;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Sized<T: UIWidget + 'static>(
|
||||||
|
height: ScreenValue,
|
||||||
|
width: ScreenValue,
|
||||||
|
inner: T,
|
||||||
|
) -> SizedWidget {
|
||||||
|
SizedWidget(Box::new(inner), height, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SizedWidget(Box<dyn UIWidget>, ScreenValue, ScreenValue);
|
||||||
|
|
||||||
|
impl Render for SizedWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for SizedWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
format!("h-{}", self.1.to_value()),
|
||||||
|
format!("w-{}", self.2.to_value()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
232
src/ui/primitives/space.rs
Normal file
232
src/ui/primitives/space.rs
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Controlling the space between child elements.
|
||||||
|
pub fn SpaceBetween<T: UIWidget + 'static>(inner: T) -> SpaceBetweenWidget {
|
||||||
|
SpaceBetweenWidget(Box::new(inner), None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SpaceBetweenWidget(Box<dyn UIWidget>, Option<ScreenValue>, Option<ScreenValue>);
|
||||||
|
|
||||||
|
impl Render for SpaceBetweenWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpaceBetweenWidget {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn x(mut self, x: ScreenValue) -> Self {
|
||||||
|
self.1 = Some(x);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn y(mut self, y: ScreenValue) -> Self {
|
||||||
|
self.2 = Some(y);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for SpaceBetweenWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
|
||||||
|
if let Some(x) = &self.1 {
|
||||||
|
ret.push(format!("space-x-{}", x.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(y) = &self.2 {
|
||||||
|
ret.push(format!("space-y-{}", y.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum ScreenValue {
|
||||||
|
_0,
|
||||||
|
_0p5,
|
||||||
|
_1,
|
||||||
|
_1p5,
|
||||||
|
_2,
|
||||||
|
_2p5,
|
||||||
|
_3,
|
||||||
|
_3p5,
|
||||||
|
_4,
|
||||||
|
_5,
|
||||||
|
_6,
|
||||||
|
_7,
|
||||||
|
_8,
|
||||||
|
_9,
|
||||||
|
_10,
|
||||||
|
_11,
|
||||||
|
_12,
|
||||||
|
_14,
|
||||||
|
_16,
|
||||||
|
_20,
|
||||||
|
_24,
|
||||||
|
_28,
|
||||||
|
_32,
|
||||||
|
_36,
|
||||||
|
_40,
|
||||||
|
_44,
|
||||||
|
_48,
|
||||||
|
_52,
|
||||||
|
_56,
|
||||||
|
_60,
|
||||||
|
_64,
|
||||||
|
_72,
|
||||||
|
_80,
|
||||||
|
_90,
|
||||||
|
px,
|
||||||
|
reverse,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
fit,
|
||||||
|
fill,
|
||||||
|
screen,
|
||||||
|
full,
|
||||||
|
auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenValue {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::_0 => "0",
|
||||||
|
Self::_0p5 => "0.5",
|
||||||
|
Self::_1 => "1",
|
||||||
|
Self::_1p5 => "1.5",
|
||||||
|
Self::_2 => "2",
|
||||||
|
Self::_2p5 => "2.5",
|
||||||
|
Self::_3 => "3",
|
||||||
|
Self::_3p5 => "3.5",
|
||||||
|
Self::_4 => "4",
|
||||||
|
Self::_5 => "5",
|
||||||
|
Self::_6 => "6",
|
||||||
|
Self::_7 => "7",
|
||||||
|
Self::_8 => "8",
|
||||||
|
Self::_9 => "9",
|
||||||
|
Self::_10 => "10",
|
||||||
|
Self::_11 => "11",
|
||||||
|
Self::_12 => "12",
|
||||||
|
Self::_14 => "14",
|
||||||
|
Self::_16 => "16",
|
||||||
|
Self::_20 => "20",
|
||||||
|
Self::_24 => "24",
|
||||||
|
Self::_28 => "28",
|
||||||
|
Self::_32 => "32",
|
||||||
|
Self::_36 => "36",
|
||||||
|
Self::_40 => "40",
|
||||||
|
Self::_44 => "44",
|
||||||
|
Self::_48 => "48",
|
||||||
|
Self::_52 => "52",
|
||||||
|
Self::_56 => "56",
|
||||||
|
Self::_60 => "60",
|
||||||
|
Self::_64 => "64",
|
||||||
|
Self::_72 => "72",
|
||||||
|
Self::_80 => "80",
|
||||||
|
Self::_90 => "90",
|
||||||
|
Self::px => "px",
|
||||||
|
Self::reverse => "reverse",
|
||||||
|
Self::min => "min",
|
||||||
|
Self::max => "max",
|
||||||
|
Self::fit => "fit",
|
||||||
|
Self::fill => "fill",
|
||||||
|
Self::screen => "screen",
|
||||||
|
Self::full => "full",
|
||||||
|
Self::auto => "auto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Fraction {
|
||||||
|
_1on2,
|
||||||
|
_1on3,
|
||||||
|
_2on3,
|
||||||
|
_1on4,
|
||||||
|
_2on4,
|
||||||
|
_3on4,
|
||||||
|
_1on5,
|
||||||
|
_2on5,
|
||||||
|
_3on5,
|
||||||
|
_4on5,
|
||||||
|
_1on6,
|
||||||
|
_2on6,
|
||||||
|
_3on6,
|
||||||
|
_4on6,
|
||||||
|
_5on6,
|
||||||
|
_1on12,
|
||||||
|
_2on12,
|
||||||
|
_3on12,
|
||||||
|
_4on12,
|
||||||
|
_5on12,
|
||||||
|
_6on12,
|
||||||
|
_7on12,
|
||||||
|
_8on12,
|
||||||
|
_9on12,
|
||||||
|
_10on12,
|
||||||
|
_11on12,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fraction {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Fraction::_1on2 => "1/2",
|
||||||
|
Fraction::_1on3 => "1/3",
|
||||||
|
Fraction::_2on3 => "2/3",
|
||||||
|
Fraction::_1on4 => "1/4",
|
||||||
|
Fraction::_2on4 => "2/4",
|
||||||
|
Fraction::_3on4 => "3/4",
|
||||||
|
Fraction::_1on5 => "1/5",
|
||||||
|
Fraction::_2on5 => "2/5",
|
||||||
|
Fraction::_3on5 => "3/5",
|
||||||
|
Fraction::_4on5 => "4/5",
|
||||||
|
Fraction::_1on6 => "1/6",
|
||||||
|
Fraction::_2on6 => "2/6",
|
||||||
|
Fraction::_3on6 => "3/6",
|
||||||
|
Fraction::_4on6 => "4/6",
|
||||||
|
Fraction::_5on6 => "5/6",
|
||||||
|
Fraction::_1on12 => "1/12",
|
||||||
|
Fraction::_2on12 => "2/12",
|
||||||
|
Fraction::_3on12 => "3/12",
|
||||||
|
Fraction::_4on12 => "4/12",
|
||||||
|
Fraction::_5on12 => "5/12",
|
||||||
|
Fraction::_6on12 => "6/12",
|
||||||
|
Fraction::_7on12 => "7/12",
|
||||||
|
Fraction::_8on12 => "8/12",
|
||||||
|
Fraction::_9on12 => "9/12",
|
||||||
|
Fraction::_10on12 => "10/12",
|
||||||
|
Fraction::_11on12 => "11/12",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
src/ui/primitives/svg.rs
Normal file
76
src/ui/primitives/svg.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::{UIWidget, color::UIColor};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn SVG<T: UIWidget + 'static>(inner: T) -> SVGWidget {
|
||||||
|
SVGWidget(Box::new(inner), None, None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SVGWidget(
|
||||||
|
Box<dyn UIWidget>,
|
||||||
|
Option<Box<dyn UIColor>>,
|
||||||
|
Option<Box<dyn UIColor>>,
|
||||||
|
Option<u32>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl SVGWidget {
|
||||||
|
pub fn fill<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.1 = Some(Box::new(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stroke<C: UIColor + 'static>(mut self, color: C) -> Self {
|
||||||
|
self.2 = Some(Box::new(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stroke_width(mut self, width: u32) -> Self {
|
||||||
|
self.3 = Some(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SVGWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for SVGWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = vec![];
|
||||||
|
|
||||||
|
if let Some(fill) = &self.1 {
|
||||||
|
ret.push(format!("fill-{}", fill.color_class()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(stroke) = &self.2 {
|
||||||
|
ret.push(format!("stroke-{}", stroke.color_class()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(stroke_width) = &self.3 {
|
||||||
|
ret.push(format!("stroke-[{stroke_width}px]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
svg class=(self.base_class().join(" ")) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
217
src/ui/primitives/table.rs
Normal file
217
src/ui/primitives/table.rs
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
|
use super::{div::Div, space::ScreenValue};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Table<T: UIWidget + 'static + Clone>(inner: Vec<Vec<T>>) -> TableWidget {
|
||||||
|
let inner = Div().vanish().push_for_each(&inner, |row| {
|
||||||
|
TableRow(
|
||||||
|
Div()
|
||||||
|
.vanish()
|
||||||
|
.push_for_each(&row, |col| TableData(col.clone())),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
TableWidget(Box::new(inner), Vec::new(), None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn TableRaw<T: UIWidget + 'static + Clone>(inner: T) -> TableWidget {
|
||||||
|
TableWidget(Box::new(inner), Vec::new(), None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TableWidget(
|
||||||
|
Box<dyn UIWidget>,
|
||||||
|
Vec<String>,
|
||||||
|
Option<Box<dyn UIWidget>>,
|
||||||
|
Option<Caption>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl TableWidget {
|
||||||
|
pub fn header<T: UIWidget + 'static>(mut self, header: T) -> Self {
|
||||||
|
self.2 = Some(Box::new(header));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn caption(mut self, caption: Caption) -> Self {
|
||||||
|
self.3 = Some(caption);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_collapse(mut self) -> Self {
|
||||||
|
self.1.push("border-collapse".to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_seperate(mut self) -> Self {
|
||||||
|
self.1.push("border-separate".to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_spacing(mut self, spacing: ScreenValue) -> Self {
|
||||||
|
self.1
|
||||||
|
.push(format!("border-spacing-{}", spacing.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_spacing_x(mut self, spacing: ScreenValue) -> Self {
|
||||||
|
self.1
|
||||||
|
.push(format!("border-spacing-x-{}", spacing.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_spacing_y(mut self, spacing: ScreenValue) -> Self {
|
||||||
|
self.1
|
||||||
|
.push(format!("border-spacing-y-{}", spacing.to_value()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout_fixed(mut self) -> Self {
|
||||||
|
self.1.push("table-fixed".to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout_auto(mut self) -> Self {
|
||||||
|
self.1.push("table-auto".to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TableWidget {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for TableWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
self.1.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
self.base_class()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> maud::Markup {
|
||||||
|
html! {
|
||||||
|
table class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
@if let Some(caption) = &self.3 {
|
||||||
|
(caption)
|
||||||
|
}
|
||||||
|
|
||||||
|
@if let Some(header) = &self.2 {
|
||||||
|
thead {
|
||||||
|
(header)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
(self.0.as_ref())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Caption(Box<dyn UIWidget>, bool);
|
||||||
|
|
||||||
|
impl Caption {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Top<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Bottom<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Caption {
|
||||||
|
fn render(&self) -> maud::Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for Caption {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
if self.1 {
|
||||||
|
vec!["caption-top".to_string()]
|
||||||
|
} else {
|
||||||
|
vec!["caption-bottom".to_string()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
self.base_class()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> maud::Markup {
|
||||||
|
html! {
|
||||||
|
caption class=(self.base_class().join(" ")) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! element_widget {
|
||||||
|
($name:ident, $widget:ident, $element:ident) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn $name<T: UIWidget + 'static>(inner: T) -> $widget {
|
||||||
|
$widget(Box::new(inner))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct $widget(Box<dyn UIWidget>);
|
||||||
|
|
||||||
|
impl Render for $widget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for $widget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
$element class=(class) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
element_widget!(TableRow, TableRowWidget, tr);
|
||||||
|
element_widget!(TableHead, TableHeadWidget, th);
|
||||||
|
element_widget!(TableData, TableDataWidget, td);
|
||||||
|
element_widget!(Header, HeaderWidget, header);
|
||||||
|
|
||||||
|
// https://flowbite.com/docs/components/tables/
|
||||||
|
// TODO : tables
|
||||||
|
// table options
|
||||||
|
// wrap data to table (crud)
|
||||||
|
|
||||||
|
// TODO: TABLE https://flowbite.com/docs/components/pagination/#table-data-pagination
|
1145
src/ui/primitives/text.rs
Normal file
1145
src/ui/primitives/text.rs
Normal file
File diff suppressed because it is too large
Load diff
384
src/ui/primitives/transform.rs
Normal file
384
src/ui/primitives/transform.rs
Normal file
|
@ -0,0 +1,384 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
Side,
|
||||||
|
flex::Either,
|
||||||
|
space::{Fraction, ScreenValue},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Scale<T: UIWidget + 'static>(size: f64, inner: T) -> ScaleWidget {
|
||||||
|
ScaleWidget(Box::new(inner), size, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScaleWidget(Box<dyn UIWidget>, f64, u8);
|
||||||
|
|
||||||
|
impl ScaleWidget {
|
||||||
|
pub fn x(mut self) -> Self {
|
||||||
|
self.2 = 1;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y(mut self) -> Self {
|
||||||
|
self.2 = 2;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ScaleWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for ScaleWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
match self.2 {
|
||||||
|
1 => vec![format!("scale-x-[{:.2}]", self.1)],
|
||||||
|
2 => vec![format!("scale-y-[{:.2}]", self.1)],
|
||||||
|
_ => vec![format!("scale-[{:.2}]", self.1)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Rotate<T: UIWidget + 'static>(deg: u32, inner: T) -> RotateWidget {
|
||||||
|
RotateWidget(Box::new(inner), deg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RotateWidget(Box<dyn UIWidget>, u32);
|
||||||
|
|
||||||
|
impl Render for RotateWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for RotateWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![format!("rotate-[{:.2}deg]", self.1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn RenderTransformCPU<T: UIWidget + 'static>(inner: T) -> HardwareAccelerationWidget {
|
||||||
|
HardwareAccelerationWidget(Box::new(inner), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn RenderTransformGPU<T: UIWidget + 'static>(inner: T) -> HardwareAccelerationWidget {
|
||||||
|
HardwareAccelerationWidget(Box::new(inner), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HardwareAccelerationWidget(Box<dyn UIWidget>, u8);
|
||||||
|
|
||||||
|
impl Render for HardwareAccelerationWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for HardwareAccelerationWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
match self.1 {
|
||||||
|
1 => vec!["transform-gpu".to_string()],
|
||||||
|
_ => vec!["transform-cpu".to_string()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Transform<
|
||||||
|
T: UIWidget + 'static,
|
||||||
|
X: Into<Either<ScreenValue, Fraction>>,
|
||||||
|
Y: Into<Either<ScreenValue, Fraction>>,
|
||||||
|
>(
|
||||||
|
x: Option<X>,
|
||||||
|
y: Option<Y>,
|
||||||
|
inner: T,
|
||||||
|
) -> TransformWidget {
|
||||||
|
TransformWidget {
|
||||||
|
inner: Box::new(inner),
|
||||||
|
x: x.map(|x| x.into()),
|
||||||
|
y: y.map(|y| y.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TransformWidget {
|
||||||
|
inner: Box<dyn UIWidget>,
|
||||||
|
x: Option<Either<ScreenValue, Fraction>>,
|
||||||
|
y: Option<Either<ScreenValue, Fraction>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TransformWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for TransformWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
|
||||||
|
if let Some(x) = &self.x {
|
||||||
|
ret.push(format!(
|
||||||
|
"translate-x-{}",
|
||||||
|
x.map(|x| x.to_value().to_string(), |x| x.to_value().to_string())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(y) = &self.y {
|
||||||
|
ret.push(format!(
|
||||||
|
"translate-y-{}",
|
||||||
|
y.map(|y| y.to_value().to_string(), |y| y.to_value().to_string())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.inner.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.inner.as_ref().can_inherit() {
|
||||||
|
self.inner
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.inner.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SkewValue {
|
||||||
|
_0,
|
||||||
|
_1,
|
||||||
|
_2,
|
||||||
|
_3,
|
||||||
|
_6,
|
||||||
|
_12,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SkewValue {
|
||||||
|
pub const fn to_value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
SkewValue::_0 => "0",
|
||||||
|
SkewValue::_1 => "1",
|
||||||
|
SkewValue::_2 => "2",
|
||||||
|
SkewValue::_3 => "3",
|
||||||
|
SkewValue::_6 => "6",
|
||||||
|
SkewValue::_12 => "12",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Skew<T: UIWidget + 'static>(
|
||||||
|
x: Option<SkewValue>,
|
||||||
|
y: Option<SkewValue>,
|
||||||
|
inner: T,
|
||||||
|
) -> SkewWidget {
|
||||||
|
SkewWidget {
|
||||||
|
inner: Box::new(inner),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SkewWidget {
|
||||||
|
inner: Box<dyn UIWidget>,
|
||||||
|
x: Option<SkewValue>,
|
||||||
|
y: Option<SkewValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SkewWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for SkewWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
|
||||||
|
if let Some(x) = &self.x {
|
||||||
|
ret.push(format!("skew-x-{}", x.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(y) = &self.y {
|
||||||
|
ret.push(format!("skew-y-{}", y.to_value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.inner.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.inner.as_ref().can_inherit() {
|
||||||
|
self.inner
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.inner.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn TransformOrigin<T: UIWidget + 'static>(origin: Side, inner: T) -> TransformOriginWidget {
|
||||||
|
TransformOriginWidget(Box::new(inner), origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TransformOriginWidget(Box<dyn UIWidget>, Side);
|
||||||
|
|
||||||
|
impl Render for TransformOriginWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for TransformOriginWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let side = match self.1 {
|
||||||
|
Side::Start => "top",
|
||||||
|
Side::End => "bottom",
|
||||||
|
Side::Top => "top",
|
||||||
|
Side::Right => "right",
|
||||||
|
Side::Bottom => "bottom",
|
||||||
|
Side::Left => "left",
|
||||||
|
Side::StartStart => "top-left",
|
||||||
|
Side::StartEnd => "top-right",
|
||||||
|
Side::EndEnd => "bottom-right",
|
||||||
|
Side::EndStart => "bottom-left",
|
||||||
|
Side::TopLeft => "top-left",
|
||||||
|
Side::TopRight => "top-right",
|
||||||
|
Side::BottomRight => "bottom-right",
|
||||||
|
Side::BottomLeft => "bottom-left",
|
||||||
|
Side::Center => "center",
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![format!("origin-{side}")]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
src/ui/primitives/visibility.rs
Normal file
54
src/ui/primitives/visibility.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
pub struct Visibility(Box<dyn UIWidget>, String);
|
||||||
|
|
||||||
|
impl Visibility {
|
||||||
|
pub fn visible<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "visible".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hidden<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "invisible".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collapsed<T: UIWidget + 'static>(inner: T) -> Self {
|
||||||
|
Self(Box::new(inner), "collapse".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Visibility {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for Visibility {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![self.1.clone()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
91
src/ui/primitives/width.rs
Normal file
91
src/ui/primitives/width.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
flex::Either,
|
||||||
|
space::{Fraction, ScreenValue},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Width<T: UIWidget + 'static, S: Into<Either<ScreenValue, Fraction>>>(
|
||||||
|
size: S,
|
||||||
|
inner: T,
|
||||||
|
) -> WidthWidget {
|
||||||
|
WidthWidget(Box::new(inner), size.into(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn MinWidth<T: UIWidget + 'static, S: Into<Either<ScreenValue, Fraction>>>(
|
||||||
|
size: S,
|
||||||
|
inner: T,
|
||||||
|
) -> WidthWidget {
|
||||||
|
WidthWidget(Box::new(inner), size.into(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn MaxWidth<T: UIWidget + 'static, S: Into<Either<ScreenValue, Fraction>>>(
|
||||||
|
size: S,
|
||||||
|
inner: T,
|
||||||
|
) -> WidthWidget {
|
||||||
|
WidthWidget(Box::new(inner), size.into(), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WidthWidget(Box<dyn UIWidget>, Either<ScreenValue, Fraction>, u8);
|
||||||
|
|
||||||
|
impl Render for WidthWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for WidthWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
match self.2 {
|
||||||
|
1 => {
|
||||||
|
return vec![format!(
|
||||||
|
"min-w-{}",
|
||||||
|
self.1
|
||||||
|
.map(|x| x.to_value().to_string(), |x| x.to_value().to_string())
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
return vec![format!(
|
||||||
|
"max-w-{}",
|
||||||
|
self.1
|
||||||
|
.map(|x| x.to_value().to_string(), |x| x.to_value().to_string())
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![format!(
|
||||||
|
"w-{}",
|
||||||
|
self.1
|
||||||
|
.map(|x| x.to_value().to_string(), |x| x.to_value().to_string())
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
src/ui/wrapper/mod.rs
Normal file
135
src/ui/wrapper/mod.rs
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
macro_rules! wrapper {
|
||||||
|
($constr:ident, $widgetname:ident, $class:literal) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn $constr<I: UIWidget + 'static>(inherit: I) -> $widgetname {
|
||||||
|
$widgetname(None, Box::new(inherit))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct $widgetname(Option<Box<dyn UIWidget>>, Box<dyn UIWidget>);
|
||||||
|
|
||||||
|
impl $widgetname {
|
||||||
|
fn wrapped_class(&self) -> Vec<String> {
|
||||||
|
self.1
|
||||||
|
.extended_class()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|x| !x.is_empty())
|
||||||
|
.map(|x| {
|
||||||
|
let mut s = $class.to_string();
|
||||||
|
s.push_str(":");
|
||||||
|
s.push_str(&x);
|
||||||
|
s
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on<T: UIWidget + 'static>(mut self, inner: T) -> Self {
|
||||||
|
self.0 = Some(Box::new(inner));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for $widgetname {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for $widgetname {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
self.wrapped_class()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = self.base_class();
|
||||||
|
if let Some(inner) = &self.0 {
|
||||||
|
ret.extend_from_slice(&inner.extended_class());
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().unwrap().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.render_with_class(&format!("{} {class}", self.wrapped_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.wrapped_class().join(" "))) {
|
||||||
|
(self.0.as_ref().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper!(Hover, HoverWrapper, "hover");
|
||||||
|
wrapper!(DarkMode, DarkModeWrapper, "dark");
|
||||||
|
wrapper!(Active, ActiveWrapper, "active");
|
||||||
|
wrapper!(Focus, FocusWrapper, "focus");
|
||||||
|
wrapper!(First, FirstWrapper, "first");
|
||||||
|
wrapper!(Odd, OddWrapper, "odd");
|
||||||
|
wrapper!(Even, EvenWrapper, "even");
|
||||||
|
|
||||||
|
wrapper!(Required, RequiredWrapper, "required");
|
||||||
|
wrapper!(Invalid, InvalidWrapper, "invalid");
|
||||||
|
wrapper!(Disabled, DisabledWrapper, "disabled");
|
||||||
|
wrapper!(Placeholder, PlaceholderWrapper, "placeholder");
|
||||||
|
wrapper!(FileButton, FileButtonWrapper, "file");
|
||||||
|
wrapper!(Marker, MarkerWrapper, "marker");
|
||||||
|
wrapper!(Selection, SelectionWrapper, "selection");
|
||||||
|
wrapper!(FirstLine, FirstLineWrapper, "first-line");
|
||||||
|
wrapper!(FirstLetter, FirstLetterWrapper, "first-letter");
|
||||||
|
|
||||||
|
wrapper!(Portrait, PortraitWrapper, "portrait");
|
||||||
|
wrapper!(Landscape, LandscapeWrapper, "landscape");
|
||||||
|
wrapper!(Print, PrintWrapper, "print");
|
||||||
|
wrapper!(LeftToRight, LeftToRightWrapper, "ltr");
|
||||||
|
wrapper!(RightToLeft, RightToLeftWrapper, "rtl");
|
||||||
|
wrapper!(Opened, OpenWrapper, "open");
|
||||||
|
|
||||||
|
wrapper!(SmallScreen, SmallScreenWrapper, "sm");
|
||||||
|
wrapper!(MediumScreen, MediumScreenWrapper, "md");
|
||||||
|
wrapper!(LargeScreen, LargeScreenWrapper, "lg");
|
||||||
|
wrapper!(XLScreen, XLScreenWrapper, "xl");
|
||||||
|
wrapper!(_2XLScreen, _2XLScreenWrapper, "2xl");
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub mod Screen {
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
_2XLScreen, _2XLScreenWrapper, LargeScreen, LargeScreenWrapper, MediumScreen,
|
||||||
|
MediumScreenWrapper, SmallScreen, SmallScreenWrapper, XLScreen, XLScreenWrapper,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn small<I: UIWidget + 'static>(inherit: I) -> SmallScreenWrapper {
|
||||||
|
SmallScreen(inherit)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn medium<I: UIWidget + 'static>(inherit: I) -> MediumScreenWrapper {
|
||||||
|
MediumScreen(inherit)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn large<I: UIWidget + 'static>(inherit: I) -> LargeScreenWrapper {
|
||||||
|
LargeScreen(inherit)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xl<I: UIWidget + 'static>(inherit: I) -> XLScreenWrapper {
|
||||||
|
XLScreen(inherit)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _2xl<I: UIWidget + 'static>(inherit: I) -> _2XLScreenWrapper {
|
||||||
|
_2XLScreen(inherit)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue